49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
160 lines
5.1 KiB
Python
Executable File
160 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Update README.md with live code snippets from example files.
|
|
|
|
This script finds specially marked code blocks in README.md and updates them
|
|
with the actual code from the referenced files.
|
|
|
|
Usage:
|
|
python scripts/update_readme_snippets.py
|
|
python scripts/update_readme_snippets.py --check # Check mode for CI
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def get_github_url(file_path: str) -> str:
|
|
"""Generate a GitHub URL for the file.
|
|
|
|
Args:
|
|
file_path: Path to the file relative to repo root
|
|
|
|
Returns:
|
|
GitHub URL
|
|
"""
|
|
base_url = "https://github.com/modelcontextprotocol/python-sdk/blob/main"
|
|
return f"{base_url}/{file_path}"
|
|
|
|
|
|
def process_snippet_block(match: re.Match[str], check_mode: bool = False) -> str:
|
|
"""Process a single snippet-source block.
|
|
|
|
Args:
|
|
match: The regex match object
|
|
check_mode: If True, return original if no changes needed
|
|
|
|
Returns:
|
|
The updated block content
|
|
"""
|
|
full_match = match.group(0)
|
|
indent = match.group(1)
|
|
file_path = match.group(2)
|
|
|
|
try:
|
|
# Read the entire file. A missing source file must be fatal: a "Warning"
|
|
# that returns the stale block lets --check pass with exit 0, so a
|
|
# renamed or deleted snippet is invisible to CI. SystemExit deliberately
|
|
# escapes the `except Exception` below.
|
|
file = Path(file_path)
|
|
if not file.exists():
|
|
sys.exit(f"Error: snippet-source file not found: {file_path}")
|
|
|
|
code = file.read_text().rstrip()
|
|
github_url = get_github_url(file_path)
|
|
|
|
# Build the replacement block
|
|
indented_code = code.replace("\n", f"\n{indent}")
|
|
replacement = f"""{indent}<!-- snippet-source {file_path} -->
|
|
{indent}```python
|
|
{indent}{indented_code}
|
|
{indent}```
|
|
|
|
{indent}_Full example: [{file_path}]({github_url})_
|
|
{indent}<!-- /snippet-source -->"""
|
|
|
|
# In check mode, only check if code has changed
|
|
if check_mode:
|
|
# Extract existing code from the match
|
|
existing_content = match.group(3)
|
|
if existing_content is not None:
|
|
existing_lines = existing_content.strip().split("\n")
|
|
# Find code between ```python and ```
|
|
code_lines: list[str] = []
|
|
in_code = False
|
|
for line in existing_lines:
|
|
if line.strip() == "```python":
|
|
in_code = True
|
|
elif line.strip() == "```":
|
|
break
|
|
elif in_code:
|
|
code_lines.append(line)
|
|
existing_code = "\n".join(code_lines).strip()
|
|
# Compare with the indented version we would generate
|
|
expected_code = code.replace("\n", f"\n{indent}").strip()
|
|
if existing_code == expected_code:
|
|
return full_match
|
|
|
|
return replacement
|
|
|
|
except Exception as e:
|
|
print(f"Error processing {file_path}: {e}")
|
|
return full_match
|
|
|
|
|
|
def update_readme_snippets(check_mode: bool = False) -> bool:
|
|
"""Update code snippets in README.md with live code from source files.
|
|
|
|
Args:
|
|
check_mode: If True, only check if updates are needed without modifying
|
|
|
|
Returns:
|
|
True if file is up to date or was updated, False if check failed
|
|
"""
|
|
readme_path = Path("README.md")
|
|
if not readme_path.exists():
|
|
print(f"Error: README file not found: {readme_path}")
|
|
return False
|
|
|
|
content = readme_path.read_text()
|
|
original_content = content
|
|
|
|
# Pattern to match snippet-source blocks
|
|
# Matches: <!-- snippet-source path/to/file.py -->
|
|
# ... any content ...
|
|
# <!-- /snippet-source -->
|
|
pattern = r"^(\s*)<!-- snippet-source ([^\s]+) -->\n" r"(.*?)" r"^\1<!-- /snippet-source -->"
|
|
|
|
# Process all snippet-source blocks
|
|
updated_content = re.sub(
|
|
pattern, lambda m: process_snippet_block(m, check_mode), content, flags=re.MULTILINE | re.DOTALL
|
|
)
|
|
|
|
if check_mode:
|
|
if updated_content != original_content:
|
|
print(
|
|
f"Error: {readme_path} has outdated code snippets. "
|
|
"Run 'python scripts/update_readme_snippets.py' to update."
|
|
)
|
|
return False
|
|
else:
|
|
print(f"✓ {readme_path} code snippets are up to date")
|
|
return True
|
|
else:
|
|
if updated_content != original_content:
|
|
readme_path.write_text(updated_content)
|
|
print(f"✓ Updated {readme_path}")
|
|
else:
|
|
print(f"✓ {readme_path} already up to date")
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(description="Update README code snippets from source files")
|
|
parser.add_argument(
|
|
"--check", action="store_true", help="Check mode - verify snippets are up to date without modifying"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
success = update_readme_snippets(check_mode=args.check)
|
|
|
|
if not success:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|