Skip to content

Commit f505655

Browse files
committed
Support and default to inplace stub generation
1 parent 04b17b0 commit f505655

File tree

3 files changed

+56
-31
lines changed

3 files changed

+56
-31
lines changed

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ ignore = [
9090
"RET504", # Assignment before `return` statement facilitates debugging
9191
"PTH123", # Using builtin open() instead of Path.open() is fine
9292
"SIM108", # Terniary operator is always more readable
93+
"SIM103", # Don't recommend returning the condition directly
9394
]
9495

9596

src/docstub/_cli.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,17 @@
1515
from ._cache import FileCache
1616
from ._config import Config
1717
from ._stubs import (
18+
STUB_HEADER_COMMENT,
1819
Py2StubTransformer,
1920
try_format_stub,
20-
walk_source,
21+
walk_python_package,
2122
walk_source_and_targets,
2223
)
2324
from ._version import __version__
2425

2526
logger = logging.getLogger(__name__)
2627

2728

28-
STUB_HEADER_COMMENT = "# File generated with docstub"
29-
30-
3129
def _load_configuration(config_path=None):
3230
"""Load and merge configuration from CWD and optional files.
3331
@@ -96,7 +94,7 @@ def _build_import_map(config, source_dir):
9694
cache_dir=Path.cwd() / ".docstub_cache",
9795
name=f"{__version__}/collected_types",
9896
)
99-
for source_path in walk_source(source_dir):
97+
for source_path in walk_python_package(source_dir):
10098
logger.info("collecting types in %s", source_path)
10199
known_imports_in_source = collect_cached_types(source_path)
102100
known_imports.update(known_imports_in_source)
@@ -135,7 +133,7 @@ def report_execution_time():
135133
"-o",
136134
"--out-dir",
137135
type=click.Path(file_okay=False),
138-
help="Set output directory explicitly.",
136+
help="Set output directory explicitly. Otherwise, stubs are generated inplace.",
139137
)
140138
@click.option(
141139
"--config",
@@ -170,7 +168,7 @@ def main(source_dir, out_dir, config_path, verbose):
170168
)
171169

172170
if not out_dir:
173-
out_dir = source_dir.parent / (source_dir.name + "-stubs")
171+
out_dir = source_dir
174172
out_dir = Path(out_dir)
175173
out_dir.mkdir(parents=True, exist_ok=True)
176174

src/docstub/_stubs.py

+50-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
"""Transform Python source files to typed stub files."""
1+
"""Transform Python source files to typed stub files.
2+
3+
Attributes
4+
----------
5+
STUB_HEADER_COMMENT : Final[str]
6+
"""
27

38
import enum
49
import logging
10+
import re
511
from dataclasses import dataclass
612
from functools import wraps
713
from typing import ClassVar
@@ -16,7 +22,10 @@
1622
logger = logging.getLogger(__name__)
1723

1824

19-
def _is_python_package(path):
25+
STUB_HEADER_COMMENT = "# File generated with docstub"
26+
27+
28+
def is_python_package(path):
2029
"""
2130
Parameters
2231
----------
@@ -30,8 +39,31 @@ def _is_python_package(path):
3039
return is_package
3140

3241

33-
def walk_source(root_dir):
34-
"""Iterate modules in a Python package and its target stub files.
42+
def is_docstub_generated(path):
43+
"""Check if the stub file was generated by docstub.
44+
45+
Parameters
46+
----------
47+
path : Path
48+
49+
Returns
50+
-------
51+
is_generated : bool
52+
"""
53+
assert path.suffix == ".pyi"
54+
with path.open("r") as fo:
55+
content = fo.read()
56+
if re.match(f"^{re.escape(STUB_HEADER_COMMENT)}", content):
57+
return True
58+
return False
59+
60+
61+
def walk_python_package(root_dir):
62+
"""Iterate source files in a Python package.
63+
64+
Given a Python package, yield the path of contained Python modules. If an
65+
alternate stub file already exists and isn't generated by docstub, it is
66+
returned instead.
3567
3668
Parameters
3769
----------
@@ -43,26 +75,24 @@ def walk_source(root_dir):
4375
source_path : Path
4476
Either a Python file or a stub file that takes precedence.
4577
"""
46-
queue = [root_dir]
47-
while queue:
48-
path = queue.pop(0)
49-
78+
for path in root_dir.iterdir():
5079
if path.is_dir():
51-
if _is_python_package(path):
52-
queue.extend(path.iterdir())
80+
if is_python_package(path):
81+
yield from walk_python_package(path)
5382
else:
54-
logger.debug("skipping directory %s", path)
83+
logger.debug("skipping directory %s which isn't a Python package", path)
5584
continue
5685

5786
assert path.is_file()
58-
5987
suffix = path.suffix.lower()
60-
if suffix not in {".py", ".pyi"}:
61-
continue
62-
if suffix == ".py" and path.with_suffix(".pyi").exists():
63-
continue # Stub file already exists and takes precedence
6488

65-
yield path
89+
if suffix == ".py":
90+
stub = path.with_suffix(".pyi")
91+
if stub.exists() and not is_docstub_generated(stub):
92+
# Non-generated stub file already exists and takes precedence
93+
yield stub
94+
else:
95+
yield path
6696

6797

6898
def walk_source_and_targets(root_dir, target_dir):
@@ -75,18 +105,14 @@ def walk_source_and_targets(root_dir, target_dir):
75105
target_dir : Path
76106
Root directory in which a matching stub package will be created.
77107
78-
Returns
79-
-------
108+
Yields
109+
------
80110
source_path : Path
81111
Either a Python file or a stub file that takes precedence.
82112
stub_path : Path
83113
Target stub file.
84-
85-
Notes
86-
-----
87-
Files starting with "test_" are skipped entirely for now.
88114
"""
89-
for source_path in walk_source(root_dir):
115+
for source_path in walk_python_package(root_dir):
90116
stub_path = target_dir / source_path.with_suffix(".pyi").relative_to(root_dir)
91117
yield source_path, stub_path
92118

0 commit comments

Comments
 (0)