Skip to content

Commit

Permalink
Add support for Python type parameter lists (#366)
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.

Co-authored-by: Brendan <[email protected]>
  • Loading branch information
jbms and 2bndy5 authored Jul 21, 2024
1 parent 9d011ed commit 7e2d559
Show file tree
Hide file tree
Showing 14 changed files with 920 additions and 87 deletions.
5 changes: 4 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,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 @@ -735,8 +735,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 @@ -1049,8 +1049,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
122 changes: 122 additions & 0 deletions docs/type_param_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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"]
5 changes: 4 additions & 1 deletion sphinx_immaterial/apidoc/format_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,10 @@ def _format_with_black(
break
if found_name:
break
if isinstance(orig_child, sphinx.addnodes.desc_type_parameter_list):
if (
isinstance(orig_child, docutils.nodes.Element)
and orig_child.get("sphinx_immaterial_parent_type_parameter_list") is True
):
# This is the parent entity type parameter list added by apigen.
parent_type_param_i = i
break
Expand Down
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 7e2d559

Please sign in to comment.