Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/python_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,17 @@ jobs:
# Job-specific step(s):
- name: Check public API imports
run: uv run pytest tests/unit_tests/test_public_api_imports.py -m linting

docstring-markdown-check:
name: Python Docstring Format Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Install uv
uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7
with:
version: "latest"

- name: Check docstring formatting
run: ./scripts/check_docstring_markdown.uv
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Use **Markdown** formatting in all docstrings — not reStructuredText (reST).
- Use single backticks for inline code: `` `MyClass` ``, not double backticks (` ``MyClass`` `).
- Reference methods as `` `get_column_info()` ``, not `:meth:\`get_column_info\``.
- Use standard Markdown for emphasis, lists, and links.
- The `Python Docstring Format Check` CI job validates this style; run `poe lint-docstrings` locally or invoke the standalone `./scripts/check_docstring_markdown.uv` script with only uv installed.

## MCP Tools Are a Presentation Layer

Expand Down
8 changes: 4 additions & 4 deletions airbyte/_util/api_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3582,10 +3582,10 @@ def replace_connection_state(
perform per-stream deduplication or merging on the backend. Callers that need
to update a single stream must first fetch the current state, merge the desired
stream change into the full state object, and then send the complete state back.
See ``CloudConnection.set_stream_state()`` for this fetch-modify-push pattern.
See `CloudConnection.set_stream_state()` for this fetch-modify-push pattern.

The provided ``connection_id`` is injected into both the outer request
wrapper and the inner ``connection_state`` payload to ensure consistency.
The provided `connection_id` is injected into both the outer request
wrapper and the inner `connection_state` payload to ensure consistency.

Args:
connection_id: The connection ID to update state for.
Expand Down Expand Up @@ -3685,7 +3685,7 @@ def replace_connection_catalog(

Args:
connection_id: The connection ID to update catalog for.
configured_catalog_dict: The configured catalog dict (``{"streams": [...]}``) to set.
configured_catalog_dict: The configured catalog dict (`{"streams": [...]}`) to set.
api_root: The API root URL.
client_id: OAuth client ID.
client_secret: OAuth client secret.
Expand Down
10 changes: 5 additions & 5 deletions airbyte/cli/smoke_test_source/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
schema variations, and batch size variations.

Predefined scenarios are always available. Additional scenarios can be
injected dynamically via the ``custom_scenarios`` config field.
injected dynamically via the `custom_scenarios` config field.

.. warning::
This module is experimental and subject to change without notice.
Expand Down Expand Up @@ -75,7 +75,7 @@ class SourceSmokeTest(Source):

Generates synthetic data across predefined scenarios that cover
common destination failure patterns. Supports dynamic injection
of additional scenarios via the ``custom_scenarios`` config field.
of additional scenarios via the `custom_scenarios` config field.
"""

def spec(
Expand Down Expand Up @@ -197,11 +197,11 @@ def _get_all_scenarios(
"""Combine predefined and custom scenarios.

Selection logic:
1. Boolean flags control groups: ``all_fast_streams``
1. Boolean flags control groups: `all_fast_streams`
(default true) enables non-high-volume scenarios,
``all_slow_streams`` (default false) enables
`all_slow_streams` (default false) enables
high-volume scenarios.
2. ``scenario_filter`` names are unioned with the boolean sets.
2. `scenario_filter` names are unioned with the boolean sets.
3. Custom scenarios are always included.
4. The final list is deduplicated by name.
"""
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ coverage-report = { shell = "coverage report" }
coverage-html = { shell = "coverage html -d htmlcov && open htmlcov/index.html" }
coverage-reset = { shell = "coverage erase" }

check = { shell = "ruff check . && pyrefly check && pytest --collect-only -qq" }
check = { shell = "ruff check . && pyrefly check && pytest --collect-only -qq && ./scripts/check_docstring_markdown.uv" }
lint-docstrings = { cmd = "./scripts/check_docstring_markdown.uv", help = "Fail on reStructuredText syntax in docstrings (pdoc renders Markdown)." }

docs-generate = {env = {PDOC_ALLOW_EXEC = "1"}, cmd = "python -m docs.generate run"}
docs-preview = {shell = "poe docs-generate && open docs/generated/index.html"}
Expand Down
114 changes: 114 additions & 0 deletions scripts/check_docstring_markdown.uv
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env -S uv run --script --python 3.13
# /// script
# requires-python = ">=3.11,<3.14"
# dependencies = []
# ///
"""Fail on reStructuredText syntax in docstrings.

pdoc renders docstrings as Markdown, so RST reaches readers verbatim: ``Foo``
shows up with literal backticks and `:meth:` roles as literal text.

Only `*.py` files are checked, so standalone `*.uv` scripts -- this one
included -- are exempt and may use RST syntax in their own docstrings.

Standalone: uv resolves the interpreter from the metadata below and no
dependencies beyond the standard library are needed, so this runs without the
project environment.

```bash
./scripts/check_docstring_markdown.uv # defaults to _DEFAULT_PACKAGE
./scripts/check_docstring_markdown.uv my_package/errors.py
uv run scripts/check_docstring_markdown.uv # same, without the executable bit
```

Exits 1 with a `file:line: message` report per violation.
"""

from __future__ import annotations

import ast
import re
import sys
from pathlib import Path

# Package scanned when no path argument is given, relative to the project root
# (this script's parent directory's parent). Adapt per repo.
_DEFAULT_PACKAGE = "airbyte"

# Double backticks, excluding a run of three or more (fence delimiters).
_DOUBLE_BACKTICK = re.compile(r"(?<!`)``(?!`)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Code examples falsely fail docstring checks

When a fenced Markdown example contains ``, _DOUBLE_BACKTICK flags it as invalid inline markup. Valid examples then fail CI despite containing no RST formatting.

Learn more

_DOUBLE_BACKTICK matches pairs anywhere in a docstring. Markdown code fences can contain arbitrary source code, including doubled backticks, without using reStructuredText formatting. The checker already permits three-backtick delimiters, but still scans the example inside them.

Example: A docstring with a fenced Python block containing print('``') reports a double-backtick violation and exits 1, although the fenced block is valid Markdown.

Recommended fix: Exclude fenced code blocks before searching for inline RST syntax. Cover both backtick and tilde Markdown fences in tests.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid edge case, but nothing in the codebase triggers it. The checker passes on release-candidate/v1, so no current docstring has a fenced example containing double backticks. This script is a verbatim copy of the ai-skills checker (only _DEFAULT_PACKAGE and the hint path differ). Skipping fenced blocks should therefore go into ai-skills first and then be re-copied here, so the two copies don't drift. Not opening a follow-up for now. If a real docstring ever hits this, the CI failure will name the file and line.

Written by Devin

_RST_ROLE = re.compile(r":(?:meth|func|class|mod|attr|obj|exc|data|ref|doc):`")
_RST_FIELD = re.compile(r"^\s*:(?:param|parameter|arg|argument|type|returns?|rtype|raises|yields?|ivar|cvar|vartype)\b", re.MULTILINE)

_CHECKS = (
(_DOUBLE_BACKTICK, "double backticks -- use single backticks for inline code"),
(_RST_ROLE, "reStructuredText role -- use a plain backticked reference"),
(_RST_FIELD, "reStructuredText field list -- use a Google-style `Args:` / `Returns:` / `Raises:` section"),
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.


def _docstrings(tree: ast.Module) -> list[ast.Constant]:
"""Every docstring node in *tree*, module and definitions alike."""
nodes: list[ast.Constant] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
continue
body = node.body
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str):
nodes.append(body[0].value)
Comment thread
aaronsteers marked this conversation as resolved.
return nodes


def _offset_to_line(text: str, offset: int, start_line: int) -> int:
return start_line + text.count("\n", 0, offset)


def check_file(path: Path) -> list[str]:
"""Violations in *path*, formatted as `file:line: message`."""
source = path.read_text(encoding="utf-8")
try:
tree = ast.parse(source, filename=str(path))
except SyntaxError as exc:
return [f"{path}:{exc.lineno or 0}: could not parse file ({exc.msg})"]

seen: set[tuple[int, str]] = set()
for node in _docstrings(tree):
# Constant.lineno is the opening quote; the body starts on that line.
for pattern, message in _CHECKS:
for match in pattern.finditer(node.value):
# One report per line and rule: a backtick pair matches twice.
seen.add((_offset_to_line(node.value, match.start(), node.lineno), message))
return [f"{path}:{line}: {message}" for line, message in sorted(seen)]


def iter_python_files(targets: list[Path]) -> list[Path]:
files: list[Path] = []
for target in targets:
if target.is_dir():
files.extend(sorted(p for p in target.rglob("*.py") if "generated" not in p.parts))
elif target.suffix == ".py":
files.append(target)
return files


def main(argv: list[str]) -> int:
root = Path(__file__).resolve().parent.parent
targets = [Path(arg) for arg in argv] or [root / _DEFAULT_PACKAGE]
violations = [v for path in iter_python_files(targets) for v in check_file(path)]

if violations:
print(f"Found {len(violations)} reStructuredText docstring violation(s):", file=sys.stderr)
for violation in violations:
print(violation, file=sys.stderr)
print(
"\nDocstrings are rendered as Markdown by pdoc. See CONTRIBUTING.md.",
file=sys.stderr,
)
return 1

print("✓ no reStructuredText syntax found in docstrings")
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Loading