sphinx
sphinx copied to clipboard
autodoc for generic classes should include the type parameters
Currently, when autodoc renders a class of the form class Foo(Generic[T])
, the resulting HTML just shows "class modulename.Foo", which omits information, as the type parameter is not shown. The only way to show the type parameter at the moment is to use :show-inheritance:
to add a line of the form "Bases: Generic[T]". A generic class should instead be rendered as "class modulename.Foo[T]".
PR welcome. What's the precedent here, though?
A
I don't know of any precedent; I just feel that a class being generic should be indicated on the class name itself and not only as part of the base classes.
At present, autodoc and python domain expect to represent base classes via the :Bases:
field. They also expect the parenthesis following the class name is a list of arguments list of class constructor.
I'm not sure the best way to represent the generic classes in the document. But we need to separate the class definition and constructor before the new implementation.
What's the precedent here, though?
There's quite a bit of precedent. If you're talking about usage of generic types: I'm not sure what was possible in Python 3.5, when type hints and generics were first introduced, because I don't have easy access to an interpreter for it. But certainly by Python 3.6 (released in 2016) you could use type parameters and regular constructor parameters together with both built-in classes and user-defined classes based on Generic[T]
. For example,
x = list[float]([1, 2, 3])
y = MyGenericClass[int]("x")
The run-time types of the objects will be simply list
and MyGenericClass
(NOT list[float]
and MyGenericClass[int]
), but type hinters will take the static type of those variables to be list[float]
and MyGenericClass[int]
.
If you're talking about declaration rather than usage: that only came in with Python 3.12 (released in October 2023). Now instead of writing:
T = TypeVar("T")
class MyGenericClass(SomeNonGenericBase, Generic[T]):
...
You can simply write (without declaring T
beforehand):
class MyGenericClass[T](SomeNonGenericBase):
...
Given the above, especially the usage syntax, it would make a lot of sense to me if you could automatically erase the Generic[...]
base class from the list of base classes and show the generated docs for the class (regardless of how defined) as simply:
class my_package.MyGenericClass[T](arg_1: str, arg_2: int)
Edit:
Having written all the above, I've just noticed that the Sphinx Python domain supports type lists to classes and function – presumably added since this ticket was filed to support Python 3.12.
.. py:class:: name
.. py:class:: name(parameters)
.. py:class:: name[type parameters](parameters)
Just to preempt a potential implementation question: There's a slight complication with getting the type list when you have generic base classes.
In that case, there is no requirement to explicitly list the types with a Python 3.12-style type list or an older-style Generic[...]
base. You can simply write the generic base classes you need. For example:
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
class Base1(Generic[T1, T2]): pass
class Base2(Generic[T1, T2]): pass
class Derived(Base1[T1, T2], Base2[T2, T3]): pass
In that case, the type list is all the type variables used in the base list collected in order of first usage, i.e. for Derived
it is [T1, T2, T3]
.
If Generic[T]
(or Protocol[T]
) is listed as an explicit base class then it must include all type variables used (otherwise that is a mypy error). Presumably this also applies to Python 3.12-style class lists, though I haven't tried it. For example:
class Derived(Base1[T1, T2], Base2[T2, T3], Generic[T3, T2, T1]): pass
In that case, the type list to Generic[...]
instead defines the type list ordering. For example, it would be [T3, T2, T1]
in the example above.
I implemented PEP 695 since cpython asked for it but we'll probably rewrite it when 3.12 becomes the minimal version. However I did not consider any autodoc approach because extracting the type variables are painful depending on the python version...
I should have worked with astroid or implemented an improved version of the AST parser (+take care of inheritance and different modules!!!). So at that time I just gave up I think.
Oh wow I didn't realise you needed to parse the code to get the type parameters out 😢
I can see that in 3.12 (if you use the new syntax) it's just a matter of reading the MyClass.__type_params__
variable at runtime.
Yes, that's why I think we should postpone this until 3.12 becomes the base version. The main resaon is that the AST parser in <3.12 would not even recognize the syntax ! so I needed to make my own tokenizer (and also take into account some weird stuff because spaces are gobbled).
I had another look at this and it turns out you don't need to parse the code to pick out the type parameters of a Generic[...]
base class. The trick is to use the __orig_bases__
member, which, unlike __bases__
, does not strip out the type parameters:
>>> from typing import get_type_hints, TypeVar, Generic
>>> T = TypeVar("T")
>>> U = TypeVar("U")
>>> class C(Generic[T, U]): pass
...
>>> C.__bases__
(<class 'typing.Generic'>,)
>>> C.__bases__[0]
<class 'typing.Generic'>
>>> C.__orig_bases__
(typing.Generic[~T, ~U],)
>>> C.__orig_bases__[0]
typing.Generic[~T, ~U]
>>> C.__orig_bases__[0].__parameters__[0].__name__
'T'
Actually, we are not handling the object directly. We are given a signature as a string. So I need to do a parser-like approach and cannot rely on runtime analysis (for instance, we support PEP 695 syntax given at the RST level directly, so there is no class object)