sphinx-autoapi/autoapi/mappers/base.py

341 lines
11 KiB
Python
Raw Normal View History

import os
import fnmatch
from collections import OrderedDict, namedtuple
2019-10-05 22:11:23 +00:00
import re
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
import sphinx
import sphinx.util
from sphinx.util.console import darkgreen, bold
from sphinx.util.osutil import ensuredir
from sphinx.util.docstrings import prepare_docstring
import sphinx.util.logging
2019-10-05 22:11:23 +00:00
import unidecode
2020-01-03 17:26:32 +00:00
from ..settings import API_ROOT, TEMPLATE_DIR
2015-10-27 18:13:08 +00:00
LOGGER = sphinx.util.logging.getLogger(__name__)
2019-01-27 05:20:45 +00:00
Path = namedtuple("Path", ["absolute", "relative"])
2015-06-06 20:44:01 +00:00
class PythonMapperBase(object):
2015-04-08 05:54:53 +00:00
2019-01-27 05:20:45 +00:00
"""
2015-06-10 20:12:18 +00:00
Base object for JSON -> Python object mapping.
Subclasses of this object will handle their language specific JSON input,
and map that onto this standard Python object.
Subclasses may also include language-specific attributes on this object.
Arguments:
:param obj: JSON object representing this object
:param jinja_env: A template environment for rendering this object
Required attributes:
2015-08-03 18:24:52 +00:00
:var str id: A globally unique indentifier for this object.
Generally a fully qualified name, including namespace.
2015-06-10 20:12:18 +00:00
:var str name: A short "display friendly" name for this object.
Optional attributes:
:var str docstring: The documentation for this object
:var list imports: Imports in this object
:var list children: Children of this object
:var list parameters: Parameters to this object
:var list methods: Methods on this object
2019-01-27 05:20:45 +00:00
"""
2015-06-10 20:12:18 +00:00
2019-01-27 05:20:45 +00:00
language = "base"
type = "base"
# Create a page in the output for this object.
top_level_object = False
_RENDER_LOG_LEVEL = "VERBOSE"
2015-04-08 05:54:53 +00:00
def __init__(self, obj, jinja_env, app, options=None):
2019-09-04 21:44:35 +00:00
self.app = app
2015-04-08 05:54:53 +00:00
self.obj = obj
self.options = options
2019-10-05 22:11:23 +00:00
self.jinja_env = jinja_env
self.url_root = os.path.join("/", API_ROOT)
self.name = None
self.id = None
2015-04-08 05:54:53 +00:00
def __getstate__(self):
"""Obtains serialisable data for pickling."""
__dict__ = self.__dict__.copy()
__dict__.update(app=None, jinja_env=None) # clear unpickable attributes
return __dict__
def render(self, **kwargs):
2019-01-31 05:53:08 +00:00
LOGGER.log(self._RENDER_LOG_LEVEL, "Rendering %s", self.id)
ctx = {}
try:
template = self.jinja_env.get_template(
2019-01-27 05:20:45 +00:00
"{language}/{type}.rst".format(language=self.language, type=self.type)
)
except TemplateNotFound:
# Use a try/except here so we fallback to language specific defaults, over base defaults
template = self.jinja_env.get_template(
2019-01-27 05:20:45 +00:00
"base/{type}.rst".format(type=self.type)
)
ctx.update(**self.get_context_data())
ctx.update(**kwargs)
return template.render(**ctx)
2015-04-08 05:54:53 +00:00
@property
def rendered(self):
2019-04-07 00:56:05 +00:00
"""Shortcut to render an object in templates."""
return self.render()
def get_context_data(self):
return {
"autoapi_options": self.app.config.autoapi_options,
"include_summaries": self.app.config.autoapi_include_summaries,
"obj": self,
"sphinx_version": sphinx.version_info,
}
def __lt__(self, other):
2019-01-27 05:20:45 +00:00
"""Object sorting comparison"""
if isinstance(other, PythonMapperBase):
return self.id < other.id
return super(PythonMapperBase, self).__lt__(other)
2015-06-06 20:44:01 +00:00
def __str__(self):
2019-01-27 05:20:45 +00:00
return "<{cls} {id}>".format(cls=self.__class__.__name__, id=self.id)
@property
def short_name(self):
2019-01-27 05:20:45 +00:00
"""Shorten name property"""
return self.name.split(".")[-1]
@property
def pathname(self):
2019-01-27 05:20:45 +00:00
"""Sluggified path for filenames
Slugs to a filename using the follow steps
* Decode unicode to approximate ascii
* Remove existing hypens
* Substitute hyphens for non-word characters
* Break up the string as paths
2019-01-27 05:20:45 +00:00
"""
slug = self.name
slug = unidecode.unidecode(slug)
2019-01-27 05:20:45 +00:00
slug = slug.replace("-", "")
slug = re.sub(r"[^\w\.]+", "-", slug).strip("-")
return os.path.join(*slug.split("."))
def include_dir(self, root):
"""Return directory of file"""
parts = [root]
parts.extend(self.pathname.split(os.path.sep))
2019-01-27 05:20:45 +00:00
return "/".join(parts)
2015-10-27 18:28:07 +00:00
@property
def include_path(self):
"""Return 'absolute' path without regarding OS path separator
This is used in ``toctree`` directives, as Sphinx always expects Unix
path separators
"""
parts = [self.include_dir(root=self.url_root)]
2019-01-27 05:20:45 +00:00
parts.append("index")
return "/".join(parts)
2015-10-27 18:28:07 +00:00
2019-04-06 17:33:38 +00:00
@property
def display(self):
"""Whether to display this object or not.
2019-10-05 22:11:23 +00:00
2019-04-06 17:33:38 +00:00
:type: bool
"""
return True
@property
def ref_type(self):
return self.type
@property
def ref_directive(self):
return self.type
class SphinxMapperBase(object):
2019-01-27 05:20:45 +00:00
"""Base class for mapping `PythonMapperBase` objects to Sphinx.
:param app: Sphinx application instance
2019-01-27 05:20:45 +00:00
"""
2015-10-27 18:13:08 +00:00
def __init__(self, app, template_dir=None, url_root=None):
self.app = app
2015-08-03 18:24:52 +00:00
template_paths = [TEMPLATE_DIR]
if template_dir:
# Put at the front so it's loaded first
2015-08-03 18:24:52 +00:00
template_paths.insert(0, template_dir)
self.jinja_env = Environment(
2015-08-03 18:24:52 +00:00
loader=FileSystemLoader(template_paths),
trim_blocks=True,
lstrip_blocks=True,
)
2016-11-04 22:19:56 +00:00
def _wrapped_prepare(value):
2019-01-27 05:20:45 +00:00
return "\n".join(prepare_docstring(value))
2016-11-04 22:19:56 +00:00
2019-01-27 05:20:45 +00:00
self.jinja_env.filters["prepare_docstring"] = _wrapped_prepare
if self.app.config.autoapi_prepare_jinja_env:
self.app.config.autoapi_prepare_jinja_env(self.jinja_env)
2015-10-27 18:13:08 +00:00
self.url_root = url_root
# Mapping of {filepath -> raw data}
self.paths = OrderedDict()
# Mapping of {object id -> Python Object}
self.objects = OrderedDict()
2017-08-31 23:42:47 +00:00
# Mapping of {object id -> Python Object}
self.all_objects = OrderedDict()
# Mapping of {namespace id -> Python Object}
self.namespaces = OrderedDict()
# Mapping of {namespace id -> Python Object}
self.top_level_objects = OrderedDict()
2017-11-05 23:29:39 +00:00
def load(self, patterns, dirs, ignore=None):
2019-01-27 05:20:45 +00:00
"""
Load objects from the filesystem into the ``paths`` dictionary.
2015-05-29 22:22:06 +00:00
2019-01-27 05:20:45 +00:00
"""
paths = list(self.find_files(patterns=patterns, dirs=dirs, ignore=ignore))
for path in sphinx.util.status_iterator(
paths, bold("[AutoAPI] Reading files... "), "darkgreen", len(paths)
):
data = self.read_file(path=path)
if data:
self.paths[path] = data
return True
2019-10-05 22:11:23 +00:00
@staticmethod
def find_files(patterns, dirs, ignore):
2016-01-13 00:33:10 +00:00
# pylint: disable=too-many-nested-blocks
2015-08-03 18:24:52 +00:00
if not ignore:
ignore = []
pattern_regexes = []
for pattern in patterns:
2020-09-01 04:24:27 +00:00
regex = re.compile(fnmatch.translate(pattern).replace(".*", "(.*)"))
pattern_regexes.append((pattern, regex))
for _dir in dirs:
2019-10-05 22:11:23 +00:00
for root, _, filenames in os.walk(_dir):
seen = set()
for pattern, pattern_re in pattern_regexes:
for filename in fnmatch.filter(filenames, pattern):
skip = False
match = re.match(pattern_re, filename)
norm_name = match.groups()
if norm_name in seen:
continue
# Skip ignored files
for ignore_pattern in ignore:
2019-01-27 05:20:45 +00:00
if fnmatch.fnmatch(
os.path.join(root, filename), ignore_pattern
):
LOGGER.info(
2019-01-27 05:20:45 +00:00
bold("[AutoAPI] ")
+ darkgreen("Ignoring %s/%s" % (root, filename))
)
skip = True
if skip:
continue
# Make sure the path is full
if not os.path.isabs(filename):
filename = os.path.join(root, filename)
yield filename
seen.add(norm_name)
2015-05-29 22:22:06 +00:00
2015-06-10 18:35:30 +00:00
def read_file(self, path, **kwargs):
2019-01-27 05:20:45 +00:00
"""Read file input into memory
:param path: Path of file to read
2019-01-27 05:20:45 +00:00
"""
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
raise NotImplementedError
def add_object(self, obj):
2019-01-27 05:20:45 +00:00
"""
Add object to local and app environment storage
:param obj: Instance of a AutoAPI object
2019-01-27 05:20:45 +00:00
"""
self.objects[obj.id] = obj
2017-08-31 23:42:47 +00:00
self.all_objects[obj.id] = obj
child_stack = list(obj.children)
while child_stack:
child = child_stack.pop()
2017-08-31 23:42:47 +00:00
self.all_objects[child.id] = child
2019-01-27 05:20:45 +00:00
child_stack.extend(getattr(child, "children", ()))
2015-06-10 21:23:50 +00:00
def map(self, options=None):
2019-01-27 05:20:45 +00:00
"""Trigger find of serialized sources and build objects"""
2019-10-05 22:11:23 +00:00
for _, data in sphinx.util.status_iterator(
self.paths.items(),
bold("[AutoAPI] ") + "Mapping Data... ",
length=len(self.paths),
stringify_func=(lambda x: x[0]),
):
2017-06-29 21:48:06 +00:00
for obj in self.create_class(data, options=options):
self.add_object(obj)
2015-05-29 22:22:06 +00:00
2017-06-29 21:37:59 +00:00
def create_class(self, data, options=None, **kwargs):
2019-01-27 05:20:45 +00:00
"""
2015-05-29 22:22:06 +00:00
Create class object.
2017-11-05 23:29:39 +00:00
:param data: Instance of a AutoAPI object
2019-01-27 05:20:45 +00:00
"""
2015-05-29 22:22:06 +00:00
raise NotImplementedError
2015-06-06 20:44:01 +00:00
def output_rst(self, root, source_suffix):
2019-10-05 22:11:23 +00:00
for _, obj in sphinx.util.status_iterator(
self.objects.items(),
bold("[AutoAPI] ") + "Rendering Data... ",
length=len(self.objects),
verbosity="INFO",
stringify_func=(lambda x: x[0]),
):
rst = obj.render()
if not rst:
continue
detail_dir = obj.include_dir(root=root)
ensuredir(detail_dir)
2019-01-27 05:20:45 +00:00
path = os.path.join(detail_dir, "%s%s" % ("index", source_suffix))
with open(path, "wb+") as detail_file:
detail_file.write(rst.encode("utf-8"))
if self.app.config.autoapi_add_toctree_entry:
self._output_top_rst(root)
def _output_top_rst(self, root):
# Render Top Index
2019-01-27 05:20:45 +00:00
top_level_index = os.path.join(root, "index.rst")
pages = self.objects.values()
2019-08-25 22:53:58 +00:00
with open(top_level_index, "wb") as top_level_file:
2019-01-27 05:20:45 +00:00
content = self.jinja_env.get_template("index.rst")
2019-08-25 22:53:58 +00:00
top_level_file.write(content.render(pages=pages).encode("utf-8"))