Skip to content

Commit

Permalink
Add support for Python type parameter lists
Browse files Browse the repository at this point in the history
Sphinx has partial support for type parameter lists: they are supported
by the Python domain in signatures, but are not supported by autodoc.

This adds the following support:

- Sphinx Python domain for type parameter fields in docstrings, with
  sphinx.ext.napoleon support as well.

- Support for type parameters as Sphinx objects, with cross-linking, like
  the existing support for function parameters as Sphinx objects.

- Support in apigen for PEP 695 type parameters, and for displaying
  pre-PEP 695 separately-defined TypeVar types as PEP 695 type parameters.
  • Loading branch information
jbms committed Jul 10, 2024
1 parent d50317c commit 4d8f8d5
Show file tree
Hide file tree
Showing 11 changed files with 857 additions and 79 deletions.
14 changes: 11 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ def get_colors(color_t: str):
unique_colors.append(m.group(1))
return unique_colors


# jinja contexts
example_python_apigen_modules = {
"my_module": "my_api/",
Expand All @@ -373,8 +374,12 @@ def get_colors(color_t: str):
(
full_name,
overload_id,
python_apigen._get_docname(example_python_apigen_modules, full_name, overload_id, False),
python_apigen._get_docname(example_python_apigen_modules, full_name, overload_id, True),
python_apigen._get_docname(
example_python_apigen_modules, full_name, overload_id, False
),
python_apigen._get_docname(
example_python_apigen_modules, full_name, overload_id, True
),
)
for full_name, overload_id in example_python_apigen_objects
],
Expand All @@ -401,7 +406,10 @@ def get_colors(color_t: str):
.. highlight:: json
"""

python_apigen_modules = {"tensorstore_demo": "python_apigen_generated/"}
python_apigen_modules = {
"tensorstore_demo": "python_apigen_generated/",
"type_param_demo": "python_apigen_generated/",
}

python_apigen_default_groups = [
("class:.*", "Classes"),
Expand Down
5 changes: 5 additions & 0 deletions docs/python_apigen_demo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ Some other group
----------------

.. python-apigen-group:: some-other-group

Type parameter demo
-------------------

.. python-apigen-group:: type-param
8 changes: 4 additions & 4 deletions docs/tensorstore_demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,8 @@ def __getitem__(self, *args, **kwargs) -> None:
})
More generally, specifying an ``n``-dimensional `bool` array is equivalent to
specifying ``n`` 1-dimensional index arrays, where the ``i``\ th index array specifies
the ``i``\ th coordinate of the `True` values:
specifying ``n`` 1-dimensional index arrays, where the ``i``\\ th index array specifies
the ``i``\\ th coordinate of the `True` values:
>>> x = ts.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]],
... dtype=ts.int32)
Expand Down Expand Up @@ -1048,8 +1048,8 @@ def __getitem__(self, *args, **kwargs) -> None:
:python:`dims[i] = self.labels.index(other.labels[i])`. It is an
error if no such dimension exists.
2. Otherwise, ``i`` is the ``j``\ th unlabeled dimension of :python:`other`
(left to right), and :python:`dims[i] = k`, where ``k`` is the ``j``\ th
2. Otherwise, ``i`` is the ``j``\\ th unlabeled dimension of :python:`other`
(left to right), and :python:`dims[i] = k`, where ``k`` is the ``j``\\ th
unlabeled dimension of :python:`self` (left to right). It is an error
if no such dimension exists.
Expand Down
128 changes: 128 additions & 0 deletions docs/type_param_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import collections.abc
from typing import (
TypeVar,
Generic,
overload,
Iterable,
Optional,
KeysView,
ValuesView,
ItemsView,
Iterator,
Union,
)

K = TypeVar("K")
V = TypeVar("V")
T = TypeVar("T")
U = TypeVar("U")


class Map(Generic[K, V]):
"""Maps keys of type :py:param:`.K` to values of type :py:param:`.V`.
Type parameters:
K:
Key type.
V:
Mapped value type.
Group:
type-param
"""

@overload
def __init__(self):
...

@overload
def __init__(self, items: collections.abc.Mapping[K, V]):
...

@overload
def __init__(self, items: Iterable[tuple[K, V]]):
...

def __init__(self, items):
"""Construct from the specified items."""
...

def clear(self):
"""Clear the map."""
...

def keys(self) -> KeysView[K]:
"""Return a dynamic view of the keys."""
...

def items(self) -> ItemsView[K, V]:
"""Return a dynamic view of the items."""
...

def values(self) -> ValuesView[V]:
"""Return a dynamic view of the values."""
...

@overload
def get(self, key: K) -> Optional[V]:
...

@overload
def get(self, key: K, default: V) -> V:
...

@overload
def get(self, key: K, default: T) -> Union[V, T]:
...

def get(self, key: K, default=None):
"""Return the mapped value, or the specified default."""
...

def __len__(self) -> int:
"""Return the number of items in the map."""
...

def __contains__(self, key: K) -> bool:
"""Check if the map contains :py:param:`.key`."""
...

def __getitem__(self, key: K) -> V:
"""Return the value associated with :py:param:`.key`.
Raises:
KeyError: if :py:param:`.key` is not present.
"""
...

def __setitem__(self, key: K, value: V):
"""Set the value associated with the specified key."""
...

def __delitem__(self, key: K):
"""Remove the value associated with the specified key.
Raises:
KeyError: if :py:param:`.key` is not present.
"""
...

def __iter__(self) -> Iterator[K]:
"""Iterate over the keys."""
...


class Derived(Map[int, U], Generic[U]):
"""Map from integer keys to arbitrary values.
Type parameters:
U: Mapped value type.
Group:
type-param
"""

pass


__all__ = ["Map", "Derived"]
17 changes: 16 additions & 1 deletion sphinx_immaterial/apidoc/object_description_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,22 @@ def format_object_description_tooltip(
("py:property", {"toc_icon_class": "alias", "toc_icon_text": "P"}),
("py:attribute", {"toc_icon_class": "alias", "toc_icon_text": "A"}),
("py:data", {"toc_icon_class": "alias", "toc_icon_text": "V"}),
("py:parameter", {"toc_icon_class": "sub-data", "toc_icon_text": "p"}),
(
"py:parameter",
{
"toc_icon_class": "sub-data",
"toc_icon_text": "p",
"generate_synopses": "first_sentence",
},
),
(
"py:typeParameter",
{
"toc_icon_class": "alias",
"toc_icon_text": "T",
"generate_synopses": "first_sentence",
},
),
("c:member", {"toc_icon_class": "alias", "toc_icon_text": "V"}),
("c:var", {"toc_icon_class": "alias", "toc_icon_text": "V"}),
("c:function", {"toc_icon_class": "procedure", "toc_icon_text": "F"}),
Expand Down
1 change: 0 additions & 1 deletion sphinx_immaterial/apidoc/object_toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def _make_section_from_field(
source: docutils.nodes.field,
) -> Optional[docutils.nodes.section]:
fieldname = cast(docutils.nodes.field_name, source[0])
fieldbody = cast(docutils.nodes.field_body, source[1])
ids = fieldname["ids"]
if not ids:
# Not indexed
Expand Down
Loading

0 comments on commit 4d8f8d5

Please sign in to comment.