2017-09-12 17:59:21 +00:00
|
|
|
import re
|
2016-11-02 23:29:28 +00:00
|
|
|
import sys
|
2016-08-25 23:24:14 +00:00
|
|
|
import os
|
|
|
|
import textwrap
|
2016-11-02 23:29:28 +00:00
|
|
|
import ast
|
2017-04-07 18:46:57 +00:00
|
|
|
import tokenize as tk
|
2018-02-19 19:09:11 +00:00
|
|
|
import collections
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
import astroid
|
2017-09-01 00:32:04 +00:00
|
|
|
import sphinx
|
|
|
|
import sphinx.util.docstrings
|
2015-04-08 05:54:53 +00:00
|
|
|
|
2016-11-02 23:45:41 +00:00
|
|
|
from .base import PythonMapperBase, SphinxMapperBase
|
2017-09-12 17:59:21 +00:00
|
|
|
from . import astroid_utils
|
2016-11-02 23:45:41 +00:00
|
|
|
from ..utils import slugify
|
|
|
|
|
2016-11-02 23:29:28 +00:00
|
|
|
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
|
|
|
|
2016-11-02 23:29:28 +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
|
2016-11-02 23:29:28 +00:00
|
|
|
"""
|
|
|
|
|
2017-11-05 23:29:39 +00:00
|
|
|
def load(self, patterns, dirs, ignore=None):
|
2016-11-02 23:29:28 +00:00
|
|
|
"""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:
|
2017-11-05 23:29:39 +00:00
|
|
|
for path in self.find_files(patterns=patterns, dirs=[dir_], ignore=ignore):
|
2016-11-02 23:29:28 +00:00
|
|
|
data = self.read_file(path=path)
|
|
|
|
if data:
|
2018-02-19 19:09:11 +00:00
|
|
|
data['relative_path'] = os.path.relpath(path, dir_)
|
2016-11-02 23:29:28 +00:00
|
|
|
self.paths[path] = data
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2015-06-06 23:11:49 +00:00
|
|
|
def read_file(self, path, **kwargs):
|
2016-11-02 23:29:28 +00:00
|
|
|
"""Read file input into memory, returning deserialized objects
|
2015-06-06 23:11:49 +00:00
|
|
|
|
|
|
|
:param path: Path of file to read
|
2016-11-02 23:29:28 +00:00
|
|
|
"""
|
2015-06-06 23:11:49 +00:00
|
|
|
try:
|
2017-09-12 17:59:21 +00:00
|
|
|
parsed_data = Parser().parse_file(path)
|
2015-06-06 23:11:49 +00:00
|
|
|
return parsed_data
|
2016-11-02 23:29:28 +00:00
|
|
|
except (IOError, TypeError, ImportError):
|
2015-07-07 23:19:25 +00:00
|
|
|
self.app.warn('Error reading file: {0}'.format(path))
|
2015-06-06 23:11:49 +00:00
|
|
|
return None
|
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
def map(self, options=None):
|
|
|
|
super(PythonSphinxMapper, self).map(options)
|
|
|
|
|
|
|
|
parents = {obj.name: obj for obj in self.objects.values()}
|
|
|
|
for obj in self.objects.values():
|
|
|
|
parent_name = obj.name.rsplit('.', 1)[0]
|
|
|
|
if parent_name in parents and parent_name != obj.name:
|
|
|
|
parent = parents[parent_name]
|
|
|
|
attr = 'sub{}s'.format(obj.type)
|
|
|
|
getattr(parent, attr).append(obj)
|
|
|
|
|
|
|
|
for obj in self.objects.values():
|
|
|
|
obj.submodules.sort()
|
|
|
|
obj.subpackages.sort()
|
|
|
|
|
2017-06-29 21:37:59 +00:00
|
|
|
def create_class(self, data, options=None, **kwargs):
|
2016-10-25 23:26:30 +00:00
|
|
|
"""Create a class from the passed in data
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
:param data: dictionary data of parser output
|
2016-10-25 23:26:30 +00:00
|
|
|
"""
|
2015-05-31 05:03:19 +00:00
|
|
|
obj_map = dict((cls.type, cls) for cls
|
2016-11-02 23:29:28 +00:00
|
|
|
in [PythonClass, PythonFunction, PythonModule,
|
2017-09-12 17:59:21 +00:00
|
|
|
PythonMethod, PythonPackage, PythonAttribute,
|
|
|
|
PythonData])
|
2015-05-31 05:03:19 +00:00
|
|
|
try:
|
2017-09-12 17:59:21 +00:00
|
|
|
cls = obj_map[data['type']]
|
2015-05-31 05:03:19 +00:00
|
|
|
except KeyError:
|
2017-09-12 17:59:21 +00:00
|
|
|
self.app.warn("Unknown type: %s" % data['type'])
|
2015-05-31 05:03:19 +00:00
|
|
|
else:
|
2016-08-25 23:24:14 +00:00
|
|
|
obj = cls(data, jinja_env=self.jinja_env,
|
2016-11-02 23:29:28 +00:00
|
|
|
options=self.app.config.autoapi_options, **kwargs)
|
2017-09-01 00:32:04 +00:00
|
|
|
|
|
|
|
lines = sphinx.util.docstrings.prepare_docstring(obj.docstring)
|
|
|
|
try:
|
2017-09-12 17:59:21 +00:00
|
|
|
if lines:
|
|
|
|
self.app.emit(
|
|
|
|
'autodoc-process-docstring',
|
|
|
|
cls.type,
|
|
|
|
obj.name,
|
|
|
|
None, # object
|
|
|
|
None, # options
|
|
|
|
lines,
|
|
|
|
)
|
2017-09-01 00:32:04 +00:00
|
|
|
except KeyError:
|
|
|
|
if (sphinx.version_info >= (1, 6)
|
|
|
|
and 'autodoc-process-docstring' in self.app.events.events):
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
obj.docstring = '\n'.join(lines)
|
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
for child_data in data.get('children', []):
|
2016-11-02 23:29:28 +00:00
|
|
|
for child_obj in self.create_class(child_data, options=options,
|
|
|
|
**kwargs):
|
2016-06-09 22:43:55 +00:00
|
|
|
obj.children.append(child_obj)
|
2015-05-31 05:03:19 +00:00
|
|
|
yield obj
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
def _output_top_rst(self, root):
|
|
|
|
# Render Top Index
|
|
|
|
top_level_index = os.path.join(root, 'index.rst')
|
|
|
|
pages = [obj for obj in self.objects.values() if '.' not in obj.name]
|
|
|
|
with open(top_level_index, 'w+') as top_level_file:
|
|
|
|
content = self.jinja_env.get_template('index.rst')
|
|
|
|
top_level_file.write(content.render(pages=pages))
|
|
|
|
|
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'
|
2016-11-02 23:29:28 +00:00
|
|
|
is_callable = False
|
2015-04-08 05:54:53 +00:00
|
|
|
|
2015-06-10 18:01:06 +00:00
|
|
|
def __init__(self, obj, **kwargs):
|
2015-06-10 21:23:50 +00:00
|
|
|
super(PythonPythonMapper, self).__init__(obj, **kwargs)
|
2015-06-06 23:11:49 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
self.name = obj['name']
|
2016-11-02 23:29:28 +00:00
|
|
|
self.id = slugify(self.name)
|
2015-04-08 05:54:53 +00:00
|
|
|
|
2015-04-21 05:54:32 +00:00
|
|
|
# Optional
|
|
|
|
self.children = []
|
2017-09-12 17:59:21 +00:00
|
|
|
self.args = obj.get('args')
|
|
|
|
self.docstring = obj['doc']
|
2015-04-08 05:54:53 +00:00
|
|
|
|
2015-04-21 05:54:32 +00:00
|
|
|
# For later
|
2018-02-19 19:09:11 +00:00
|
|
|
self.item_map = collections.defaultdict(list)
|
2015-04-08 05:54:53 +00:00
|
|
|
|
2017-09-01 17:47:49 +00:00
|
|
|
@property
|
|
|
|
def args(self):
|
|
|
|
return self._args
|
|
|
|
|
|
|
|
@args.setter
|
|
|
|
def args(self, value):
|
|
|
|
self._args = value
|
|
|
|
|
2015-06-10 20:33:42 +00:00
|
|
|
@property
|
2016-11-02 23:29:28 +00:00
|
|
|
def is_undoc_member(self):
|
2018-06-02 17:57:45 +00:00
|
|
|
return not bool(self.docstring)
|
2015-06-10 20:33:42 +00:00
|
|
|
|
|
|
|
@property
|
2016-11-02 23:29:28 +00:00
|
|
|
def is_private_member(self):
|
2017-09-12 17:59:21 +00:00
|
|
|
return (
|
|
|
|
self.short_name.startswith('_')
|
|
|
|
and not self.short_name.endswith('__')
|
|
|
|
)
|
2015-06-10 20:33:42 +00:00
|
|
|
|
|
|
|
@property
|
2016-11-02 23:29:28 +00:00
|
|
|
def is_special_member(self):
|
2017-04-07 18:46:57 +00:00
|
|
|
return (
|
2017-09-12 17:59:21 +00:00
|
|
|
self.short_name.startswith('__')
|
|
|
|
and self.short_name.endswith('__')
|
2017-04-07 18:46:57 +00:00
|
|
|
)
|
2015-06-10 20:33:42 +00:00
|
|
|
|
2015-06-10 20:58:52 +00:00
|
|
|
@property
|
|
|
|
def display(self):
|
2016-11-02 23:29:28 +00:00
|
|
|
if self.is_undoc_member and 'undoc-members' not in self.options:
|
2015-06-10 20:33:42 +00:00
|
|
|
return False
|
2016-11-02 23:29:28 +00:00
|
|
|
if self.is_private_member and 'private-members' not in self.options:
|
2015-06-10 20:33:42 +00:00
|
|
|
return False
|
2016-11-02 23:29:28 +00:00
|
|
|
if self.is_special_member and 'special-members' not in self.options:
|
2015-06-10 20:33:42 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2017-08-31 23:42:47 +00:00
|
|
|
@property
|
|
|
|
def summary(self):
|
|
|
|
for line in self.docstring.splitlines():
|
|
|
|
line = line.strip()
|
|
|
|
if line:
|
|
|
|
return line
|
|
|
|
|
|
|
|
return ''
|
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
def _children_of_type(self, type_):
|
|
|
|
return list(child for child in self.children if child.type == type_)
|
|
|
|
|
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'
|
2016-11-02 23:29:28 +00:00
|
|
|
is_callable = True
|
2017-08-31 23:42:47 +00:00
|
|
|
ref_directive = 'func'
|
2015-04-08 05:54:53 +00:00
|
|
|
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2016-06-09 22:43:55 +00:00
|
|
|
class PythonMethod(PythonPythonMapper):
|
|
|
|
type = 'method'
|
2016-11-02 23:29:28 +00:00
|
|
|
is_callable = True
|
2017-08-31 23:42:47 +00:00
|
|
|
ref_directive = 'meth'
|
2016-06-09 22:43:55 +00:00
|
|
|
|
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
class PythonData(PythonPythonMapper):
|
|
|
|
"""Global, module level data."""
|
|
|
|
type = 'data'
|
|
|
|
|
|
|
|
def __init__(self, obj, **kwargs):
|
|
|
|
super(PythonData, self).__init__(obj, **kwargs)
|
|
|
|
|
|
|
|
self.value = obj.get('value')
|
|
|
|
|
|
|
|
|
|
|
|
class PythonAttribute(PythonData):
|
|
|
|
"""An object/class level attribute."""
|
|
|
|
type = 'attribute'
|
|
|
|
|
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
class TopLevelPythonPythonMapper(PythonPythonMapper):
|
2015-06-10 20:13:34 +00:00
|
|
|
top_level_object = True
|
2017-08-31 23:44:02 +00:00
|
|
|
ref_directive = 'mod'
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
def __init__(self, obj, **kwargs):
|
|
|
|
super(TopLevelPythonPythonMapper, self).__init__(obj, **kwargs)
|
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
self._resolve_name()
|
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
self.subpackages = []
|
|
|
|
self.submodules = []
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
@property
|
|
|
|
def functions(self):
|
|
|
|
return self._children_of_type('function')
|
2015-04-21 05:54:32 +00:00
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
@property
|
|
|
|
def classes(self):
|
|
|
|
return self._children_of_type('class')
|
|
|
|
|
|
|
|
|
|
|
|
class PythonModule(TopLevelPythonPythonMapper):
|
|
|
|
type = 'module'
|
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
def _resolve_name(self):
|
|
|
|
name = self.obj['relative_path']
|
|
|
|
name = name.replace('/', '.')
|
|
|
|
ext = '.py'
|
|
|
|
if name.endswith(ext):
|
|
|
|
name = name[:-len(ext)]
|
|
|
|
|
|
|
|
self.name = name
|
|
|
|
|
2017-08-31 23:44:02 +00:00
|
|
|
|
|
|
|
class PythonPackage(TopLevelPythonPythonMapper):
|
2016-08-25 23:24:14 +00:00
|
|
|
type = 'package'
|
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
def _resolve_name(self):
|
|
|
|
name = self.obj['relative_path']
|
|
|
|
|
|
|
|
exts = ['/__init__.py', '.py']
|
|
|
|
for ext in exts:
|
|
|
|
if name.endswith(ext):
|
|
|
|
name = name[:-len(ext)]
|
|
|
|
name = name.replace('/', '.')
|
|
|
|
|
|
|
|
self.name = name
|
|
|
|
|
2016-08-25 23:24:14 +00:00
|
|
|
|
2015-06-10 21:23:50 +00:00
|
|
|
class PythonClass(PythonPythonMapper):
|
2015-04-21 05:54:32 +00:00
|
|
|
type = 'class'
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
def __init__(self, obj, **kwargs):
|
|
|
|
super(PythonClass, self).__init__(obj, **kwargs)
|
|
|
|
|
|
|
|
self.bases = obj['bases']
|
|
|
|
|
2017-09-01 17:47:49 +00:00
|
|
|
@PythonPythonMapper.args.getter
|
|
|
|
def args(self):
|
2017-09-12 17:59:21 +00:00
|
|
|
args = self._args
|
2017-09-01 17:47:49 +00:00
|
|
|
|
|
|
|
for child in self.children:
|
|
|
|
if child.short_name == '__init__':
|
2017-09-12 17:59:21 +00:00
|
|
|
args = child.args
|
|
|
|
break
|
2017-09-01 17:47:49 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
if args.startswith('self'):
|
|
|
|
args = args[4:].lstrip(',').lstrip()
|
2017-09-01 17:47:49 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
return args
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
@property
|
|
|
|
def methods(self):
|
|
|
|
return self._children_of_type('method')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def attributes(self):
|
|
|
|
return self._children_of_type('attribute')
|
|
|
|
|
|
|
|
|
|
|
|
class Parser(object):
|
|
|
|
def parse_file(self, file_path):
|
2018-02-19 19:09:11 +00:00
|
|
|
directory, filename = os.path.split(file_path)
|
|
|
|
module_part = os.path.splitext(filename)[0]
|
|
|
|
module_parts = collections.deque([module_part])
|
|
|
|
while os.path.isfile(os.path.join(directory, '__init__.py')):
|
|
|
|
directory, module_part = os.path.split(directory)
|
|
|
|
if module_part:
|
|
|
|
module_parts.appendleft(module_part)
|
|
|
|
|
|
|
|
module_name = '.'.join(module_parts)
|
|
|
|
node = astroid.MANAGER.ast_from_file(file_path, module_name)
|
2017-09-12 17:59:21 +00:00
|
|
|
return self.parse(node)
|
|
|
|
|
|
|
|
def parse_assign(self, node):
|
|
|
|
doc = ''
|
|
|
|
doc_node = node.next_sibling()
|
|
|
|
if (isinstance(doc_node, astroid.nodes.Expr)
|
|
|
|
and isinstance(doc_node.value, astroid.nodes.Const)):
|
|
|
|
doc = doc_node.value.value
|
|
|
|
|
|
|
|
type_ = 'data'
|
|
|
|
if (isinstance(node.scope(), astroid.nodes.ClassDef)
|
|
|
|
or astroid_utils.is_constructor(node.scope())):
|
|
|
|
type_ = 'attribute'
|
|
|
|
|
|
|
|
assign_value = astroid_utils.get_assign_value(node)
|
|
|
|
if not assign_value:
|
|
|
|
return []
|
|
|
|
|
|
|
|
target, value = assign_value
|
|
|
|
data = {
|
|
|
|
'type': type_,
|
|
|
|
'name': target,
|
|
|
|
'doc': doc,
|
|
|
|
'value': value,
|
2018-05-11 00:49:32 +00:00
|
|
|
'from_line_no': node.fromlineno,
|
|
|
|
'to_line_no': node.tolineno,
|
2017-09-12 17:59:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return [data]
|
|
|
|
|
|
|
|
def parse_classdef(self, node, data=None):
|
|
|
|
args = ''
|
2017-04-07 18:46:57 +00:00
|
|
|
try:
|
2017-09-12 17:59:21 +00:00
|
|
|
constructor = node.lookup('__init__')[1]
|
|
|
|
except IndexError:
|
2017-04-07 18:46:57 +00:00
|
|
|
pass
|
2017-09-12 17:59:21 +00:00
|
|
|
else:
|
|
|
|
if isinstance(constructor, astroid.nodes.FunctionDef):
|
|
|
|
args = constructor.args.as_string()
|
|
|
|
|
|
|
|
basenames = list(astroid_utils.get_full_basenames(node.bases, node.basenames))
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'type': 'class',
|
|
|
|
'name': node.name,
|
|
|
|
'args': args,
|
|
|
|
'bases': basenames,
|
|
|
|
'doc': node.doc or '',
|
2018-05-11 00:49:32 +00:00
|
|
|
'from_line_no': node.fromlineno,
|
|
|
|
'to_line_no': node.tolineno,
|
2017-09-12 17:59:21 +00:00
|
|
|
'children': [],
|
|
|
|
}
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
for child in node.get_children():
|
|
|
|
child_data = self.parse(child)
|
|
|
|
if child_data:
|
|
|
|
data['children'].extend(child_data)
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
return [data]
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
def _parse_property(self, node):
|
|
|
|
data = {
|
|
|
|
'type': 'attribute',
|
|
|
|
'name': node.name,
|
|
|
|
'doc': node.doc or '',
|
2018-05-11 00:49:32 +00:00
|
|
|
'from_line_no': node.fromlineno,
|
|
|
|
'to_line_no': node.tolineno,
|
2017-09-12 17:59:21 +00:00
|
|
|
}
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
return [data]
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
def parse_functiondef(self, node):
|
|
|
|
if astroid_utils.is_decorated_with_property(node):
|
|
|
|
return self._parse_property(node)
|
|
|
|
elif astroid_utils.is_decorated_with_property_setter(node):
|
|
|
|
return []
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
type_ = 'function'
|
|
|
|
if isinstance(node.parent.scope(), astroid.nodes.ClassDef):
|
|
|
|
type_ = 'method'
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
data = {
|
|
|
|
'type': type_,
|
|
|
|
'name': node.name,
|
|
|
|
'args': node.args.as_string(),
|
|
|
|
'doc': node.doc or '',
|
2018-05-11 00:49:32 +00:00
|
|
|
'from_line_no': node.fromlineno,
|
|
|
|
'to_line_no': node.tolineno,
|
2017-09-12 17:59:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result = [data]
|
|
|
|
|
|
|
|
if node.name == '__init__':
|
|
|
|
for child in node.get_children():
|
|
|
|
if isinstance(child, astroid.Assign):
|
|
|
|
child_data = self.parse_assign(child)
|
|
|
|
result.extend(child_data)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
def parse_module(self, node):
|
|
|
|
type_ = 'module'
|
|
|
|
if node.path.endswith('__init__.py'):
|
|
|
|
type_ = 'package'
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'type': type_,
|
|
|
|
'name': node.name,
|
|
|
|
'doc': node.doc or '',
|
|
|
|
'children': [],
|
2018-05-11 00:49:32 +00:00
|
|
|
'file_path': node.path,
|
2017-09-12 17:59:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for child in node.get_children():
|
|
|
|
child_data = self.parse(child)
|
|
|
|
if child_data:
|
|
|
|
data['children'].extend(child_data)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
def parse(self, node):
|
|
|
|
data = {}
|
|
|
|
|
|
|
|
node_type = node.__class__.__name__.lower()
|
|
|
|
parse_func = getattr(self, 'parse_' + node_type, None)
|
|
|
|
if parse_func:
|
|
|
|
data = parse_func(node)
|
|
|
|
else:
|
|
|
|
for child in node.get_children():
|
|
|
|
data = self.parse(child)
|
|
|
|
if data:
|
|
|
|
break
|
2017-04-07 18:46:57 +00:00
|
|
|
|
2017-09-12 17:59:21 +00:00
|
|
|
return data
|