1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
|
# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
"""
A Trait Documenter
(Subclassed from the autodoc ClassLevelDocumenter)
"""
from importlib import import_module
import inspect
import io
import token
import tokenize
import traceback
from sphinx.ext.autodoc import ClassLevelDocumenter
from sphinx.util import logging
from traits.has_traits import MetaHasTraits
from traits.trait_type import TraitType
from traits.traits import generic_trait
logger = logging.getLogger(__name__)
def _is_class_trait(name, cls):
""" Check if the name is in the list of class defined traits of ``cls``.
"""
return (
isinstance(cls, MetaHasTraits)
and name in cls.__class_traits__
and cls.__class_traits__[name] is not generic_trait
)
class TraitDocumenter(ClassLevelDocumenter):
""" Specialized Documenter subclass for trait attributes.
The class defines a new documenter that recovers the trait definition
signature of module level and class level traits.
To use the documenter, append the module path in the extension
attribute of the `conf.py`.
"""
# ClassLevelDocumenter interface #####################################
objtype = "traitattribute"
directivetype = "attribute"
member_order = 60
# must be higher than other attribute documenters
priority = 12
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
""" Check that the documented member is a trait instance.
"""
check = (
isattr
and issubclass(type(member), TraitType)
or _is_class_trait(membername, parent.object)
)
return check
def document_members(self, all_members=False):
""" Trait attributes have no members """
pass
def import_object(self):
""" Get the Trait object.
Notes
-----
Code adapted from autodoc.Documenter.import_object.
"""
try:
current = self.module = import_module(self.modname)
for part in self.objpath[:-1]:
current = self.get_attr(current, part)
name = self.objpath[-1]
self.object_name = name
self.object = None
self.parent = current
return True
# this used to only catch SyntaxError, ImportError and
# AttributeError, but importing modules with side effects can raise
# all kinds of errors.
except Exception as err:
if self.env.app and not self.env.app.quiet:
self.env.app.info(traceback.format_exc().rstrip())
msg = (
"autodoc can't import/find {0} {r1}, it reported error: "
'"{2}", please check your spelling and sys.path'
)
self.directive.warn(
msg.format(self.objtype, str(self.fullname), err)
)
self.env.note_reread()
return False
def add_directive_header(self, sig):
""" Add the directive header 'attribute' with the annotation
option set to the trait definition.
"""
ClassLevelDocumenter.add_directive_header(self, sig)
try:
definition = trait_definition(
cls=self.parent,
trait_name=self.object_name,
)
except ValueError:
# Without this, a failure to find the trait definition aborts
# the whole documentation build.
logger.warning(
"No definition for the trait {!r} could be found in "
"class {!r}.".format(self.object_name, self.parent),
exc_info=True)
return
# Workaround for enthought/traits#493: if the definition is multiline,
# throw away all lines after the first.
if "\n" in definition:
definition = definition.partition("\n")[0] + " …"
self.add_line(" :annotation: = {0}".format(definition), "<autodoc>")
def trait_definition(*, cls, trait_name):
""" Retrieve the portion of the source defining a Trait attribute.
For example, given a class::
class MyModel(HasStrictTraits)
foo = List(Int, [1, 2, 3])
``trait_definition(cls=MyModel, trait_name="foo")`` returns
``"List(Int, [1, 2, 3])"``.
Parameters
----------
cls : MetaHasTraits
Class being documented.
trait_name : str
Name of the trait being documented.
Returns
-------
str
The portion of the source containing the trait definition. For
example, for a class trait defined as ``"my_trait = Float(3.5)"``,
the returned string will contain ``"Float(3.5)"``.
Raises
------
ValueError
If *trait_name* doesn't appear as a class-level variable in the
source.
"""
# Get the class source and tokenize it.
source = inspect.getsource(cls)
string_io = io.StringIO(source)
tokens = tokenize.generate_tokens(string_io.readline)
# find the trait definition start
trait_found = False
name_found = False
while not trait_found:
item = next(tokens, None)
if item is None:
break
if name_found and item[:2] == (token.OP, "="):
trait_found = True
continue
if item[:2] == (token.NAME, trait_name):
name_found = True
if not trait_found:
raise ValueError(
"No trait definition for {!r} found in {!r}".format(
trait_name, cls)
)
# Retrieve the trait definition.
definition_tokens = _get_definition_tokens(tokens)
definition = tokenize.untokenize(definition_tokens).strip()
return definition
def _get_definition_tokens(tokens):
""" Given the tokens, extracts the definition tokens.
Parameters
----------
tokens : iterator
An iterator producing tokens.
Returns
-------
A list of tokens for the definition.
"""
# Retrieve the trait definition.
definition_tokens = []
first_line = None
for type, name, start, stop, line_text in tokens:
if first_line is None:
first_line = start[0]
if type == token.NEWLINE:
break
item = (
type,
name,
(start[0] - first_line + 1, start[1]),
(stop[0] - first_line + 1, stop[1]),
line_text,
)
definition_tokens.append(item)
return definition_tokens
def setup(app):
""" Add the TraitDocumenter in the current sphinx autodoc instance. """
app.add_autodocumenter(TraitDocumenter)
|