Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autodoc for generic classes should include the type parameters #10568

Open
jwodder opened this issue Jun 17, 2022 · 10 comments
Open

autodoc for generic classes should include the type parameters #10568

jwodder opened this issue Jun 17, 2022 · 10 comments
Labels
domains:py expert issue that is not easily fixed extensions:autodoc type:enhancement enhance or introduce a new feature

Comments

@jwodder
Copy link

jwodder commented Jun 17, 2022

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]".

@jwodder jwodder added the type:enhancement enhance or introduce a new feature label Jun 17, 2022
@AA-Turner
Copy link
Member

PR welcome. What's the precedent here, though?

A

@jwodder
Copy link
Author

jwodder commented Jun 17, 2022

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.

@tk0miya
Copy link
Member

tk0miya commented Jun 19, 2022

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.

@arthur-tacca
Copy link

arthur-tacca commented Feb 22, 2024

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)

@arthur-tacca
Copy link

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.

@picnixz
Copy link
Member

picnixz commented Feb 23, 2024

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.

@picnixz picnixz added the expert issue that is not easily fixed label Feb 24, 2024
@arthur-tacca
Copy link

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.

@picnixz
Copy link
Member

picnixz commented Feb 26, 2024

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).

@arthur-tacca
Copy link

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'

@picnixz
Copy link
Member

picnixz commented Aug 19, 2024

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domains:py expert issue that is not easily fixed extensions:autodoc type:enhancement enhance or introduce a new feature
Projects
None yet
Development

No branches or pull requests

5 participants