|
| 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 sync_to_async_docstring(docstring: str) -> str: |
| 50 | + """Convert a docstring from a sync component version into a docstring for its async analogue.""" |
| 51 | + result = docstring |
| 52 | + for pattern, replacement in _SUBSTITUTIONS: |
| 53 | + result = pattern.sub(replacement, result) |
| 54 | + return result |
| 55 | + |
| 56 | + |
| 57 | +def iter_docstring_mismatches(package: Module) -> Generator[tuple[Class, Function, Class, Function, str, bool]]: |
| 58 | + """Yield docstring mismatches between sync and async client methods. |
| 59 | +
|
| 60 | + Yields (async_class, async_method, sync_class, sync_method, expected_docstring, has_existing). |
| 61 | + """ |
| 62 | + for module in walk_modules(package): |
| 63 | + for async_class in module.classes.values(): |
| 64 | + if not async_class.name.endswith('ClientAsync'): |
| 65 | + continue |
| 66 | + |
| 67 | + sync_class = module.classes.get(async_class.name.replace('ClientAsync', 'Client')) |
| 68 | + if not sync_class: |
| 69 | + continue |
| 70 | + |
| 71 | + for async_method in async_class.functions.values(): |
| 72 | + if any(str(d.value) == 'ignore_docs' for d in async_method.decorators): |
| 73 | + continue |
| 74 | + |
| 75 | + if async_method.name in SKIPPED_METHODS: |
| 76 | + continue |
| 77 | + |
| 78 | + sync_method = sync_class.functions.get(async_method.name) |
| 79 | + if not sync_method or not sync_method.docstring: |
| 80 | + continue |
| 81 | + |
| 82 | + expected_docstring = sync_to_async_docstring(sync_method.docstring.value) |
| 83 | + |
| 84 | + if not async_method.docstring: |
| 85 | + yield async_class, async_method, sync_class, sync_method, expected_docstring, False |
| 86 | + elif async_method.docstring.value != expected_docstring: |
| 87 | + yield async_class, async_method, sync_class, sync_method, expected_docstring, True |
0 commit comments