Skip to content

Commit d1d09e4

Browse files
authored
ci: Replace redbaron with griffe in docstring scripts (#678)
## Summary - Replace `redbaron` with `griffe` for parsing Python source in `check_docstrings.py` and `fix_docstrings.py`. - Use `ast` module for precise docstring source locations in the fix script. - Remove all RedBaron bug workarounds (indentation, whitespace, except/finally formatting). - Remove `redbaron` from dev dependencies. - Complete rewrite of these scripts. ## Reasoning - `redbaron` is more than 6 years non-maintained ## Test plan - [x] Checked the scripts manually - [x] CI passes
1 parent 6185d55 commit d1d09e4

File tree

13 files changed

+297
-295
lines changed

13 files changed

+297
-295
lines changed

.github/workflows/_check_docstrings.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ jobs:
3636
run: uv run poe install-dev
3737

3838
- name: Async docstrings check
39-
run: uv run poe check-async-docstrings
39+
run: uv run poe check-docstrings

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ repos:
1515

1616
- id: docstrings-check
1717
name: Docstrings check
18-
entry: uv run poe check-async-docstrings
18+
entry: uv run poe check-docstrings
1919
language: system
2020
pass_filenames: false

.rules.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ uv run poe lint # Ruff format check + ruff check
1717
uv run poe format # Auto-fix lint issues and format
1818
uv run poe type-check # Run ty type checker
1919
uv run poe unit-tests # Run unit tests
20-
uv run poe check-async-docstrings # Verify async docstrings match sync
21-
uv run poe fix-async-docstrings # Auto-fix async docstrings
20+
uv run poe check-docstrings # Verify async docstrings match sync
21+
uv run poe fix-docstrings # Auto-fix async docstrings
2222

2323
# Run a single test
2424
uv run pytest tests/unit/test_file.py
@@ -50,7 +50,7 @@ Each resource client can create child clients (e.g., `ActorClient.builds()` →
5050

5151
Every resource client has both sync and async variants in the **same file**. For example, `actor.py` contains both `ActorClient` and `ActorClientAsync`. Both share the same interface — async versions use `async/await`.
5252

53-
Docstrings are written on sync clients and **automatically copied** to async clients via `scripts/check_async_docstrings.py`. When modifying docstrings, edit only the sync client and run `uv run poe fix-async-docstrings`.
53+
Docstrings are written on sync clients and **automatically copied** to async clients via `scripts/check_docstrings.py`. When modifying docstrings, edit only the sync client and run `uv run poe fix-docstrings`.
5454

5555
### Dependency Injection via ClientRegistry
5656

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ All tasks are defined in `pyproject.toml` under `[tool.poe.tasks]` and can be ru
2424
| `unit-tests-cov` | Run unit tests with coverage |
2525
| `integration-tests` | Run integration tests |
2626
| `integration-tests-cov` | Run integration tests with coverage |
27-
| `check-async-docstrings` | Check async client docstrings |
28-
| `fix-async-docstrings` | Fix async client docstrings |
27+
| `check-docstrings` | Check async client docstrings |
28+
| `fix-docstrings` | Fix async client docstrings |
2929
| `build-docs` | Build documentation website |
3030
| `run-docs` | Run documentation website locally |
3131
| `build` | Build package |

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ dev = [
4848
"black>=24.3.0",
4949
"datamodel-code-generator[http,ruff]<1.0.0",
5050
"dycw-pytest-only<3.0.0",
51-
"griffe",
51+
"griffe<3.0.0",
5252
"poethepoet<1.0.0",
5353
"pre-commit<5.0.0",
5454
"pydoc-markdown<5.0.0",
@@ -58,7 +58,6 @@ dev = [
5858
"pytest-timeout<3.0.0",
5959
"pytest-xdist<4.0.0",
6060
"pytest<9.0.0",
61-
"redbaron<1.0.0",
6261
"ruff~=0.15.0",
6362
"setuptools", # setuptools are used by pytest but not explicitly required
6463
"types-colorama<0.5.0",
@@ -240,9 +239,9 @@ unit-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/unit
240239
unit-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_client --cov-report=xml:coverage-unit.xml tests/unit"
241240
integration-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/integration"
242241
integration-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_client --cov-report=xml:coverage-integration.xml tests/integration"
243-
check-async-docstrings = "uv run python scripts/check_async_docstrings.py"
244-
fix-async-docstrings = "uv run python scripts/fix_async_docstrings.py"
245-
check-code = ["lint", "type-check", "unit-tests", "check-async-docstrings"]
242+
check-docstrings = "uv run python -m scripts.check_docstrings"
243+
fix-docstrings = "uv run python -m scripts.fix_docstrings"
244+
check-code = ["lint", "type-check", "check-docstrings", "unit-tests"]
246245

247246
[tool.poe.tasks.install-dev]
248247
shell = "uv sync --all-extras && uv run pre-commit install"

scripts/__init__.py

Whitespace-only changes.

scripts/_utils.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
from typing import TYPE_CHECKING
6+
7+
from griffe import Module, load
8+
9+
if TYPE_CHECKING:
10+
from collections.abc import Generator
11+
12+
from griffe import Class, Function
13+
14+
SKIPPED_METHODS = {
15+
'with_custom_http_client',
16+
}
17+
"""Methods where the async and sync docstrings are intentionally different."""
18+
19+
SRC_PATH = Path(__file__).resolve().parent.parent / 'src'
20+
"""Path to the source code of the apify_client package."""
21+
22+
_SUBSTITUTIONS = [
23+
(re.compile(r'Client'), 'ClientAsync'),
24+
(re.compile(r'\bsynchronously\b'), 'asynchronously'),
25+
(re.compile(r'\bSynchronously\b'), 'Asynchronously'),
26+
(re.compile(r'\bsynchronous\b'), 'asynchronous'),
27+
(re.compile(r'\bSynchronous\b'), 'Asynchronous'),
28+
(re.compile(r'Retry a function'), 'Retry an async function'),
29+
(re.compile(r'Function to retry'), 'Async function to retry'),
30+
]
31+
"""Patterns for converting sync docstrings to async docstrings."""
32+
33+
34+
def load_package() -> Module:
35+
"""Load the apify_client package using griffe."""
36+
package = load('apify_client', search_paths=[str(SRC_PATH)])
37+
if not isinstance(package, Module):
38+
raise TypeError('Expected griffe to load a Module')
39+
return package
40+
41+
42+
def walk_modules(module: Module) -> Generator[Module]:
43+
"""Recursively yield all modules in the package."""
44+
yield module
45+
for submodule in module.modules.values():
46+
yield from walk_modules(submodule)
47+
48+
49+
def iter_docstring_mismatches(package: Module) -> Generator[tuple[Class, Function, Class, Function, str, bool]]:
50+
"""Yield docstring mismatches between sync and async client methods.
51+
52+
Yields (async_class, async_method, sync_class, sync_method, expected_docstring, has_existing).
53+
"""
54+
for module in walk_modules(package):
55+
for async_class in module.classes.values():
56+
if not async_class.name.endswith('ClientAsync'):
57+
continue
58+
59+
sync_class = module.classes.get(async_class.name.replace('ClientAsync', 'Client'))
60+
if not sync_class:
61+
continue
62+
63+
for async_method in async_class.functions.values():
64+
if any(str(d.value) == 'ignore_docs' for d in async_method.decorators):
65+
continue
66+
67+
if async_method.name in SKIPPED_METHODS:
68+
continue
69+
70+
sync_method = sync_class.functions.get(async_method.name)
71+
if not sync_method or not sync_method.docstring:
72+
continue
73+
74+
expected_docstring = _sync_to_async_docstring(sync_method.docstring.value)
75+
76+
if not async_method.docstring:
77+
yield async_class, async_method, sync_class, sync_method, expected_docstring, False
78+
elif async_method.docstring.value != expected_docstring:
79+
yield async_class, async_method, sync_class, sync_method, expected_docstring, True
80+
81+
82+
def _sync_to_async_docstring(docstring: str) -> str:
83+
"""Convert a docstring from a sync component version into a docstring for its async analogue."""
84+
result = docstring
85+
for pattern, replacement in _SUBSTITUTIONS:
86+
result = pattern.sub(replacement, result)
87+
return result

scripts/check_async_docstrings.py

Lines changed: 0 additions & 72 deletions
This file was deleted.

scripts/check_docstrings.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Check that async client docstrings are in sync with their sync counterparts."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
from ._utils import iter_docstring_mismatches, load_package
8+
9+
10+
def main() -> None:
11+
"""Check all async client methods for docstring mismatches."""
12+
package = load_package()
13+
found_issues = False
14+
15+
for (
16+
async_class,
17+
async_method,
18+
sync_class,
19+
sync_method,
20+
_expected_docstring,
21+
has_existing,
22+
) in iter_docstring_mismatches(package):
23+
found_issues = True
24+
if has_existing:
25+
print(
26+
f'Docstring mismatch: "{async_class.name}.{async_method.name}"'
27+
f' vs "{sync_class.name}.{sync_method.name}"'
28+
)
29+
else:
30+
print(f'Missing docstring for "{async_class.name}.{async_method.name}"!')
31+
32+
if found_issues:
33+
print()
34+
print('Issues with async docstrings found. Fix them by running `uv run poe fix-docstrings`.')
35+
sys.exit(1)
36+
else:
37+
print('Success: async method docstrings are in sync with sync method docstrings.')
38+
39+
40+
if __name__ == '__main__':
41+
main()

scripts/fix_async_docstrings.py

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)