Skip to content

Commit 90e7d6e

Browse files
authored
Include array shape in generated annotations (#132)
This bundles a few things that I should have probably committed separately while iterating on them: 1. Closes #76. Shape information in doctypes is now translated into annotations. This even goes for shapes with optional dimensions – which use expansion like in the plugin example to create a union of arrays – one for each supported dimensionality. Shapes with "..." are treated as unbounded dimensionality and translated to `tuple[Any, ...]` like [NumPy v2.5 suggests](https://numpy.org/doc/stable/reference/typing.html#api). Empty shapes are supported too. 3. Dtype unions are now also expanded into a union of arrays each with one of the dtypes. This is because I'm not sure if `np.dtype` actually supports unions. I can't find examples of that in NumPy's repo. For now, expansion should be on the safe side. 4. Closes #125. Shape and dtype are now stripped from *ArrayLikes* which don't support them as their are a union that can't be subscribed. This means, *ArrayLikes* are a really generic broad type. 5. Cleans up some issues with the grammar: the literal and subscriptions rules have some necessary overlap. The subscription rule should now win unless the expression contains an actual literal type. Also some refactoring, and especially new stuff like `expand_expression`, `Expr.{find_all|try_find_one|find_one}` enable all this.
1 parent d61ea7b commit 90e7d6e

13 files changed

Lines changed: 593 additions & 363 deletions

File tree

‎docs/conf.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444

4545
intersphinx_mapping = {
4646
"python": ("https://docs.python.org/3/", None),
47+
"numpy": ("https://numpy.org/doc/stable/", None),
4748
"typing": ("https://typing.python.org/en/latest/", None),
4849
"lark": ("https://lark-parser.readthedocs.io/en/latest/", None),
4950
"libcst": ("https://libcst.readthedocs.io/en/latest/", None),

‎docs/create_plugin.md‎

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ from docstub.doctype import DoctypeTransformer, Expr, Term
2626

2727
my_grammar = """
2828
%import doctype (start, WS_INLINE, ARRAY_NAME)
29-
3029
%extend ARRAY_NAME: "my_array"
31-
3230
%ignore WS_INLINE
3331
"""
3432

@@ -62,13 +60,11 @@ from itertools import product
6260

6361
import lark
6462
from docstub.plugins import hookimpl
65-
from docstub.doctype import DoctypeTransformer, Expr, Term
63+
from docstub.doctype import DoctypeTransformer, Expr, Term, expand_expression
6664

6765
my_grammar = """
6866
%import doctype (start, WS_INLINE, qualname)
69-
7067
%extend qualname: "{" qualname ("," qualname)+ "}" -> expansion
71-
7268
%ignore WS_INLINE
7369
"""
7470

@@ -86,59 +82,9 @@ class ExpansionTransformer(DoctypeTransformer):
8682
-------
8783
expr : Expr
8884
"""
89-
children = map(self._expand_expression, tree.children)
90-
return Expr(rule="start", children=tuple(children))
91-
92-
def expansion(self, tree):
93-
"""Transform "expansion" rule.
94-
95-
Actual expansion happens in `start` when the complete expression with
96-
all potential expansions can be seen.
97-
98-
Parameters
99-
----------
100-
tree : lark.Tree
101-
102-
Returns
103-
-------
104-
expr : Expr
105-
"""
106-
return Expr(rule="expansion", children=tuple(tree.children))
107-
108-
def _expand_expression(self, expr):
109-
"""Join combination of all expansions in a new union.
110-
111-
Parameters
112-
----------
113-
expr : Expr
114-
115-
Returns
116-
-------
117-
expanded : Expr
118-
"""
119-
expansions = expr.filter(rule="expansion")
120-
if not expansions:
121-
return expr
122-
123-
# Create Cartesian product of all expansions where each
124-
# value is prefixed by the expansion it will replace
125-
expansions = [
126-
[(expansion, value) for value in expansion.children]
127-
for expansion in expansions
128-
]
129-
combinations = list(product(*expansions))
130-
131-
# Create a new expression for each combination
132-
interpolated = []
133-
for combination in combinations:
134-
new_expr = expr
135-
for expansion, value in combination:
136-
new_expr = new_expr.replace(expansion, value)
137-
interpolated.append(new_expr)
138-
139-
# Join new expressions as union
140-
expanded = self.union(lark.Tree("union", children=interpolated))
141-
return expanded
85+
assert len(tree.children) == 1
86+
child = expand_expression(tree.children[0], expand_rule="expansion")
87+
return Expr(rule="start", children=(child,))
14288

14389

14490
@hookimpl

‎docs/introduction.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def example_metric(image, *, mask=None, sigma=1.0, method='standard'):
1919
----------
2020
image : array-like
2121
First image.
22-
mask : array of dtype uint8, optional
22+
mask : array of dtype uint8 and shape (M, N), optional
2323
Second image.
2424
sigma : float or Iterable of float, default: 1.0
2525
Sigma value for each dimension in `image`. A single value is broadcast
@@ -51,18 +51,18 @@ will create the [stub file](https://typing.python.org/en/latest/spec/distributin
5151
# File generated with docstub
5252
5353
from collections.abc import Iterable
54-
from typing import Literal
54+
from typing import Any, Literal
5555
5656
import numpy as np
5757
from numpy.typing import ArrayLike, NDArray
5858
5959
def example_metric(
6060
image: ArrayLike,
6161
*,
62-
mask: NDArray[np.uint8] | None = ...,
62+
mask: NDArray[tuple[int, int], np.dtype[np.uint8]] | None = ...,
6363
sigma: float | Iterable[float] = ...,
6464
method: Literal["standard", "modified"] = ...
65-
) -> NDArray[float]: ...
65+
) -> NDArray[tuple[Any, ...], np.dtype[float]]: ...
6666
```
6767

6868
<!--- end example.pyi --->

‎docs/typing_syntax.md‎

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
# Typing syntax in docstrings
1+
# Extended typing syntax
22

3-
Docstub defines its own [grammar](../src/docstub/doctype.lark) to parse and transform type information in docstrings into valid Python type expressions.
4-
This grammar fully supports [Python's conventional typing syntax](https://typing.python.org/en/latest/index.html).
5-
So any {term}`annotation expression` that is valid in Python, can be used in a docstrings as is.
6-
In addition, docstub extends this syntax with several "natural language" expressions that are commonly used in the scientific Python ecosystem.
3+
Any valid Python {term}`annotation expression` can be used in a docstrings as is.
4+
However, Docstub extends [Python's conventional typing syntax](https://typing.python.org/en/latest/index.html) with several "natural language" expressions that are commonly used in the scientific Python ecosystem.
5+
This extended syntax is defined in its own [grammar](#appendix-grammar).
76

87
Docstrings should follow a form that is inspired by the [NumPyDoc style](https://numpydoc.readthedocs.io/en/latest/format.html):
98
```none
@@ -28,83 +27,87 @@ Combining multiple names that share a doctype and description is supported.
2827

2928
In addition to Python's conventional shorthand `|` syntax for [union types](https://typing.python.org/en/latest/spec/concepts.html#union-types), you can use `or` to join types.
3029

31-
| Docstring type | Python type annotation |
32-
|----------------|------------------------|
33-
| `X or Y` | `X \| Y` |
34-
| `int or float` | `int \| float` |
30+
| doctype | Python annotation |
31+
|----------------|-------------------|
32+
| `X or Y` | `X \| Y` |
33+
| `int or float` | `int \| float` |
3534

3635

3736
## Containers
3837

3938
The content of containers can be typed using a `CONTAINER of X` like form.
4039
This extends the basic subscription syntax for [generics](https://typing.python.org/en/latest/spec/generics.html#generics).
4140

42-
| Docstring type | Python type annotation |
43-
|-------------------------|------------------------|
44-
| `CONTAINER of X` | `CONTAINER[X]` |
45-
| `CONTAINER of (X or Y)` | `CONTAINER[X \| Y]` |
41+
| doctype | Python annotation |
42+
|-------------------------|---------------------|
43+
| `CONTAINER of X` | `CONTAINER[X]` |
44+
| `CONTAINER of (X or Y)` | `CONTAINER[X \| Y]` |
4645

4746
For the simple case `CONTAINER of X`, where `X` is a name, you can append `(s)` to indicate the plural form.
4847
For example, `list of float(s)`.
4948

5049
Variants of for [**tuples**](https://typing.python.org/en/latest/spec/tuples.html)
5150

52-
| Docstring type | Python type annotation |
53-
|---------------------|------------------------|
54-
| `tuple of (X, Y)` | `tuple[X, Y]` |
55-
| `tuple of (X, ...)` | `tuple[X, ...]` |
51+
| doctype | Python annotation |
52+
|---------------------|-------------------|
53+
| `tuple of (X, Y)` | `tuple[X, Y]` |
54+
| `tuple of (X, ...)` | `tuple[X, ...]` |
5655

5756
and **mappings** exist.
5857

59-
| Docstring type | Python type annotation |
60-
|----------------------|------------------------|
61-
| `MAPPING of {X: Y}` | `MAPPING[X, Y]` |
62-
| `dict of {str: int}` | `dict[str, int]` |
58+
| doctype | Python annotation |
59+
|----------------------|-------------------|
60+
| `MAPPING of {X: Y}` | `MAPPING[X, Y]` |
61+
| `dict of {str: int}` | `dict[str, int]` |
6362

6463

6564
:::{tip}
66-
While it is possible to nest these variants repeatedly, it decreases the readability.
65+
While it is possible to nest these variants, it decreases the readability.
6766
For complex nested annotations with nested containers, consider using Python's conventional syntax.
6867
In the future, docstub may warn against or disallow nesting these natural language variants.
6968
:::
7069

7170

7271
(natlang-array)=
73-
## Shape and dtype syntax for arrays
72+
## NDArrays with shape and dtype
7473

75-
This expression allows adding shape and datatype information for data structures like [NumPy arrays](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html).
74+
This "natural language" expression describes shape and dtype information for [NumPy arrays](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html).
75+
The basic form is as follows:
7676

77-
`array` and `ndarray`, and `array-like` and `array_like` can be used interchange-ably for the variable `ARRAY` below.
77+
| doctype | Python annotation |
78+
|----------------------------------------|-------------------------------------------|
79+
| `ARRAY of shape SHAPE` | `ARRAY[SHAPE]` |
80+
| `ARRAY of dtype DTYPE` | `ARRAY[tuple[Any, ...], np.dtype[DTYPE]]` |
81+
| `ARRAY of dtype DTYPE and shape SHAPE` | `ARRAY[SHAPE, np.dtype[DTYPE]]` |
82+
| `ARRAY of shape SHAPE and dtype DTYPE` | `ARRAY[SHAPE, np.dtype[DTYPE]]` |
7883

79-
| Docstring type | Python type annotation |
80-
|----------------------------------------|------------------------|
81-
| `ARRAY of dtype DTYPE` | `ARRAY[DTYPE]` |
82-
| `ARRAY of dtype DTYPE and shape SHAPE` | `ARRAY[DTYPE]` |
83-
| `ARRAY of shape SHAPE` | `ARRAY[DTYPE]` |
84-
| `ARRAY of shape SHAPE and dtype DTYPE` | `ARRAY[DTYPE]` |
84+
Only `array`, `ndarray`, `array-like` and `array_like` can be used in place of `ARRAY` above.
85+
`array-like` and `array_like` map to {obj}`numpy.typing.ArrayLike` which does not support adding shape and dtype information.
86+
In this case, Docstub omits the dtype and shape automatically.
8587

86-
For example
88+
`SHAPE` can be given in forms like `(n,)`, `(M, N)`, `(x, y, 4)`, or `(..., 2)`.
89+
Optional dimensions are supported by enclosing them in square brackets like `(x, y[, z])`.
8790

88-
| Docstring type | Python type annotation |
89-
|------------------------------------------|------------------------|
90-
| `array of dtype int` | `ndarray[int]` |
91-
| `ndarray of dtype bool and shape (4, 4)` | `ndarray[bool]` |
92-
| `array-like of dtype float` | `ArrayLike[float]` |
93-
| `array_like of shape (M, 2)` | `ArrayLike` |
91+
`DTYPE` can be any valid name or a union of names.
9492

93+
For example:
9594

96-
:::{note}
97-
Noting the **shape** of an array in the docstring is supported.
98-
However, [support for including shapes in generated stubs](https://github.com/scientific-python/docstub/issues/76) is not yet included in docstub.
99-
:::
95+
| doctype | Python annotation |
96+
|------------------------------------------|--------------------------------------------|
97+
| `array of dtype int` | `NDArray[tuple[Any, ...], np.dtype[int]]` |
98+
| `array of shape (..., 2)` | `NDArray[tuple[Any, ...]]` |
99+
| `array of shape (x, y[, z])` | `NDArray[tuple[int, int]]`<br>`\| NDArray[tuple[int, int, int]]` |
100+
| `ndarray of dtype bool and shape (4, 4)` | `NDArray[tuple[int, int], np.dtype[bool]]` |
101+
| `array-like of dtype float` | `ArrayLike` |
102+
| `array_like of shape (M, 2)` | `ArrayLike` |
100103

101104

102105
## Literals
103106

104107
[Literals](https://typing.python.org/en/latest/spec/literal.html#literals) indicate a concrete value instead of type.
105108
Instead of using [`typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal), you can enclose literal values in `{...}` in docstrings.
106109

107-
| Docstring type | Python type annotation |
110+
| doctype | Python annotation |
108111
|---------------------------|----------------------------------|
109112
| `{-1, 0, 3, True, False}` | `Literal[-1, 0, 3, True, False]` |
110113
| `{"red", "blue", None}` | `Literal["red", "blue", None]` |
@@ -117,19 +120,27 @@ However, `Literal[X]` is more explicit.
117120
:::{warning}
118121
Python's `typing.Literal` only supports a restricted set of parameters.
119122
For example, `float` literals are not yet supported by the type system but are allowed by docstub.
120-
Addressing this use case is on the roadmap.
121-
See [issue 47](https://github.com/scientific-python/docstub/issues/47) for more details.
123+
Use a broader inlined type such as `Literal[0, 1] | float` if you want to use something like `{0, .5, 1}` in the docstring.
122124
:::
123125

124126
## reStructuredText role
125127

126128
Since docstrings are also used to generate documentation with Sphinx, you may want to use [restructuredText roles](https://docutils.sourceforge.io/docs/ref/rst/roles.html).
127-
This is supported anywhere in where a {term}`type name` is used in a {term}`doctype`.
128-
129-
| Docstring type | Python type annotation |
130-
|-------------------------|------------------------|
131-
| `` `X` `` | `X` |
132-
| ``:ref:`X` `` | `X` |
133-
| ``:class:`X`[Y, ...] `` | `X[Y, ...]` |
134-
| ``:class:`Y.X` `` | `Y.X` |
135-
| ``:py:class:`Y.X` `` | `Y.X` |
129+
This is supported anywhere in where a {term}`type name` used in a {term}`doctype`.
130+
131+
| doctype | Python annotation |
132+
|-------------------------|-------------------|
133+
| `` `X` `` | `X` |
134+
| ``:ref:`X` `` | `X` |
135+
| ``:class:`X`[Y, ...] `` | `X[Y, ...]` |
136+
| ``:class:`Y.X` `` | `Y.X` |
137+
| ``:py:class:`Y.X` `` | `Y.X` |
138+
139+
140+
---
141+
142+
## Appendix: Grammar
143+
144+
:::{literalinclude} ../src/docstub/doctype.lark
145+
:caption: doctype.lark
146+
:::
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# File generated with docstub
22

3+
from typing import Any
4+
35
import numpy
46
import numpy as np
57
from numpy.typing import ArrayLike, NDArray
@@ -8,8 +10,14 @@ def func_object_with_numpy_objects(
810
a1: numpy.int8, a2: np.int16, a3: numpy.typing.DTypeLike, a4: np.typing.DTypeLike
911
) -> None: ...
1012
def func_ndarray(
11-
a1: NDArray, a2: np.NDArray, a3: NDArray[float], a4: NDArray[np.uint8] | None = ...
12-
) -> tuple[NDArray[np.uint8], NDArray[complex]]: ...
13+
a1: NDArray,
14+
a2: np.NDArray,
15+
a3: NDArray[tuple[int, int], np.dtype[float]],
16+
a4: NDArray[tuple[int], np.dtype[np.uint8]] | None = ...,
17+
) -> tuple[
18+
NDArray[tuple[Any, ...], np.dtype[np.uint8]],
19+
NDArray[tuple[Any, ...], np.dtype[complex]],
20+
]: ...
1321
def func_array_like(
14-
a1: ArrayLike, a2: ArrayLike, a3: ArrayLike[float], a4: ArrayLike[np.uint8]
15-
) -> tuple[ArrayLike[np.uint8], ArrayLike[complex]]: ...
22+
a1: ArrayLike, a2: ArrayLike, a3: ArrayLike, a4: ArrayLike
23+
) -> tuple[ArrayLike, ArrayLike]: ...

‎src/docstub-stubs/_docstrings.pyi‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import numpydoc.docscrape as npds
1515
from ._analysis import PyImport, TypeMatcher
1616
from ._report import ContextReporter, Stats
1717
from ._utils import escape_qualname
18-
from .doctype import BlacklistedQualname, DoctypeTranspiler, TermKind
18+
from .doctype import DoctypeTranspiler, QualnameIsForbidden, TermKind
1919

2020
logger: logging.Logger
2121

0 commit comments

Comments
 (0)