File: trait_documenter.py

package info (click to toggle)
python-traits 6.4.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,648 kB
  • sloc: python: 34,801; ansic: 4,266; makefile: 102
file content (238 lines) | stat: -rw-r--r-- 7,037 bytes parent folder | download
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)