Refactor into nicer top-level interface to the Domains.

Languages refactored:

* Python
* JS
pull/8/head
Eric Holscher 9 years ago
parent 819dfa92ca
commit 5ca2ee76a5

@ -3,8 +3,9 @@ import yaml
import json
import fnmatch
from jinja2 import Environment, FileSystemLoader
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
from sphinx.util.console import darkgreen
from sphinx.util.osutil import ensuredir
from ..settings import TEMPLATE_DIR
@ -18,22 +19,19 @@ class AutoAPIBase(object):
def __init__(self, obj):
self.obj = obj
TEMPLATE_PATHS = [TEMPLATE_DIR]
USER_TEMPLATE_DIR = self.get_config('autoapi_template_dir')
if USER_TEMPLATE_DIR:
# Put at the front so it's loaded first
TEMPLATE_PATHS.insert(0, USER_TEMPLATE_DIR)
self.jinja_env = Environment(
loader=FileSystemLoader(TEMPLATE_PATHS)
)
def render(self, ctx=None):
if not ctx:
ctx = {}
template = self.jinja_env.get_template(
'{language}/{type}.rst'.format(language=self.language, type=self.type)
)
try:
template = self.jinja_env.get_template(
'{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(
'base/{type}.rst'.format(language=self.language, type=self.type)
)
ctx.update(**self.get_context_data())
return template.render(**ctx)
@ -58,6 +56,25 @@ class AutoAPIBase(object):
return '<{cls} {id}>'.format(cls=self.__class__.__name__,
id=self.id)
@property
def short_name(self):
'''Shorten name property'''
return self.name.split('.')[-1]
@property
def ref_type(self):
return self.type
@property
def ref_directive(self):
return self.type
@property
def namespace(self):
pieces = self.id.split('.')[:-1]
if pieces:
return '.'.join(pieces)
class AutoAPIDomain(object):
@ -66,61 +83,49 @@ class AutoAPIDomain(object):
:param app: Sphinx application instance
'''
namespaces = {}
# Mapping of {filepath -> raw data}
paths = {}
# Mapping of {object id -> Python Object}
objects = {}
namespaces = {}
top_level_objects = {}
def __init__(self, app):
self.app = app
def read_file(self, path, format='yaml'):
'''Read file input into memory, returning deserialized objects
TEMPLATE_PATHS = [TEMPLATE_DIR]
USER_TEMPLATE_DIR = self.get_config('autoapi_template_dir')
if USER_TEMPLATE_DIR:
# Put at the front so it's loaded first
TEMPLATE_PATHS.insert(0, USER_TEMPLATE_DIR)
:param path: Path of file to read
'''
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
try:
with open(path, 'r') as handle:
if format == 'yaml':
obj = yaml.safe_load(handle)
elif format == 'json':
obj = json.load(handle)
except IOError:
raise Warning('Error reading file: {0}'.format(path))
except yaml.YAMLError:
raise Warning('Error parsing file: {0}'.format(path))
except ValueError:
raise Warning('Error parsing file: {0} at {1}'.format(path, json.last_error_position))
return obj
self.jinja_env = Environment(
loader=FileSystemLoader(TEMPLATE_PATHS)
)
def add_object(self, obj):
def load(self, pattern, dir, ignore=[]):
'''
Add object to local and app environment storage
Load objects from the filesystem into the ``paths`` dictionary.
:param obj: Instance of a AutoAPI object
'''
self.app.env.autoapi_data.append(obj)
self.objects[obj.name] = obj
def get_config(self, key, default=None):
if self.app.config is not None:
return getattr(self.app.config, key, default)
for path in self.find_files(pattern=pattern, dir=dir, ignore=ignore):
data = self.read_file(path=path)
if data:
self.paths[path] = data
def find_files(self, pattern='*.yaml'):
'''Find YAML/JSON files to parse for namespace information'''
# TODO do an intelligent glob here, we're picking up too much
def find_files(self, pattern, dir, ignore):
files_to_read = []
absolute_dir = os.path.normpath(self.get_config('autoapi_dir'))
for root, dirnames, filenames in os.walk(absolute_dir):
for root, dirnames, filenames in os.walk(dir):
for filename in fnmatch.filter(filenames, pattern):
# Skip ignored files
for ignore_pattern in self.get_config('autoapi_ignore'):
for ignore_pattern in ignore:
if fnmatch.fnmatch(filename, ignore_pattern):
print "Ignoring %s/%s" % (root, filename)
continue
# Make sure the path is full
if os.path.isabs(filename):
files_to_read.append(os.path.join(filename))
else:
@ -133,13 +138,32 @@ class AutoAPIDomain(object):
len(files_to_read)):
yield _path
def get_objects(self, pattern, format='yaml'):
def read_file(self, path, format='yaml'):
'''Read file input into memory
:param path: Path of file to read
'''
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
raise NotImplementedError
def add_object(self, obj):
'''
Add object to local and app environment storage
:param obj: Instance of a AutoAPI object
'''
self.objects[obj.id] = obj
def get_config(self, key, default=None):
if self.app.config is not None:
return getattr(self.app.config, key, default)
def map(self):
'''Trigger find of serialized sources and build objects'''
for path in self.find_files(pattern):
data = self.read_file(path, format=format)
if data:
for obj in self.create_class(data):
self.add_object(obj)
for path, data in self.paths.items():
for obj in self.create_class(data):
self.add_object(obj)
def create_class(self, obj):
'''
@ -149,10 +173,24 @@ class AutoAPIDomain(object):
'''
raise NotImplementedError
def write_indexes(self):
# Write Index
top_level_index = os.path.join(self.get_config('autoapi_root'),
'index.rst')
def output_rst(self, root, source_suffix):
for id, obj in self.objects.items():
if not obj:
continue
rst = obj.render()
if not rst:
continue
detail_dir = os.path.join(root, *id.split('.'))
ensuredir(detail_dir)
path = os.path.join(detail_dir, '%s%s' % ('index', source_suffix))
with open(path, 'w+') as detail_file:
detail_file.write(rst.encode('utf-8'))
# Render Top Index
top_level_index = os.path.join(root, 'index.rst')
with open(top_level_index, 'w+') as top_level_file:
content = self.jinja_env.get_template('index.rst')
top_level_file.write(content.render())

@ -4,7 +4,6 @@ from collections import defaultdict
from sphinx.util.osutil import ensuredir
from .base import AutoAPIBase, AutoAPIDomain
from ..settings import env
MADE = set()

@ -3,7 +3,6 @@ import os
from sphinx.util.osutil import ensuredir
from .base import AutoAPIBase, AutoAPIDomain
from ..settings import env
class GoDomain(AutoAPIDomain):
@ -68,7 +67,7 @@ class GoDomain(AutoAPIDomain):
yield obj
else:
# Recurse for children
obj = cls(data)
obj = cls(data, env=self.jinja_env)
for child_type in ['consts', 'types', 'vars', 'funcs']:
for child_data in data.get(child_type, []):
obj.children += list(self.create_class(child_data))

@ -2,10 +2,8 @@ import os
import json
import subprocess
from sphinx.util.osutil import ensuredir
from .base import AutoAPIBase, AutoAPIDomain
from ..settings import env
class JavaScriptDomain(AutoAPIDomain):
@ -17,6 +15,31 @@ class JavaScriptDomain(AutoAPIDomain):
:param app: Sphinx application passed in as part of the extension
'''
def read_file(self, path, **kwargs):
'''Read file input into memory, returning deserialized objects
:param path: Path of file to read
'''
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
try:
parsed_data = json.loads(subprocess.check_output(['jsdoc', '-X', path]))
return parsed_data
except IOError:
print Warning('Error reading file: {0}'.format(path))
except TypeError:
print Warning('Error reading file: {0}'.format(path))
return None
def map(self):
'''Trigger find of serialized sources and build objects'''
for path, data in self.paths.items():
for item in data:
for obj in self.create_class(item):
obj.jinja_env = self.jinja_env
self.add_object(obj)
def create_class(self, data):
'''Return instance of class based on Javascript data
@ -48,72 +71,26 @@ class JavaScriptDomain(AutoAPIDomain):
obj.children.append(child_obj)
yield obj
def read_file(self, path, **kwargs):
'''Read file input into memory, returning deserialized objects
:param path: Path of file to read
'''
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
try:
parsed_data = json.loads(subprocess.check_output(['jsdoc', '-X', path]))
return parsed_data
except IOError:
print Warning('Error reading file: {0}'.format(path))
except TypeError:
print Warning('Error reading file: {0}'.format(path))
return None
def get_objects(self, pattern, format='yaml'):
'''Trigger find of serialized sources and build objects'''
for path in self.find_files(pattern):
data = self.read_file(path, format=format)
if data:
# Returns a list of objects
for item in data:
for obj in self.create_class(item):
self.add_object(obj)
def full(self):
self.get_objects(self.get_config('autoapi_file_pattern'), format='json')
self.generate_output()
self.write_indexes()
def generate_output(self):
for obj in self.app.env.autoapi_data:
if not obj:
continue
rst = obj.render()
# Detail
try:
filename = obj.name.split('(')[0]
except IndexError:
filename = obj.name
detail_dir = os.path.join(self.get_config('autoapi_root'),
*filename.split('.'))
ensuredir(detail_dir)
# TODO: Better way to determine suffix?
path = os.path.join(detail_dir, '%s%s' % ('index', self.get_config('source_suffix')[0]))
if rst:
with open(path, 'w+') as detail_file:
detail_file.write(rst.encode('utf-8'))
class JavaScriptBase(AutoAPIBase):
language = 'javascript'
def __init__(self, obj):
'''
Map JSON data into Python object.
This is the standard object that will be rendered into the templates,
so we try and keep standard naming to keep templates more re-usable.
'''
super(JavaScriptBase, self).__init__(obj)
self.name = obj.get('name')
self.id = self.name
# Second level
self.docstring = obj.get('description', '')
#self.docstring = obj.get('comment', '')
# self.docstring = obj.get('comment', '')
self.imports = obj.get('imports', [])
self.children = []
@ -127,30 +104,6 @@ class JavaScriptBase(AutoAPIBase):
pass
@property
def short_name(self):
'''Shorten name property'''
return self.name.split('.')[-1]
@property
def namespace(self):
pieces = self.id.split('.')[:-1]
if pieces:
return '.'.join(pieces)
@property
def ref_type(self):
return self.type
@property
def ref_directive(self):
return self.type
@property
def methods(self):
return self.obj.get('methods', [])
class JavaScriptClass(JavaScriptBase):
type = 'class'
ref_directive = 'class'

@ -1,11 +1,8 @@
import os
from collections import defaultdict
from sphinx.util.osutil import ensuredir
from epyparse import parsed
from .base import AutoAPIBase, AutoAPIDomain
from ..settings import env
class PythonDomain(AutoAPIDomain):
@ -17,6 +14,23 @@ class PythonDomain(AutoAPIDomain):
:param app: Sphinx application passed in as part of the extension
'''
def read_file(self, path, **kwargs):
'''Read file input into memory, returning deserialized objects
:param path: Path of file to read
'''
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
try:
parsed_data = parsed(path)
return parsed_data
except IOError:
print Warning('Error reading file: {0}'.format(path))
except TypeError:
print Warning('Error reading file: {0}'.format(path))
return None
def create_class(self, data):
'''Return instance of class based on Roslyn type property
@ -38,155 +52,36 @@ class PythonDomain(AutoAPIDomain):
except KeyError:
self.app.warn("Unknown Type: %s" % data['type'])
else:
obj = cls(data)
obj = cls(data, jinja_env=self.jinja_env)
if 'children' in data:
for child_data in data['children']:
for child_obj in self.create_class(child_data):
obj.children.append(child_obj)
yield obj
def read_file(self, path):
'''Read file input into memory, returning deserialized objects
:param path: Path of file to read
'''
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
try:
parsed_data = parsed(path)
return parsed_data
except IOError:
print Warning('Error reading file: {0}'.format(path))
except TypeError:
print Warning('Error reading file: {0}'.format(path))
return None
def get_objects(self, pattern):
'''Trigger find of serialized sources and build objects'''
for path in self.find_files(pattern):
data = self.read_file(path)
if data:
for obj in self.create_class(data):
self.add_object(obj)
def add_object(self, obj):
'''Add object to local and app environment storage
:param obj: Instance of a AutoAPI object
'''
self.app.env.autoapi_data.append(obj)
self.objects[obj.name] = obj
def organize_objects(self):
'''Organize objects and namespaces'''
def _recurse_ns(obj):
if not obj:
return
namespace = obj.namespace
if namespace is not None:
ns_obj = None
for (n, search_obj) in enumerate(self.app.env.autoapi_data):
if (search_obj.id == namespace and
isinstance(search_obj, PythonModule)):
ns_obj = self.app.env.autoapi_data[n]
if ns_obj is None:
ns_obj = list(self.create_class({'id': namespace,
'type': 'module'}))[0]
self.app.env.autoapi_data.append(ns_obj)
self.namespaces[ns_obj.id] = ns_obj
if obj.id not in (child.id for child in ns_obj.children):
ns_obj.children.append(obj)
_recurse_ns(ns_obj)
for obj in self.app.env.autoapi_data:
_recurse_ns(obj)
def full(self):
print "Reading"
self.get_objects(self.get_config('autoapi_file_pattern'))
self.organize_objects()
print "Writing"
self.generate_output()
self.write_indexes()
def generate_output(self):
for obj in self.app.env.autoapi_data:
# TODO not here!
for child in obj.children:
obj.item_map[child.type].append(child)
for key in obj.item_map.keys():
obj.item_map[key].sort()
rst = obj.render()
# Detail
detail_dir = os.path.join(self.get_config('autoapi_root'),
*obj.name.split('.'))
ensuredir(detail_dir)
# TODO: Better way to determine suffix?
path = os.path.join(detail_dir, '%s%s' % ('index', self.get_config('source_suffix')[0]))
if rst:
with open(path, 'w+') as detail_file:
detail_file.write(rst)
class PythonBase(AutoAPIBase):
language = 'python'
def __init__(self, obj):
def __init__(self, obj, jinja_env):
super(PythonBase, self).__init__(obj)
self.jinja_env = jinja_env
# Always exist
self.id = obj['fullname']
self.name = self.obj.get('fullname', self.id)
# Optional
self.imports = obj.get('imports', [])
self.children = []
self.parameters = obj.get('params', [])
self.docstring = obj.get('docstring', '')
self.methods = obj.get('methods', [])
# For later
self.item_map = defaultdict(list)
def __str__(self):
return '<{cls} {id}>'.format(cls=self.__class__.__name__,
id=self.id)
@property
def name(self):
'''Return short name for member id
'''
try:
return self.obj['fullname']
except KeyError:
return self.id
@property
def short_name(self):
'''Shorten name property'''
return self.name.split('.')[-1]
@property
def namespace(self):
pieces = self.id.split('.')[:-1]
if pieces:
return '.'.join(pieces)
@property
def ref_type(self):
return self.type
@property
def ref_directive(self):
return self.type
@property
def methods(self):
return self.obj.get('methods', [])
class PythonFunction(PythonBase):
type = 'function'

@ -3,6 +3,7 @@
Sphinx Auto-API
"""
import os
import fnmatch
import shutil
@ -26,15 +27,29 @@ def load_yaml(app):
return
app.env.autoapi_data = []
if app.config.autoapi_type == 'dotnet':
domain = DotNetDomain(app)
elif app.config.autoapi_type == 'python':
domain = PythonDomain(app)
elif app.config.autoapi_type == 'go':
domain = GoDomain(app)
elif app.config.autoapi_type == 'javascript':
domain = JavaScriptDomain(app)
domain.full()
mapping = {
'python': PythonDomain,
'dotnet': DotNetDomain,
'go': GoDomain,
'javascript': JavaScriptDomain,
}
domain = mapping[app.config.autoapi_type]
domain_obj = domain(app)
app.info('[AutoAPI] Loading Data')
domain_obj.load(
pattern=app.config.autoapi_file_pattern,
dir=os.path.normpath(app.config.autoapi_dir),
ignore=app.config.autoapi_ignore,
)
app.info('[AutoAPI] Mapping Data')
domain_obj.map()
app.info('[AutoAPI] Rendering Data')
domain_obj.output_rst(
root=app.config.autoapi_root,
# TODO: Better way to determine suffix?
source_suffix=app.config.source_suffix[0]
)
def build_finished(app, exception):

@ -18,3 +18,14 @@
{%- endfor %}
{% endif %}
{% block content %}
{%- for obj_item in obj.children|sort %}
{% macro render() %}{{ obj_item.render() }}{% endmacro %}
{{ render()|indent(0) }}
{%- endfor %}
{% endblock %}

@ -28,25 +28,13 @@
{% block content %}
{%- macro display_type(item_type) %}
{{ item_type.title() }}
{{ "*" * item_type|length }}
{% block content %}
{%- for obj_item in obj.children|sort %}
{%- for obj_item in obj.item_map.get(item_type, []) %}
{% macro render() %}{{ obj_item.render() }}{% endmacro %}
{{ render()|indent(4) }}
{{ render()|indent(0) }}
{%- endfor %}
{%- endmacro %}
{%- for item_type in obj.item_map.keys() %}
{% if item_type.lower() != 'module' %}
{{ display_type(item_type) }}
{% endif %}
{%- endfor %}
{% endblock %}

@ -18,17 +18,7 @@ class FullPythonTests(unittest.TestCase):
with open('_build/text/autoapi/example/index.txt') as fin:
text = fin.read().strip()
self.assertEquals(text, '''example
*******
Function
========
example.example_function()
Compute the square root of x and return it.''')
self.assertIn('Compute the square root of x and return it.', text)
finally:
os.chdir('../..')

Loading…
Cancel
Save