Skip to content

Commit

Permalink
maint: refresh autodoc_traits.py for long term maintenance
Browse files Browse the repository at this point in the history
  • Loading branch information
consideRatio committed Dec 11, 2022
1 parent 9cfdfd8 commit 1acc764
Showing 1 changed file with 168 additions and 40 deletions.
208 changes: 168 additions & 40 deletions autodoc_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
The code here is similar to the official code example in
https://www.sphinx-doc.org/en/master/development/tutorials/autodoc_ext.html#writing-the-extension.
Links to relevant source code in sphinx.ext.autodoc:
- class Documenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L270-L299
- Documenter.generate: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L838-L929
- class ClassDocumenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L1395-L1408
- class AttributeDocumenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L2509-L2524
"""
from sphinx.ext.autodoc import AttributeDocumenter, ClassDocumenter
from traitlets import MetaHasTraits, TraitType, Undefined
Expand All @@ -12,72 +19,193 @@
#
__version__ = "1.0.0"

# # patch sphinx.util.inspect.getdoc
# def _separate_metadata(obj, attrgetter = safe_getattr, allow_inherited = False, cls = None, name = None):
# if isinstance(obj, TraitType):
# assert False, obj.help
# return obj.help
# else:
# return getdoc(obj, attrgetter, allow_inherited, cls, name)
# sphinx.util.inspect.getdoc = _separate_metadata


# ClassDocumenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L1395
class ConfigurableDocumenter(ClassDocumenter):
"""Specialized Documenter subclass for traits with config=True"""
"""
Specialized Documenter subclass for traits with config=True
Links to relevant source code in sphinx.ext.autodoc:
- Documenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L270-L299
- ClassDocumenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L1395-L1408
"""

# objtype: The suffix to "auto" for a Sphinx directive name that will be
# created (and the default value for "directivetype").
objtype = "configurable"

# directivetype: Clarifies that this Documenter class could be capable of
# documenting this kind of members of other parent like
# Documenter classes.
directivetype = "class"

# priority: Declares this class' priority for use if multiple classes
# "can_document_member" of the "directivetype". This is only
# relevant if a parent class has a member.
#
# ConfigurableDocumenter can document traitlets configurable
# classes, so a parent like Documenter class can be the
# ModuleDocumenter.
#
priority = 100 # higher priority than ClassDocumenter's 10

@classmethod
def can_document_member(cls, member, membername, isattr, parent):
"""
If the member is a class with traitlets then we can document it, and we
will document it thanks to a high priority.
This function is not considered if the ``autoconfigurable`` or
``autoclass`` directives are called directly. can_document_member is
only used by other parent like Documenter classes having members of this
class' configured "documentertype" - such as ModuleDocumenter.
"""
return isinstance(member, MetaHasTraits)

def get_object_members(self, want_all):
"""Add traits with .tag(config=True) to members list"""
"""
This get_object_members function override is a hack, manipulating
__doc__ values of trait configuration objects, but otherwise behaving
exactly like the super class get_object_members.
It sets truthy strings to the class' traits __doc__ attributes. They
will otherwise be filtered out by the Documenter.filter_members
function, unless ``undoc-members`` option is set.
Links to relevant source code in sphinx.ext.autodoc:
- Documenter.filter_members: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L616-L769
- Documenter.get_object_members: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L607-L614
- ClassDocumenter.get_object_members: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L1656-L1674
- traitlets.HasTraits.class_traits: https://github.com/ipython/traitlets/blob/v5.6.0/traitlets/traitlets.py#L1620-L1652
"""
check, members = super().get_object_members(want_all)
if not isinstance(self.object, MetaHasTraits):
return check, members
# The directive can have been passed a inherited-members option to
# influence it, and we rely on two traitlets provided functions for it.

truthy_string = (
"A hack by autodoc_traits introduced in 1.1.0 for trait "
"configurations that have falsy help strings - this should "
"never be seen!"
)
for trait in self.object.class_traits(config=True).values():
trait.__doc__ = truthy_string

# We add all traits, also the inherited, bypassing :members: and
# :inherit-members: options.
#
# class_own_traits returns the class own defined traits, while
# class_traits includes super classes' defined traits.
# FIXME: We have been adding the trait_members unconditionally, but
# should we keep doing that?
#
# class_traits definition: https://github.com/ipython/traitlets/blob/v5.6.0/traitlets/traitlets.py#L1620-L1652
# class_own_traits definition: https://github.com/ipython/traitlets/blob/v5.6.0/traitlets/traitlets.py#L1654-L1665
# See https://github.com/jupyterhub/autodoc-traits/issues/27
#
get_traits = (
# FIXME: Is this backwards?
# Tracked in https://github.com/jupyterhub/autodoc-traits/issues/19
#
self.object.class_own_traits
if self.options.inherited_members
else self.object.class_traits
)
trait_members = []
for name, trait in sorted(get_traits(config=True).items()):
# put help in __doc__ where autodoc will look for it
trait.__doc__ = trait.help
trait_members.append((name, trait))
# Remove duplicates between members and trait_members. We
# can't use sets, because not all items are hashable. Modify
# trait_members in place for returning.
for item in members:
if item not in trait_members:
trait_members.append(item)
return check, trait_members
trait_members = self.object.class_traits(config=True).items()
for trait in trait_members:
if trait not in members:
members.append(trait)

return check, members


class TraitDocumenter(AttributeDocumenter):
"""
Links to relevant source code in sphinx.ext.autodoc:
- Documenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L270-L299
- AttributeDocumenter: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L2509-L2524
"""

# objtype: The suffix to "auto" for a Sphinx directive name that will be
# created (and the default value for "directivetype").
objtype = "trait"

# directivetype: Clarifies that this Documenter class could be capable of
# documenting this kind of members of other parent like
# Documenter classes.
directivetype = "attribute"
member_order = 1
priority = 100

# priority: Declares this class' priority for use if multiple classes
# "can_document_member" of the "directivetype". This is only
# relevant if a parent class has a member.
#
# TraitsDocumenter can document traitlets type attributes, so the
# parent like Documenter class is typically ClassDocumenter, but
# can also be the ConfigurableDocumenter.
#
priority = 100 # AttributeDocumenter has 10

# order: order if the autodoc_member_order in conf.py is set to "groupwise",
# by default it is "alphabetical", where lowest order comes first.
# Since traits are relevant configuration, we declare the lowest
# order for high visual priority.
member_order = 0 # AttributeDocumenter has 60

@classmethod
def can_document_member(cls, member, membername, isattr, parent):
"""
If the member is a traitlets type we can document it, and we will
document it thanks to a high priority.
This function is not considered if the ``autotrait`` or
``autoattribute`` directives are called directly. can_document_member is
only used by other parent like Documenter classes having members of this
class' configured "documentertype" - such as ClassDocumenter.
"""
return isinstance(member, TraitType)

def add_directive_header(self, sig):
default = self.object.get_default_value()
if default is Undefined:
default_s = ""
"""
add_directive_header is called by the base class' Documenter.generate
method. It is provided by both AttributeDocumenter and Documenter. This
override retains use of the super classes implementations, but influence
them.
For functions, the directive header describes the function's call
signature, but not the function's docstring.
Similarly, we look to emit rST to describe how the traitlets
configuration option can be configured and its default value.
- Documenter.generate: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L918-L929
- AttributeDocumenter.add_directive_header: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L2592-L2620
- Documenter.add_directive_header: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L504-L524
"""
default_value = self.object.get_default_value()
if default_value is Undefined:
default_value = ""
else:
default_s = repr(default)
self.options.annotation = "c.{name} = {trait}({default})".format(
name=self.format_name(),
trait=self.object.__class__.__name__,
default=default_s,
default_value = repr(default_value)

self.options.annotation = "c.{name} = {traitlets_type}({default_value})".format(
name=self.format_name(), # TestConfigurator.trait
traitlets_type=self.object.__class__.__name__, # Bool
default_value=default_value,
)

super().add_directive_header(sig)

def get_doc(self):
"""
get_doc (get docstring) is called by add_content, which is called by
generate. We override it to not unconditionally provide the docstring of
the traitlets type, but instead provide the traits help text if its
available.
Links to relevant source code in sphinx.ext.autodoc:
- Documenter.generate: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L918-L929
- AttributeDocumenter.add_content: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L2655-L2663
- Documenter.add_content: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L568-L605
- AttributeDocumenter.get_doc: https://github.com/sphinx-doc/sphinx/blob/v6.0.0b2/sphinx/ext/autodoc/__init__.py#L2639-L2653
"""
if isinstance(self.object.help, str):
return [[self.object.help]]
return super().get_doc()


def setup(app):
"""
Expand Down

0 comments on commit 1acc764

Please sign in to comment.