Skip to content

Commit 51d76ff

Browse files
authored
Add --workers option controlling parallel stub generation (#110)
Add the new experimental `--workers` option to `docstub run` to generate stub files in parallel. This can reduce runtime significantly for projects with many files. For now, multiprocessing has to be explicitly enabled by requesting a number of workers. Also ensure that docstub still returns a non-zero exit code when it is run with output completely disabled (`-qq`). The same applies when running docstub with `--quiet --fail-on-warnings`. -- * Draft multiprocessing with PoolExecutor * Count errors in dedicated handler Splitting these two responsibilities (logging to console and counting) simplifies both in context of multiprocessing. * Update how stats are propagated back to the main process * Add critical method to ContextReporter * Use "--workers" instead of "--jobs" The former is what scikit-image agreed on. * Test error and warning propagation with -qq Mark these as "slow" (because they are) * Add reminders to remove vendored stdlib code eventually * Make docstest independent of available CPUs * Add tests for `guess_concurrency_params` * Fix swallowing exit code for diffs * Improve exit function of LoggingProcessExecutor * Fix stubtest errors * Update example_pkg-stubs * Sync _concurrency.pyi * Mark --worker option as experimental and default to single process This is probably safest for now. I don't want to break peoples setup the new version of docstub suddenly hangs or fails. This way they users have to opt in explicitly and will now what might have caused it. * Fix typo in docstest caused by accidental refactor * Test multiprocessing in CI * Factor CI multiprocessing step into its own step * Fix test setup for monkeypatching process_cpu_count * Skip subprocess tests on Linux 3.12-3.13 * Start logging queue is in same multiprocessing context as pool This fixes the following: RuntimeError: A SemLock created in a fork context is being shared with a process in a spawn context. This is not supported. Please use the same context to create multiprocessing objects and Process. * Fix unkown "Queue" in doctype
1 parent 00a52a1 commit 51d76ff

File tree

22 files changed

+799
-95
lines changed

22 files changed

+799
-95
lines changed

.github/scripts/assert-unchanged.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ UNTRACKED=$(git ls-files --others --exclude-standard "$CHECK_DIR")
1515
echo "$UNTRACKED" | xargs -I _ git --no-pager diff /dev/null _ || true
1616

1717
# Display changes in tracked files and capture non-zero exit code if so
18-
git diff --exit-code HEAD "$CHECK_DIR" || true
18+
set +e
19+
git diff --exit-code HEAD "$CHECK_DIR"
1920
GIT_DIFF_HEAD_EXIT_CODE=$?
21+
set -e
2022

2123
# Display changes in tracked files and capture exit status
2224
if [ $GIT_DIFF_HEAD_EXIT_CODE -ne 0 ] || [ -n "$UNTRACKED" ]; then

.github/workflows/ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,21 @@ jobs:
8989
examples/example_pkg
9090
.github/scripts/assert-unchanged.sh examples/
9191
92-
- name: Check docstub-stubs
92+
- name: Check docstub-stubs (single process)
9393
# Check that stubs for docstub are up-to-date by regenerating them
9494
# with docstub and looking for differences.
9595
run: |
9696
rm -rf src/docstub-stubs
9797
python -m docstub run -v src/docstub -o src/docstub-stubs
9898
.github/scripts/assert-unchanged.sh src/docstub-stubs/
9999
100+
- name: Check docstub-stubs (multiprocess)
101+
# Repeat test with multiprocessing enabled
102+
run: |
103+
rm -rf src/docstub-stubs
104+
python -m docstub run -v src/docstub -o src/docstub-stubs --workers 2
105+
.github/scripts/assert-unchanged.sh src/docstub-stubs/
106+
100107
- name: Check with mypy.stubtest
101108
run: |
102109
python -m mypy.stubtest \

REMINDERS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Reminders
2+
3+
## With Python >=3.13
4+
5+
Remove vendored `glob_translate` in `docstub._vendored.stdlib`.
6+
7+
8+
## With Python >=3.14
9+
10+
Remove vendored `ProcessPoolExecutor` in `docstub._vendored.stdlib`.

docs/command_line.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ Options:
4949
Set output directory explicitly. Stubs will be directly written into
5050
that directory while preserving the directory structure under
5151
PACKAGE_PATH. Otherwise, stubs are generated inplace.
52-
--config PATH
53-
Set one or more configuration file(s) explicitly. Otherwise, it will
54-
look for a `pyproject.toml` or `docstub.toml` in the current
55-
directory.
5652
--ignore GLOB
5753
Ignore files matching this glob-style pattern. Can be used multiple
5854
times.
@@ -67,8 +63,15 @@ Options:
6763
-W, --fail-on-warning
6864
Return non-zero exit code when a warning is raised. Will add to
6965
--allow-errors.
66+
--workers INT
67+
Experimental: Process files in parallel with the desired number of
68+
workers. By default, no multiprocessing is used. [default: 1]
7069
--no-cache
7170
Ignore pre-existing cache and don't create a new one.
71+
--config PATH
72+
Set one or more configuration file(s) explicitly. Otherwise, it will
73+
look for a `pyproject.toml` or `docstub.toml` in the current
74+
directory.
7275
-v, --verbose
7376
Print more details. Use once to show information messages. Use -vv to
7477
print debug messages.

examples/example_pkg-stubs/_basic.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ __all__ = [
1717
"func_empty",
1818
]
1919

20-
def func_empty(a1: Incomplete, a2: Incomplete, a3) -> None: ...
20+
def func_empty(a1: Incomplete, a2: Incomplete, a3: Incomplete) -> None: ...
2121
def func_contains(
2222
a1: list[float],
2323
a2: dict[str, Union[int, str]],

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ xfail_strict = true
124124
filterwarnings = ["error"]
125125
log_cli_level = "info"
126126
testpaths = ["src", "tests"]
127+
markers = [
128+
"slow: marks tests as slow (deselect with `-m 'not slow'`)",
129+
]
127130

128131

129132
[tool.coverage]

src/docstub-stubs/_cli.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ from _typeshed import Incomplete
1616
from ._analysis import PyImport, TypeCollector, TypeMatcher, common_known_types
1717
from ._cache import CACHE_DIR_NAME, FileCache, validate_cache
1818
from ._cli_help import HelpFormatter
19+
from ._concurrency import LoggingProcessExecutor, guess_concurrency_params
1920
from ._config import Config
2021
from ._path_utils import (
2122
STUB_HEADER_COMMENT,
@@ -25,6 +26,7 @@ from ._path_utils import (
2526
)
2627
from ._report import setup_logging
2728
from ._stubs import Py2StubTransformer, try_format_stub
29+
from ._utils import update_with_add_values
2830
from ._version import __version__
2931

3032
logger: logging.Logger
@@ -45,6 +47,9 @@ click.Context.formatter_class = HelpFormatter
4547
@click.group()
4648
def cli() -> None: ...
4749
def _add_verbosity_options(func: Callable) -> Callable: ...
50+
def _transform_to_stub(
51+
task: tuple[Path, Path, Py2StubTransformer],
52+
) -> dict[str, int | list[str]]: ...
4853
@cli.command()
4954
def run(
5055
*,
@@ -55,6 +60,7 @@ def run(
5560
group_errors: bool,
5661
allow_errors: int,
5762
fail_on_warning: bool,
63+
desired_worker_count: int,
5864
no_cache: bool,
5965
verbose: int,
6066
quiet: int,

src/docstub-stubs/_concurrency.pyi

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# File generated with docstub
2+
3+
import logging
4+
import logging.handlers
5+
import math
6+
import multiprocessing
7+
import os
8+
from collections.abc import Callable, Iterator
9+
from concurrent.futures import Executor
10+
from dataclasses import dataclass
11+
from types import TracebackType
12+
from typing import Any
13+
14+
from ._vendored.stdlib import ProcessPoolExecutor
15+
16+
logger: logging.Logger
17+
18+
class MockPoolExecutor(Executor):
19+
def map[T](
20+
self, fn: Callable[..., T], *iterables: Any, **__: Any
21+
) -> Iterator[T]: ...
22+
23+
@dataclass(kw_only=True)
24+
class LoggingProcessExecutor:
25+
26+
max_workers: int | None = ...
27+
logging_handlers: tuple[logging.Handler, ...] = ...
28+
initializer: Callable | None = ...
29+
initargs: tuple | None = ...
30+
31+
@staticmethod
32+
def _initialize_worker(
33+
queue: multiprocessing.Queue,
34+
worker_log_level: int,
35+
initializer: Callable,
36+
initargs: tuple[Any],
37+
) -> None: ...
38+
def __enter__(self) -> ProcessPoolExecutor | MockPoolExecutor: ...
39+
def __exit__(
40+
self,
41+
exc_type: type[BaseException] | None,
42+
exc_val: BaseException | None,
43+
exc_tb: TracebackType,
44+
) -> bool: ...
45+
46+
def guess_concurrency_params(
47+
*, task_count: int, desired_worker_count: int | None = ...
48+
) -> tuple[int, int]: ...

src/docstub-stubs/_report.pyi

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class ContextReporter:
4747
def error(
4848
self, short: str, *args: Any, details: str | None = ..., **log_kw: Any
4949
) -> None: ...
50+
def critical(
51+
self, short: str, *args: Any, details: str | None = ..., **log_kw: Any
52+
) -> None: ...
5053
def __post_init__(self) -> None: ...
5154
@staticmethod
5255
def underline(line: str, *, char: str = ...) -> str: ...
@@ -62,9 +65,17 @@ class ReportHandler(logging.StreamHandler):
6265
self, stream: TextIO | None = ..., group_errors: bool = ...
6366
) -> None: ...
6467
def format(self, record: logging.LogRecord) -> str: ...
65-
def emit(self, record: logging.LogRecord) -> None: ...
68+
def handle(self, record: logging.LogRecord) -> bool: ...
6669
def emit_grouped(self) -> None: ...
6770

71+
class LogCounter(logging.NullHandler):
72+
critical_count: int
73+
error_count: int
74+
warning_count: int
75+
76+
def __init__(self) -> None: ...
77+
def handle(self, record: logging.LogRecord) -> bool: ...
78+
6879
def setup_logging(
6980
*, verbosity: Literal[-2, -1, 0, 1, 2, 3], group_errors: bool
70-
) -> ReportHandler: ...
81+
) -> tuple[ReportHandler, LogCounter]: ...

src/docstub-stubs/_stubs.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ from ._docstrings import (
1919
FallbackAnnotation,
2020
)
2121
from ._report import ContextReporter
22-
from ._utils import module_name_from_path
22+
from ._utils import module_name_from_path, update_with_add_values
2323

2424
logger: logging.Logger
2525

@@ -73,6 +73,9 @@ class Py2StubTransformer(cst.CSTTransformer):
7373
@property
7474
def is_inside_function_def(self) -> bool: ...
7575
def python_to_stub(self, source: str, *, module_path: Path | None = ...) -> str: ...
76+
def collect_stats(
77+
self, *, reset_after: bool = ...
78+
) -> dict[str, int | list[str]]: ...
7679
def visit_ClassDef(self, node: cst.ClassDef) -> Literal[True]: ...
7780
def leave_ClassDef(
7881
self, original_node: cst.ClassDef, updated_node: cst.ClassDef

0 commit comments

Comments
 (0)