sphinx-autoapi/autoapi/mappers/python.py

263 lines
8.4 KiB
Python
Raw Normal View History

import sys
import os
import textwrap
import ast
from collections import defaultdict
from pydocstyle.parser import Parser
2015-04-08 05:54:53 +00:00
2016-11-02 23:45:41 +00:00
from .base import PythonMapperBase, SphinxMapperBase
from ..utils import slugify
if sys.version_info < (3,):
from itertools import izip_longest as zip_longest
else:
from itertools import zip_longest
2015-04-21 05:54:32 +00:00
2015-06-10 21:23:50 +00:00
class PythonSphinxMapper(SphinxMapperBase):
2015-04-21 05:54:32 +00:00
"""Auto API domain handler for Python
2015-04-21 05:54:32 +00:00
Parses directly from Python files.
:param app: Sphinx application passed in as part of the extension
"""
def load(self, patterns, dirs, **kwargs):
"""Load objects from the filesystem into the ``paths`` dictionary
Also include an attribute on the object, ``relative_path`` which is the
shortened, relative path the package/module
"""
for dir_ in dirs:
for path in self.find_files(patterns=patterns, dirs=[dir_], **kwargs):
data = self.read_file(path=path)
data.relative_path = os.path.relpath(path, dir_)
if data:
self.paths[path] = data
2015-04-21 05:54:32 +00:00
def read_file(self, path, **kwargs):
"""Read file input into memory, returning deserialized objects
:param path: Path of file to read
"""
try:
2016-10-20 21:57:07 +00:00
parsed_data = Parser()(open(path), path)
return parsed_data
except (IOError, TypeError, ImportError):
self.app.warn('Error reading file: {0}'.format(path))
return None
def create_class(self, data, options=None, path=None, **kwargs):
"""Create a class from the passed in data
2015-04-21 05:54:32 +00:00
:param data: dictionary data of pydocstyle output
"""
obj_map = dict((cls.type, cls) for cls
in [PythonClass, PythonFunction, PythonModule,
PythonMethod, PythonPackage])
try:
cls = obj_map[data.kind]
except KeyError:
self.app.warn("Unknown type: %s" % data.kind)
else:
obj = cls(data, jinja_env=self.jinja_env,
options=self.app.config.autoapi_options, **kwargs)
for child_data in data.children:
for child_obj in self.create_class(child_data, options=options,
**kwargs):
obj.children.append(child_obj)
yield obj
2015-04-21 05:54:32 +00:00
2015-04-08 05:54:53 +00:00
2015-06-10 21:23:50 +00:00
class PythonPythonMapper(PythonMapperBase):
2015-04-08 05:54:53 +00:00
language = 'python'
is_callable = False
2015-04-08 05:54:53 +00:00
def __init__(self, obj, **kwargs):
2015-06-10 21:23:50 +00:00
super(PythonPythonMapper, self).__init__(obj, **kwargs)
self.name = self._get_full_name(obj)
self.id = slugify(self.name)
2015-04-08 05:54:53 +00:00
2015-04-21 05:54:32 +00:00
# Optional
self.children = []
self.args = []
if self.is_callable:
self.args = self._get_arguments(obj)
self.docstring = obj.docstring or ''
self.docstring = textwrap.dedent(self.docstring)
self.docstring = self.docstring.replace("'''", '').replace('"""', '')
if getattr(obj, 'parent'):
self.inheritance = [obj.parent.name]
else:
2016-11-02 23:45:41 +00:00
self.inheritance = []
2015-04-08 05:54:53 +00:00
2015-04-21 05:54:32 +00:00
# For later
self.item_map = defaultdict(list)
2015-04-08 05:54:53 +00:00
@property
def is_undoc_member(self):
return self.docstring == ''
@property
def is_private_member(self):
return self.short_name[0] == '_'
@property
def is_special_member(self):
return self.short_name[0:2] == '__'
@property
def display(self):
if self.is_undoc_member and 'undoc-members' not in self.options:
return False
if self.is_private_member and 'private-members' not in self.options:
return False
if self.is_special_member and 'special-members' not in self.options:
return False
return True
@staticmethod
def _get_full_name(obj):
"""Recursively build the full name of the object from pydocstyle
Uses an additional attribute added to the object, ``relative_path``.
This is the shortened path of the object name, if the object is a
package or module.
:param obj: pydocstyle object, as returned from Parser()
:returns: Dotted name of object
:rtype: str
"""
2016-11-02 23:45:41 +00:00
def _inner(obj, parts=None):
if parts is None:
parts = []
obj_kind = obj.kind
obj_name = obj.name
if obj_kind == 'module':
obj_name = getattr(obj, 'relative_path', None) or obj.name
obj_name = obj_name.replace('/', '.')
ext = '.py'
if obj_name.endswith(ext):
obj_name = obj_name[:-len(ext)]
elif obj_kind == 'package':
obj_name = getattr(obj, 'relative_path', None) or obj.name
exts = ['/__init__.py', '.py']
for ext in exts:
if obj_name.endswith(ext):
obj_name = obj_name[:-len(ext)]
obj_name = obj_name.split('/').pop()
parts.insert(0, obj_name)
try:
return _inner(obj.parent, parts)
except AttributeError:
pass
return parts
return '.'.join(_inner(obj))
@staticmethod
def _get_arguments(obj):
"""Get arguments from a pydocstyle object
:param obj: pydocstyle object, as returned from Parser()
:returns: list of argument or argument and value pairs
:rtype: list
"""
arguments = []
source = textwrap.dedent(obj.source)
# Bare except here because AST parsing can throw any number of
# exceptions, including SyntaxError
try:
parsed = ast.parse(source)
2016-11-03 20:10:22 +00:00
except Exception as e: # noqa
print("Error parsing AST: %s" % str(e))
return []
parsed_args = parsed.body[0].args
arg_names = [arg.id if sys.version_info < (3,) else arg.arg
for arg in parsed_args.args]
# Get defaults for display based on AST node type
arg_defaults = []
pydocstyle_map = {
ast.Name: 'id',
ast.Num: 'n',
ast.Str: lambda obj: '"{0}"'.format(obj.s),
ast.Call: lambda obj: obj.func.id,
# TODO these require traversal into the AST nodes. Add this for more
# complete argument parsing, or handle with a custom AST traversal.
ast.List: lambda _: 'list',
ast.Tuple: lambda _: 'tuple',
ast.Set: lambda _: 'set',
ast.Dict: lambda _: 'dict',
}
if sys.version_info >= (3,):
pydocstyle_map.update({
ast.NameConstant: 'value',
})
for value in parsed_args.defaults:
default = None
try:
default = pydocstyle_map[type(value)](value)
except TypeError:
default = getattr(value, pydocstyle_map[type(value)])
except KeyError:
pass
if default is None:
default = 'None'
arg_defaults.append(default)
# Apply defaults padded to the end of the longest list. AST returns
# argument defaults as a short array that applies to the end of the list
# of arguments
for (name, default) in zip_longest(reversed(arg_names),
reversed(arg_defaults)):
arg = name
if default is not None:
arg = '{0}={1}'.format(name, default)
arguments.insert(0, arg)
# Add *args and **kwargs
if parsed_args.vararg:
arguments.append('*{0}'.format(
parsed_args.vararg
2016-11-02 23:45:41 +00:00
if sys.version_info < (3, 3)
else parsed_args.vararg.arg
))
if parsed_args.kwarg:
arguments.append('**{0}'.format(
parsed_args.kwarg
2016-11-02 23:45:41 +00:00
if sys.version_info < (3, 3)
else parsed_args.kwarg.arg
))
return arguments
2015-04-08 05:54:53 +00:00
2015-06-10 21:23:50 +00:00
class PythonFunction(PythonPythonMapper):
2015-04-21 05:54:32 +00:00
type = 'function'
is_callable = True
2015-04-08 05:54:53 +00:00
2015-04-21 05:54:32 +00:00
class PythonMethod(PythonPythonMapper):
type = 'method'
is_callable = True
2015-06-10 21:23:50 +00:00
class PythonModule(PythonPythonMapper):
2015-04-21 05:54:32 +00:00
type = 'module'
top_level_object = True
2015-04-21 05:54:32 +00:00
class PythonPackage(PythonPythonMapper):
type = 'package'
top_level_object = True
2015-06-10 21:23:50 +00:00
class PythonClass(PythonPythonMapper):
2015-04-21 05:54:32 +00:00
type = 'class'