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

Autosummary additional links for inherited methods and attributes #11712

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e62a37c
Add links for inherited methods and attributes
satr-cowi Aug 22, 2023
4e71d5b
Docs update for additional autosummary inherited options
satr-cowi Oct 6, 2023
6a2b58a
Merge branch 'sphinx-doc:master' into Autosummary_additional_links_fo…
satr-cowi Oct 6, 2023
2f4d125
Formatting update for linter
satr-cowi Oct 6, 2023
bee1d6b
Review comment changes
satr-cowi Oct 9, 2023
4302a97
Autosummary documentation improvements
satr-cowi Oct 9, 2023
092f706
Docs linter updates
satr-cowi Oct 9, 2023
063d33f
Update autosummary.rst
satr-cowi Oct 9, 2023
b8dff3f
Change inherited_members into ordered list
satr-cowi Oct 9, 2023
697be33
Added unittest to existing tests for inherited methods and attributes
satr-cowi Oct 9, 2023
76c0194
Merge branch 'Autosummary_additional_links_for_inherited_methods_and_…
satr-cowi Oct 9, 2023
192ad95
lint formatting
satr-cowi Oct 9, 2023
e9edee7
Removed use of __dict__
satr-cowi Oct 9, 2023
b7a0049
Test shouldn't depend on Python version or os. Includes attributes in…
satr-cowi Oct 9, 2023
0901005
docs updated
satr-cowi Oct 9, 2023
de412e0
Linting corrections
satr-cowi Oct 9, 2023
4f3aa01
correction
satr-cowi Oct 9, 2023
8e62aed
another linting correction
satr-cowi Oct 9, 2023
71c1729
Refactor to minimise _get_class_members calls
satr-cowi Oct 13, 2023
89adbd9
Merge branch 'sphinx-doc:master' into Autosummary_additional_links_fo…
satr-cowi Nov 30, 2023
2d56014
Merge remote-tracking branch 'upstream/master' into Autosummary_addit…
satr-cowi Jan 10, 2024
bff342f
Merge remote-tracking branch 'upstream/master' into Autosummary_addit…
satr-cowi Feb 5, 2024
13895a9
Merge branch 'master' into Autosummary_additional_links_for_inherited…
satr-cowi Feb 15, 2024
7f06fcb
Merge branch 'master' into Autosummary_additional_links_for_inherited…
satr-cowi Mar 28, 2024
8ea3cba
Merge branch 'master' into Autosummary_additional_links_for_inherited…
satr-cowi Jun 13, 2024
219cc23
Merge remote-tracking branch 'upstream/master' into Autosummary_addit…
satr-cowi Sep 23, 2024
3199fe0
ruff format
satr-cowi Sep 25, 2024
9884579
Only return the first instance of inheritance
satr-cowi Sep 25, 2024
5421b26
Additional tests for more conditions
satr-cowi Sep 25, 2024
a34b648
Clarification on members including private and template example
satr-cowi Sep 25, 2024
6f3fbc3
ruff format
satr-cowi Sep 25, 2024
9037a22
lint corrections
satr-cowi Sep 25, 2024
224076b
Merge branch 'master' into Autosummary_additional_links_for_inherited…
satr-cowi Sep 25, 2024
065a1a0
Merge branch 'master' into Autosummary_additional_links_for_inherited…
satr-cowi Oct 28, 2024
58ff1e3
add in version dependent attributes
satr-cowi Oct 29, 2024
3c9b0bb
ruff
satr-cowi Oct 29, 2024
336d3e3
update for 3_11 and remove extra term
satr-cowi Oct 29, 2024
ceb2b99
add in members too
satr-cowi Oct 29, 2024
479ff7d
Simplify way to add new builtin members
satr-cowi Oct 29, 2024
4987598
tweak for 3_14 test
satr-cowi Oct 29, 2024
a3adf19
fix 3_14
satr-cowi Oct 29, 2024
d6abcbe
correct order of members2
satr-cowi Oct 29, 2024
73827e2
Improved comment
satr-cowi Oct 29, 2024
242f21a
Improved docstring for inherited_qualnames
satr-cowi Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 94 additions & 4 deletions doc/usage/extensions/autosummary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -293,16 +293,106 @@ The following variables are available in the templates:

.. data:: members

List containing names of all members of the module or class. Only available
for modules and classes.
List containing names of all members of the module or class including private
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just reconfirm, but this was already the case that it returned private attributes right? since we are doing members = dir(self).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the existing attributes members and private members already included private ones. Therefore there shouldn't be any change here, just clarifying this in the docs.

ones. Only available for modules and classes.

.. data:: inherited_members

List containing names of all inherited members of class. Only available for
classes.
List containing names of all inherited members of class including private ones.
Only available for classes.

.. versionadded:: 1.8.0

.. data:: inherited_qualnames
satr-cowi marked this conversation as resolved.
Show resolved Hide resolved

List containing the fully qualified names of each inherited member including
private ones. Will return just the closest parent from which this class was
inherited. Only available for classes.

The following example assumes that this code block has been written as
part of a module ``mypackage.test``

.. code-block:: python

class foo():
foo_attr = "I'm an attribute"
def __init__(self):
print("object initialised")

def do_something(self):
print("Foo something")

def _i_am_private(self):
print("I'm a private method")

class bar(foo):
def do_something_else(self):
print("Bar something")

Some available parameters for the autosummary of class ``bar`` in the above
satr-cowi marked this conversation as resolved.
Show resolved Hide resolved
example are:

* ``name`` returns ``'bar'``
* ``objname`` returns ``'bar'``
* ``fullname`` returns ``'mypackage.test.bar'``
* ``methods`` returns ``['__init__', 'do_something', 'do_something_else']``
* ``attributes`` returns ``['foo_attr']``
* ``members`` returns a list with many built in methods/attributes and
``['__init__', '_i_am_private', 'do_something', 'do_something_else',
'foo_attr']``
* ``inherited_members`` returns a list with many built in methods/attributes
and ``['__init__', '_i_am_private','do_something', 'foo_attr']``
* ``inherited_qualnames`` returns a list with many built in methods/
attributes and ``['mypackage.test.foo.__init__',
'mypackage.test.foo._i_am_private', 'mypackage.test.foo.do_something',
'mypackage.test.foo.foo_attr']``
* ``inherited_methods`` returns ``['mypackage.test.foo.__init__',
'mypackage.test.foo.do_something']``
* ``inherited_attributes`` returns ``['mypackage.test.foo.foo_attr']``

These parameters could then be used in a Sphinx template ``class.rst`` like
the following

.. code-block:: RST
:dedent: 0

.. currentmodule:: {{ module }}

.. autoclass:: {{ objname }}
:show-inheritance:

.. rubric:: Methods
.. autosummary::
:toctree:
:nosignatures:
{% for item in methods %}
{% if item not in inherited_members %}
{{objname}}.{{ item }}
{%- endif %}
{%- endfor %}

.. rubric:: Inherited Methods
.. autosummary::
{% for item in inherited_methods %}
{{item}}
{%- endfor %}

.. versionadded:: 7.3.0

.. data:: inherited_methods

List containing fully qualified names of "public" inherited methods only.
``__init__`` is still included. Only available for classes.

.. versionadded:: 7.3.0

.. data:: inherited_attributes

List containing qualified names of "public" inherited attributes only.
Only available for classes.

.. versionadded:: 7.3.0

.. data:: functions

List containing names of "public" functions in the module. Here, "public"
Expand Down
40 changes: 36 additions & 4 deletions sphinx/ext/autosummary/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,15 +346,44 @@ def generate_autosummary_content(
ns['modules'] = imported_modules + modules
ns['all_modules'] = all_imported_modules + all_modules
elif doc.objtype == 'class':
ns['members'] = dir(obj)
ns['inherited_members'] = set(dir(obj)) - set(obj.__dict__.keys())
ns['members'] = list(_get_class_members(obj))
obj_classinfo = _get_class_members(obj, False)
obj_local = [j for j, k in obj_classinfo.items() if k == obj]

ns['inherited_members'] = [
x for x in dict.fromkeys(ns['members']) if x not in obj_local
]

ns['methods'], ns['all_methods'] = _get_members(
doc, app, obj, {'method'}, include_public={'__init__'}
)
ns['attributes'], ns['all_attributes'] = _get_members(
doc, app, obj, {'attribute', 'property'}
)

method_string = [m.split('.')[-1] for m in ns['methods']]
attr_string = [m.split('.')[-1] for m in ns['attributes']]

# Find the first link for this attribute in higher classes
ns['inherited_qualnames'] = []
ns['inherited_methods'] = []
ns['inherited_attributes'] = []

inherited_set = set(ns['inherited_members'])
for cl in obj.__mro__:
classinfo = _get_class_members(cl, False)
local = [j for j, k in classinfo.items() if k == cl]

for i in local:
if i in inherited_set:
cl_str = f'{cl.__module__}.{cl.__name__}.{i}'
ns['inherited_qualnames'].append(cl_str)
if i in method_string:
ns['inherited_methods'].append(cl_str)
elif i in attr_string:
ns['inherited_attributes'].append(cl_str)
inherited_set.remove(i)

if modname is None or qualname is None:
modname, qualname = _split_full_qualified_name(name)

Expand Down Expand Up @@ -398,9 +427,12 @@ def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool:
return False


def _get_class_members(obj: Any) -> dict[str, Any]:
def _get_class_members(obj: Any, return_object: bool = True) -> dict[str, Any]:
Copy link
Member

@picnixz picnixz Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually flip the boolean and use return_types=False by default. What do you think?

And whether we change the boolean or not, I think it would be good to make it keyword-only just so that we know what we do when we call the function (we don't need to go to its implementation).

members = sphinx.ext.autodoc.importer.get_class_members(obj, None, safe_getattr)
return {name: member.object for name, member in members.items()}
if return_object:
return {name: member.object for name, member in members.items()}
else:
return {name: member.class_ for name, member in members.items()}


def _get_module_members(app: Sphinx, obj: Any) -> dict[str, Any]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import sys

class Parent:

relation = "Family"

def __init__(self):
self.name = "Andrew"
self.age = 35

def get_name(self):
return self.name

def get_age(self):
return f"Parent class - {self.age}"

# Test mangled name behaviour
Copy link
Member

@picnixz picnixz Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a normal private attribute/method (no mangled, just simple underscore)?

__get_age = get_age # private copy of original get_age method
__private_parent_attribute = "Private_parent"


class Child(Parent):

def __init__(self):
self.name = "Bobby"
self.age = 15

def get_name(self):
return self.name

def get_age(self):
return f"Child class - {self.age}"

@staticmethod
def addition(a, b):
return a + b


class Baby(Child):

# Test a private attribute
__private_baby_name = "Private1234"

def __init__(self):
self.name = "Charlie"
self.age = 2

def get_age(self):
return f"Baby class - {self.age}"

class BabyInnerClass():
baby_inner_attribute = "An attribute of an inner class"


class Job:

def __init__(self):
self.salary = 10

def get_salary(self):
return self.salary


class Architect(Job):

def __init__(self):
self.salary = 20

def get_age(self):
return f"Architect age - {self.age}"

# Test a class that inherits multiple classes
class Jerry(Architect, Child):

def __init__(self):
self.name = "Jerry"
self.age = 25


__all__ = ["Parent", "Child", "Baby", "Job", "Architect", "Jerry"]

# The following is used to process the expected builtin members for different
# versions of Python. The base list is for v3.11 (the latest testing when added)
# and new builtin attributes/methods in later versions can be appended below.
Comment on lines +82 to +84
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have those values declared in the test instead? the test roots should be only contains the code we "test" (so this file should be like a real one on which you run autodoc).


members_3_11 = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__']

attr_3_11 = ['__annotations__', '__dict__', '__doc__', '__module__', '__weakref__']


def concat_and_sort(list1, list2):
return sorted(list1 + list2)


built_in_attr = attr_3_11
built_in_members = members_3_11
# The second test has an extra builtin member
built_in_members2 = ['__annotations__', *members_3_11]

if sys.version_info[:2] >= (3, 13):
add_3_13 = ['__firstlineno__', '__static_attributes__']
built_in_attr = concat_and_sort(built_in_attr, add_3_13)
built_in_members = concat_and_sort(built_in_members, add_3_13)
built_in_members2 = concat_and_sort(built_in_members2, add_3_13)

if sys.version_info[:2] >= (3, 14):
built_in_attr = concat_and_sort(built_in_attr, ['__annotate__'])
built_in_members2 = concat_and_sort(built_in_members2, ['__annotate__'])
Loading
Loading