Skip to content

Commit fee233a

Browse files
committed
WIP
1 parent 8fb0b84 commit fee233a

9 files changed

Lines changed: 88 additions & 43 deletions

File tree

src/docstub-stubs/_app_generate_stubs.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from collections import Counter
66
from collections.abc import Iterable, Sequence
77
from contextlib import contextmanager
88
from pathlib import Path
9+
from typing import Literal
910

1011
from ._analysis import PyImport, TypeCollector, TypeMatcher, common_known_types
1112
from ._cache import CACHE_DIR_NAME, FileCache

src/docstub-stubs/_cli.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import logging
44
import sys
55
from collections.abc import Callable, Sequence
66
from pathlib import Path
7+
from typing import Literal
78

89
import click
910
from _typeshed import Incomplete

src/docstub-stubs/_docstrings.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import click
1010
import lark
1111
import lark.visitors
1212
import numpydoc.docscrape as npds
13+
from _typeshed import Incomplete as Expr
1314

1415
from ._analysis import PyImport, TypeMatcher
1516
from ._doctype import BlacklistedQualname, Expr, Term, TermKind, parse_doctype

src/docstub-stubs/_doctype.pyi

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ from collections.abc import Generator, Iterable, Sequence
77
from dataclasses import dataclass
88
from pathlib import Path
99
from textwrap import indent
10-
from typing import Any, Final
10+
from typing import Any, Final, Literal, Self
1111

1212
import lark
1313
import lark.visitors
1414
from _typeshed import Incomplete
15+
from _typeshed import Incomplete as Expression
1516

1617
from ._utils import DocstubError
1718

@@ -47,14 +48,17 @@ class Term(str):
4748
class Expr:
4849

4950
rule: str
50-
children: list[Expr | Term]
51+
children: list[Self | Term]
5152

5253
@property
5354
def terms(self) -> list[Term]: ...
5455
@property
5556
def names(self) -> list[Term]: ...
57+
@property
58+
def sub_expressions(self) -> list[Expression] | Literal[1]: ...
5659
def __iter__(self) -> Generator[Expr | Term]: ...
5760
def format_tree(self) -> str: ...
61+
def print_tree(self) -> None: ...
5862
def __repr__(self) -> str: ...
5963
def __str__(self) -> str: ...
6064
def as_code(self) -> str: ...
@@ -67,9 +71,14 @@ class BlacklistedQualname(DocstubError):
6771
class DoctypeTransformer(lark.visitors.Transformer):
6872
def start(self, tree: lark.Tree) -> Expr: ...
6973
def qualname(self, tree: lark.Tree) -> Term: ...
74+
def qualname(self, tree: lark.Tree) -> Term: ...
75+
def rst_role(self, tree: lark.Tree) -> Expr: ...
7076
def ELLIPSES(self, token: lark.Token) -> Term: ...
7177
def union(self, tree: lark.Tree) -> Expr: ...
7278
def subscription(self, tree: lark.Tree) -> Expr: ...
79+
def param_spec(self, tree: lark.Tree) -> Expr: ...
80+
def callable(self, tree: lark.Tree) -> Expr: ...
81+
def literal(self, tree: lark.Tree) -> Expr: ...
7382
def natlang_literal(self, tree: lark.Tree) -> Expr: ...
7483
def literal_item(self, tree: lark.Tree) -> Term: ...
7584
def natlang_container(self, tree: lark.Tree) -> Expr: ...
@@ -79,9 +88,7 @@ class DoctypeTransformer(lark.visitors.Transformer):
7988
def shape(self, tree: lark.Tree) -> lark.visitors._DiscardType: ...
8089
def optional_info(self, tree: lark.Tree) -> lark.visitors._DiscardType: ...
8190
def extra_info(self, tree: lark.Tree) -> lark.visitors._DiscardType: ...
82-
def _format_subscription(
83-
self, sequence: Sequence[str], rule: str = ...
84-
) -> Expr: ...
91+
def _format_subscription(self, sequence: Sequence[str], *, rule: str) -> Expr: ...
8592

8693
_transformer: Final
8794

src/docstub-stubs/_report.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import logging
55
from collections.abc import Hashable, Iterator, Mapping, Sequence
66
from pathlib import Path
77
from textwrap import indent
8-
from typing import Any, ClassVar, Self, TextIO
8+
from typing import Any, ClassVar, Literal, Self, TextIO
99

1010
import click
1111
from pre_commit.envcontext import UNSET

src/docstub/_doctype.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ def names(self):
146146
"""
147147
return [term for term in self.terms if term.kind == TermKind.NAME]
148148

149+
@property
150+
def sub_expressions(self):
151+
"""Iterate expressions inside the current one.
152+
153+
Returns
154+
-------
155+
names : list of Expr or {1}
156+
"""
157+
cls = type(self)
158+
for child in self.children:
159+
if isinstance(child, cls):
160+
yield child
161+
yield from child.sub_expressions
162+
149163
def __iter__(self):
150164
"""Iterate over children of this expression.
151165
@@ -253,6 +267,21 @@ def qualname(self, tree):
253267
)
254268
return _qualname
255269

270+
def rst_role(self, tree):
271+
"""
272+
Parameters
273+
----------
274+
tree : lark.Tree
275+
276+
Returns
277+
-------
278+
out : Expr
279+
"""
280+
# Drop rst_prefix
281+
children = [c for c in tree.children if isinstance(c, Term)]
282+
expr = Expr(rule="rst_role", children=children)
283+
return expr
284+
256285
def ELLIPSES(self, token):
257286
"""
258287
Parameters
@@ -332,11 +361,7 @@ def literal(self, tree):
332361
-------
333362
out : Expr
334363
"""
335-
items = [
336-
Term("Literal", kind=TermKind.NAME),
337-
*tree.children,
338-
]
339-
out = self._format_subscription(items, rule="literal")
364+
out = self._format_subscription(tree.children, rule="literal")
340365
return out
341366

342367
def natlang_literal(self, tree):

src/docstub/doctype.lark

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ _type: qualname
3434
// [1] https://docutils.sourceforge.io/docs/ref/rst/roles.html
3535
//
3636
qualname: (/~/ ".")? (NAME ".")* NAME
37-
| (":" (NAME ":")? NAME ":")? "`" qualname "`"
37+
| (":" (NAME ":")? NAME ":")? "`" qualname "`" -> rst_role
3838

3939

4040
// An union of different types, joined either by "or" or "|".
@@ -73,7 +73,7 @@ natlang_literal: "{" literal_item ("," literal_item)* "}"
7373
// isn't allowed to contain "literal_items", so we need to define this.
7474
// Assign a higher priority so that things like `Literal[Some.ENUM]` is marked
7575
// as a literal expression.
76-
literal.1: "Literal[" literal_item ("," literal_item)* "]"
76+
literal.1: qualname "[" literal_item ("," literal_item)* "]"
7777

7878

7979
// An single item in a literal expression (or `optional`). We must also allow

tests/test_docstrings.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from docstub._analysis import PyImport
66
from docstub._docstrings import (
77
Annotation,
8+
doctype_to_annotation,
89
DocstringAnnotations,
910
)
1011

@@ -35,6 +36,37 @@ def test_unexpected_value(self):
3536
Annotation(value="~.foo")
3637

3738

39+
class Test_doctype_to_annotation:
40+
41+
def test_unknown_name(self):
42+
# Simple unknown name is aliased to typing.Any
43+
annotation = doctype_to_annotation("a")
44+
assert annotation.value == "a"
45+
assert annotation.imports == {
46+
PyImport(import_="Incomplete", from_="_typeshed", as_="a")
47+
}
48+
assert unknown_names == [("a", 0, 1)]
49+
50+
def test_unknown_qualname(self):
51+
# Unknown qualified name is escaped and aliased to typing.Any as well
52+
annotation = doctype_to_annotation("a.b")
53+
assert annotation.value == "a_b"
54+
assert annotation.imports == {
55+
PyImport(import_="Incomplete", from_="_typeshed", as_="a_b")
56+
}
57+
assert unknown_names == [("a.b", 0, 3)]
58+
59+
def test_multiple_unknown_names(self):
60+
# Multiple names are aliased to typing.Any
61+
annotation = doctype_to_annotation("a.b of c")
62+
assert annotation.value == "a_b[c]"
63+
assert annotation.imports == {
64+
PyImport(import_="Incomplete", from_="_typeshed", as_="a_b"),
65+
PyImport(import_="Incomplete", from_="_typeshed", as_="c"),
66+
}
67+
assert unknown_names == [("a.b", 0, 3), ("c", 7, 8)]
68+
69+
3870
class Test_DocstringAnnotations:
3971
def test_empty_docstring(self):
4072
docstring = dedent("""No sections in this docstring.""")
@@ -247,4 +279,4 @@ def test_combined_numpydoc_params(self):
247279
assert annotations.parameters["c"].value == "bool"
248280

249281
assert "d" not in annotations.parameters
250-
assert "e" not in annotations.parameters
282+
assert "e" not in annotations.parameters

tests/test_doctype.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,15 @@ def test_subscription_error(self, doctype):
130130
"Literal[SomeEnum.FIRST, 3]",
131131
# Nesting
132132
"dict[Literal['a', 'b'], int]",
133+
# Custom qualname for literal
134+
"MyLiteral[0]",
135+
"MyLiteral[SomeEnum.FIRST]",
133136
],
134137
)
135138
def test_literals(self, doctype):
136139
expr = parse_doctype(doctype)
137140
assert expr.as_code() == doctype
141+
assert "literal" in [e.rule for e in expr.sub_expressions]
138142

139143
@pytest.mark.parametrize(
140144
("doctype", "expected"),
@@ -165,6 +169,7 @@ def test_literals(self, doctype):
165169
def test_natlang_literals(self, doctype, expected):
166170
expr = parse_doctype(doctype)
167171
assert expr.as_code() == expected
172+
assert "natlang_literal" in [e.rule for e in expr.sub_expressions]
168173

169174
def test_single_natlang_literal_warning(self, caplog):
170175
expr = parse_doctype("{True}")
@@ -220,6 +225,7 @@ def test_optional_info(self, doctype, expected, optional_info):
220225
def test_callable(self, doctype):
221226
expr = parse_doctype(doctype)
222227
assert expr.as_code() == doctype
228+
assert "callable" in [e.rule for e in expr.sub_expressions]
223229

224230
@pytest.mark.parametrize(
225231
"doctype",
@@ -240,7 +246,7 @@ def test_callable_error(self, doctype):
240246
(":class:`Generator`", "Generator"),
241247
(":py:class:`Generator`", "Generator"),
242248
(":py:class:`Generator`[int]", "Generator[int]"),
243-
(":py:ref:`~.Foo`[int]", "_Foo[int]"),
249+
(":py:ref:`~.Foo`[int]", "~.Foo[int]"),
244250
("list[:py:class:`Generator`]", "list[Generator]"),
245251
],
246252
)
@@ -284,31 +290,3 @@ def test_natlang_array_invalid_shape(self, shape):
284290
doctype = f"array of shape {shape}"
285291
with pytest.raises(lark.exceptions.UnexpectedInput):
286292
_ = parse_doctype(doctype)
287-
288-
def test_unknown_name(self):
289-
# Simple unknown name is aliased to typing.Any
290-
annotation, unknown_names = parse_doctype("a")
291-
assert annotation.value == "a"
292-
assert annotation.imports == {
293-
PyImport(import_="Incomplete", from_="_typeshed", as_="a")
294-
}
295-
assert unknown_names == [("a", 0, 1)]
296-
297-
def test_unknown_qualname(self):
298-
# Unknown qualified name is escaped and aliased to typing.Any as well
299-
annotation, unknown_names = parse_doctype("a.b")
300-
assert annotation.value == "a_b"
301-
assert annotation.imports == {
302-
PyImport(import_="Incomplete", from_="_typeshed", as_="a_b")
303-
}
304-
assert unknown_names == [("a.b", 0, 3)]
305-
306-
def test_multiple_unknown_names(self):
307-
# Multiple names are aliased to typing.Any
308-
annotation, unknown_names = parse_doctype("a.b of c")
309-
assert annotation.value == "a_b[c]"
310-
assert annotation.imports == {
311-
PyImport(import_="Incomplete", from_="_typeshed", as_="a_b"),
312-
PyImport(import_="Incomplete", from_="_typeshed", as_="c"),
313-
}
314-
assert unknown_names == [("a.b", 0, 3), ("c", 7, 8)]

0 commit comments

Comments
 (0)