Skip to content

Commit 6cad9c5

Browse files
authored
Merge pull request #25 from scientific-python/use-mypy (Use mypy.stubtest in CI)
Use mypy.stubtest in CI
2 parents 06d730d + da99630 commit 6cad9c5

17 files changed

+543
-102
lines changed

.github/workflows/ci.yml

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ env:
1515
# Many color libraries just need this to be set to any value, but at least
1616
# one distinguishes color depth, where "3" -> "256-bit color".
1717
FORCE_COLOR: 3
18+
MYPYPATH: ${{ github.workspace }}/stubs
19+
20+
defaults:
21+
run:
22+
# Make sure that bash specific stuff works on Windows
23+
shell: bash
1824

1925
jobs:
2026
lint:
@@ -69,4 +75,8 @@ jobs:
6975
7076
- name: Generate docstub stubs
7177
run: |
72-
python -m docstub -v src/docstub
78+
python -m docstub -v src/docstub -o ${MYPYPATH}/docstub
79+
80+
- name: Check docstub stubs with mypy
81+
run: |
82+
python -m mypy.stubtest --allowlist stubtest_allow.txt docstub

examples/example_pkg-stubs/__init__.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated with docstub. Manual edits will be overwritten!
1+
# File generated with docstub
22

33
import _numpy as np_
44
from _basic import func_contains

examples/example_pkg-stubs/_basic.pyi

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Generated with docstub. Manual edits will be overwritten!
1+
# File generated with docstub
2+
23
import configparser
34
import logging
45
from collections.abc import Sequence

examples/example_pkg-stubs/_numpy.pyi

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Generated with docstub. Manual edits will be overwritten!
1+
# File generated with docstub
2+
23
import numpy as np
34
from numpy.typing import ArrayLike, NDArray
45

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dev = [
4545
test = [
4646
"pytest >=5.0.0",
4747
"pytest-cov >= 5.0.0",
48+
"mypy",
4849
]
4950

5051
[project.urls]
@@ -80,6 +81,7 @@ extend-select = [
8081
"UP", # pyupgrade
8182
"YTT", # flake8-2020
8283
"EXE", # flake8-executable
84+
# "PYI", # flake8-pyi
8385
]
8486
ignore = [
8587
"PLR09", # Too many <...>

src/docstub/_analysis.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,12 @@ class KnownImport:
7171
<KnownImport 'from numpy import uint8 as ui8'>
7272
"""
7373

74-
import_name: str = None
75-
import_path: str = None
76-
import_alias: str = None
77-
builtin_name: str = None
74+
# docstub: off
75+
import_name: str | None = None
76+
import_path: str | None = None
77+
import_alias: str | None = None
78+
builtin_name: str | None = None
79+
# docstub: on
7880

7981
@classmethod
8082
@cache
@@ -194,15 +196,15 @@ def __post_init__(self):
194196
elif self.import_name is None:
195197
raise ValueError("non builtin must at least define an `import_name`")
196198

197-
def __repr__(self):
199+
def __repr__(self) -> str:
198200
if self.builtin_name:
199201
info = f"{self.target} (builtin)"
200202
else:
201203
info = f"{self.format_import()!r}"
202204
out = f"<{type(self).__name__} {info}>"
203205
return out
204206

205-
def __str__(self):
207+
def __str__(self) -> str:
206208
out = self.format_import()
207209
return out
208210

@@ -406,7 +408,10 @@ class TypesDatabase:
406408
407409
Attributes
408410
----------
409-
current_source : ~.PackageFile | None
411+
current_source : Path | None
412+
source_pkgs : list[Path]
413+
known_imports: dict[str, KnownImport]
414+
stats : dict[str, Any]
410415
411416
Examples
412417
--------
@@ -427,11 +432,13 @@ def __init__(
427432
----------
428433
source_pkgs: list[Path], optional
429434
known_imports: dict[str, KnownImport], optional
435+
If not provided, defaults to imports returned by
436+
:func:`common_known_imports`.
430437
"""
431438
if source_pkgs is None:
432439
source_pkgs = []
433440
if known_imports is None:
434-
known_imports = {}
441+
known_imports = common_known_imports()
435442

436443
self.current_source = None
437444
self.source_pkgs = source_pkgs
@@ -524,6 +531,6 @@ def query(self, search_name):
524531

525532
return annotation_name, known_import
526533

527-
def __repr__(self):
534+
def __repr__(self) -> str:
528535
repr = f"{type(self).__name__}({self.source_pkgs})"
529536
return repr

src/docstub/_cache.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def create_cache(path):
5454

5555
gitignore_path = path / ".gitignore"
5656
gitignore_content = (
57-
"# This file is a cache directory tag automatically created by docstub.\n" "*\n"
57+
"# This file is a cache directory automatically created by docstub.\n" "*\n"
5858
)
5959
if not gitignore_path.is_file():
6060
with open(gitignore_path, "w") as fp:

src/docstub/_cli.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@
1414
)
1515
from ._cache import FileCache
1616
from ._config import Config
17-
from ._stubs import Py2StubTransformer, walk_source, walk_source_and_targets
17+
from ._stubs import (
18+
Py2StubTransformer,
19+
try_format_stub,
20+
walk_source,
21+
walk_source_and_targets,
22+
)
1823
from ._version import __version__
1924

2025
logger = logging.getLogger(__name__)
2126

2227

28+
STUB_HEADER_COMMENT = "# File generated with docstub"
29+
30+
2331
def _load_configuration(config_path=None):
2432
"""Load and merge configuration from CWD and optional files.
2533
@@ -139,6 +147,14 @@ def report_execution_time():
139147
@click.help_option("-h", "--help")
140148
@report_execution_time()
141149
def main(source_dir, out_dir, config_path, verbose):
150+
"""
151+
Parameters
152+
----------
153+
source_dir : Path
154+
out_dir : Path
155+
config_path : Path
156+
verbose : str
157+
"""
142158
_setup_logging(verbose=verbose)
143159

144160
source_dir = Path(source_dir)
@@ -171,6 +187,8 @@ def main(source_dir, out_dir, config_path, verbose):
171187
stub_content = stub_transformer.python_to_stub(
172188
py_content, module_path=source_path
173189
)
190+
stub_content = f"{STUB_HEADER_COMMENT}\n\n{stub_content}"
191+
stub_content = try_format_stub(stub_content)
174192
except (SystemExit, KeyboardInterrupt):
175193
raise
176194
except Exception as e:
@@ -185,14 +203,14 @@ def main(source_dir, out_dir, config_path, verbose):
185203
successful_queries = types_db.stats["successful_queries"]
186204
click.secho(f"{successful_queries} matched annotations", fg="green")
187205

188-
grammar_errors = stub_transformer.transformer.stats["grammar_errors"]
189-
if grammar_errors:
190-
click.secho(f"{grammar_errors} grammar violations", fg="red")
206+
grammar_error_count = stub_transformer.transformer.stats["grammar_errors"]
207+
if grammar_error_count:
208+
click.secho(f"{grammar_error_count} grammar violations", fg="red")
191209

192210
unknown_doctypes = types_db.stats["unknown_doctypes"]
193211
if unknown_doctypes:
194212
click.secho(f"{len(unknown_doctypes)} unknown doctypes:", fg="red")
195213
click.echo(" " + "\n ".join(set(unknown_doctypes)))
196214

197-
if unknown_doctypes or grammar_errors:
215+
if unknown_doctypes or grammar_error_count:
198216
sys.exit(1)

src/docstub/_config.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@ class Config:
1818
_source: tuple[Path, ...] = ()
1919

2020
@classmethod
21-
def from_toml(cls, path: Path | str) -> "Config":
22-
"""Return configuration options in local TOML file if they exist."""
21+
def from_toml(cls, path):
22+
"""Return configuration options in local TOML file if they exist.
23+
24+
Parameters
25+
----------
26+
path : Path or str
27+
28+
Returns
29+
-------
30+
config : Self
31+
"""
2332
path = Path(path)
2433
with open(path, "rb") as fp:
2534
raw = tomllib.load(fp)
@@ -29,11 +38,26 @@ def from_toml(cls, path: Path | str) -> "Config":
2938

3039
@classmethod
3140
def from_default(cls):
41+
"""Create a configuration with default values.
42+
43+
Returns
44+
-------
45+
config : Self
46+
"""
3247
config = cls.from_toml(cls.DEFAULT_CONFIG_PATH)
3348
return config
3449

3550
def merge(self, other):
36-
"""Merge contents with other and return a new Config instance."""
51+
"""Merge contents with other and return a new Config instance.
52+
53+
Parameters
54+
----------
55+
other : Self
56+
57+
Returns
58+
-------
59+
merged : Self
60+
"""
3761
if not isinstance(other, type(self)):
3862
return NotImplemented
3963
new = Config(
@@ -56,7 +80,7 @@ def __post_init__(self):
5680
if not isinstance(self.replace_doctypes, dict):
5781
raise TypeError("replace_doctypes must be a string")
5882

59-
def __repr__(self):
83+
def __repr__(self) -> str:
6084
sources = " | ".join(str(s) for s in self._source)
6185
formatted = f"<{type(self).__name__}: {sources}>"
6286
return formatted

src/docstub/_docstrings.py

+19-10
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import click
1111
import lark
1212
import lark.visitors
13-
from numpydoc.docscrape import NumpyDocString
13+
from numpydoc.docscrape import NumpyDocString # type: ignore[import-untyped]
1414

15-
from ._analysis import KnownImport
15+
from ._analysis import KnownImport, TypesDatabase
1616
from ._utils import ContextFormatter, DocstubError, accumulate_qualname, escape_qualname
1717

1818
logger = logging.getLogger(__name__)
@@ -150,7 +150,10 @@ class DoctypeTransformer(lark.visitors.Transformer):
150150
151151
Attributes
152152
----------
153-
blacklisted_qualnames : frozenset[str]
153+
types_db : ~.TypesDatabase
154+
replace_doctypes : dict[str, str]
155+
stats : dict[str, Any]
156+
blacklisted_qualnames : ClassVar[frozenset[str]]
154157
All Python keywords [1]_ are blacklisted from use in qualnames except for ``True``
155158
``False`` and ``None``.
156159
@@ -161,11 +164,13 @@ class DoctypeTransformer(lark.visitors.Transformer):
161164
Examples
162165
--------
163166
>>> transformer = DoctypeTransformer()
164-
>>> annotation, unknown_names = transformer.doctype_to_annotation("tuple of int")
167+
>>> annotation, unknown_names = transformer.doctype_to_annotation(
168+
... "tuple of (int or ndarray)"
169+
... )
165170
>>> annotation.value
166-
'tuple[int]'
171+
'tuple[int | ndarray]'
167172
>>> unknown_names
168-
[('tuple', 0, 5), ('int', 9, 12)]
173+
[('ndarray', 17, 24)]
169174
"""
170175

171176
blacklisted_qualnames = frozenset(
@@ -209,15 +214,19 @@ def __init__(self, *, types_db=None, replace_doctypes=None, **kwargs):
209214
"""
210215
Parameters
211216
----------
212-
types_db : ~.TypesDatabase
213-
A static database of collected types usable as an annotation.
217+
types_db : ~.TypesDatabase, optional
218+
A static database of collected types usable as an annotation. If
219+
not given, defaults to a database with common types from the
220+
standard library (see :func:`~.common_known_imports`).
214221
replace_doctypes : dict[str, str], optional
215222
Replacements for human-friendly aliases.
216223
kwargs : dict[Any, Any], optional
217224
Keyword arguments passed to the init of the parent class.
218225
"""
219226
if replace_doctypes is None:
220227
replace_doctypes = {}
228+
if types_db is None:
229+
types_db = TypesDatabase()
221230

222231
self.types_db = types_db
223232
self.replace_doctypes = replace_doctypes
@@ -272,14 +281,14 @@ def __default__(self, data, children, meta):
272281
----------
273282
data : lark.Token
274283
The rule-token of the current node.
275-
children : list[lark.Token, ...]
284+
children : list[lark.Token]
276285
The children of the current node.
277286
meta : lark.tree.Meta
278287
Meta information for the current node.
279288
280289
Returns
281290
-------
282-
out : lark.Token or list[lark.Token, ...]
291+
out : lark.Token or list[lark.Token]
283292
Either a token or list of tokens.
284293
"""
285294
if isinstance(children, list) and len(children) == 1:

0 commit comments

Comments
 (0)