Embed code snippets for README from executable examples (#1055)

This commit is contained in:
Inna Harper 2025-07-04 17:25:54 +01:00 • committed by GitHub
commit adf327164a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1139 additions and 1131 deletions

View file

@ -3,6 +3,9 @@ name: Shared Checks
on:
workflow_call:
permissions:
contents: read
jobs:
pre-commit:
runs-on: ubuntu-latest
@ -46,3 +49,19 @@ jobs:
- name: Run pytest
run: uv run --frozen --no-sync pytest
continue-on-error: true
readme-snippets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
version: 0.7.2
- name: Install dependencies
run: uv sync --frozen --all-extras --python 3.10
- name: Check README snippets are up to date
run: uv run --frozen scripts/update_readme_snippets.py --check

View file

@ -23,6 +23,7 @@ repos:
types: [python]
language: system
pass_filenames: false
exclude: ^README\.md$
- id: pyright
name: pyright
entry: uv run pyright
@ -36,3 +37,9 @@ repos:
language: system
files: ^(pyproject\.toml|uv\.lock)$
pass_filenames: false
- id: readme-snippets
name: Check README snippets are up to date
entry: uv run scripts/update_readme_snippets.py --check
language: system
files: ^(README\.md|examples/.*\.py|scripts/update_readme_snippets\.py)$
pass_filenames: false

View file

@ -40,7 +40,12 @@ uv run ruff check .
uv run ruff format .
```
7. Submit a pull request to the same branch you branched from
7. Update README snippets if you modified example code:
```bash
uv run scripts/update_readme_snippets.py
```
8. Submit a pull request to the same branch you branched from
## Code Style

265
README.md
View file

@ -33,6 +33,8 @@
- [Context](#context)
- [Completions](#completions)
- [Elicitation](#elicitation)
- [Sampling](#sampling)
- [Logging and Notifications](#logging-and-notifications)
- [Authentication](#authentication)
- [Running Your Server](#running-your-server)
- [Development Mode](#development-mode)
@ -119,7 +121,7 @@ mcp = FastMCP("Demo")
# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
def sum(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@ -207,48 +209,57 @@ def query_db() -> str:
Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:
<!-- snippet-source examples/snippets/servers/basic_resource.py -->
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")
mcp = FastMCP(name="Resource Example")
@mcp.resource("config://app", title="Application Configuration")
def get_config() -> str:
"""Static configuration data"""
return "App configuration here"
@mcp.resource("file://documents/{name}")
def read_document(name: str) -> str:
"""Read a document by name."""
# This would normally read from disk
return f"Content of {name}"
@mcp.resource("users://{user_id}/profile", title="User Profile")
def get_user_profile(user_id: str) -> str:
"""Dynamic user data"""
return f"Profile data for user {user_id}"
@mcp.resource("config://settings")
def get_settings() -> str:
"""Get application settings."""
return """{
"theme": "dark",
"language": "en",
"debug": false
}"""
```
_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_
<!-- /snippet-source -->
### Tools
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
<!-- snippet-source examples/snippets/servers/basic_tool.py -->
```python
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")
mcp = FastMCP(name="Tool Example")
@mcp.tool(title="BMI Calculator")
def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Calculate BMI given weight in kg and height in meters"""
return weight_kg / (height_m**2)
@mcp.tool()
def sum(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@mcp.tool(title="Weather Fetcher")
async def fetch_weather(city: str) -> str:
"""Fetch current weather for a city"""
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.weather.com/{city}")
return response.text
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get weather for a city."""
# This would normally call a weather API
return f"Weather in {city}: 22degrees{unit[0].upper()}"
```
_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_tool.py)_
<!-- /snippet-source -->
#### Structured Output
@ -375,11 +386,12 @@ def get_temperature(city: str) -> float:
Prompts are reusable templates that help LLMs interact with your server effectively:
<!-- snippet-source examples/snippets/servers/basic_prompt.py -->
```python
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
mcp = FastMCP("My App")
mcp = FastMCP(name="Prompt Example")
@mcp.prompt(title="Code Review")
@ -395,6 +407,8 @@ def debug_error(error: str) -> list[base.Message]:
base.AssistantMessage("I'll help debug that. What have you tried so far?"),
]
```
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
<!-- /snippet-source -->
### Images
@ -419,21 +433,31 @@ def create_thumbnail(image_path: str) -> Image:
The Context object gives your tools and resources access to MCP capabilities:
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
```python
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP("My App")
mcp = FastMCP(name="Progress Example")
@mcp.tool()
async def long_task(files: list[str], ctx: Context) -> str:
"""Process multiple files with progress tracking"""
for i, file in enumerate(files):
ctx.info(f"Processing {file}")
await ctx.report_progress(i, len(files))
data, mime_type = await ctx.read_resource(f"file://{file}")
return "Processing complete"
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
"""Execute a task with progress updates."""
await ctx.info(f"Starting: {task_name}")
for i in range(steps):
progress = (i + 1) / steps
await ctx.report_progress(
progress=progress,
total=1.0,
message=f"Step {i + 1}/{steps}",
)
await ctx.debug(f"Completed step {i + 1}")
return f"Task '{task_name}' completed"
```
_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_
<!-- /snippet-source -->
### Completions
@ -465,8 +489,10 @@ async def use_completion(session: ClientSession):
```
Server implementation:
<!-- snippet-source examples/snippets/servers/completion.py -->
```python
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.types import (
Completion,
CompletionArgument,
@ -475,72 +501,167 @@ from mcp.types import (
ResourceTemplateReference,
)
server = Server("example-server")
mcp = FastMCP(name="Example")
@server.completion()
@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
"""GitHub repository resource."""
return f"Repository: {owner}/{repo}"
@mcp.prompt(description="Code review prompt")
def review_code(language: str, code: str) -> str:
"""Generate a code review."""
return f"Review this {language} code:\n{code}"
@mcp.completion()
async def handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
"""Provide completions for prompts and resources."""
# Complete programming languages for the prompt
if isinstance(ref, PromptReference):
if ref.name == "review_code" and argument.name == "language":
languages = ["python", "javascript", "typescript", "go", "rust"]
return Completion(
values=[lang for lang in languages if lang.startswith(argument.value)],
hasMore=False,
)
# Complete repository names for GitHub resources
if isinstance(ref, ResourceTemplateReference):
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
# Use context to provide owner-specific repos
if context and context.arguments:
owner = context.arguments.get("owner")
if owner == "modelcontextprotocol":
repos = ["python-sdk", "typescript-sdk", "specification"]
# Filter based on partial input
filtered = [r for r in repos if r.startswith(argument.value)]
return Completion(values=filtered)
if context and context.arguments and context.arguments.get("owner") == "modelcontextprotocol":
repos = ["python-sdk", "typescript-sdk", "specification"]
return Completion(values=repos, hasMore=False)
return None
```
_Full example: [examples/snippets/servers/completion.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/completion.py)_
<!-- /snippet-source -->
### Elicitation
Request additional information from users during tool execution:
<!-- snippet-source examples/snippets/servers/elicitation.py -->
```python
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.elicitation import (
AcceptedElicitation,
DeclinedElicitation,
CancelledElicitation,
)
from pydantic import BaseModel, Field
mcp = FastMCP("Booking System")
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP(name="Elicitation Example")
class BookingPreferences(BaseModel):
"""Schema for collecting user preferences."""
checkAlternative: bool = Field(description="Would you like to check another date?")
alternativeDate: str = Field(
default="2024-12-26",
description="Alternative date (YYYY-MM-DD)",
)
@mcp.tool()
async def book_table(date: str, party_size: int, ctx: Context) -> str:
"""Book a table with confirmation"""
async def book_table(
date: str,
time: str,
party_size: int,
ctx: Context,
) -> str:
"""Book a table with date availability check."""
# Check if date is available
if date == "2024-12-25":
# Date unavailable - ask user for alternative
result = await ctx.elicit(
message=(f"No tables available for {party_size} on {date}. Would you like to try another date?"),
schema=BookingPreferences,
)
# Schema must only contain primitive types (str, int, float, bool)
class ConfirmBooking(BaseModel):
confirm: bool = Field(description="Confirm booking?")
notes: str = Field(default="", description="Special requests")
if result.action == "accept" and result.data:
if result.data.checkAlternative:
return f"[SUCCESS] Booked for {result.data.alternativeDate}"
return "[CANCELLED] No booking made"
return "[CANCELLED] Booking cancelled"
result = await ctx.elicit(
message=f"Confirm booking for {party_size} on {date}?", schema=ConfirmBooking
)
match result:
case AcceptedElicitation(data=data):
if data.confirm:
return f"Booked! Notes: {data.notes or 'None'}"
return "Booking cancelled"
case DeclinedElicitation():
return "Booking declined"
case CancelledElicitation():
return "Booking cancelled"
# Date available
return f"[SUCCESS] Booked for {date} at {time}"
```
_Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/elicitation.py)_
<!-- /snippet-source -->
The `elicit()` method returns an `ElicitationResult` with:
- `action`: "accept", "decline", or "cancel"
- `data`: The validated response (only when accepted)
- `validation_error`: Any validation error message
### Sampling
Tools can interact with LLMs through sampling (generating text):
<!-- snippet-source examples/snippets/servers/sampling.py -->
```python
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import SamplingMessage, TextContent
mcp = FastMCP(name="Sampling Example")
@mcp.tool()
async def generate_poem(topic: str, ctx: Context) -> str:
"""Generate a poem using LLM sampling."""
prompt = f"Write a short poem about {topic}"
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
if result.content.type == "text":
return result.content.text
return str(result.content)
```
_Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/sampling.py)_
<!-- /snippet-source -->
### Logging and Notifications
Tools can send logs and notifications through the context:
<!-- snippet-source examples/snippets/servers/notifications.py -->
```python
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP(name="Notifications Example")
@mcp.tool()
async def process_data(data: str, ctx: Context) -> str:
"""Process data with logging."""
# Different log levels
await ctx.debug(f"Debug: Processing '{data}'")
await ctx.info("Info: Starting processing")
await ctx.warning("Warning: This is experimental")
await ctx.error("Error: (This is just a demo)")
# Notify about resource changes
await ctx.session.send_resource_list_changed()
return f"Processed: {data}"
```
_Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/notifications.py)_
<!-- /snippet-source -->
### Authentication
Authentication can be used by servers that want to expose tools accessing protected resources.
@ -664,8 +785,9 @@ from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="EchoServer", stateless_http=True)
@mcp.tool(description="A simple echo tool")
@mcp.tool()
def echo(message: str) -> str:
"""A simple echo tool"""
return f"Echo: {message}"
```
@ -676,8 +798,9 @@ from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="MathServer", stateless_http=True)
@mcp.tool(description="A simple add tool")
@mcp.tool()
def add_two(n: int) -> int:
"""Tool to add two to the input"""
return n + 2
```

View file

@ -20,6 +20,6 @@ def desktop() -> list[str]:
@mcp.tool()
def add(a: int, b: int) -> int:
def sum(a: int, b: int) -> int:
"""Add two numbers"""
return a + b

View file

@ -6,7 +6,7 @@ mcp = FastMCP("Demo")
# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
def sum(a: int, b: int) -> int:
"""Add two numbers"""
return a + b

View file

@ -90,7 +90,7 @@ def get_weather_alerts(region: str) -> list[WeatherAlert]:
WeatherAlert(
severity="high",
title="Heat Wave Warning",
description="Temperatures expected to exceed 40°C",
description="Temperatures expected to exceed 40 degrees",
affected_areas=["Los Angeles", "San Diego", "Riverside"],
valid_until=datetime(2024, 7, 15, 18, 0),
),

View file

@ -0,0 +1,18 @@
[project]
name = "mcp-snippets"
version = "0.1.0"
description = "MCP Example Snippets"
requires-python = ">=3.10"
dependencies = [
"mcp",
]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["servers"]
[project.scripts]
server = "servers:run_server"

View file

@ -0,0 +1,36 @@
"""MCP Snippets.
This package contains simple examples of MCP server features.
Each server demonstrates a single feature and can be run as a standalone server.
To run a server, use the command:
uv run server basic_tool sse
"""
import importlib
import sys
from typing import Literal, cast
def run_server():
"""Run a server by name with optional transport.
Usage: server <server-name> [transport]
Example: server basic_tool sse
"""
if len(sys.argv) < 2:
print("Usage: server <server-name> [transport]")
print("Available servers: basic_tool, basic_resource, basic_prompt, tool_progress,")
print(" sampling, elicitation, completion, notifications")
print("Available transports: stdio (default), sse, streamable-http")
sys.exit(1)
server_name = sys.argv[1]
transport = sys.argv[2] if len(sys.argv) > 2 else "stdio"
try:
module = importlib.import_module(f".{server_name}", package=__name__)
module.mcp.run(cast(Literal["stdio", "sse", "streamable-http"], transport))
except ImportError:
print(f"Error: Server '{server_name}' not found")
sys.exit(1)

View file

@ -0,0 +1,18 @@
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
mcp = FastMCP(name="Prompt Example")
@mcp.prompt(title="Code Review")
def review_code(code: str) -> str:
return f"Please review this code:\n\n{code}"
@mcp.prompt(title="Debug Assistant")
def debug_error(error: str) -> list[base.Message]:
return [
base.UserMessage("I'm seeing this error:"),
base.UserMessage(error),
base.AssistantMessage("I'll help debug that. What have you tried so far?"),
]

View file

@ -0,0 +1,20 @@
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="Resource Example")
@mcp.resource("file://documents/{name}")
def read_document(name: str) -> str:
"""Read a document by name."""
# This would normally read from disk
return f"Content of {name}"
@mcp.resource("config://settings")
def get_settings() -> str:
"""Get application settings."""
return """{
"theme": "dark",
"language": "en",
"debug": false
}"""

View file

@ -0,0 +1,16 @@
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="Tool Example")
@mcp.tool()
def sum(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get weather for a city."""
# This would normally call a weather API
return f"Weather in {city}: 22degrees{unit[0].upper()}"

View file

@ -0,0 +1,49 @@
from mcp.server.fastmcp import FastMCP
from mcp.types import (
Completion,
CompletionArgument,
CompletionContext,
PromptReference,
ResourceTemplateReference,
)
mcp = FastMCP(name="Example")
@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
"""GitHub repository resource."""
return f"Repository: {owner}/{repo}"
@mcp.prompt(description="Code review prompt")
def review_code(language: str, code: str) -> str:
"""Generate a code review."""
return f"Review this {language} code:\n{code}"
@mcp.completion()
async def handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
"""Provide completions for prompts and resources."""
# Complete programming languages for the prompt
if isinstance(ref, PromptReference):
if ref.name == "review_code" and argument.name == "language":
languages = ["python", "javascript", "typescript", "go", "rust"]
return Completion(
values=[lang for lang in languages if lang.startswith(argument.value)],
hasMore=False,
)
# Complete repository names for GitHub resources
if isinstance(ref, ResourceTemplateReference):
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
if context and context.arguments and context.arguments.get("owner") == "modelcontextprotocol":
repos = ["python-sdk", "typescript-sdk", "specification"]
return Completion(values=repos, hasMore=False)
return None

View file

@ -0,0 +1,41 @@
from pydantic import BaseModel, Field
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP(name="Elicitation Example")
class BookingPreferences(BaseModel):
"""Schema for collecting user preferences."""
checkAlternative: bool = Field(description="Would you like to check another date?")
alternativeDate: str = Field(
default="2024-12-26",
description="Alternative date (YYYY-MM-DD)",
)
@mcp.tool()
async def book_table(
date: str,
time: str,
party_size: int,
ctx: Context,
) -> str:
"""Book a table with date availability check."""
# Check if date is available
if date == "2024-12-25":
# Date unavailable - ask user for alternative
result = await ctx.elicit(
message=(f"No tables available for {party_size} on {date}. Would you like to try another date?"),
schema=BookingPreferences,
)
if result.action == "accept" and result.data:
if result.data.checkAlternative:
return f"[SUCCESS] Booked for {result.data.alternativeDate}"
return "[CANCELLED] No booking made"
return "[CANCELLED] Booking cancelled"
# Date available
return f"[SUCCESS] Booked for {date} at {time}"

View file

@ -0,0 +1,18 @@
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP(name="Notifications Example")
@mcp.tool()
async def process_data(data: str, ctx: Context) -> str:
"""Process data with logging."""
# Different log levels
await ctx.debug(f"Debug: Processing '{data}'")
await ctx.info("Info: Starting processing")
await ctx.warning("Warning: This is experimental")
await ctx.error("Error: (This is just a demo)")
# Notify about resource changes
await ctx.session.send_resource_list_changed()
return f"Processed: {data}"

View file

@ -0,0 +1,24 @@
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import SamplingMessage, TextContent
mcp = FastMCP(name="Sampling Example")
@mcp.tool()
async def generate_poem(topic: str, ctx: Context) -> str:
"""Generate a poem using LLM sampling."""
prompt = f"Write a short poem about {topic}"
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
if result.content.type == "text":
return result.content.text
return str(result.content)

View file

@ -0,0 +1,20 @@
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP(name="Progress Example")
@mcp.tool()
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
"""Execute a task with progress updates."""
await ctx.info(f"Starting: {task_name}")
for i in range(steps):
progress = (i + 1) / steps
await ctx.report_progress(
progress=progress,
total=1.0,
message=f"Step {i + 1}/{steps}",
)
await ctx.debug(f"Completed step {i + 1}")
return f"Task '{task_name}' completed"

View file

@ -99,13 +99,14 @@ ignore = ["PERF203"]
[tool.ruff]
line-length = 120
target-version = "py310"
extend-exclude = ["README.md"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/server/fastmcp/test_func_metadata.py" = ["E501"]
[tool.uv.workspace]
members = ["examples/servers/*"]
members = ["examples/servers/*", "examples/snippets"]
[tool.uv.sources]
mcp = { workspace = true }

158
scripts/update_readme_snippets.py Executable file
View file

@ -0,0 +1,158 @@
#!/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, 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
file = Path(file_path)
if not file.exists():
print(f"Warning: File not found: {file_path}")
return full_match
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 = []
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(readme_path: Path = Path("README.md"), check_mode: bool = False) -> bool:
"""Update code snippets in README.md with live code from source files.
Args:
readme_path: Path to the README file
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
"""
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"
)
parser.add_argument("--readme", default="README.md", help="Path to README file (default: README.md)")
args = parser.parse_args()
success = update_readme_snippets(Path(args.readme), check_mode=args.check)
if not success:
sys.exit(1)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more