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

Add traitlets documenter for documentation #163

Merged
merged 7 commits into from
Jul 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions doc-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
sphinx>=1.4.4
astor>=0.5
1 change: 1 addition & 0 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'traitlets_documenter',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
17 changes: 17 additions & 0 deletions traitlets_documenter/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traitlets Documenter
--------------------

A sphinx autodoc extension for documenting Traitlets classes.
Currently support documenting classes with members of Traitlets TraitType.
This code is inspired from `Traits Documenter <https://github.com/enthought/trait-documenter>`_

Installation
^^^^^^^^^^^^
This package is installed as a subpackage of `simphony/simphony-remote <https://github.com/simphony/simphony-remote>`_.


Usage
^^^^^
Add `traitlets_documenter` to the extensions variables in your `conf.py`::

extensions.append('traitlets_documenter')
11 changes: 11 additions & 0 deletions traitlets_documenter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__all__ = ['setup']


def setup(app):
""" Add the TraitletDocumenter in the current sphinx autodoc instance.

"""
from traitlets_documenter.class_traitlets_documenter import (
ClassTraitletDocumenter)

app.add_autodocumenter(ClassTraitletDocumenter)
79 changes: 79 additions & 0 deletions traitlets_documenter/class_traitlets_documenter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from sphinx.ext.autodoc import ClassLevelDocumenter
from traitlets import TraitType

from .util import get_trait_definition, DefinitionError


class ClassTraitletDocumenter(ClassLevelDocumenter):

objtype = 'traitletattribute'
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.
"""
return isinstance(member, TraitType)

def document_members(self, all_members=False):
pass

def add_content(self, more_content, no_docstring=False):
""" Add content

If the help attribute is defined, add the help content,
otherwise, add the info content.

If the default value is found in the definition,
add the default value as well
"""
# Never try to get a docstring from the trait object.
super(ClassTraitletDocumenter, self).add_content(
more_content, no_docstring=True)

if hasattr(self, 'get_sourcename'):
sourcename = self.get_sourcename()
else:
sourcename = u'<autodoc>'

# Get help message, if defined, otherwise, add the info
if self.object.help:
self.add_line(self.object.help, sourcename)
else:
self.add_line(self.object.info(), sourcename)

# Add default value if it is coded in the definition,
# otherwise it maybe dynamically calculated and we don't want to
# include it.
try:
definition = get_trait_definition(self.parent, self.object_name)
except DefinitionError as error:
self.directive.warn(error.args[0])
else:
default_value = self.object.default_value_repr()
if default_value in definition:
self.add_line('', sourcename)
self.add_line(
'Default: {}'.format(default_value),
sourcename)

def add_directive_header(self, sig):
""" Add the sphinx directives.

Add the 'attribute' directive with the annotation option
set to the traitlet type

"""
ClassLevelDocumenter.add_directive_header(self, sig)
if hasattr(self, 'get_sourcename'):
sourcename = self.get_sourcename()
else:
sourcename = u'<autodoc>'

trait_type = type(self.object).__name__
self.add_line(
' :annotation: = {0}'.format(trait_type), sourcename)
68 changes: 68 additions & 0 deletions traitlets_documenter/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import ast
import inspect
import collections
from _ast import ClassDef, Assign, Name

import astor


class DefinitionError(Exception):
pass


def get_trait_definition(parent, trait_name):
""" Retrieve the Trait attribute definition from the source file.

Parameters
----------
parent :
The module or class where the trait is defined.

trait_name : string
The name of the trait.

Returns
-------
definition : string
The trait definition from the source.

"""
# Get the class source.
source = inspect.getsource(parent)
nodes = ast.parse(source)

if not inspect.ismodule(parent):
for node in ast.iter_child_nodes(nodes):
if isinstance(node, ClassDef):
parent_node = node
break
else:
message = 'Could not find class definition {0} for {1}'
raise DefinitionError(message.format(parent, trait_name))
else:
parent_node = nodes

# Get the container node(s)
targets = collections.defaultdict(list)
for node in ast.walk(parent_node):
if isinstance(node, Assign):
target = trait_node(node, trait_name)
if target is not None:
targets[node.col_offset].append((node, target))

if len(targets) == 0:
message = 'Could not find trait definition of {0} in {1}'
raise DefinitionError(message.format(trait_name, parent))
else:
# keep the assignment with the smallest column offset
assignments = targets[min(targets)]
# we always get the last assignment in the file
node, name = assignments[-1]

return astor.to_source(node.value).strip()


def trait_node(node, trait_name):
target = node.targets[0]
if isinstance(target, Name) and target.id == trait_name:
return node