diff --git a/doc/_extensions/autojsdoc/README.rst b/doc/_extensions/autojsdoc/README.rst new file mode 100644 index 00000000000..ef7681778a1 --- /dev/null +++ b/doc/_extensions/autojsdoc/README.rst @@ -0,0 +1,87 @@ +:orphan: + +====================================== +JSDoc parser/Sphinx extension for Odoo +====================================== + +Why? +==== + +Spent about a week trying to coerce "standard" javascript tools (jsdoc_ with +the hope of using sphinx-js_ for integration or `documentation.js`_) and +failed to ever get a sensible result: failed to get any result with the +current state of the documentation, significant changes/additions/fixes to +docstrings brought this up to "garbage output" level. + +Bug reports and mailing list posts didn't show any path to improvement on the +ES5 codebase (if we ever go whole-hog on ES6 modules and classes things could +be different, in fact most of JSDoc's current effort seem focused on +ES6/ES2015 features) but both experience and looking at the mailing lists +told me that spending more time would be wasted. + +Even more so as I was writing visitors/rewriters to generate documentation +from our existing structure, which broadly speaking is relatively strict, and +thus + +What? +===== + +If it were possible to generate JSDoc annotations from our relatively +well-defined code structures, it was obviously possible to extract documentary +information directly from it, hence this Odoo-specific package/extension +trying to do exactly that. + +This package should eventually provide: + +* a command-line interface which can be invoked via ``-m autojsdoc`` (assuming + your ``PYTHONPATH`` can find it) which should allow dumping the parsed AST + in a convenient-ish form, possibly doing searches through the AST, a + dependency graph extractor/analysis and a text dumper for the + documentation. + +* a sphinx extension (``autojsdoc.sphinx``) which can be used to integrate the + parsed JSDoc information into the Sphinx doc. + +How? +==== + +Sphinx-aside, the package relies on 3 libraries: + +* pyjsparser_, an Esprima-compliant ES5.1 parser (with bits of ES6 support), + sadly it does not support comments in its current form so I had to fork it. + Fed a javascript source file, pyjsparser_ simply generates a bunch of nested + dicts representing an Esprima ast, ast-types_ does a reasonably good job of + describing it once you understand that "bases" are basically just structural + mixins. + + Because the original does not, this package provides a ``visitor`` module + for pyjsparser_ ASTs. + +* pyjsdoc_, a one-file "port" of jsdoc, can actually do much of the JS parsing + (using string munging) but its core semantics don't fit our needs so I'm + only using it to parse the actual JSDoc content, and the ``jsdoc`` module + contains some replacement classes, extensions & monkey patches for things + `pyjsdoc`_ itself does not support, at the time of this writing: + + - a bug in FunctionDoc.return_val + - a type on FunctionDoc so it's compatible with ParamDoc + - a more reliable comments-parsing function + - a replacement ModuleDoc as the original does not materialise AMD modules + - a ClassDoc extension to support mixins + - two additional CommentDoc extensions for "namespaces" objects (bag of + attributes without any more information) and mixin objects + +* pytest_ to configure and run the test suite, which you can run by invoking + ``pytest doc/_extensions`` from the project top-level, the tests represent + both "happy path" things we want to parse and various code patterns which + tripped the happy path because e.g. they were matched and should not have, + they were not matched and should have, or they were more complex than the + happy path had expected + +.. _ast-types: _https://github.com/benjamn/ast-types/blob/master/def/core.js +.. _documentation.js: http://documentation.js.org +.. _jsdoc: http://usejsdoc.org +.. _pyjsdoc: https://github.com/nostrademons/pyjsdoc +.. _pyjsparser: https://github.com/PiotrDabkowski/pyjsparser +.. _pytest: https://pytest.org/ +.. _sphinx-js: https://sphinx-js-howto.readthedocs.io diff --git a/doc/_extensions/autojsdoc/__init__.py b/doc/_extensions/autojsdoc/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/doc/_extensions/autojsdoc/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/doc/_extensions/autojsdoc/__main__.py b/doc/_extensions/autojsdoc/__main__.py new file mode 100644 index 00000000000..20ab65c2c37 --- /dev/null +++ b/doc/_extensions/autojsdoc/__main__.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +import cgitb +import fnmatch +import io +import logging + +import click + +import pyjsdoc +import pyjsparser +import sys + +from .parser.parser import ModuleMatcher +from .parser.visitor import Visitor, SKIP +from . import jsdoc + +class Printer(Visitor): + def __init__(self, level=0): + super(Printer, self).__init__() + self.level = level + + def _print(self, text): + print ' ' * self.level, text + + def enter_generic(self, node): + self._print(node['type']) + self.level += 1 + + def exit_generic(self, node): + self.level -= 1 + + def enter_Identifier(self, node): + self._print(node['name']) + return SKIP + + def enter_Literal(self, node): + self._print(node['value']) + return SKIP + + def enter_BinaryExpression(self, node): + self._print(node['operator']) + self.level += 1 + +def visit_files(files, visitor, ctx): + for name in files: + with io.open(name) as f: + ctx.logger.info("%s", name) + try: + yield visitor().visit(pyjsparser.parse(f.read())) + except Exception as e: + if ctx.logger.isEnabledFor(logging.DEBUG): + ctx.logger.exception("while visiting %s", name) + else: + ctx.logger.error("%s while visiting %s", e, name) + +# bunch of modules various bits depend on which are not statically defined +# (or are outside the scope of the system) +ABSTRACT_MODULES = [ + jsdoc.ModuleDoc({ + 'module': 'web.web_client', + 'dependency': {'web.AbstractWebClient'}, + 'exports': jsdoc.NSDoc({ + 'name': 'web_client', + 'doc': 'instance of AbstractWebClient', + }), + }), + jsdoc.ModuleDoc({ + 'module': 'web.Tour', + 'dependency': {'web_tour.TourManager'}, + 'exports': jsdoc.NSDoc({ + 'name': 'Tour', + 'doc': 'maybe tourmanager instance?', + }), + }), + # OH FOR FUCK'S SAKE + jsdoc.ModuleDoc({ + 'module': 'summernote/summernote', + 'exports': jsdoc.NSDoc({'doc': "totally real summernote"}), + }) +] + +@click.group(context_settings={'help_option_names': ['-h', '--help']}) +@click.option('-v', '--verbose', count=True) +@click.option('-q', '--quiet', count=True) +@click.pass_context +def autojsdoc(ctx, verbose, quiet): + logging.basicConfig( + level=logging.INFO + (quiet - verbose) * 10, + format="[%(levelname)s %(created)f] %(message)s", + ) + ctx.logger = logging.getLogger('autojsdoc') + ctx.visitor = None + ctx.files = [] + ctx.kw = {} + +@autojsdoc.command() +@click.argument('files', type=click.Path(exists=True), nargs=-1) +@click.pass_context +def ast(ctx, files): + """ Prints a structure tree of the provided files + """ + if not files: + print(ctx.get_help()) + visit_files(files, lambda: Printer(level=1), ctx.parent) + +@autojsdoc.command() +@click.option('-m', '--module', multiple=True, help="Only shows dependencies matching any of the patterns") +@click.argument('files', type=click.Path(exists=True), nargs=-1) +@click.pass_context +def dependencies(ctx, module, files): + """ Prints a dot file of all modules to stdout + """ + if not files: + print(ctx.get_help()) + byname = { + mod.name: mod.dependencies + for mod in ABSTRACT_MODULES + } + for modules in visit_files(files, ModuleMatcher, ctx.parent): + for mod in modules: + byname[mod.name] = mod.dependencies + + print('digraph dependencies {') + + todo = set() + # if module filters, roots are only matching modules + if module: + for f in module: + todo.update(fnmatch.filter(byname.keys(), f)) + + for m in todo: + # set a different box for selected roots + print(' "%s" [color=orangered]' % m) + else: + # otherwise check all modules + todo.update(byname) + + done = set() + while todo: + node = todo.pop() + if node in done: + continue + + done.add(node) + deps = byname[node] + todo.update(deps - done) + for dep in deps: + print(' "%s" -> "%s";' % (node, dep)) + print('}') + +try: + autojsdoc.main(prog_name='autojsdoc') +except Exception: + print(cgitb.text(sys.exc_info())) diff --git a/doc/_extensions/autojsdoc/ext/__init__.py b/doc/_extensions/autojsdoc/ext/__init__.py new file mode 100644 index 00000000000..e27a9e3d318 --- /dev/null +++ b/doc/_extensions/autojsdoc/ext/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from .directives import automodule_bound, autodirective_bound +from .extractor import _get_roots + + +def setup(app): + app.add_config_value('js_roots', _get_roots, 'env') + modules = {} + app.add_directive_to_domain('js', 'automodule', automodule_bound(app, modules) + ) + autodirective = autodirective_bound(app, modules) + for n in ['autonamespace', 'automixin', 'autoclass', 'autofunction']: + app.add_directive_to_domain('js', n, autodirective) diff --git a/doc/_extensions/autojsdoc/ext/directives.py b/doc/_extensions/autojsdoc/ext/directives.py new file mode 100644 index 00000000000..98b3f9e8322 --- /dev/null +++ b/doc/_extensions/autojsdoc/ext/directives.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- +import abc +import collections +import contextlib +import fnmatch +import io +import re + +from docutils import nodes +from docutils.parsers.rst import Directive +from docutils.statemachine import StringList +from sphinx import addnodes +from sphinx.ext.autodoc import members_set_option, bool_option, ALL + +from autojsdoc.ext.extractor import read_js +from ..parser import jsdoc, types + + +@contextlib.contextmanager +def addto(parent, newnode): + assert isinstance(newnode, nodes.Node), \ + "Expected newnode to be a Node, got %s" % (type(newnode)) + yield newnode + parent.append(newnode) + + +def documenter_for(directive, doc): + if isinstance(doc, jsdoc.FunctionDoc): + return FunctionDocumenter(directive, doc) + if isinstance(doc, jsdoc.ClassDoc): + return ClassDocumenter(directive, doc) + if isinstance(doc, jsdoc.MixinDoc): + return MixinDocumenter(directive, doc) + if isinstance(doc, (jsdoc.PropertyDoc, jsdoc.LiteralDoc)): + return PropertyDocumenter(directive, doc) + if isinstance(doc, jsdoc.InstanceDoc): + return InstanceDocumenter(directive, doc) + if isinstance(doc, jsdoc.Unknown): + return UnknownDocumenter(directive, doc) + if isinstance(doc, jsdoc.NSDoc): + return NSDocumenter(directive, doc) + + raise TypeError("No documenter for %s" % type(doc)) + +def to_list(doc, source=None): + return StringList([ + line.rstrip('\n') + for line in io.StringIO(doc) + ], source=source) + +DIRECTIVE_OPTIONS = { + 'members': members_set_option, + 'undoc-members': bool_option, + 'private-members': bool_option, + 'undoc-matches': bool_option, +} +def automodule_bound(app, modules): + class AutoModuleDirective(Directive): + required_arguments = 1 + has_content = True + option_spec = DIRECTIVE_OPTIONS + + # self.state.nested_parse(string, offset, node) => parse context for sub-content (body which can contain RST data) + # => needed for doc (converted?) and for actual directive body + def run(self): + self.env = self.state.document.settings.env + modname = self.arguments[0].strip() + + # TODO: cache/memoize modules & symbols? + if not modules: + read_js(app, modules) + + mods = [ + (name, mod) + for name, mod in modules.items() + if fnmatch.fnmatch(name, modname) + ] + ret = [] + for name, mod in mods: + if mod.is_private: + continue + if name != modname and not (mod.doc or mod.exports): + # this module has no documentation, no exports and was + # not specifically requested through automodule -> skip + # unless requested + if not self.options.get('undoc-matches'): + continue + modsource = mod['sourcefile'] + if modsource: + self.env.note_dependency(modsource) + # not sure what that's used for as normal xrefs are resolved using the id directly + target = nodes.target('', '', ids=['module-' + name], ismod=True) + self.state.document.note_explicit_target(target) + + documenter = ModuleDocumenter(self, mod) + + ret.append(target) + ret.extend(documenter.generate()) + return ret + + return AutoModuleDirective + +def autodirective_bound(app, modules): + documenters = { + 'js:autoclass': ClassDocumenter, + 'js:autonamespace': NSDocumenter, + 'js:autofunction': FunctionDocumenter, + 'js:automixin': MixinDocumenter, + } + class AutoDirective(Directive): + required_arguments = 1 + has_content = True + option_spec = DIRECTIVE_OPTIONS + + def run(self): + self.env = self.state.document.settings.env + + # strip 'js:auto' + objname = self.arguments[0].strip() + + path = self.env.temp_data.get('autojs:prefix', []) + [objname] + item = modules[path[0]] + # deref' namespaces until we reach the object we're looking for + for k in path[1:]: + item = item.get_property(k) + + docclass = documenters[self.name] + return docclass(self, item).generate() + + return AutoDirective + +class Documenter(object): + objtype = None + def __init__(self, directive, doc): + self.directive = directive + self.env = directive.env + self.item = doc + + @property + def modname(self): + return self.env.temp_data.get('autojs:module', '') + @property + def classname(self): + return self.env.temp_data.get('autojs:class', '') + def generate(self, all_members=False): + """ + :rtype: List[nodes.Node] + """ + objname = self.item.name + prefixed = (self.item['sourcemodule'].name + '.' + objname) if self.item['sourcemodule'] else None + objtype = self.objtype + assert objtype, '%s has no objtype' % type(self) + root = addnodes.desc(domain='js', desctype=objtype, objtype=objtype) + with addto(root, addnodes.desc_signature( + module=self.modname or '', + fullname=objname, + )) as s: + s['class'] = self.classname + + s['ids'] = [] + if objname: + s['ids'].append(objname) + if prefixed: + s['ids'].append(prefixed) + + if objtype: + s += addnodes.desc_annotation( + objtype, objtype, + nodes.Text(' '), + ) + + env = self.env + if objname: + env.domaindata['js']['objects'][objname] = (env.docname, objtype) + if prefixed: + env.domaindata['js']['objects'][prefixed] = (env.docname, objtype) + + # TODO: linkcode_resolve + s += self.make_signature() + with addto(root, addnodes.desc_content()) as c: + # must be here otherwise nested_parse(self.content) will not have + # the prefix set + self.env.temp_data.setdefault('autojs:prefix', []).append(self.item.name) + c += self.make_content(all_members=all_members) + self.env.temp_data['autojs:prefix'].pop() + return [root] + + def make_signature(self): + """ + :rtype: List[nodes.Node] + """ + return [addnodes.desc_name(self.item.name, self.item.name)] + + @abc.abstractmethod + def make_content(self, all_members): + """ + :rtype: List[nodes.Node] + """ + + def document_subtypes(self, subtypes): + docs = [] + with with_mapping_value(self.directive.options, 'undoc-members', True): + for cls in subtypes: + docs += ClassDocumenter(self.directive, cls).generate(all_members=True) + return docs + + +class NSDocumenter(Documenter): + objtype = 'namespace' + def make_content(self, all_members): + doc = self.item + ret = nodes.section() + if doc.doc: + self.directive.state.nested_parse(to_list(doc.doc), 0, ret) + ret += self.document_properties(all_members) + return ret.children + + def should_document(self, member, name, all_members): + """ + :type member: jsdoc.CommentDoc + :type name: str + :type all_members: bool + :rtype: bool + """ + options = self.directive.options + + members = options.get('members') or [] + if not (all_members or members is ALL): + # if a member is requested by name, it's always documented + return name in members + + # ctor params are merged into the class doc + if member.is_constructor: + return False + + # only document "private" members if option is set + if self.is_private(member, name) and not options.get('private-members'): + return False + + # TODO: what if member doesn't have a description but has non-desc tags set? + # TODO: add @public to force documenting symbol? => useful for implicit typedef + return bool(member.doc or options.get('undoc-members')) + + def is_private(self, member, name): + return member.is_private + + def document_properties(self, all_members): + ret = nodes.section() + # TODO: :member-order: [alphabetical | groupwise | bysource] + for (n, p) in self.item.properties: + if not self.should_document(p, n, all_members): + continue + # FIXME: maybe should use property name as name inside? + ret += documenter_for(self.directive, p).generate(all_members=True) + return ret.children + +_NONE = object() +@contextlib.contextmanager +def with_mapping_value(mapping, key, value, restore_to=_NONE): + """ Sets ``key`` to ``value`` for the duration of the context. + + If ``restore_to`` is not provided, restores ``key``'s old value + afterwards, removes it entirely if there was no value for ``key`` in the + mapping. + + .. warning:: for defaultdict & similar mappings, may restore the default + value (depends how the collections' .get behaves) + """ + if restore_to is _NONE: + restore_to = mapping.get(key, _NONE) + mapping[key] = value + try: + yield + finally: + if restore_to is _NONE: + del mapping[key] + else: + mapping[key] = restore_to +class ModuleDocumenter(NSDocumenter): + objtype = 'module' + def document_properties(self, all_members): + with with_mapping_value(self.env.temp_data, 'autojs:module', self.item.name, ''): + return super(ModuleDocumenter, self).document_properties(all_members) + + def make_content(self, all_members): + doc = self.item + content = addnodes.desc_content() + + if doc.exports or doc.dependencies: + with addto(content, nodes.field_list()) as fields: + if doc.exports: + with addto(fields, nodes.field()) as field: + field += nodes.field_name('Exports', 'Exports') + with addto(field, nodes.field_body()) as body: + ref = doc['exports'] # warning: not the same as doc.exports + label = ref or '' + link = addnodes.pending_xref( + ref, nodes.paragraph(ref, label), + refdomain='js', + reftype='any', + reftarget=ref, + ) + link['js:module'] = doc.name + body += link + + if doc.dependencies: + with addto(fields, nodes.field()) as field: + self.make_dependencies(field, doc) + + self.directive.state.nested_parse(self.directive.content, 0, content) + + if doc.doc: + # FIXME: source offset + self.directive.state.nested_parse(to_list(doc.doc, source=doc['sourcefile']), 0, content) + + content += self.document_properties(all_members) + + return content + + def make_dependencies(self, field, doc): + field += nodes.field_name("Depends On", "Depends On") + with addto(field, nodes.field_body()) as body: + with addto(body, nodes.bullet_list()) as deps: + for dep in sorted(doc.dependencies): + ref = addnodes.pending_xref( + dep, nodes.paragraph(dep, dep), + refdomain='js', + reftype='module', + reftarget=dep, + ) + deps += nodes.list_item(dep, ref) + + def should_document(self, member, name, all_members): + # member can be Nothing? + if not member: + return False + modname = getattr(member['sourcemodule'], 'name', None) + doc = self.item + + # always document exported symbol (regardless undoc, private, ...) + # otherwise things become... somewhat odd + if name == doc['exports']: + return True + + # if doc['exports'] the module is exporting a "named" item which + # does not need to be documented twice, if not doc['exports'] it's + # exporting an anonymous item (e.g. object literal) which needs to + # be documented on its own + if name == '' and not doc['exports']: + return True + + # TODO: :imported-members: + # FIXME: *directly* re-exported "foreign symbols"? + return (not modname or modname == doc.name) \ + and super(ModuleDocumenter, self).should_document(member, name, all_members) + + +class ClassDocumenter(NSDocumenter): + objtype = 'class' + + def document_properties(self, all_members): + with with_mapping_value(self.env.temp_data, 'autojs:class', self.item.name, ''): + return super(ClassDocumenter, self).document_properties(all_members) + + def make_signature(self): + sig = super(ClassDocumenter, self).make_signature() + sig.append(self.make_parameters()) + return sig + + def make_parameters(self): + params = addnodes.desc_parameterlist('', '') + ctor = self.item.constructor + if ctor: + params += make_desc_parameters(ctor.params) + + return params + + def make_content(self, all_members): + doc = self.item + ret = nodes.section() + + ctor = self.item.constructor + params = subtypes = [] + if ctor: + check_parameters(self, ctor) + params, subtypes = extract_subtypes(doc.name, ctor) + + fields = nodes.field_list() + fields += self.make_super() + fields += self.make_mixins() + fields += self.make_params(params) + if fields.children: + ret += fields + + if doc.doc: + self.directive.state.nested_parse(to_list(doc.doc), 0, ret) + + ret += self.document_properties(all_members) + + ret += self.document_subtypes(subtypes) + + return ret.children + + def is_private(self, member, name): + return name.startswith('_') or super(ClassDocumenter, self).is_private(member, name) + + def make_super(self): + doc = self.item + if not doc.superclass: + return [] + + sup_link = addnodes.pending_xref( + doc.superclass.name, nodes.paragraph(doc.superclass.name, doc.superclass.name), + refdomain='js', reftype='class', reftarget=doc.superclass.name, + ) + sup_link['js:module'] = doc.superclass['sourcemodule'].name + return nodes.field( + '', + nodes.field_name("Extends", "Extends"), + nodes.field_body(doc.superclass.name, sup_link), + ) + + def make_mixins(self): + doc = self.item + if not doc.mixins: + return [] + + ret = nodes.field('', nodes.field_name("Mixes", "Mixes")) + with addto(ret, nodes.field_body()) as body: + with addto(body, nodes.bullet_list()) as mixins: + for mixin in sorted(doc.mixins, key=lambda m: m.name): + mixin_link = addnodes.pending_xref( + mixin.name, nodes.paragraph(mixin.name, mixin.name), + refdomain='js', reftype='mixin', reftarget=mixin.name + ) + mixin_link['js:module'] = mixin['sourcemodule'].name + mixins += nodes.list_item('', mixin_link) + return ret + + def make_params(self, params): + if not params: + return [] + + ret = nodes.field('', nodes.field_name('Parameters', 'Parameters')) + with addto(ret, nodes.field_body()) as body,\ + addto(body, nodes.bullet_list()) as holder: + holder += make_parameters(params, mod=self.modname) + return ret + +class InstanceDocumenter(Documenter): + objtype = 'object' + def make_signature(self): + cls = self.item.cls + ret = super(InstanceDocumenter, self).make_signature() + if cls: + super_ref = addnodes.pending_xref( + cls.name, nodes.Text(cls.name, cls.name), + refdomain='js', reftype='class', reftarget=cls.name + ) + super_ref['js:module'] = cls['sourcemodule'].name + ret.append(addnodes.desc_annotation(' instance of ', ' instance of ')) + ret.append(addnodes.desc_type(cls.name, '', super_ref)) + if not ret: + return [addnodes.desc_name('???', '???')] + return ret + + def make_content(self, all_members): + ret = nodes.section() + if self.item.doc: + self.directive.state.nested_parse(to_list(self.item.doc), 0, ret) + return ret.children + return ret.children + +class FunctionDocumenter(Documenter): + @property + def objtype(self): + return 'method' if self.classname else 'function' + def make_signature(self): + ret = super(FunctionDocumenter, self).make_signature() + with addto(ret, addnodes.desc_parameterlist()) as params: + params += make_desc_parameters(self.item.params) + retval = self.item.return_val + if retval.type or retval.doc: + ret.append(addnodes.desc_returns(retval.type or '*', retval.type or '*')) + return ret + + def make_content(self, all_members): + ret = nodes.section() + doc = self.item + if doc.doc: + self.directive.state.nested_parse(to_list(doc.doc), 0, ret) + + check_parameters(self, doc) + + params, subtypes = extract_subtypes(self.item.name, self.item) + rdoc = doc.return_val.doc + rtype = doc.return_val.type + if params or rtype or rdoc: + with addto(ret, nodes.field_list()) as fields: + if params: + with addto(fields, nodes.field()) as field: + field += nodes.field_name('Parameters', 'Parameters') + with addto(field, nodes.field_body()) as body,\ + addto(body, nodes.bullet_list()) as holder: + holder.extend(make_parameters(params, mod=doc['sourcemodule'].name)) + if rdoc: + with addto(fields, nodes.field()) as field: + field += nodes.field_name("Returns", "Returns") + with addto(field, nodes.field_body()) as body,\ + addto(body, nodes.paragraph()) as p: + p += nodes.inline(rdoc, rdoc) + if rtype: + with addto(fields, nodes.field()) as field: + field += nodes.field_name("Return Type", "Return Type") + with addto(field, nodes.field_body()) as body, \ + addto(body, nodes.paragraph()) as p: + p += make_types(rtype, mod=doc['sourcemodule'].name) + + ret += self.document_subtypes(subtypes) + + return ret.children + +def pascal_case_ify(name): + """ + Uppercase first letter of ``name``, or any letter following an ``_``. In + the latter case, also strips out the ``_``. + + => key_for becomes KeyFor + => options becomes Options + """ + return re.sub(r'(^|_)\w', lambda m: m.group(0)[-1].upper(), name) +def extract_subtypes(parent_name, doc): + """ Extracts composite parameters (a.b) into sub-types for the parent + parameter, swaps the parent's type from whatever it is to the extracted + one, and returns the extracted type for inclusion into the parent. + + :arg parent_name: name of the containing symbol (function, class), will + be used to compose subtype names + :type parent_name: str + :type doc: FunctionDoc + :rtype: (List[ParamDoc], List[ClassDoc]) + """ + # map of {param_name: [ParamDoc]} (from complete doc) + subparams = collections.defaultdict(list) + for p in map(jsdoc.ParamDoc, doc.get_as_list('param')): + pair = p.name.split('.', 1) + if len(pair) == 2: + k, p.name = pair # remove prefix from param name + subparams[k].append(p) + + # keep original params order as that's the order of formal parameters in + # the function signature + params = collections.OrderedDict((p.name, p) for p in doc.params) + subtypes = [] + # now we can use the subparams map to extract "compound" parameter types + # and swap the new type for the original param's type + for param_name, subs in subparams.items(): + typename = '%s%s' % ( + pascal_case_ify(parent_name), + pascal_case_ify(param_name), + ) + param = params[param_name] + param.type = typename + subtypes.append(jsdoc.ClassDoc({ + 'name': typename, + 'doc': param.doc, + '_members': [ + (sub.name, jsdoc.PropertyDoc(dict(sub.to_dict(), sourcemodule=doc['sourcemodule']))) + for sub in subs + ], + 'sourcemodule': doc['sourcemodule'], + })) + return params.values(), subtypes + +def check_parameters(documenter, doc): + """ + Check that all documented parameters match a formal parameter for the + function. Documented params which don't match the actual function may be + typos. + """ + guessed = set(doc['guessed_params'] or []) + if not guessed: + return + + documented = { + # param name can be of the form [foo.bar.baz=default]\ndescription + jsdoc.ParamDoc(text).name.split('.')[0] + for text in doc.get_as_list('param') + } + odd = documented - guessed + if not odd: + return + + app = documenter.directive.env.app + app.warn("Found documented params %s not in formal parameter list " + "of function %s in module %s (%s)" % ( + ', '.join(odd), + doc.name, + documenter.modname, + doc['sourcemodule']['sourcefile'], + )) + +def make_desc_parameters(params): + for p in params: + if '.' in p.name: + continue + + node = addnodes.desc_parameter(p.name, p.name) + if p.optional: + node = addnodes.desc_optional('', '', node) + yield node + +def make_parameters(params, mod=None): + for param in params: + p = nodes.paragraph('', '', nodes.strong(param.name, param.name)) + if param.default is not None: + p += nodes.Text('=', '=') + p += nodes.emphasis(param.default, param.default) + if param.type: + p += nodes.Text(' (') + p += make_types(param.type, mod=mod) + p += nodes.Text(')') + if param.doc: + p += [ + nodes.Text(' -- '), + nodes.inline(param.doc, param.doc) + ] + yield p + + +def _format_value(v): + if v == '|': + return nodes.emphasis(' or ', ' or ') + if v == ',': + return nodes.Text(', ', ', ') + return nodes.Text(v, v) +def make_types(typespec, mod=None): + # TODO: in closure notation {type=} => optional, do we care? + def format_type(t): + ref = addnodes.pending_xref( + t, addnodes.literal_emphasis(t, t), + refdomain='js', reftype='class', reftarget=t, + ) + if mod: + ref['js:module'] = mod + return ref + + try: + return types.iterate( + types.parse(typespec), + format_type, + _format_value + ) + except ValueError as e: + raise ValueError("%s in '%s'" % (e, typespec)) + + +class MixinDocumenter(NSDocumenter): + objtype = 'mixin' + +class PropertyDocumenter(Documenter): + objtype = 'attribute' + def make_signature(self): + ret = super(PropertyDocumenter, self).make_signature() + proptype = self.item.type + if proptype: + typeref = addnodes.pending_xref( + proptype, nodes.Text(proptype, proptype), + refdomain='js', reftype='class', reftarget=proptype + ) + typeref['js:module'] = self.item['sourcemodule'].name + ret.append(nodes.Text(' ')) + ret.append(typeref) + return ret + + def make_content(self, all_members): + doc = self.item + ret = nodes.section() + if doc.doc: + self.directive.state.nested_parse(to_list(doc.doc), 0, ret) + return ret.children + +class UnknownDocumenter(Documenter): + objtype = 'unknown' + def make_content(self, all_members): + return [] diff --git a/doc/_extensions/autojsdoc/ext/extractor.py b/doc/_extensions/autojsdoc/ext/extractor.py new file mode 100644 index 00000000000..36211cfd3b3 --- /dev/null +++ b/doc/_extensions/autojsdoc/ext/extractor.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import io +import os + +import pyjsdoc +import pyjsparser + +try: + from sphinx.util import status_iterator + def it(app): return status_iterator +except ImportError: + # 1.2: Builder.status_iterator + # 1.3: Application.status_iterator (with alias on Builder) + # 1.6: sphinx.util.status_iterator (with *deprecated* aliases on Application and Builder) + # 1.7: removed Application and Builder aliases + # => if no sphinx.util.status_iterator, fallback onto the builder one for + # 1.2 compatibility, remove this entirely if we ever require 1.6+ + status_iterator = None + def it(app): return app.builder.status_iterator + +from ..parser import jsdoc, parser + + +def _get_roots(conf): + env_roots = os.environ.get('AUTOJSDOC_ROOTS_PATH') + if env_roots: + return env_roots.split(':') + return [] + +def read_js(app, modules): + """ + :type app: sphinx.application.Sphinx + :type modules: Dict[str, jsdoc.ModuleDoc] + """ + roots = map(os.path.normpath, app.config.js_roots or [os.path.join(app.confdir, '..')]) + files = [ + os.path.join(r, f) + for root in roots + for r, _, fs in os.walk(root) + if 'static/src/js' in r + for f in fs + if f.endswith('.js') + ] + + modules.update((mod.name, mod) for mod in ABSTRACT_MODULES) + for name in it(app)(files, "Parsing javascript files...", length=len(files)): + with io.open(name) as f: + ast = pyjsparser.parse(f.read()) + modules.update( + (mod.name, mod) + for mod in parser.ModuleMatcher(name).visit(ast) + ) + _resolve_references(modules) + +def _resolve_references(byname): + # must be done in topological order otherwise the dependent can't + # resolve non-trivial references to a dependency properly + for name in pyjsdoc.topological_sort( + *pyjsdoc.build_dependency_graph( + list(byname.keys()), + byname + ) + ): + byname[name].post_process(byname) + +ABSTRACT_MODULES = [ + jsdoc.ModuleDoc({ + 'module': u'web.web_client', + 'dependency': {u'web.AbstractWebClient'}, + 'exports': jsdoc.NSDoc({ + 'name': u'web_client', + 'doc': u'instance of AbstractWebClient', + }), + }), + jsdoc.ModuleDoc({ + 'module': u'web.Tour', + 'dependency': {u'web_tour.TourManager'}, + 'exports': jsdoc.NSDoc({ + 'name': u'Tour', + 'doc': u'maybe tourmanager instance?', + }), + }), + jsdoc.ModuleDoc({ + 'module': u'summernote/summernote', + 'exports': jsdoc.NSDoc({'doc': u"totally real summernote"}), + }) +] diff --git a/doc/_extensions/autojsdoc/parser/__init__.py b/doc/_extensions/autojsdoc/parser/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/doc/_extensions/autojsdoc/parser/jsdoc.py b/doc/_extensions/autojsdoc/parser/jsdoc.py new file mode 100644 index 00000000000..00406df42c1 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/jsdoc.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +import re + +import collections + +import pyjsdoc + +def strip_stars(doc_comment): + """ + Version of jsdoc.strip_stars which always removes 1 space after * if + one is available. + """ + return re.sub('\n\s*?\*[\t ]?', '\n', doc_comment[3:-2]).strip() + +class ParamDoc(pyjsdoc.ParamDoc): + """ + Replace ParamDoc because FunctionDoc doesn't properly handle optional + params or default values (TODO: or compounds) if guessed_params is used + + => augment paramdoc with "required" and "default" items to clean up name + """ + def __init__(self, text): + super(ParamDoc, self).__init__(text) + # param name and doc can be separated by - or :, strip it + self.doc = self.doc.strip().lstrip('-:').lstrip() + self.optional = False + self.default = None + # there may not be a space between the param name and the :, in which + # case the : gets attached to the name, strip *again* + # TODO: formal @param/@property parser to handle this crap properly once and for all + self.name = self.name.strip().rstrip(':') + if self.name.startswith('['): + self.name = self.name.strip('[]') + self.optional = True + if '=' in self.name: + self.name, self.default = self.name.rsplit('=', 1) + def to_dict(self): + d = super(ParamDoc, self).to_dict() + d['optional'] = self.optional + d['default'] = self.default + return d +pyjsdoc.ParamDoc = ParamDoc + +class CommentDoc(pyjsdoc.CommentDoc): + namekey = object() + is_constructor = False + + @property + def name(self): + return self[self.namekey] or self['name'] or self['guessed_name'] + def set_name(self, name): + # not great... + if name != '': + self.parsed['guessed_name'] = name + + @property + def is_private(self): + return 'private' in self.parsed + + def to_dict(self): + d = super(CommentDoc, self).to_dict() + d['name'] = self.name + return d + +class PropertyDoc(CommentDoc): + @classmethod + def from_param(cls, s, sourcemodule=None): + parsed = ParamDoc(s).to_dict() + parsed['sourcemodule'] = sourcemodule + return cls(parsed) + + @property + def type(self): + return self['type'].strip('{}') + + def to_dict(self): + d = super(PropertyDoc, self).to_dict() + d['type'] = self.type + d['is_private'] = self.is_private + return d + +class InstanceDoc(CommentDoc): + @property + def cls(self): + return self['cls'] + + def to_dict(self): + return dict(super(InstanceDoc, self).to_dict(), cls=self.cls) + +class LiteralDoc(CommentDoc): + @property + def type(self): + if self['type']: + return self['type'] + valtype = type(self['value']) + if valtype is bool: + return 'Boolean' + elif valtype is float: + return 'Number' + elif valtype is type(u''): + return 'String' + return '' + + @property + def value(self): + return self['value'] + + def to_dict(self): + d = super(LiteralDoc, self).to_dict() + d['type'] = self.type + d['value'] = self.value + return d + +class FunctionDoc(CommentDoc): + type = 'Function' + namekey = 'function' + + @property + def is_constructor(self): + return self.name == 'init' + + @property + def params(self): + tag_texts = self.get_as_list('param') + if self.get('guessed_params') is None: + return [ParamDoc(text) for text in tag_texts] + else: + param_dict = {} + for text in tag_texts: + param = ParamDoc(text) + param_dict[param.name] = param + return [param_dict.get(name) or ParamDoc('{} ' + name) + for name in self.get('guessed_params')] + @property + def return_val(self): + ret = self.get('return') or self.get('returns') + type = self.get('type') + if '{' in ret and '}' in ret: + if not '} ' in ret: + # Ensure that name is empty + ret = re.sub(r'\}\s*', '} ', ret) + return ParamDoc(ret) + if ret and type: + return ParamDoc('{%s} %s' % (type, ret)) + return ParamDoc(ret) + + def to_dict(self): + d = super(FunctionDoc, self).to_dict() + d['name'] = self.name + d['params'] = [param.to_dict() for param in self.params] + d['return_val']= self.return_val.to_dict() + return d + +class NSDoc(CommentDoc): + namekey = 'namespace' + def __init__(self, parsed_comment): + super(NSDoc, self).__init__(parsed_comment) + self.members = collections.OrderedDict() + def add_member(self, name, member): + """ + :type name: str + :type member: CommentDoc + """ + member.set_name(name) + self.members[name] = member + + @property + def properties(self): + if self.get('property'): + return [ + (p.name, p) + for p in ( + PropertyDoc.from_param(p, self['sourcemodule']) + for p in self.get_as_list('property') + ) + ] + return self.members.items() or self['_members'] or [] + + def has_property(self, name): + return self.get_property(name) is not None + + def get_property(self, name): + return next((p for n, p in self.properties if n == name), None) + + def to_dict(self): + d = super(NSDoc, self).to_dict() + d['properties'] = [(n, p.to_dict()) for n, p in self.properties] + return d + +class MixinDoc(NSDoc): + namekey = 'mixin' + +class ModuleDoc(NSDoc): + namekey = 'module' + def __init__(self, parsed_comment): + super(ModuleDoc, self).__init__(parsed_comment) + #: callbacks to run with the modules mapping once every module is resolved + self._post_process = [] + + def post_process(self, modules): + for callback in self._post_process: + callback(modules) + + @property + def module(self): + return self # lol + + @property + def dependencies(self): + """ + Returns the immediate dependencies of a module (only those explicitly + declared/used). + """ + return self.get('dependency', None) or set() + + @property + def exports(self): + """ + Returns the actual item exported from the AMD module, can be a + namespace, a class, a function, an instance, ... + """ + return self.get_property('') + + def to_dict(self): + vars = super(ModuleDoc, self).to_dict() + vars['dependencies'] = self.dependencies + vars['exports'] = self.exports + return vars + +class ClassDoc(NSDoc): + namekey = 'class' + @property + def constructor(self): + return self.get_property('init') + + @property + def superclass(self): + return self['extends'] or self['base'] + + def get_property(self, method_name): + if method_name == 'extend': + return FunctionDoc({ + 'doc': 'Create subclass for %s' % self.name, + 'guessed_function': 'extend', + }) + # FIXME: should ideally be a proxy namespace + if method_name == 'prototype': + return self + return super(ClassDoc, self).get_property(method_name) + + @property + def mixins(self): + return self.get_as_list('mixes') + + def to_dict(self): + d = super(ClassDoc, self).to_dict() + d['mixins'] = self.mixins + return d + +class UnknownNS(NSDoc): + def get_property(self, name): + return super(UnknownNS, self).get_property(name) or \ + UnknownNS({'name': '{}.{}'.format(self.name, name)}) + +class Unknown(CommentDoc): + @classmethod + def from_(cls, source): + def builder(parsed): + inst = cls(parsed) + inst.parsed['source'] = source + return inst + return builder + + @property + def name(self): + return self['name'] + ' ' + self['source'] + + @property + def type(self): + return "Unknown" + + def get_property(self, p): + return Unknown(dict(self.parsed, source=self.name, name=p + '<')) + +def parse_comments(comments, doctype=None): + # find last comment which starts with a * + docstring = next(( + c['value'] + for c in reversed(comments or []) + if c['value'].startswith(u'*') + ), None) or u"" + + # \n prefix necessary otherwise parse_comment fails to take first + # block comment parser strips delimiters, but strip_stars fails without + # them + extract = '\n' + strip_stars('/*' + docstring + '\n*/') + parsed = pyjsdoc.parse_comment(extract, u'') + + if doctype == 'FunctionExpression': + doctype = FunctionDoc + elif doctype == 'ObjectExpression' or doctype is None: + doctype = guess + + if doctype is guess: + return doctype(parsed) + + # in case a specific doctype is given, allow overriding it anyway + return guess(parsed, default=doctype) + +def guess(parsed, default=NSDoc): + if 'class' in parsed: + return ClassDoc(parsed) + if 'function' in parsed: + return FunctionDoc(parsed) + if 'mixin' in parsed: + return MixinDoc(parsed) + if 'namespace' in parsed: + return NSDoc(parsed) + if 'module' in parsed: + return ModuleDoc(parsed) + if 'type' in parsed: + return PropertyDoc(parsed) + + return default(parsed) diff --git a/doc/_extensions/autojsdoc/parser/parser.py b/doc/_extensions/autojsdoc/parser/parser.py new file mode 100644 index 00000000000..d91b9f22596 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/parser.py @@ -0,0 +1,552 @@ +# -*- coding: utf-8 -*- +import collections + +import pyjsdoc +from attr import attrs, Factory, attr, astuple + +from . import jsdoc +from . import utils +from .visitor import Visitor, SKIP + +DECLARATOR_INIT_TO_REF = ('Literal', 'Identifier', 'MemberExpression') + + +class ModuleMatcher(Visitor): + """Looks for structures of the form:: + + odoo.define($string, function ($name) { + + These are *Odoo module definitions*, upon encountering one the + matcher: + + * creates a module entry, optionally associated with the module comment + * spawns off a :class:`ModuleBodyMatcher` on the function's body with the + module name and the $name as "require" function + """ + def __init__(self, filename): + super(ModuleMatcher, self).__init__() + self.filename = filename + self.result = [] + def enter_Program(self, node): + pass # allows visiting toplevel + def enter_ExpressionStatement(self, node): + # we're interested in expression statements (toplevel call) + if utils.match(node, {'expression': { + 'callee': { + 'object': {'name': 'odoo'}, + 'property': {'name': 'define'}, + }, + }}): + [module, func] = node['expression']['arguments'] + mod = jsdoc.parse_comments(node.get('comments'), jsdoc.ModuleDoc) + # set module name + mod.set_name(module['value']) + mod.parsed['sourcefile'] = self.filename + self.result.append(mod) + + # get name of require parameter + require = None # a module can have no dependencies + if func['params']: + require = func['params'][0]['name'] + mod.parsed['dependency'], post = ModuleExtractor(mod, require).visit(func['body']) + mod._post_process.extend(post) + # don't recurse since we've fired off a sub-visitor for the + # bits we're interested in + return SKIP + def enter_generic(self, node): + # skip all other toplevel statements + return SKIP + +ref = collections.namedtuple('Ref', 'object property') +def _name(r): + bits = [] + while isinstance(r, ref): + bits.append(r.property) + r = r.object + return '.'.join(reversed(bits)) +def deref(item, prop=None): + assert isinstance(item, ref) + + while isinstance(item, ref): + obj = item.object + if isinstance(obj, ref): + obj = deref(obj) + + if isinstance(obj, (jsdoc.NSDoc, jsdoc.Unknown)): + item = obj.get_property(item.property) + elif isinstance(obj, dict): + item = obj[item.property] + elif isinstance(obj, jsdoc.PropertyDoc): + # f'n dynamic crap + item = jsdoc.Unknown(obj.to_dict()).get_property(item.property) + else: + raise ValueError("%r (%s) should be a dict or namespace" % (obj, type(obj))) + return item + +def m2r(me, scope): + # create a ref to the scope in case it's a hoisted function declaration, + # this may false-positive on other hoisting but w/e + if me['type'] == 'Literal': + return jsdoc.LiteralDoc({'value': me['value']}) + if me['type'] == 'Identifier': + return ref(scope, me['name']) + if me['type'] != 'MemberExpression': + raise ValueError(me) + return ref( + m2r(me['object'], scope), + utils._value(me['property'], strict=True) + ) + +@attrs(slots=True) +class Declaration(object): + id = attr(default=None) + comments = attr(default=Factory(list)) +@attrs(slots=True) +class ModuleContent(object): + dependencies = attr(default=Factory(set)) + post = attr(default=Factory(list)) + def __iter__(self): + yield self.dependencies + yield self.post + +@attrs # needs dict as it's replacing a ModuleProxy +class Nothing(object): + name = attr() + def __bool__(self): + return False + __nonzero__ = __bool__ + +class RefProxy(object): + def __init__(self, r): + self._ref = r + def become(self, modules): + s = self + other = deref(self._ref) or Nothing(_name(self._ref)) + # ??? shouldn't all previous refs have been resolved? + if isinstance(other, (RefProxy, ModuleProxy)): + other = other.become(modules) + self.__class__ = other.__class__ + self.__dict__ = other.__dict__ + return self + def set_name(self, name): + pass # ??? +class ModuleProxy(object): + def __init__(self, name): + self._name = name + + # replace the ModuleProxy by the module's exports + def become(self, modules): + s = self + m = modules[self._name].get_property('') or Nothing(self._name) + self.__class__ = m.__class__ + self.__dict__ = m.__dict__ + return self + + def set_name(self, name): + pass # FIXME: ??? + +jq = jsdoc.UnknownNS({ + 'name': u'jQuery', + 'doc': u'', +}) +window = jsdoc.UnknownNS({ + 'doc': '', + 'name': 'window', +}) +BASE_SCOPE = { + '_': jsdoc.UnknownNS({'doc': u'', 'name': u'_'}), + '$': jq, 'jQuery': jq, + 'nv': jsdoc.UnknownNS({ + 'doc': u'', + 'name': u'nv', + }), + 'window': window, + 'document': window.get_property('document'), + 'Date': jsdoc.ClassDoc({ + 'name': u'Date', + 'doc': u'', + }), + 'Backbone': jsdoc.UnknownNS({ + '_members': [ + ('Model', jsdoc.ClassDoc({ + 'name': u'Model', + 'doc': u'', + })), + ('Collection', jsdoc.ClassDoc({ + 'name': u'Collection', + 'doc': u'', + })), + ] + }), + 'odoo': jsdoc.UnknownNS({ + 'name': u'odoo', + 'doc': u"Odoo", + '_members': [ + ('name', jsdoc.PropertyDoc({'name': u'csrf_token', 'type': u'{String}'})), + ] + }), + 'undefined': jsdoc.LiteralDoc({'name': u'undefined', 'value': None}) +} + +class Scope(object): + """ + Add hoc scope versioning/SSA such that rebinding a symbol in a module + scope does not screw everything up e.g. "Foo = Foo.extend({})" should not + have the final Foo extending itself... + """ + def __init__(self, mapping): + self._namemap = {} + self._targets = [] + for k, v in mapping.items(): + self[k] = v + + def __setitem__(self, k, v): + self._namemap[k] = len(self._targets) + self._targets.append(v) + + def freeze(self): + return { + k: self._targets[v] + for k, v in self._namemap.items() + } + +class ModuleExtractor(Visitor): + def __init__(self, module, requirefunc): + super(ModuleExtractor, self).__init__() + self.module = module + self.requirefunc = requirefunc + self.result = ModuleContent() + self.scope = Scope(BASE_SCOPE) + self.declaration = None + + def enter_BlockStatement(self, node): + Hoistifier(self).visit(node) + def exit_BlockStatement(self, node): + for k, v in self.scope._namemap.items(): + if k not in BASE_SCOPE: + self.module.add_member(k, self.scope._targets[v]) + for t in TypedefMatcher(self).visit(node): + self.module.add_member(t.name, t) + + def enter_VariableDeclaration(self, node): + self.declaration = Declaration(comments=node.get('comments')) + def enter_VariableDeclarator(self, node): + # otherwise we've already hoisted the declaration so the variable + # already exist initialised to undefined, and ValueExtractor does not + # handle a None input node so it returns None which makes no sense + if node['init']: + self.declaration.id = node['id']['name'] + self.scope[self.declaration.id] = ValueExtractor( + self, self.declaration + ).visit(node['init'] or []) + self.declaration.id = None + return SKIP + def exit_VariableDeclaration(self, node): + self.declaration = None + + # as the name denotes, AssignmentExpression is an *expression*, which + # means at the module toplevel it is wrapped into an ExpressionStatement + # which is where the comments get attached + def enter_ExpressionStatement(self, node): + self.declaration = Declaration(comments=node.get('comments')) + def exit_ExpressionStatement(self, node): + self.declaration = None + def enter_AssignmentExpression(self, node): + target = node['left'] + if target['type'] == 'Identifier': + self.declaration.id = target['name'] + self.scope[self.declaration.id] = ValueExtractor( + self, self.declaration + ).visit(node['right']) + self.declaration.id = None + return SKIP + + if target['type'] != 'MemberExpression': + raise ValueError("Unhandled assign to %s" % target['type']) + + # only assign to straight a.b.c patterns (OK and trivial literals) + if target['computed'] and target['property']['type'] != 'Literal': + return SKIP + + name = utils._value(target['property'], strict=True) + if isinstance(name, type(u'')) and name.endswith('.extend'): + return SKIP # ignore overwrite of .extend (WTF) + self.declaration.id = name + it = ValueExtractor( + self, self.declaration + ).visit(node['right']) + self.declaration.id = None + assert it, "assigned a non-value from %s to %s" % (node['right'], name) + + @self.result.post.append + def _augment_module(modules): + try: + t = deref(m2r(target['object'], self.scope.freeze())) + except ValueError: + return # f'n extension of global libraries garbage + if not isinstance(t, jsdoc.NSDoc): + # function Foo(){}; Foo.prototype = bar + # fuck that yo + return + # TODO: note which module added this + m = it + if isinstance(m, jsdoc.LiteralDoc): + m = jsdoc.PropertyDoc(m.to_dict()) + t.add_member(name, m) + return SKIP + + def enter_FunctionDeclaration(self, node): + """ Already processed by hoistitifier + """ + return SKIP + + def enter_ReturnStatement(self, node): + self.declaration = Declaration(comments=node.get('comments')) + if node['argument']: + export = ValueExtractor(self, self.declaration).visit(node['argument']) + if isinstance(export, RefProxy): + self.module.parsed['exports'] = _name(export._ref) + self.scope[''] = export + self.declaration = None + return SKIP + + def enter_CallExpression(self, node): + if utils.match(node, { + 'callee': { + 'type': 'MemberExpression', + 'object': lambda n: ( + n['type'] in ('Identifier', 'MemberExpression') + # _.str.include + and not utils._name(n).startswith('_') + ), + 'property': {'name': 'include'}, + } + }): + target = RefProxy(m2r(node['callee']['object'], self.scope.freeze())) + target_name = utils._name(node['callee']['object']) + items = ClassProcessor(self).visit(node['arguments']) + @self.result.post.append + def resolve_extension(modules): + t = target.become(modules) + if not isinstance(t, jsdoc.ClassDoc): + raise ValueError("include() subjects should be classes, %s is %s" % (target_name, type(t))) + # TODO: note which module added these + for it in items: + if isinstance(it, dict): + for n, member in it.items(): + t.add_member(n, member) + else: + t.parsed.setdefault('mixes', []).append(it.become(modules)) + + return SKIP + + def refify(self, node, also=None): + it = m2r(node, self.scope.freeze()) + assert isinstance(it, ref), "Expected ref, got {}".format(it) + px = RefProxy(it) + @self.result.post.append + def resolve(modules): + p = px.become(modules) + if also: also(p) + return px + +class ValueExtractor(Visitor): + def __init__(self, parent, declaration=None): + super(ValueExtractor, self).__init__() + self.parent = parent + self.declaration = declaration or Declaration() + + def enter_generic(self, node): + self.result = jsdoc.parse_comments( + self.declaration.comments, + jsdoc.Unknown.from_(node['type']) + ) + self._update_result_meta() + return SKIP + + def _update_result_meta(self, name=None): + self.result.parsed['sourcemodule'] = self.parent.module + n = name or self.declaration.id + if n: + self.result.set_name(n) + + def enter_Literal(self, node): + self.result = jsdoc.parse_comments( + self.declaration.comments, jsdoc.LiteralDoc) + self._update_result_meta() + self.result.parsed['value'] = node['value'] + return SKIP + def enter_Identifier(self, node): + self.result = self.parent.refify(node) + return SKIP + + def enter_MemberExpression(self, node): + self.result = RefProxy(ref( + ValueExtractor(self.parent).visit(node['object']), + utils._value(node['property'], strict=True) + )) + self.parent.result.post.append(self.result.become) + return SKIP + + def enter_FunctionExpression(self, node): + name, comments = astuple(self.declaration) + self.result = jsdoc.parse_comments(comments, jsdoc.FunctionDoc) + self.result.parsed['name'] = node['id'] and node['id']['name'] + self._update_result_meta() + self.result.parsed['guessed_params'] = [p['name'] for p in node['params']] + return SKIP + + def enter_NewExpression(self, node): + comments = self.declaration.comments if self.declaration else node.get('comments') + self.result = ns = jsdoc.parse_comments(comments, jsdoc.InstanceDoc) + self._update_result_meta() + + def _update_contents(cls): + if not isinstance(cls, jsdoc.ClassDoc): + return + ns.parsed['cls'] = cls + self.parent.refify(node['callee'], also=_update_contents) + return SKIP + + def enter_ObjectExpression(self, node): + self.result = obj = jsdoc.parse_comments(self.declaration.comments) + self._update_result_meta() + for n, p in MemberExtractor(parent=self.parent).visit(node['properties']).items(): + obj.add_member(n, p) + return SKIP + + def enter_CallExpression(self, node): + # require(a_module_name) + if utils.match(node, {'callee': {'type': 'Identifier', 'name': self.parent.requirefunc}}): + depname = node['arguments'][0]['value'] + # TODO: clean this up + self.parent.result.dependencies.add(depname) + self.result = ModuleProxy(name=depname) + self.parent.result.post.append(self.result.become) + + # Class.extend(..mixins, {}) + elif utils.match(node, { + 'callee': { + 'type': 'MemberExpression', + 'object': lambda n: ( + n['type'] in ('Identifier', 'MemberExpression') + # $.extend, $.fn.extend, _.extend + and not utils._name(n).startswith(('_', '$')) + ), + 'property': {'name': 'extend'}, + }, + }): # creates a new class, but may not actually return it + obj = node['callee']['object'] + _, comments = astuple(self.declaration) + self.result = cls = jsdoc.parse_comments(comments, jsdoc.ClassDoc) + cls.parsed['extends'] = self.parent.refify(obj) + self._update_result_meta() + items = ClassProcessor(self.parent).visit(node['arguments']) + + @self.parent.result.post.append + def add_to_class(modules): + for item in items: + if isinstance(item, dict): + # methods/attributes + for n, method in item.items(): + cls.add_member(n, method) + else: + cls.parsed.setdefault('mixes', []).append(item) + + # other function calls + else: + self.result = jsdoc.parse_comments(self.declaration.comments, jsdoc.guess) + self._update_result_meta() + + return SKIP + +class ClassProcessor(Visitor): + def __init__(self, parent): + super(ClassProcessor, self).__init__() + self.result = [] + self.parent = parent + + def enter_generic(self, node): + self.result.append(self.parent.refify(node)) + return SKIP + def enter_ObjectExpression(self, node): + self.result.append(MemberExtractor(parent=self.parent, for_class=True).visit(node['properties'])) + return SKIP + +String = type(u'') +class MemberExtractor(Visitor): + def __init__(self, parent, for_class=False): + super(MemberExtractor, self).__init__() + self.result = collections.OrderedDict() + self.parent = parent + self.for_class = for_class + + def enter_Property(self, node): + name = utils._value(node['key']) + prop = ValueExtractor( + self.parent, + Declaration(id=name, comments=node.get('comments')) + ).visit(node['value']) + + if isinstance(prop, jsdoc.LiteralDoc): + prop = jsdoc.PropertyDoc(prop.to_dict()) + + # ValueExtractor can return a Ref, maybe this should be sent as part + # of the decl/comments? + if name.startswith('_') and hasattr(prop, 'parsed'): + prop.parsed['private'] = True + self.result[name] = prop + + return SKIP + +class Hoistifier(Visitor): + """ + Processor for variable and function declarations properly hoisting them + to the "top" of a module such that they are available with the relevant + value afterwards. + """ + def __init__(self, parent): + super(Hoistifier, self).__init__() + self.parent = parent + + def enter_generic(self, node): + return SKIP + + # nodes to straight recurse into, others are just skipped + enter_BlockStatement = enter_VariableDeclaration = lambda self, node: None + + def enter_VariableDeclarator(self, node): + self.parent.scope[node['id']['name']] = BASE_SCOPE['undefined'] + + def enter_FunctionDeclaration(self, node): + funcname = node['id']['name'] + self.parent.scope[funcname] = fn = jsdoc.parse_comments( + node.get('comments'), + jsdoc.FunctionDoc, + ) + fn.parsed['sourcemodule'] = self.parent.module + fn.parsed['name'] = funcname + fn.parsed['guessed_params'] = [p['name'] for p in node['params']] + return SKIP + +class TypedefMatcher(Visitor): + def __init__(self, parent): + super(TypedefMatcher, self).__init__() + self.parent = parent + self.result = [] + + enter_BlockStatement = lambda self, node: None + def enter_generic(self, node): + # just traverse all top-level statements, check their comments, and + # bail + for comment in node.get('comments') or []: + if '@typedef' in comment['value']: + extract = '\n' + jsdoc.strip_stars('/*' + comment['value'] + '\n*/') + parsed = pyjsdoc.parse_comment(extract, u'') + p = jsdoc.ParamDoc(parsed['typedef']) + parsed['name'] = p.name + parsed['sourcemodule'] = self.parent.module + # TODO: add p.type as superclass somehow? Builtin types not in scope :( + self.result.append(jsdoc.ClassDoc(parsed)) + + return SKIP diff --git a/doc/_extensions/autojsdoc/parser/tests/README.rst b/doc/_extensions/autojsdoc/parser/tests/README.rst new file mode 100644 index 00000000000..dd862beae66 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/README.rst @@ -0,0 +1,6 @@ +:orphan: + +These files should be run via pytest_, simply install pytest and from the top +of the Odoo project run ``pytest doc/_extensions``. + +.. _pytest: https://pytest.org/ diff --git a/doc/_extensions/autojsdoc/parser/tests/support.py b/doc/_extensions/autojsdoc/parser/tests/support.py new file mode 100644 index 00000000000..f533f98d90c --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/support.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +import operator + +import pyjsparser + +from autojsdoc.parser import jsdoc, parser + +params = operator.attrgetter('name', 'type', 'doc') + + +def parse(s, source=None): + tree = pyjsparser.parse(s) + mods = parser.ModuleMatcher(source).visit(tree) + post(mods) + return mods + +def post(mods): + modules = dict(BASE_MODULES) + modules.update((m.name, m) for m in mods) + + for mod in mods: + mod.post_process(modules) + +BASE_MODULES = { + 'other': jsdoc.ModuleDoc({ + 'module': 'other', + '_members': [ + ('', jsdoc.LiteralDoc({'name': 'value', 'value': "ok"})), + ], + }), + 'dep2': jsdoc.ModuleDoc({ + 'module': 'dep2', + '_members': [ + ('', jsdoc.LiteralDoc({'value': 42.})), + ], + }), + 'dep3': jsdoc.ModuleDoc({ + 'module': 'dep3', + '_members': [ + ('', jsdoc.LiteralDoc({'value': 56.})), + ], + }), + 'Class': jsdoc.ModuleDoc({ + 'module': 'Class', + '_members': [ + ('', jsdoc.ClassDoc({ + 'name': 'Class', + 'doc': "Base Class" + })), + ], + }), + 'mixins': jsdoc.ModuleDoc({ + 'module': 'mixins', + '_members': [ + ('', jsdoc.NSDoc({ + 'name': 'mixins', + '_members': [ + ('Bob', jsdoc.ClassDoc({'class': "Bob"})), + ] + })), + ], + }), + 'Mixin': jsdoc.ModuleDoc({ + 'module': 'Mixin', + '_members': [ + ('', jsdoc.MixinDoc({ + '_members': [ + ('a', jsdoc.FunctionDoc({'function': 'a'})), + ] + })), + ], + }) +} diff --git a/doc/_extensions/autojsdoc/parser/tests/test_class.py b/doc/_extensions/autojsdoc/parser/tests/test_class.py new file mode 100644 index 00000000000..403ae9b48b7 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/test_class.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +import pytest + +from autojsdoc.parser import jsdoc +from support import params, parse + +def test_classvar(): + [mod] = parse(""" + odoo.define('a.A', function(require) { + var Class = require('Class'); + /** + * This is my class-kai + */ + var A = Class.extend({}); + return A; + }); + """) + cls = mod.exports + assert type(cls) == jsdoc.ClassDoc + + assert type(cls.superclass) == jsdoc.ClassDoc + assert cls.superclass.name == 'Class' + + assert cls.name == 'A' + assert cls.constructor is None + assert cls.properties == [] + assert cls['doc'] == 'This is my class-kai' + +def test_classret(): + [mod] = parse(""" + odoo.define('a.A', function(require) { + var Class = require('Class'); + /** + * This is my class-kai + */ + return Class.extend({}); + }); + """) + cls = mod.exports + assert type(cls) == jsdoc.ClassDoc + + assert cls.name == '' + assert cls.constructor is None + assert cls.properties == [] + assert cls['doc'] == 'This is my class-kai' + +def test_methods(): + [mod] = parse(""" + odoo.define('a.A', function(require) { + var Class = require('Class'); + return Class.extend({ + /** + * @param {Widget} parent + */ + init: function (parent) {}, + /** + * @returns {Widget} + */ + itself: function () { return this; }, + /** + * @param {MouseEvent} e + */ + _onValidate: function (e) {}, + }); + }); + """) + cls = mod.exports + assert len(cls.properties) == 3 + assert cls.constructor + # assume methods are in source order + [_, init] = cls.properties[0] + assert init == cls.constructor + assert init.name == 'init' + assert not init.is_private + assert init.is_constructor + [param] = init.params + assert params(param) == ('parent', 'Widget', '') + + [_, itself] = cls.properties[1] + assert itself.name == 'itself' + assert not itself.is_private + assert not itself.is_constructor + assert not itself.params + assert params(itself.return_val) == ('', 'Widget', '') + + [_, _on] = cls.properties[2] + assert _on.name == '_onValidate' + assert _on.is_private + assert not _on.is_constructor + [param] = _on.params + assert params(param) == ('e', 'MouseEvent', '') + +def test_mixin_explicit(): + [mod] = parse(""" + odoo.define('a.A', function (require) { + var Class = require('Class'); + var mixins = require('mixins'); + /** + * This is my class-kai + * @mixes mixins.Bob + */ + return Class.extend({}); + }); + """) + cls = mod.exports + # FIXME: ClassDoc may want to m2r(mixin, scope)? + assert cls.mixins == ['mixins.Bob'] + +def test_mixin_implicit(): + [mod] = parse(""" + odoo.define('a.A', function(require) { + var Class = require('Class'); + var Mixin = require('Mixin'); + /** + * This is my class-kai + */ + return Class.extend(Mixin, { foo: function() {} }); + }); + """) + cls = mod.exports + [mixin] = cls.mixins + assert type(mixin) == jsdoc.MixinDoc + assert params(mixin.properties[0][1]) == ('a', 'Function', '') + assert params(mixin.get_property('a')) == ('a', 'Function', '') + + assert params(cls.get_property('foo')) == ('foo', 'Function', '') + +def test_instanciation(): + [A, a] = parse(""" + odoo.define('A', function (r) { + var Class = r('Class'); + /** + * @class A + */ + return Class.extend({ + foo: function () {} + }); + }); + odoo.define('a', function (r) { + var A = r('A'); + var a = new A; + return a; + }); + """) + assert type(a.exports) == jsdoc.InstanceDoc + assert a.exports.cls.name == A.exports.name + +def test_non_function_properties(): + [A] = parse(""" + odoo.define('A', function (r) { + var Class = r('Class'); + return Class.extend({ + template: 'thing', + a_prop: [1, 2, 3], + 'other': {a: 7} + }); + }); + """) + t = A.exports.get_property('template') + assert type(t) == jsdoc.PropertyDoc + assert params(t) == ('template', 'String', '') + assert not t.is_private + +def test_non_extend_classes(): + [mod] = parse(""" + odoo.define('A', function () { + /** + * @class + */ + var Class = function () {} + return Class; + }); + """) + assert type(mod.exports) == jsdoc.ClassDoc + +def test_extend(): + [a, _] = parse(""" + odoo.define('A', function (require) { + var Class = require('Class'); + return Class.extend({}); + }); + odoo.define('B', function (require) { + var A = require('A'); + A.include({ + /** A property */ + a: 3, + /** A method */ + b: function () {} + }); + }); + """) + cls = a.exports + assert type(cls) == jsdoc.ClassDoc + a = cls.get_property('a') + assert type(a) == jsdoc.PropertyDoc + assert params(a) == ('a', 'Number', 'A property') + b = cls.get_property('b') + assert type(b) == jsdoc.FunctionDoc + assert params(b) == ('b', 'Function', 'A method') + +# TODO: also support virtual members? +# TODO: computed properties? +@pytest.mark.skip(reason="Need to implement member/var-parsing?") +def test_members(): + [mod] = parse(""" + odoo.define('A', function (r) { + var Class = r('Class'); + return Class.extend({ + init: function () { + /** + * This is bob + * @var {Foo} + */ + this.foo = 3; + this.bar = 42; + /** + * @member {Baz} + */ + this.baz = null; + } + }); + }); + """) + cls = mod.exports + assert params(cls.members[0]) == ('foo', 'Foo', 'This is bob') + assert params(cls.members[1]) == ('bar', '', '') + assert params(cls.members[2]) == ('baz', 'Baz', '') diff --git a/doc/_extensions/autojsdoc/parser/tests/test_crap.py b/doc/_extensions/autojsdoc/parser/tests/test_crap.py new file mode 100644 index 00000000000..70063e6dd47 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/test_crap.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +Test various crap patterns found in Odoo code to ensure they don't blow up +the parser thingie +""" +from autojsdoc.parser import jsdoc +from support import parse + +def test_export_external(): + [mod] = parse(""" + odoo.define('module', function () { + return $.Deferred().reject(); + }); + """) + assert isinstance(mod.exports, jsdoc.CommentDoc) + assert mod.exports.doc == '' + +def test_extend_jq(): + parse(""" + odoo.define('a', function (r) { + $.extend($.expr[':'], { a: function () {} }); + $.fn.extend({ a: function () {} }); + }); + """) + +def test_extend_dynamic(): + parse(""" + odoo.define('a', function () { + foo.bar.baz[qux + '_external'] = function () {}; + }); + """) + +def test_extend_deep(): + parse(""" + odoo.define('a', function () { + var eventHandler = $.summernote.eventHandler; + var dom = $.summernote.core.dom; + dom.thing = function () {}; + + var fn_editor_currentstyle = eventHandler.modules.editor.currentStyle; + eventHandler.modules.editor.currentStyle = function () {} + }); + """) + +def test_arbitrary(): + parse(""" + odoo.define('bob', function () { + var page = window.location.href.replace(/^.*\/\/[^\/]+/, ''); + var mailWidgets = ['mail_followers', 'mail_thread', 'mail_activity', 'kanban_activity']; + var bob; + var fldj = foo.getTemplate().baz; + }); + """) + +def test_prototype(): + [A, B] = parse(""" + odoo.define('mod1', function () { + var exports = {}; + exports.Foo = Backbone.Model.extend({}); + exports.Bar = Backbone.Model.extend({}); + var BarCollection = Backbone.Collection.extend({ + model: exports.Bar, + }); + exports.Baz = Backbone.Model.extend({}); + return exports; + }); + odoo.define('mod2', function (require) { + var models = require('mod1'); + var _super_orderline = models.Bar.prototype; + models.Foo = models.Bar.extend({}); + var _super_order = models.Baz.prototype; + models.Bar = models.Baz.extend({}); + }); + """) + diff --git a/doc/_extensions/autojsdoc/parser/tests/test_module.py b/doc/_extensions/autojsdoc/parser/tests/test_module.py new file mode 100644 index 00000000000..2a63cc1754c --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/test_module.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +from autojsdoc.parser import jsdoc +from support import params, parse, BASE_MODULES + + +def test_single(): + [mod] = parse(""" + /** + * This is a super module! + */ + odoo.define('supermodule', function (req) { + var other = req('other'); + }); + """) + assert mod.name == 'supermodule' + assert mod.dependencies == {'other'} + assert mod.exports is None + assert mod.doc == "This is a super module!" + m = mod.get_property('other') + assert m.name == 'value', "property is exported value not imported module" + +def test_multiple(): + [mod1, mod2, mod3] = parse(""" + odoo.define('module1', function (req) { + return 1; + }); + odoo.define('module2', function (req) { + return req('dep2'); + }); + odoo.define('module3', function (req) { + var r = req('dep3'); + return r; + }); + """) + assert mod1.name == 'module1' + assert mod1.dependencies == set() + assert isinstance(mod1.exports, jsdoc.LiteralDoc) + assert mod1.exports.value == 1.0 + assert mod1.exports['sourcemodule'] is mod1 + assert mod1.doc == "" + + assert mod2.name == 'module2' + assert mod2.dependencies == {'dep2'} + assert isinstance(mod2.exports, jsdoc.LiteralDoc) + assert mod2.exports.value == 42.0 + assert mod2.doc == "" + + assert mod3.name == 'module3' + assert mod3.dependencies == {'dep3'} + assert isinstance(mod3.exports, jsdoc.LiteralDoc) + assert mod3.exports.value == 56.0 + assert mod3.doc == '' + +def test_func(): + [mod] = parse(""" + odoo.define('module', function (d) { + /** + * @param {Foo} bar this is a bar + * @param {Baz} qux this is a qux + */ + return function (bar, qux) { + return 42; + } + }); + """) + exports = mod.exports + assert type(exports) == jsdoc.FunctionDoc + assert exports['sourcemodule'] is mod + + assert exports.name == '' + assert exports.is_constructor == False + assert exports.is_private == False + + assert params(exports.params[0]) == ('bar', 'Foo', 'this is a bar') + assert params(exports.params[1]) == ('qux', 'Baz', 'this is a qux') + assert params(exports.return_val) == ('', '', '') + +def test_hoist(): + [mod] = parse(""" + odoo.define('module', function() { + return foo; + /** + * @param a_thing + */ + function foo(a_thing) { + return 42; + } + }); + """) + actual = mod.exports + assert type(actual) == jsdoc.FunctionDoc + [param] = actual.params + assert params(param) == ('a_thing', '', '') + +def test_export_instance(): + [mod] = parse(""" + odoo.define('module', function (require) { + var Class = require('Class'); + /** + * Provides an instance of Class + */ + return new Class(); + }); + """) + assert type(mod.exports) == jsdoc.InstanceDoc + assert mod.exports.doc == 'Provides an instance of Class' + assert mod.exports['sourcemodule'] is mod + +def test_bounce(): + [m2, m1] = parse(""" + odoo.define('m2', function (require) { + var Item = require('m1'); + return { + Item: Item + }; + }); + odoo.define('m1', function (require) { + var Class = require('Class'); + var Item = Class.extend({}); + return Item; + }); + """) + assert type(m2.exports) == jsdoc.NSDoc + it = m2.exports.get_property('Item') + assert type(it) == jsdoc.ClassDoc + assert it['sourcemodule'] is m1 + assert sorted([n for n, _ in m1.properties]) == ['', 'Class', 'Item'] + +def test_reassign(): + [m] = parse(""" + odoo.define('m', function (require) { + var Class = require('Class'); + /** local class */ + var Class = Class.extend({}); + return Class + }); + """) + assert m.exports.doc == 'local class' + # can't use equality or identity so use class comment... + assert m.exports.superclass.doc == 'Base Class' + +def test_attr(): + [m1, m2] = parse(""" + odoo.define('m1', function (require) { + var Class = require('Class'); + var Item = Class.extend({}); + return {Item: Item}; + }); + odoo.define('m2', function (require) { + var Item = require('m1').Item; + Item.include({}); + return Item.extend({}); + }); + """) + assert type(m2.exports) == jsdoc.ClassDoc + # that these two are resolved separately may be an issue at one point (?) + assert m2.exports.superclass.to_dict() == m1.exports.get_property('Item').to_dict() + +def test_nothing_implicit(): + [m] = parse(""" + odoo.define('m', function () { + }); + """) + assert m.exports is None + +def test_nothing_explicit(): + [m] = parse(""" + odoo.define('m', function () { + return; + }); + """) + assert m.exports is None diff --git a/doc/_extensions/autojsdoc/parser/tests/test_namespace.py b/doc/_extensions/autojsdoc/parser/tests/test_namespace.py new file mode 100644 index 00000000000..54cbcf8cbb4 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/test_namespace.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +from autojsdoc.parser import jsdoc +from support import parse, params + + +def test_empty(): + [mod] = parse(""" + odoo.define('a.ns', function (r) { + return {}; + }); + """) + assert type(mod.exports) == jsdoc.NSDoc + assert mod.exports.properties == [] + +def test_inline(): + [mod] = parse(""" + odoo.define('a.ns', function (r) { + return { + /** + * a thing + * @type {Boolean} + */ + a: true + } + }); + """) + assert isinstance(mod.exports, jsdoc.NSDoc) + [(n, p)] = mod.exports.properties + assert n == 'a' + assert params(p) == ('a', 'Boolean', 'a thing') + +def test_header(): + [mod] = parse(""" + odoo.define('a.ns', function (r) { + /** + * @property {Boolean} a a thing + */ + return { a: true } + }); + """) + assert isinstance(mod.exports, jsdoc.NSDoc) + [(_, p)] = mod.exports.properties + assert params(p) == ('a', 'Boolean', 'a thing') + +def test_header_conflict(): + """ should the header or the inline comment take precedence? """ + [mod] = parse(""" + odoo.define('a.ns', function (r) { + /** + * @property {Boolean} a a thing + */ + return { + /** @type {String} */ + a: true + } + }); + """) + assert isinstance(mod.exports, jsdoc.NSDoc) + [(_, p)] = mod.exports.properties + assert params(p) == ('a', 'Boolean', 'a thing') + +def test_mixin(): + [mod] = parse(""" + odoo.define('a.mixin', function (r) { + /** + * @mixin + */ + return { + /** + * @returns {Number} a number + */ + do_thing: function other() { return 42; } + } + }); + """) + assert isinstance(mod.exports, jsdoc.MixinDoc) + [(n, p)] = mod.exports.properties + assert n == 'do_thing' + assert params(p) == ('other', 'Function', '') + assert params(p.return_val) == ('', 'Number', 'a number') + +def test_literal(): + [mod] = parse(""" + odoo.define('a.ns', function (r) { + /** whop whop */ + return { + 'a': 1, + /** wheee */ + 'b': 2, + }; + }); + """) + assert mod.exports.doc == 'whop whop' + [(_1, a), (_2, b)] = mod.exports.properties + assert params(a) == ('a', 'Number', '') + assert params(b) == ('b', 'Number', 'wheee') + +def test_fill_ns(): + [mod] = parse(""" + odoo.define('a.ns', function (r) { + var Class = r('Class'); + var ns = {}; + /** ok */ + ns.a = 1; + /** @type {String} */ + ns['b'] = 2; + /** Ike */ + ns.c = Class.extend({}); + ns.d = function () {} + return ns; + }); + """) + ns = mod.exports + assert type(ns) == jsdoc.NSDoc + [(_a, a), (_b, b), (_c, c), (_d, d)] = ns.properties + assert params(a) == ('a', 'Number', 'ok') + assert params(b) == ('b', 'String', '') + assert type(c) == jsdoc.ClassDoc + assert type(d) == jsdoc.FunctionDoc + +def test_extend_other(): + [o, b] = parse(""" + odoo.define('a.ns', function () { + /** @name outer */ + return { + /** @name inner */ + a: {} + }; + }); + odoo.define('b', function (r) { + var o = r('a.ns'); + var Class = r('Class'); + /** Class 1 */ + o.a.b = Class.extend({m_b: 1}); + /** Class 2 */ + o.a['c'] = Class.extend({m_c: 1}); + }); + """) + [(_, m)] = o.exports.properties + assert type(m) == jsdoc.NSDoc + + b = m.get_property('b') + assert type(b) == jsdoc.ClassDoc + assert b.get_property('m_b') + assert b.doc == 'Class 1' + + c = m.get_property('c') + assert type(c) == jsdoc.ClassDoc + assert c.get_property('m_c') + assert c.doc == 'Class 2' + +def test_ns_variables(): + [mod] = parse(""" + odoo.define('A', function (r) { + var Class = r('Class'); + var Thing = Class.extend({}); + return { + Thing: Thing + }; + }); + """) + p = mod.exports.get_property('Thing') + assert type(p) == jsdoc.ClassDoc + +def test_diff(): + """ Have the NS key and the underlying object differ + """ + [mod] = parse(""" + odoo.define('mod', function (r) { + var Class = r('Class'); + var Foo = Class.extend({}); + return { Class: Foo }; + }); + """) + c = mod.exports.get_property('Class') + assert type(c) == jsdoc.ClassDoc + assert c.name == 'Foo' diff --git a/doc/_extensions/autojsdoc/parser/tests/test_params.py b/doc/_extensions/autojsdoc/parser/tests/test_params.py new file mode 100644 index 00000000000..c51139b8cb3 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/test_params.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +from autojsdoc.parser.jsdoc import ParamDoc + +"Lorem ipsum dolor sit amet, consectetur adipiscing elit." + +def test_basic(): + d = ParamDoc("Lorem ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': 'Lorem', + 'type': '', + 'optional': False, + 'default': None, + 'doc': 'ipsum dolor sit amet, consectetur adipiscing elit.', + } + + d = ParamDoc("{Lorem} ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': 'ipsum', + 'type': 'Lorem', + 'optional': False, + 'default': None, + 'doc': 'dolor sit amet, consectetur adipiscing elit.', + } + +def test_optional(): + d = ParamDoc("[Lorem] ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': 'Lorem', + 'type': '', + 'optional': True, + 'default': None, + 'doc': 'ipsum dolor sit amet, consectetur adipiscing elit.', + } + + d = ParamDoc("[Lorem=42] ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': 'Lorem', + 'type': '', + 'optional': True, + 'default': "42", + 'doc': 'ipsum dolor sit amet, consectetur adipiscing elit.', + } + + d = ParamDoc("{Lorem} [ipsum] dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': 'ipsum', + 'type': 'Lorem', + 'optional': True, + 'default': None, + 'doc': 'dolor sit amet, consectetur adipiscing elit.', + } + + d = ParamDoc("{Lorem} [ipsum=42] dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': 'ipsum', + 'type': 'Lorem', + 'optional': True, + 'default': '42', + 'doc': 'dolor sit amet, consectetur adipiscing elit.', + } + +def test_returns(): + d = ParamDoc("{} ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': '', + 'type': '', + 'optional': False, + 'default': None, + 'doc': 'ipsum dolor sit amet, consectetur adipiscing elit.', + } + d = ParamDoc("{Lorem} ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d == { + 'name': '', + 'type': 'Lorem', + 'optional': False, + 'default': None, + 'doc': 'ipsum dolor sit amet, consectetur adipiscing elit.', + } + +def test_odd(): + d = ParamDoc("{jQuery} [$target] the node where content will be prepended").to_dict() + assert d == { + 'name': '$target', + 'type': 'jQuery', + 'optional': True, + 'default': None, + 'doc': 'the node where content will be prepended', + } + + d = ParamDoc("""{htmlString} [content] DOM element, + array of elements, HTML string or jQuery object to prepend to $target""").to_dict() + assert d == { + 'name': 'content', + 'type': 'htmlString', + 'optional': True, + 'default': None, + 'doc': "DOM element,\n array of elements, HTML string or jQuery object to prepend to $target", + } + + d = ParamDoc("{Boolean} [options.in_DOM] true if $target is in the DOM").to_dict() + assert d == { + 'name': 'options.in_DOM', + 'type': 'Boolean', + 'optional': True, + 'default': None, + 'doc': 'true if $target is in the DOM', + } + + d = ParamDoc("Lorem\n ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d['doc'] == 'ipsum dolor sit amet, consectetur adipiscing elit.' + + d = ParamDoc("Lorem - ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d['doc'] == 'ipsum dolor sit amet, consectetur adipiscing elit.' + + d = ParamDoc("Lorem : ipsum dolor sit amet, consectetur adipiscing elit.").to_dict() + assert d['doc'] == 'ipsum dolor sit amet, consectetur adipiscing elit.' diff --git a/doc/_extensions/autojsdoc/parser/tests/test_typespec.py b/doc/_extensions/autojsdoc/parser/tests/test_typespec.py new file mode 100644 index 00000000000..d90fe7995dd --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/tests/test_typespec.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytest + +from autojsdoc.parser import types + +def test_parser(): + assert types.parse("MouseEvent|TouchEvent") == types.Alt([ + types.Type('MouseEvent', []), + types.Type('TouchEvent', []), + ]) + + assert types.parse('Deferred') == types.Alt([ + types.Type('Deferred', [ + types.Alt([types.Type('Object', [])]) + ]) + ]) + + assert types.parse('Object|undefined') == types.Alt([ + types.Type('Object', []), + types.Literal('undefined'), + ]) + assert types.parse('any[]') == types.Alt([ + types.Type('Array', [types.Type('any', [])]) + ]) + assert types.parse("'xml' | 'less'") == types.Alt([ + types.Literal('xml'), + types.Literal('less'), + ]) + assert types.parse('Foo') == types.Alt([ + types.Type('Foo', [types.Alt([ + types.Type('Bar', []), + types.Type('Array', [types.Type('Bar', [])]), + types.Literal('null'), + ])]) + ]) + +def test_tokens(): + toks = list(types.tokenize('A')) + assert toks == [(types.NAME, 'A')] + + toks = list(types.tokenize('"foo" | "bar"')) + assert toks == [(types.LITERAL, 'foo'), (types.OP, '|'), (types.LITERAL, 'bar')] + + toks = list(types.tokenize('1 or 2')) + assert toks == [(types.LITERAL, 1), (types.OP, '|'), (types.LITERAL, 2)] + + toks = list(types.tokenize('a.b.c.d')) + assert toks == [ + (types.NAME, 'a'), (types.OP, '.'), + (types.NAME, 'b'), (types.OP, '.'), + (types.NAME, 'c'), (types.OP, '.'), + (types.NAME, 'd'), + ] + + toks = list(types.tokenize('Function')) + assert toks == [ + (types.NAME, 'Function'), + (types.OP, '<'), + (types.NAME, 'String'), + (types.OP, ','), + (types.NAME, 'Object'), + (types.OP, '>') + ] + +def test_peekable(): + p = types.Peekable(range(5)) + + assert p.peek() == 0 + assert next(p) == 0, "consuming should yield previously peeked value" + next(p) + next(p) + assert next(p) == 3 + assert p.peek() == 4 + next(p) + assert p.peek(None) is None + with pytest.raises(StopIteration): + p.peek() + with pytest.raises(StopIteration): + next(p) diff --git a/doc/_extensions/autojsdoc/parser/types.py b/doc/_extensions/autojsdoc/parser/types.py new file mode 100644 index 00000000000..ecbccfe8651 --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/types.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +import ast +import collections +import io +import token +from tokenize import generate_tokens + +LITERAL_NAME = ('null', 'undefined', 'true', 'false') + +__all__ = ['tokenize', 'parse', 'iterate', 'Alt', 'Type', 'Literal'] + +class _Marker(object): + __slots__ = ['name'] + def __init__(self, name): self.name = name + def __repr__(self): return '<%s>' % self.name + +OP = _Marker('OP') +NAME = _Marker('NAME') +LITERAL = _Marker('LITERAL') +def tokenize(typespec): + typespec = typespec.replace('$', 'jQuery') + for toktype, string, _, _, _ in generate_tokens(io.StringIO(typespec).readline): + if toktype == token.NAME: + if string == 'or': # special case "A or B" + # TODO: deprecation warning + yield (OP, '|') + else: + yield (NAME, string) + elif toktype == token.OP: + if string in '|<>[].,': + yield (OP, string) + elif string == '*': # maybe? + yield (NAME, 'any') + elif string in '()': + # TODO: deprecation warning + # seems useless unless we add support for "..." + continue + else: + raise ValueError("Unknown typespec operator %r" % string) + elif toktype in (token.STRING, token.NUMBER): # enum-ish + yield (LITERAL, ast.literal_eval(string)) + elif toktype == token.ENDMARKER: + return + else: + raise ValueError("Unknown typespec token %s" % token.tok_name[toktype]) + +Alt = collections.namedtuple('Alt', ['params']) +Literal = collections.namedtuple('Literal', ['value']) +Type = collections.namedtuple('Type', ['name', 'params']) + +def iterate(t, fortype, forliteral): + if isinstance(t, Alt): + ps = Peekable(t.params) + for param in ps: + for it in iterate(param, fortype, forliteral): + yield it + if ps.peek(None): + yield forliteral('|') + elif isinstance(t, Type): + yield fortype(t.name) + if not t.params: + return + yield forliteral('<') + ps = Peekable(t.params) + for param in ps: + for it in iterate(param, fortype, forliteral): + yield it + if ps.peek(None): + yield forliteral(',') + yield forliteral('>') + elif isinstance(t, Literal): + yield forliteral(t.value) + else: + raise ValueError('Unknown item %s' % t) + +def parse(typespec): + tokens = Peekable(tokenize(typespec)) + return parse_alt(tokens) + +# alt = param +# | param '|' alt +def parse_alt(tokens): + alt = Alt([]) + while True: + alt.params.append(parse_param(tokens)) + if not next_is(tokens, (OP, '|')): + break + return alt + +# param = 'null' | 'undefined' | 'true' | 'false' +# | number +# | string +# | typename '[]' +# X '[' alt ']' +# | typename '<' params '>' +# params = alt | alt ',' params +def parse_param(tokens): + t, v = tokens.peek() + if t == LITERAL or (t == NAME and v in LITERAL_NAME): + next(tokens) + return Literal(str(v)) + + # # [typespec] # should this be Array or an n-uple allowing multiple items? + # if tok == (OP, '['): + # next(tokens) + # t = Type('Array', [parse_alt(tokens)]) + # rest = next(tokens) + # expect(rest, (OP, ']')) + # return t + + t = Type(parse_typename(tokens), []) + # type[] + peek = tokens.peek(None) + if peek == (OP, '['): + next(tokens) # skip peeked + expect(next(tokens), (OP, ']')) + return Type('Array', [t]) + # type + if peek == (OP, '<'): + next(tokens) # skip peeked + while True: + t.params.append(parse_alt(tokens)) + n = next(tokens) + if n == (OP, ','): + continue + if n == (OP, '>'): + break + raise ValueError("Expected OP ',' or OP ',', got %s '%s'" % n) + return t + +# typename = name | name '.' typename +def parse_typename(tokens): + typename = [] + while True: + (t, n) = next(tokens) + if t != NAME: + raise ValueError("Expected a name token, got %s '%s'" % (t, n)) + if n in LITERAL_NAME: + raise ValueError("Expected a type name, got literal %s" % n) + typename.append(n) + if not next_is(tokens, (OP, '.')): + break + return '.'.join(typename) + +def next_is(tokens, expected): + """ + Consumes the next token if it's `expected` otherwise does not touch the + tokens. + + Returns whether it consumed a token + """ + if tokens.peek(None) == expected: + next(tokens) + return True + return False + +def expect(actual, expected): + """ Raises ValueError if `actual` and `expected` are different + + :type actual: (object, str) + :type expected: (object, str) + """ + if actual != expected: + raise ValueError("Expected %s '%s', got %s '%s'" % (expected + actual)) + + +NONE = object() +class Peekable(object): + __slots__ = ['_head', '_it'] + def __init__(self, iterable): + self._head = NONE + self._it = iter(iterable) + + def __iter__(self): + return self + def __next__(self): + if self._head is not NONE: + r, self._head = self._head, NONE + return r + return next(self._it) + next = __next__ + + def peek(self, default=NONE): + if self._head is NONE: + try: + self._head = next(self._it) + except StopIteration: + if default is not NONE: + return default + raise + return self._head diff --git a/doc/_extensions/autojsdoc/parser/utils.py b/doc/_extensions/autojsdoc/parser/utils.py new file mode 100644 index 00000000000..b8b1a97e7aa --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/utils.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +def _name(node): + if node['type'] == 'Identifier': + return node['name'] + if node['type'] == 'MemberExpression': + return "%s.%s" % (_name(node['object']), _name(node['property'])) + raise ValueError("Unnamable node type %s" % node['type']) + +def _value(node, strict=False): + t = node['type'] + if t == 'Identifier': + return node['name'] + elif t == 'Literal': + return node['value'] + msg = '<%s has no value>' % _identify(node, {}) + if strict: + raise ValueError(msg) + return msg + +def _identify(valnode, ns): + if valnode is None: + return "None" + # already identified and re-set in the ns? + if isinstance(valnode, Printable): + return valnode + + # check other non-empty returns + t = valnode['type'] + if t == "Literal": + if valnode.get('regex'): + return valnode['regex']['pattern'] + return valnode['value'] + elif t == "Identifier": + n = valnode['name'] + return ns.get(n, Global(n)) + elif t == "NewExpression": + return Instance(_identify(valnode['callee'], ns)) + elif t == "ObjectExpression": + return Namespace({ + _value(prop['key']): _identify(prop['value'], ns) + for prop in valnode['properties'] + }) + elif t == "MemberExpression": + return Deref( + _identify(valnode['object'], ns), + _identify(valnode['property'], ns) if valnode['computed'] else valnode['property']['name'], + ) + elif t == "CallExpression": + return Call( + _identify(valnode['callee'], ns), + [_identify(arg, ns) for arg in valnode['arguments']], + ) + elif t == 'BinaryExpression': + return "%s %s %s" % ( + _identify(valnode['left'], ns), + valnode['operator'], + _identify(valnode['right'], ns), + ) + else: + return t + +class Counter(object): + __slots__ = ['_number'] + def __init__(self): + self._number = 0 + + def __bool__(self): + return bool(self._number) + __nonzero__ = __bool__ + def __int__(self): + return self._number + + def get(self): + return self._number + def increment(self): + self._number += 1 + return self._number + def decrement(self): + self._number -= 1 + return self._number + + def __enter__(self): + self._number += 1 + return self._number + def __exit__(self, *args): + self._number -= 1 + +class Printable(object): + __slots__ = [] + depth = Counter() + def __repr__(self): + with self.depth as i: + if i > 2: + return "%s(...)" % type(self).__name__ + return "%s(%s)" % (type(self).__name__, ', '.join( + "%s=%r" % (k, getattr(self, k)) + for k in self.__slots__ + if not k.startswith('_') + if getattr(self, k) + )) + + +def resolve(obj, store): + if obj is None: + return None + + if isinstance(obj, (unicode, bool, int, float)): + return obj + + if isinstance(obj, ModuleProxy): + return resolve(obj.get(), store) + + try: + if getattr(obj, 'resolved', False): + return obj + return obj.resolve(store) + except AttributeError: + raise TypeError("Unresolvable {!r}".format(obj)) + +class Resolvable(Printable): + """ + For types resolved in place + """ + __slots__ = ['resolved'] + + def __init__(self): + super(Resolvable, self).__init__() + self.resolved = False + def resolve(self, store): + self.resolved = True + return self + +class Namespace(Resolvable): + __slots__ = ['attrs'] + def __init__(self, attrs): + super(Namespace, self).__init__() + self.attrs = attrs + def resolve(self, store): + r = super(Namespace, self).resolve(store) + self.attrs = { + k: resolve(v, store) + for k, v in self.attrs.iteritems() + } + return r + def __getitem__(self, key): + assert isinstance(key, unicode), "%r is not a namespace key" % key + try: + return self.attrs[key] + except KeyError: + return Global(self)[key] + +class Module(Resolvable): + __slots__ = ('name', 'comments', 'exports', 'augments', 'dependencies', '_store') + def __init__(self, name, store, comments=()): + super(Module, self).__init__() + self.name = name + self._store = store + self.comments = tuple(comments) + self.exports = None + self.augments = [] + self.dependencies = set() + store[name] = self + def add_dependency(self, depname): + dep = ModuleProxy(depname, self._store) + self.dependencies.add(dep) + return dep + def get(self): + return self + def resolve(self, store=None): + r = super(Module, self).resolve(store) + self.exports = resolve(self.exports, self._store) + self.augments = [resolve(a, self._store) for a in self.augments] + self.dependencies = { + resolve(d, self._store) + for d in self.dependencies + } + return r + def __getitem__(self, k): + try: + return self.exports[k] + except KeyError: + return Global(self)[k] + +class ModuleProxy(object): + def __init__(self, name, store): + self.name = name + self.store = store + def get(self): + # web.web_client is not an actual module + return self.store.get(self.name, '' % self.name) + def __repr__(self): + return repr(self.get()) + def __hash__(self): + return hash(self.name) + def __eq__(self, other): + return self.name == other.name + +class Class(Resolvable): + __slots__ = ['name', 'members', 'extends', 'mixins', 'comments'] + def __init__(self, name, extends): + super(Class, self).__init__() + self.name = name + self.extends = extends + self.members = {} + self.mixins = [] + self.comments = [] + + def resolve(self, store): + r = super(Class, self).resolve(store) + self.extends = resolve(self.extends, store) + self.mixins = [resolve(m, store) for m in self.mixins] + return r + +class Instance(Resolvable): + __slots__ = ['type'] + def __init__(self, type): + super(Instance, self).__init__() + self.type = type + + def resolve(self, store): + r = super(Instance, self).resolve(store) + self.type = resolve(self.type, store) + if isinstance(self.type, Module): + self.type = self.type.exports + return r + + def __getitem__(self, key): + return Global(self)['key'] + +class Function(Resolvable): + __slots__ = ['comments'] + def __init__(self, comments): + super(Function, self).__init__() + self.comments = comments + +class Deref(Printable): + __slots__ = ['object', 'property'] + def __init__(self, object, property): + self.object = object + self.property = property + def resolve(self, store): + return resolve(self.object, store)[self.property] + +class Call(Resolvable): + __slots__ = ['callable'] + def __init__(self, callable, arguments): + super(Call, self).__init__() + self.callable = callable + def resolve(self, store): + r = super(Call, self).resolve(store) + self.callable = resolve(self.callable, store) + return r + def __getitem__(self, item): + return Global(self)[item] + +def get(node, it, *path): + if not path: + return node[it] + return get(node[it], *path) + + +def match(node, pattern): + """Checks that ``pattern`` is a subset of ``node``. + + If a sub-pattern is a callable it will be called with the current + sub-node and its result is the match's. Other sub-patterns are + checked by equality against the correspoding sub-node. + + Can be used to quickly check the descendants of a node of suitable + type. + + TODO: support checking list content? Would be convenient to + constraint e.g. CallExpression based on their parameters + + """ + if node is None and pattern is not None: + return + if callable(pattern): + return pattern(node) + if isinstance(node, list): + return all(len(node) > i and match(node[i], v) for i, v in pattern.items()) + if isinstance(pattern, dict): + return all(match(node.get(k), v) for k, v in pattern.items()) + return node == pattern + + +class Global(object): + def __init__(self, name): + self.name = name + def __repr__(self): + return '' % self.name + def __getitem__(self, name): + return Global('%s.%s' % (self.name, name)) + def resolve(self, store): + return self diff --git a/doc/_extensions/autojsdoc/parser/visitor.py b/doc/_extensions/autojsdoc/parser/visitor.py new file mode 100644 index 00000000000..e5529d511dc --- /dev/null +++ b/doc/_extensions/autojsdoc/parser/visitor.py @@ -0,0 +1,126 @@ +from pyjsparser.pyjsparserdata import Syntax + +_binary = lambda n: [n['left'], n['right']] +_function = lambda n: n['params'] + n['defaults'] + [n['body']] +# yields children in visitation order +_children = { + Syntax.ArrayExpression: lambda n: n['elements'], + Syntax.ArrayPattern: lambda n: n['elements'], + Syntax.ArrowFunctionExpression: _function, + Syntax.AssignmentExpression: _binary, + Syntax.AssignmentPattern: _binary, + Syntax.BinaryExpression: _binary, + Syntax.BlockStatement: lambda n: n['body'], + Syntax.BreakStatement: lambda n: [], + Syntax.CallExpression: lambda n: [n['callee']] + n['arguments'], + Syntax.CatchClause: lambda n: [n['param'], n['body']], + Syntax.ClassBody: lambda n: [n['body']], + Syntax.ClassDeclaration: lambda n: [n['superClass'], n['body']], + Syntax.ClassExpression: lambda n: [n['superClass'], n['body']], + Syntax.ConditionalExpression: lambda n: [n['test'], n['consequent'], n['alternate']], + Syntax.ContinueStatement: lambda n: [], + Syntax.DebuggerStatement: lambda n: [], + Syntax.DoWhileStatement: lambda n: [n['body'], n['test']], + Syntax.EmptyStatement: lambda n: [], + Syntax.ExportAllDeclaration: lambda n: [n['source']], + Syntax.ExportDefaultDeclaration: lambda n: [n['declaration']], + Syntax.ExportNamedDeclaration: lambda n: ([n['declaration']] if n['declaration'] else n['specifiers']) + [n['source']], + Syntax.ExportSpecifier: lambda n: [n['local'], n['exported']], + Syntax.ExpressionStatement: lambda n: [n['expression']], + Syntax.ForStatement: lambda n: [n['init'], n['test'], n['update'], n['body']], + Syntax.ForInStatement: lambda n: [n['left'], n['right'], n['body']], + Syntax.FunctionDeclaration: _function, + Syntax.FunctionExpression: _function, + Syntax.Identifier: lambda n: [], + Syntax.IfStatement: lambda n: [n['test'], n['consequent'], n['alternate']], + Syntax.ImportDeclaration: lambda n: n['specifiers'] + [n['source']], + Syntax.ImportDefaultSpecifier: lambda n: [n['local']], + Syntax.ImportNamespaceSpecifier: lambda n: [n['local']], + Syntax.ImportSpecifier: lambda n: [n['local'], n['imported']], + Syntax.LabeledStatement: lambda n: [n['body']], + Syntax.Literal: lambda n: [], + Syntax.LogicalExpression: _binary, + Syntax.MemberExpression: lambda n: [n['object'], n['property']], + #Syntax.MethodDefinition: lambda n: [], + Syntax.NewExpression: lambda n: [n['callee']] + n['arguments'], + Syntax.ObjectExpression: lambda n: n['properties'], + Syntax.ObjectPattern: lambda n: n['properties'], + Syntax.Program: lambda n: n['body'], + Syntax.Property: lambda n: [n['key'], n['value']], + Syntax.RestElement: lambda n: [n['argument']], + Syntax.ReturnStatement: lambda n: [n['argument']], + Syntax.SequenceExpression: lambda n: n['expressions'], + Syntax.SpreadElement: lambda n: [n['argument']], + Syntax.Super: lambda n: [], + Syntax.SwitchCase: lambda n: [n['test'], n['consequent']], + Syntax.SwitchStatement: lambda n: [n['discriminant']] + n['cases'], + Syntax.TaggedTemplateExpression: lambda n: [n['tag'], n['quasi']], + Syntax.TemplateElement: lambda n: [], + Syntax.TemplateLiteral: lambda n: n['quasis'] + n['expressions'], + Syntax.ThisExpression: lambda n: [], + Syntax.ThrowStatement: lambda n: [n['argument']], + Syntax.TryStatement: lambda n: [n['block'], n['handler'], n['finalizer']], + Syntax.UnaryExpression: lambda n: [n['argument']], + Syntax.UpdateExpression: lambda n: [n['argument']], + Syntax.VariableDeclaration: lambda n: n['declarations'], + Syntax.VariableDeclarator: lambda n: [n['id'], n['init']], + Syntax.WhileStatement: lambda n: [n['test'], n['body']], + Syntax.WithStatement: lambda n: [n['object'], n['body']], +} + +SKIP = object() +class Visitor(object): + """ + Generic visitor for the pyjsparser AST. + + Visitation is driven by the ``visit`` method, which iterates the tree in + depth-first pre-order. + + For each node, calls ``enter_$NODETYPE``, visits the children then calls + ``exit_$NODETYPE``. If the enter or exit methods are not present on the + visitor, falls back on ``enter_generic`` and ``exit_generic``. + + Any ``enter_`` method can return ``SKIP`` to suppress both the traversal + of the subtree *and* the call to the corresponding ``exit_`` method + (whether generic or specific). + + For convenience, ``visit`` will return whatever is set as the visitor's + ``result`` attribute, ``None`` by default. + + ``visit`` can be given multiple root nodes, and it can be called multiple + times. The ``result`` attribute is cleared at each call but not between + two roots of the same ``visit`` call. + """ + def __init__(self): + super(Visitor, self).__init__() + self.result = None + + def enter_generic(self, node): pass + def exit_generic(self, node): pass + + def visit(self, nodes): + if isinstance(nodes, dict): + nodes = [nodes] + # if multiple nodes are passed in, we need to reverse the order in + # order to traverse front-to-back rather than the other way around + nodes = list(reversed(nodes)) + + while nodes: + node = nodes.pop() + # should probably filter None descendants in _children... + if node is None: + continue + node_type = node['type'] + if node_type == '_exit': + node = node['node'] + getattr(self, 'exit_' + node['type'], self.exit_generic)(node) + continue + + if getattr(self, 'enter_' + node_type, self.enter_generic)(node) is SKIP: + continue + + nodes.append({'type': '_exit', 'node': node}) + nodes.extend(reversed(_children[node_type](node))) + + return self.result + diff --git a/doc/_extensions/conftest.py b/doc/_extensions/conftest.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/doc/_extensions/odoo_ext/static/style.css b/doc/_extensions/odoo_ext/static/style.css index d521ff30677..ead78f9101d 100644 --- a/doc/_extensions/odoo_ext/static/style.css +++ b/doc/_extensions/odoo_ext/static/style.css @@ -1076,7 +1076,7 @@ body { font-size: 15px; line-height: 1.42857143; color: #666c75; - background-color: #f9f9f9; + background-color: #F9F9F9; } input, button, @@ -1087,7 +1087,7 @@ textarea { line-height: inherit; } a { - color: #875a7b; + color: #875A7B; text-decoration: none; } a:hover, @@ -1122,8 +1122,8 @@ img { .img-thumbnail { padding: 4px; line-height: 1.42857143; - background-color: #f9f9f9; - border: 1px solid #dddddd; + background-color: #F9F9F9; + border: 1px solid #ddd; border-radius: 4px; -webkit-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; @@ -1139,7 +1139,7 @@ hr { margin-top: 21px; margin-bottom: 21px; border: 0; - border-top: 1px solid #f2f2f2; + border-top: 1px solid #F2F2F2; } .sr-only { position: absolute; @@ -1402,7 +1402,7 @@ a.bg-danger:focus { .page-header { padding-bottom: 9.5px; margin: 42px 0 21px; - border-bottom: 1px solid #f2f2f2; + border-bottom: 1px solid #F2F2F2; } ul, ol { @@ -1470,7 +1470,7 @@ blockquote { padding: 10.5px 21px; margin: 0 0 21px; font-size: 18.75px; - border-left: 5px solid #f2f2f2; + border-left: 5px solid #F2F2F2; } blockquote p:last-child, blockquote ul:last-child, @@ -1494,7 +1494,7 @@ blockquote .small:before { blockquote.pull-right { padding-right: 15px; padding-left: 0; - border-right: 5px solid #f2f2f2; + border-right: 5px solid #F2F2F2; border-left: 0; text-align: right; } @@ -1535,8 +1535,8 @@ code { kbd { padding: 2px 4px; font-size: 90%; - color: #ffffff; - background-color: #333333; + color: #fff; + background-color: #333; border-radius: 3px; box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); } @@ -1554,9 +1554,9 @@ pre { line-height: 1.42857143; word-break: break-all; word-wrap: break-word; - color: #6b6d70; + color: #6B6D70; background-color: #f5f5f5; - border: 1px solid #cccccc; + border: 1px solid #ccc; border-radius: 4px; } pre code { @@ -2264,11 +2264,11 @@ th { padding: 8px; line-height: 1.42857143; vertical-align: top; - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .table > thead > tr > th { vertical-align: bottom; - border-bottom: 2px solid #dddddd; + border-bottom: 2px solid #ddd; } .table > caption + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > th, @@ -2279,10 +2279,10 @@ th { border-top: 0; } .table > tbody + tbody { - border-top: 2px solid #dddddd; + border-top: 2px solid #ddd; } .table .table { - background-color: #f9f9f9; + background-color: #F9F9F9; } .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, @@ -2293,7 +2293,7 @@ th { padding: 5px; } .table-bordered { - border: 1px solid #dddddd; + border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > tbody > tr > th, @@ -2301,7 +2301,7 @@ th { .table-bordered > thead > tr > td, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > td { - border: 1px solid #dddddd; + border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > thead > tr > td { @@ -2439,7 +2439,7 @@ table th[class*="col-"] { margin-bottom: 15.75px; overflow-y: hidden; -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #dddddd; + border: 1px solid #ddd; } .table-responsive > .table { margin-bottom: 0; @@ -2491,7 +2491,7 @@ legend { margin-bottom: 21px; font-size: 22.5px; line-height: inherit; - color: #6b6d70; + color: #6B6D70; border: 0; border-bottom: 1px solid #e5e5e5; } @@ -2545,9 +2545,9 @@ output { font-size: 15px; line-height: 1.42857143; color: #888888; - background-color: #ffffff; + background-color: #fff; background-image: none; - border: 1px solid #cccccc; + border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); @@ -2562,14 +2562,14 @@ output { box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); } .form-control::-moz-placeholder { - color: #999999; + color: #999; opacity: 1; } .form-control:-ms-input-placeholder { - color: #999999; + color: #999; } .form-control::-webkit-input-placeholder { - color: #999999; + color: #999; } .form-control::-ms-expand { border: 0; @@ -2578,7 +2578,7 @@ output { .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { - background-color: #f2f2f2; + background-color: #F2F2F2; opacity: 1; } .form-control[disabled], @@ -3024,7 +3024,7 @@ select[multiple].input-lg { .btn:hover, .btn:focus, .btn.focus { - color: #333333; + color: #333; text-decoration: none; } .btn:active, @@ -3048,25 +3048,25 @@ fieldset[disabled] a.btn { pointer-events: none; } .btn-default { - color: #333333; - background-color: #ffffff; - border-color: #cccccc; + color: #333; + background-color: #fff; + border-color: #ccc; } .btn-default:focus, .btn-default.focus { - color: #333333; + color: #333; background-color: #e6e6e6; border-color: #8c8c8c; } .btn-default:hover { - color: #333333; + color: #333; background-color: #e6e6e6; border-color: #adadad; } .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { - color: #333333; + color: #333; background-color: #e6e6e6; border-color: #adadad; } @@ -3079,7 +3079,7 @@ fieldset[disabled] a.btn { .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus { - color: #333333; + color: #333; background-color: #d4d4d4; border-color: #8c8c8c; } @@ -3097,33 +3097,33 @@ fieldset[disabled] .btn-default:focus, .btn-default.disabled.focus, .btn-default[disabled].focus, fieldset[disabled] .btn-default.focus { - background-color: #ffffff; - border-color: #cccccc; + background-color: #fff; + border-color: #ccc; } .btn-default .badge { - color: #ffffff; - background-color: #333333; + color: #fff; + background-color: #333; } .btn-primary { - color: #ffffff; + color: #fff; background-color: #21b799; border-color: #1da187; } .btn-primary:focus, .btn-primary.focus { - color: #ffffff; + color: #fff; background-color: #198c75; border-color: #0a352d; } .btn-primary:hover { - color: #ffffff; + color: #fff; background-color: #198c75; border-color: #146e5c; } .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { - color: #ffffff; + color: #fff; background-color: #198c75; border-color: #146e5c; } @@ -3136,7 +3136,7 @@ fieldset[disabled] .btn-default.focus { .btn-primary:active.focus, .btn-primary.active.focus, .open > .dropdown-toggle.btn-primary.focus { - color: #ffffff; + color: #fff; background-color: #146e5c; border-color: #0a352d; } @@ -3159,28 +3159,28 @@ fieldset[disabled] .btn-primary.focus { } .btn-primary .badge { color: #21b799; - background-color: #ffffff; + background-color: #fff; } .btn-success { - color: #ffffff; - background-color: #50af51; + color: #fff; + background-color: #50AF51; border-color: #489e49; } .btn-success:focus, .btn-success.focus { - color: #ffffff; + color: #fff; background-color: #408c41; border-color: #204620; } .btn-success:hover { - color: #ffffff; + color: #fff; background-color: #408c41; border-color: #357435; } .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { - color: #ffffff; + color: #fff; background-color: #408c41; border-color: #357435; } @@ -3193,7 +3193,7 @@ fieldset[disabled] .btn-primary.focus { .btn-success:active.focus, .btn-success.active.focus, .open > .dropdown-toggle.btn-success.focus { - color: #ffffff; + color: #fff; background-color: #357335; border-color: #204620; } @@ -3211,33 +3211,33 @@ fieldset[disabled] .btn-success:focus, .btn-success.disabled.focus, .btn-success[disabled].focus, fieldset[disabled] .btn-success.focus { - background-color: #50af51; + background-color: #50AF51; border-color: #489e49; } .btn-success .badge { - color: #50af51; - background-color: #ffffff; + color: #50AF51; + background-color: #fff; } .btn-info { - color: #ffffff; - background-color: #4b9eb6; + color: #fff; + background-color: #4B9EB6; border-color: #438fa5; } .btn-info:focus, .btn-info.focus { - color: #ffffff; + color: #fff; background-color: #3b7f93; border-color: #1e404a; } .btn-info:hover { - color: #ffffff; + color: #fff; background-color: #3b7f93; border-color: #316979; } .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { - color: #ffffff; + color: #fff; background-color: #3b7f93; border-color: #316979; } @@ -3250,7 +3250,7 @@ fieldset[disabled] .btn-success.focus { .btn-info:active.focus, .btn-info.active.focus, .open > .dropdown-toggle.btn-info.focus { - color: #ffffff; + color: #fff; background-color: #316979; border-color: #1e404a; } @@ -3268,33 +3268,33 @@ fieldset[disabled] .btn-info:focus, .btn-info.disabled.focus, .btn-info[disabled].focus, fieldset[disabled] .btn-info.focus { - background-color: #4b9eb6; + background-color: #4B9EB6; border-color: #438fa5; } .btn-info .badge { - color: #4b9eb6; - background-color: #ffffff; + color: #4B9EB6; + background-color: #fff; } .btn-warning { - color: #ffffff; - background-color: #f0ad4e; + color: #fff; + background-color: #F0AD4E; border-color: #eea236; } .btn-warning:focus, .btn-warning.focus { - color: #ffffff; + color: #fff; background-color: #ec971f; border-color: #985f0d; } .btn-warning:hover { - color: #ffffff; + color: #fff; background-color: #ec971f; border-color: #d58512; } .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { - color: #ffffff; + color: #fff; background-color: #ec971f; border-color: #d58512; } @@ -3307,7 +3307,7 @@ fieldset[disabled] .btn-info.focus { .btn-warning:active.focus, .btn-warning.active.focus, .open > .dropdown-toggle.btn-warning.focus { - color: #ffffff; + color: #fff; background-color: #d58512; border-color: #985f0d; } @@ -3325,33 +3325,33 @@ fieldset[disabled] .btn-warning:focus, .btn-warning.disabled.focus, .btn-warning[disabled].focus, fieldset[disabled] .btn-warning.focus { - background-color: #f0ad4e; + background-color: #F0AD4E; border-color: #eea236; } .btn-warning .badge { - color: #f0ad4e; - background-color: #ffffff; + color: #F0AD4E; + background-color: #fff; } .btn-danger { - color: #ffffff; - background-color: #d9534f; + color: #fff; + background-color: #D9534F; border-color: #d43f3a; } .btn-danger:focus, .btn-danger.focus { - color: #ffffff; + color: #fff; background-color: #c9302c; border-color: #761c19; } .btn-danger:hover { - color: #ffffff; + color: #fff; background-color: #c9302c; border-color: #ac2925; } .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { - color: #ffffff; + color: #fff; background-color: #c9302c; border-color: #ac2925; } @@ -3364,7 +3364,7 @@ fieldset[disabled] .btn-warning.focus { .btn-danger:active.focus, .btn-danger.active.focus, .open > .dropdown-toggle.btn-danger.focus { - color: #ffffff; + color: #fff; background-color: #ac2925; border-color: #761c19; } @@ -3382,15 +3382,15 @@ fieldset[disabled] .btn-danger:focus, .btn-danger.disabled.focus, .btn-danger[disabled].focus, fieldset[disabled] .btn-danger.focus { - background-color: #d9534f; + background-color: #D9534F; border-color: #d43f3a; } .btn-danger .badge { - color: #d9534f; - background-color: #ffffff; + color: #D9534F; + background-color: #fff; } .btn-link { - color: #875a7b; + color: #875A7B; font-weight: normal; border-radius: 0; } @@ -3518,8 +3518,8 @@ tbody.collapse.in { list-style: none; font-size: 15px; text-align: left; - background-color: #ffffff; - border: 1px solid #cccccc; + background-color: #fff; + border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 4px; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); @@ -3542,7 +3542,7 @@ tbody.collapse.in { clear: both; font-weight: normal; line-height: 1.42857143; - color: #6b6d70; + color: #6B6D70; white-space: nowrap; } .dropdown-menu > li > a:hover, @@ -3554,7 +3554,7 @@ tbody.collapse.in { .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { - color: #ffffff; + color: #fff; text-decoration: none; outline: 0; background-color: #21b799; @@ -3889,8 +3889,8 @@ select[multiple].input-group-sm > .input-group-btn > .btn { line-height: 1; color: #888888; text-align: center; - background-color: #f2f2f2; - border: 1px solid #cccccc; + background-color: #F2F2F2; + border: 1px solid #ccc; border-radius: 4px; } .input-group-addon.input-sm { @@ -3975,7 +3975,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav > li > a:hover, .nav > li > a:focus { text-decoration: none; - background-color: #f2f2f2; + background-color: #F2F2F2; } .nav > li.disabled > a { color: #dcddde; @@ -3990,8 +3990,8 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { - background-color: #f2f2f2; - border-color: #875a7b; + background-color: #F2F2F2; + border-color: #875A7B; } .nav .nav-divider { height: 1px; @@ -4003,7 +4003,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { max-width: none; } .nav-tabs { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; } .nav-tabs > li { float: left; @@ -4016,14 +4016,14 @@ select[multiple].input-group-sm > .input-group-btn > .btn { border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { - border-color: #f2f2f2 #f2f2f2 #dddddd; + border-color: #F2F2F2 #F2F2F2 #ddd; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { color: #888888; - background-color: #f9f9f9; - border: 1px solid #dddddd; + background-color: #F9F9F9; + border: 1px solid #ddd; border-bottom-color: transparent; cursor: default; } @@ -4058,17 +4058,17 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #dddddd; + border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #f9f9f9; + border-bottom-color: #F9F9F9; } } .nav-pills > li { @@ -4083,7 +4083,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { - color: #ffffff; + color: #fff; background-color: #21b799; } .nav-stacked > li { @@ -4126,17 +4126,17 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { - border: 1px solid #dddddd; + border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs-justified > li > a { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { - border-bottom-color: #f9f9f9; + border-bottom-color: #F9F9F9; } } .tab-content > .tab-pane { @@ -4481,7 +4481,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { border-color: #e7e7e7; } .navbar-default .navbar-brand { - color: #777777; + color: #777; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { @@ -4489,37 +4489,37 @@ select[multiple].input-group-sm > .input-group-btn > .btn { background-color: transparent; } .navbar-default .navbar-text { - color: #777777; + color: #777; } .navbar-default .navbar-nav > li > a { - color: #777777; + color: #777; } .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { - color: #333333; + color: #333; background-color: transparent; } .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { - color: #555555; + color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { - color: #cccccc; + color: #ccc; background-color: transparent; } .navbar-default .navbar-toggle { - border-color: #dddddd; + border-color: #ddd; } .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { - background-color: #dddddd; + background-color: #ddd; } .navbar-default .navbar-toggle .icon-bar { - background-color: #888888; + background-color: #888; } .navbar-default .navbar-collapse, .navbar-default .navbar-form { @@ -4529,51 +4529,51 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { background-color: #e7e7e7; - color: #555555; + color: #555; } @media (max-width: 767px) { .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #777777; + color: #777; } .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #333333; + color: #333; background-color: transparent; } .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #555555; + color: #555; background-color: #e7e7e7; } .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #cccccc; + color: #ccc; background-color: transparent; } } .navbar-default .navbar-link { - color: #777777; + color: #777; } .navbar-default .navbar-link:hover { - color: #333333; + color: #333; } .navbar-default .btn-link { - color: #777777; + color: #777; } .navbar-default .btn-link:hover, .navbar-default .btn-link:focus { - color: #333333; + color: #333; } .navbar-default .btn-link[disabled]:hover, fieldset[disabled] .navbar-default .btn-link:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:focus { - color: #cccccc; + color: #ccc; } .navbar-inverse { - background-color: #222222; + background-color: #222; border-color: #080808; } .navbar-inverse .navbar-brand { @@ -4581,7 +4581,7 @@ fieldset[disabled] .navbar-default .btn-link:focus { } .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-text { @@ -4592,30 +4592,30 @@ fieldset[disabled] .navbar-default .btn-link:focus { } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { - color: #ffffff; + color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444444; + color: #444; background-color: transparent; } .navbar-inverse .navbar-toggle { - border-color: #333333; + border-color: #333; } .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { - background-color: #333333; + background-color: #333; } .navbar-inverse .navbar-toggle .icon-bar { - background-color: #ffffff; + background-color: #fff; } .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { @@ -4625,7 +4625,7 @@ fieldset[disabled] .navbar-default .btn-link:focus { .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { background-color: #080808; - color: #ffffff; + color: #fff; } @media (max-width: 767px) { .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { @@ -4639,19 +4639,19 @@ fieldset[disabled] .navbar-default .btn-link:focus { } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #ffffff; + color: #fff; background-color: #080808; } .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444444; + color: #444; background-color: transparent; } } @@ -4659,20 +4659,20 @@ fieldset[disabled] .navbar-default .btn-link:focus { color: #ffffff; } .navbar-inverse .navbar-link:hover { - color: #ffffff; + color: #fff; } .navbar-inverse .btn-link { color: #ffffff; } .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus { - color: #ffffff; + color: #fff; } .navbar-inverse .btn-link[disabled]:hover, fieldset[disabled] .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444444; + color: #444; } .breadcrumb { padding: 8px 15px; @@ -4687,7 +4687,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .breadcrumb > li + li:before { content: "/\00a0"; padding: 0 5px; - color: #cccccc; + color: #ccc; } .breadcrumb > .active { color: #dcddde; @@ -4708,9 +4708,9 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { padding: 6px 12px; line-height: 1.42857143; text-decoration: none; - color: #875a7b; - background-color: #ffffff; - border: 1px solid #dddddd; + color: #875A7B; + background-color: #fff; + border: 1px solid #ddd; margin-left: -1px; } .pagination > li:first-child > a, @@ -4730,8 +4730,8 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > li > span:focus { z-index: 2; color: #926485; - background-color: #f2f2f2; - border-color: #dddddd; + background-color: #F2F2F2; + border-color: #ddd; } .pagination > .active > a, .pagination > .active > span, @@ -4740,7 +4740,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > .active > a:focus, .pagination > .active > span:focus { z-index: 3; - color: #ffffff; + color: #fff; background-color: #21b799; border-color: #21b799; cursor: default; @@ -4752,8 +4752,8 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { color: #dcddde; - background-color: #ffffff; - border-color: #dddddd; + background-color: #fff; + border-color: #ddd; cursor: not-allowed; } .pagination-lg > li > a, @@ -4801,14 +4801,14 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pager li > span { display: inline-block; padding: 5px 14px; - background-color: #ffffff; - border: 1px solid #dddddd; + background-color: #fff; + border: 1px solid #ddd; border-radius: 15px; } .pager li > a:hover, .pager li > a:focus { text-decoration: none; - background-color: #f2f2f2; + background-color: #F2F2F2; } .pager .next > a, .pager .next > span { @@ -4823,7 +4823,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pager .disabled > a:focus, .pager .disabled > span { color: #dcddde; - background-color: #ffffff; + background-color: #fff; cursor: not-allowed; } .label { @@ -4832,7 +4832,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { font-size: 75%; font-weight: bold; line-height: 1; - color: #ffffff; + color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; @@ -4840,7 +4840,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { } a.label:hover, a.label:focus { - color: #ffffff; + color: #fff; text-decoration: none; cursor: pointer; } @@ -4866,28 +4866,28 @@ a.label:focus { background-color: #198c75; } .label-success { - background-color: #50af51; + background-color: #50AF51; } .label-success[href]:hover, .label-success[href]:focus { background-color: #408c41; } .label-info { - background-color: #4b9eb6; + background-color: #4B9EB6; } .label-info[href]:hover, .label-info[href]:focus { background-color: #3b7f93; } .label-warning { - background-color: #f0ad4e; + background-color: #F0AD4E; } .label-warning[href]:hover, .label-warning[href]:focus { background-color: #ec971f; } .label-danger { - background-color: #d9534f; + background-color: #D9534F; } .label-danger[href]:hover, .label-danger[href]:focus { @@ -4899,7 +4899,7 @@ a.label:focus { padding: 3px 7px; font-size: 13px; font-weight: bold; - color: #ffffff; + color: #fff; line-height: 1; vertical-align: middle; white-space: nowrap; @@ -4921,14 +4921,14 @@ a.label:focus { } a.badge:hover, a.badge:focus { - color: #ffffff; + color: #fff; text-decoration: none; cursor: pointer; } .list-group-item.active > .badge, .nav-pills > .active > a > .badge { - color: #875a7b; - background-color: #ffffff; + color: #875A7B; + background-color: #fff; } .list-group-item > .badge { float: right; @@ -4944,7 +4944,7 @@ a.badge:focus { padding-bottom: 30px; margin-bottom: 30px; color: inherit; - background-color: #f2f2f2; + background-color: #F2F2F2; } .jumbotron h1, .jumbotron .h1 { @@ -4987,8 +4987,8 @@ a.badge:focus { padding: 4px; margin-bottom: 21px; line-height: 1.42857143; - background-color: #f9f9f9; - border: 1px solid #dddddd; + background-color: #F9F9F9; + border: 1px solid #ddd; border-radius: 4px; -webkit-transition: border 0.2s ease-in-out; -o-transition: border 0.2s ease-in-out; @@ -5002,7 +5002,7 @@ a.badge:focus { a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { - border-color: #875a7b; + border-color: #875A7B; } .thumbnail .caption { padding: 9px; @@ -5114,7 +5114,7 @@ a.thumbnail.active { height: 100%; font-size: 13px; line-height: 21px; - color: #ffffff; + color: #fff; text-align: center; background-color: #21b799; -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); @@ -5137,7 +5137,7 @@ a.thumbnail.active { animation: progress-bar-stripes 2s linear infinite; } .progress-bar-success { - background-color: #50af51; + background-color: #50AF51; } .progress-striped .progress-bar-success { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); @@ -5145,7 +5145,7 @@ a.thumbnail.active { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-bar-info { - background-color: #4b9eb6; + background-color: #4B9EB6; } .progress-striped .progress-bar-info { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); @@ -5153,7 +5153,7 @@ a.thumbnail.active { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-bar-warning { - background-color: #f0ad4e; + background-color: #F0AD4E; } .progress-striped .progress-bar-warning { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); @@ -5161,7 +5161,7 @@ a.thumbnail.active { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-bar-danger { - background-color: #d9534f; + background-color: #D9534F; } .progress-striped .progress-bar-danger { background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); @@ -5225,8 +5225,8 @@ a.thumbnail.active { display: block; padding: 10px 15px; margin-bottom: -1px; - background-color: #ffffff; - border: 1px solid #dddddd; + background-color: #fff; + border: 1px solid #ddd; } .list-group-item:first-child { border-top-right-radius: 4px; @@ -5239,18 +5239,18 @@ a.thumbnail.active { } a.list-group-item, button.list-group-item { - color: #555555; + color: #555; } a.list-group-item .list-group-item-heading, button.list-group-item .list-group-item-heading { - color: #333333; + color: #333; } a.list-group-item:hover, button.list-group-item:hover, a.list-group-item:focus, button.list-group-item:focus { text-decoration: none; - color: #555555; + color: #555; background-color: #f5f5f5; } button.list-group-item { @@ -5260,7 +5260,7 @@ button.list-group-item { .list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus { - background-color: #f2f2f2; + background-color: #F2F2F2; color: #dcddde; cursor: not-allowed; } @@ -5278,7 +5278,7 @@ button.list-group-item { .list-group-item.active:hover, .list-group-item.active:focus { z-index: 2; - color: #ffffff; + color: #fff; background-color: #21b799; border-color: #21b799; } @@ -5424,7 +5424,7 @@ button.list-group-item-danger.active:focus { } .panel { margin-bottom: 21px; - background-color: #ffffff; + background-color: #fff; border: 1px solid transparent; border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); @@ -5458,7 +5458,7 @@ button.list-group-item-danger.active:focus { .panel-footer { padding: 10px 15px; background-color: #f5f5f5; - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } @@ -5572,7 +5572,7 @@ button.list-group-item-danger.active:focus { .panel > .panel-body + .table-responsive, .panel > .table + .panel-body, .panel > .table-responsive + .panel-body { - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td { @@ -5649,37 +5649,37 @@ button.list-group-item-danger.active:focus { } .panel-group .panel-heading + .panel-collapse > .panel-body, .panel-group .panel-heading + .panel-collapse > .list-group { - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .panel-group .panel-footer { border-top: 0; } .panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; } .panel-default { - border-color: #dddddd; + border-color: #ddd; } .panel-default > .panel-heading { - color: #6b6d70; + color: #6B6D70; background-color: #f5f5f5; - border-color: #dddddd; + border-color: #ddd; } .panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #dddddd; + border-top-color: #ddd; } .panel-default > .panel-heading .badge { color: #f5f5f5; - background-color: #6b6d70; + background-color: #6B6D70; } .panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #dddddd; + border-bottom-color: #ddd; } .panel-primary { border-color: #21b799; } .panel-primary > .panel-heading { - color: #ffffff; + color: #fff; background-color: #21b799; border-color: #21b799; } @@ -5688,7 +5688,7 @@ button.list-group-item-danger.active:focus { } .panel-primary > .panel-heading .badge { color: #21b799; - background-color: #ffffff; + background-color: #fff; } .panel-primary > .panel-footer + .panel-collapse > .panel-body { border-bottom-color: #21b799; @@ -5818,14 +5818,14 @@ button.list-group-item-danger.active:focus { font-size: 22.5px; font-weight: bold; line-height: 1; - color: #000000; - text-shadow: 0 1px 0 #ffffff; + color: #000; + text-shadow: 0 1px 0 #fff; opacity: 0.2; filter: alpha(opacity=20); } .close:hover, .close:focus { - color: #000000; + color: #000; text-decoration: none; cursor: pointer; opacity: 0.5; @@ -5880,8 +5880,8 @@ button.close { } .modal-content { position: relative; - background-color: #ffffff; - border: 1px solid #999999; + background-color: #fff; + border: 1px solid #999; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 6px; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); @@ -5896,7 +5896,7 @@ button.close { bottom: 0; left: 0; z-index: 1040; - background-color: #000000; + background-color: #000; } .modal-backdrop.fade { opacity: 0; @@ -6007,9 +6007,9 @@ button.close { .tooltip-inner { max-width: 200px; padding: 3px 8px; - color: #ffffff; + color: #fff; text-align: center; - background-color: #000000; + background-color: #000; border-radius: 4px; } .tooltip-arrow { @@ -6024,56 +6024,56 @@ button.close { left: 50%; margin-left: -5px; border-width: 5px 5px 0; - border-top-color: #000000; + border-top-color: #000; } .tooltip.top-left .tooltip-arrow { bottom: 0; right: 5px; margin-bottom: -5px; border-width: 5px 5px 0; - border-top-color: #000000; + border-top-color: #000; } .tooltip.top-right .tooltip-arrow { bottom: 0; left: 5px; margin-bottom: -5px; border-width: 5px 5px 0; - border-top-color: #000000; + border-top-color: #000; } .tooltip.right .tooltip-arrow { top: 50%; left: 0; margin-top: -5px; border-width: 5px 5px 5px 0; - border-right-color: #000000; + border-right-color: #000; } .tooltip.left .tooltip-arrow { top: 50%; right: 0; margin-top: -5px; border-width: 5px 0 5px 5px; - border-left-color: #000000; + border-left-color: #000; } .tooltip.bottom .tooltip-arrow { top: 0; left: 50%; margin-left: -5px; border-width: 0 5px 5px; - border-bottom-color: #000000; + border-bottom-color: #000; } .tooltip.bottom-left .tooltip-arrow { top: 0; right: 5px; margin-top: -5px; border-width: 0 5px 5px; - border-bottom-color: #000000; + border-bottom-color: #000; } .tooltip.bottom-right .tooltip-arrow { top: 0; left: 5px; margin-top: -5px; border-width: 0 5px 5px; - border-bottom-color: #000000; + border-bottom-color: #000; } .popover { position: absolute; @@ -6099,9 +6099,9 @@ button.close { word-spacing: normal; word-wrap: normal; font-size: 15px; - background-color: #ffffff; + background-color: #fff; background-clip: padding-box; - border: 1px solid #cccccc; + border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); @@ -6159,7 +6159,7 @@ button.close { bottom: 1px; margin-left: -10px; border-bottom-width: 0; - border-top-color: #ffffff; + border-top-color: #fff; } .popover.right > .arrow { top: 50%; @@ -6174,7 +6174,7 @@ button.close { left: 1px; bottom: -10px; border-left-width: 0; - border-right-color: #ffffff; + border-right-color: #fff; } .popover.bottom > .arrow { left: 50%; @@ -6189,7 +6189,7 @@ button.close { top: 1px; margin-left: -10px; border-top-width: 0; - border-bottom-color: #ffffff; + border-bottom-color: #fff; } .popover.left > .arrow { top: 50%; @@ -6203,7 +6203,7 @@ button.close { content: " "; right: 1px; border-right-width: 0; - border-left-color: #ffffff; + border-left-color: #fff; bottom: -10px; } .carousel { @@ -6297,7 +6297,7 @@ button.close { opacity: 0.5; filter: alpha(opacity=50); font-size: 20px; - color: #ffffff; + color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0); @@ -6321,7 +6321,7 @@ button.close { .carousel-control:hover, .carousel-control:focus { outline: 0; - color: #ffffff; + color: #fff; text-decoration: none; opacity: 0.9; filter: alpha(opacity=90); @@ -6376,7 +6376,7 @@ button.close { height: 10px; margin: 1px; text-indent: -999px; - border: 1px solid #ffffff; + border: 1px solid #fff; border-radius: 10px; cursor: pointer; background-color: #000 \9; @@ -6386,7 +6386,7 @@ button.close { margin: 0; width: 12px; height: 12px; - background-color: #ffffff; + background-color: #fff; } .carousel-caption { position: absolute; @@ -6396,7 +6396,7 @@ button.close { z-index: 10; padding-top: 20px; padding-bottom: 20px; - color: #ffffff; + color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); } @@ -9203,7 +9203,7 @@ main.has_code_col:after, } @media screen and (min-width: 768px) { .o-doc-dropdown.open { - background-color: #fcfcfc; + background-color: #FCFCFC; } } .o-doc-dropdown.open .dropdown-toggle { @@ -9218,7 +9218,7 @@ main.has_code_col:after, border-radius: 0; text-align: left; min-width: 100%; - background-color: #fcfcfc; + background-color: #FCFCFC; padding-bottom: 12.5px; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.17); } @@ -9253,7 +9253,7 @@ main.has_code_col:after, } @media screen and (min-width: 768px) { .o-doc-dropdown.open .dropdown-menu { - background-color: #fcfcfc; + background-color: #FCFCFC; } } .o-doc-dropdown.o_dropdown_mobile_nav { @@ -9569,7 +9569,7 @@ header { position: relative; } #wrap > .container.index { - background-color: #f9f9f9; + background-color: #F9F9F9; width: 100%; position: relative; } @@ -9651,8 +9651,8 @@ main aside { } } main article.doc-body { - background: #ffffff; - border-left: 1px solid #f2f2f2; + background: #fff; + border-left: 1px solid #F2F2F2; padding: 15px 30px; } @media (min-width: 992px) { @@ -9931,8 +9931,8 @@ article.doc-toc .toctree-wrapper > ul > li > span { } .o_main_header > .o_main_header_main > .o_primary_nav.o_sub_opened:before { visibility: visible; - background: #fcfcfc; - box-shadow: 0 1px 0 #f2f2f2; + background: #FCFCFC; + box-shadow: 0 1px 0 #F2F2F2; -webkit-transition: background 500ms cubic-bezier(0.55, 0, 0.1, 1) 0ms, visibility 0ms linear 0s, box-shadow 500ms linear 0ms; -o-transition: background 500ms cubic-bezier(0.55, 0, 0.1, 1) 0ms, visibility 0ms linear 0s, box-shadow 500ms linear 0ms; transition: background 500ms cubic-bezier(0.55, 0, 0.1, 1) 0ms, visibility 0ms linear 0s, box-shadow 500ms linear 0ms; @@ -9983,12 +9983,12 @@ article.doc-toc .toctree-wrapper > ul > li > span { transform: rotate(-45deg); } .o_main_header > .o_main_header_main > .o_primary_nav .dropdown.open > a { - color: #9e588b !important; + color: #9E588B !important; font-weight: 600; } .o_main_header > .o_main_header_main > .o_primary_nav .dropdown.open > a::before, .o_main_header > .o_main_header_main > .o_primary_nav .dropdown.open > a::after { - background-color: #9e588b !important; + background-color: #9E588B !important; box-shadow: 0 1px 1px rgba(158, 88, 139, 0.1); right: 3.53553391px; } @@ -10104,7 +10104,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { padding-top: 10px; } .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div + div { - border-left: 1px solid #f2f2f2; + border-left: 1px solid #F2F2F2; } } .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div ul { @@ -10221,12 +10221,12 @@ article.doc-toc .toctree-wrapper > ul > li > span { .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_website_apps a:hover, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_website_apps a:focus, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_website_apps .o_nav_app_family { - color: #5b899e; + color: #5B899E; } .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_sale_apps a:hover, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_sale_apps a:focus, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_sale_apps .o_nav_app_family { - color: #e46f78; + color: #E46F78; } .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_sale_apps .o_nav_app_family > span { background-position: -60px 50%; @@ -10234,7 +10234,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_operation_apps a:hover, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_operation_apps a:focus, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_operation_apps .o_nav_app_family { - color: #5c5b80; + color: #5C5B80; } .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_operation_apps .o_nav_app_family > span { background-position: -120px 50%; @@ -10242,7 +10242,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_productivity_apps a:hover, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_productivity_apps a:focus, .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_productivity_apps .o_nav_app_family { - color: #d5653e; + color: #D5653E; } .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav > .container > .row > div.o_productivity_apps .o_nav_app_family > span { background-position: -181px 50%; @@ -10258,7 +10258,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { display: block; height: 50px; line-height: 50px; - border-top: 1px solid #f2f2f2; + border-top: 1px solid #F2F2F2; text-align: center; font-size: 12px; font-weight: 600; @@ -10266,7 +10266,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { @media (min-width: 768px) { .o_main_header > .o_main_header_main > .o_primary_nav .o_secondary_nav { display: block; - background-color: #fcfcfc; + background-color: #FCFCFC; visibility: hidden; z-index: -2; -webkit-transform: translate(0, -115%); @@ -10530,11 +10530,11 @@ article.doc-toc .toctree-wrapper > ul > li > span { box-shadow: 0 1px 1px rgba(255, 255, 255, 0.1); } .o_main_header > .o_main_header_main > .o_header_buttons .dropdown.open { - background-color: #fcfcfc; + background-color: #FCFCFC; color: #5a5f68; } .o_main_header > .o_main_header_main > .o_header_buttons .dropdown.open .dropdown-menu { - background-color: #fcfcfc; + background-color: #FCFCFC; margin-top: 0; border: none; border-radius: 0; @@ -10560,7 +10560,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { } .o_main_header.o_scrolled:not(.o_transparent_blocked), .o_main_header.o_sub_opened { - background-color: #fcfcfc; + background-color: #FCFCFC; color: #5a5f68; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.17); } @@ -10584,7 +10584,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { box-shadow: 0 1px 1px rgba(90, 95, 104, 0.1); } .o_main_header.o_scrolled .o_sub_nav { - background: #fcfcfc; + background: #FCFCFC; } .o_main_header.o_scrolled .o_sub_nav > .container .navbar-header > span { -webkit-transform: translate(0, 0); @@ -10603,7 +10603,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { position: relative; height: auto; min-height: 50px; - background-color: #fcfcfc; + background-color: #FCFCFC; color: #5a5f68; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.17); } @@ -10648,7 +10648,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_header_buttons { float: none; - background-color: #25252d; + background-color: #25252D; padding: 0 15px; box-sizing: content-box; margin: 50px -15px 0; @@ -10659,7 +10659,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_header_buttons > * { overflow: visible!important; - color: #fcfcfc; + color: #FCFCFC; float: none; display: inline; } @@ -10667,7 +10667,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { display: initial!important; } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_header_buttons > * .dropdown-toggle { - color: #fcfcfc !important; + color: #FCFCFC !important; } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_header_buttons > * .dropdown-menu { position: relative; @@ -10683,7 +10683,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { display: block; margin-bottom: 5px; padding: 10px; - color: #fcfcfc !important; + color: #FCFCFC !important; font-size: 0.8em; } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_header_buttons > * .dropdown-menu > li > a:hover { @@ -10691,7 +10691,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_primary_nav { display: block; - background-color: #25252d; + background-color: #25252D; color: white; margin: 0 -15px 0; padding: 0; @@ -10704,7 +10704,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { color: white; } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_primary_nav li { - border: 0 solid #33343d; + border: 0 solid #33343D; border-width: 1px 5px 0; } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_primary_nav > li { @@ -10750,7 +10750,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_primary_nav > li.dropdown > .o_secondary_nav > .container > .row > div > .o_nav_app_family { height: 50px; line-height: 50px; - background-color: #33343d; + background-color: #33343D; } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_primary_nav > li.dropdown > .o_secondary_nav > .container > .row > div > .o_nav_app_family > * { display: none; @@ -10760,7 +10760,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { } .o_main_header.o_mobile_menu_opened > .o_main_header_main > .o_primary_nav > li.dropdown > .o_secondary_nav > .o_store_link { position: relative; - border: 0 solid #33343d; + border: 0 solid #33343D; border-width: 14px 0 0; padding-top: 14px; line-height: 1.5; @@ -10772,7 +10772,7 @@ article.doc-toc .toctree-wrapper > ul > li > span { width: 100%; height: 20px; margin-bottom: -5px; - background-color: #33343d; + background-color: #33343D; } } body > footer { @@ -10781,7 +10781,7 @@ body > footer { width: 100%; height: auto; min-height: 300px; - background-color: #282f33; + background-color: #282F33; color: #888888; z-index: 1; } @@ -10809,14 +10809,14 @@ body > footer a:focus { color: white; } body > footer .dropdown-menu a { - color: #6b6d70; + color: #6B6D70; } body > footer .dropdown-menu a:hover, body > footer .dropdown-menu a:focus { color: #5f6063; } body > footer .dropdown-menu a:active { - color: #ffffff; + color: #fff; } body > footer .divider { margin: 10px 0; @@ -11011,7 +11011,7 @@ body > footer .o_footer_bottom .social-links .fa { } .navbar-aside ul ul { max-height: 0; - background-color: #f2f2f2; + background-color: #F2F2F2; padding-left: 15px; overflow: hidden; -webkit-transition: max-height 0.3s cubic-bezier(0.55, 0, 0.1, 1); @@ -11021,7 +11021,7 @@ body > footer .o_footer_bottom .social-links .fa { .navbar-aside li.active, .navbar-aside li.active:hover, .navbar-aside li.active:focus { - border-color: #f2f2f2; + border-color: #F2F2F2; background: transparent; } .navbar-aside li.active > a, @@ -11033,7 +11033,7 @@ body > footer .o_footer_bottom .social-links .fa { max-height: 1000px; } .navbar-aside > ul > li { - border-bottom: 1px solid #f2f2f2; + border-bottom: 1px solid #F2F2F2; background: white; } .navbar-aside > ul > li > a { @@ -11263,8 +11263,8 @@ body > footer .o_footer_bottom .social-links .fa { } } .o_main_header.o_mobile_menu_opened .mdi-navigation-menu { - background-color: #282f33; - color: #f2f2f2; + background-color: #282F33; + color: #F2F2F2; } @media screen and (min-width: 768px) { .o_main_header > .o_sub_nav #o_sub-menu .o_breadcrumb { @@ -11361,13 +11361,13 @@ body > footer .o_footer_bottom .social-links .fa { padding: 0 15px; margin: 0; height: 38px; - background-color: #f2f2f2; + background-color: #F2F2F2; } .o_main_header > .o_sub_nav .navbar-toggle span { line-height: 38px; } .o_main_header > .o_sub_nav .navbar-toggle.collapsed { - background-color: #fcfcfc; + background-color: #FCFCFC; } .o_main_header > .o_sub_nav .collapse .nav { margin-right: 0; @@ -11413,7 +11413,7 @@ body > footer .o_footer_bottom .social-links .fa { } @media screen and (min-width: 768px) { .o_main_header > .o_sub_nav .collapse .nav > li.dropdown.open { - background-color: #fcfcfc; + background-color: #FCFCFC; } } .o_main_header > .o_sub_nav .collapse .nav > li.dropdown.open .dropdown-toggle { @@ -11428,7 +11428,7 @@ body > footer .o_footer_bottom .social-links .fa { border-radius: 0; text-align: left; min-width: 100%; - background-color: #fcfcfc; + background-color: #FCFCFC; padding-bottom: 12.5px; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.17); } @@ -11463,7 +11463,7 @@ body > footer .o_footer_bottom .social-links .fa { } @media screen and (min-width: 768px) { .o_main_header > .o_sub_nav .collapse .nav > li.dropdown.open .dropdown-menu { - background-color: #fcfcfc; + background-color: #FCFCFC; } } .o_main_header > .o_sub_nav .collapse .nav > li.dropdown.o_dropdown_mobile_nav { @@ -11526,7 +11526,7 @@ body > footer .o_footer_bottom .social-links .fa { } @media screen and (min-width: 768px) { .o_main_header > .o_sub_nav li.dropdown.open { - background-color: #fcfcfc; + background-color: #FCFCFC; } } .o_main_header > .o_sub_nav li.dropdown.open .dropdown-toggle { @@ -11541,7 +11541,7 @@ body > footer .o_footer_bottom .social-links .fa { border-radius: 0; text-align: left; min-width: 100%; - background-color: #fcfcfc; + background-color: #FCFCFC; padding-bottom: 12.5px; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.17); } @@ -11576,7 +11576,7 @@ body > footer .o_footer_bottom .social-links .fa { } @media screen and (min-width: 768px) { .o_main_header > .o_sub_nav li.dropdown.open .dropdown-menu { - background-color: #fcfcfc; + background-color: #FCFCFC; } } .o_main_header > .o_sub_nav li.dropdown.o_dropdown_mobile_nav { @@ -11660,7 +11660,7 @@ body > footer .o_footer_bottom .social-links .fa { #wrap { z-index: 1; position: relative; - background: #f9f9f9; + background: #F9F9F9; padding-bottom: 40px; } main { @@ -11669,7 +11669,7 @@ main { margin: -10px auto 0; display: block; border-radius: 2px; - background-color: #ffffff; + background-color: #fff; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.17); } main.deep_hover:hover { @@ -11700,7 +11700,7 @@ main.index .card { overflow: hidden; margin-bottom: 20px; min-height: 200px; - background-color: #ffffff; + background-color: #fff; will-change: transform; box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 2px 0 rgba(0, 0, 0, 0.05); -webkit-transform: scale3d(0, 0, 0) translate3d(50px, 0, 0); @@ -11808,11 +11808,11 @@ main.index.animating .card { } .card-img { background: #75526b; - background: -webkit-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: -moz-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: -ms-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: -o-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: linear-gradient(99deg, #875a7b 10%, #62495b 90%); + background: -webkit-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: -moz-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: -ms-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: -o-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: linear-gradient(99deg, #875A7B 10%, #62495B 90%); position: absolute; top: 0; left: auto; @@ -11832,11 +11832,11 @@ main.index.animating .card { overflow: hidden; margin: 0; background: #75526b; - background: -webkit-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: -moz-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: -ms-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: -o-linear-gradient(99deg, #875a7b 10%, #62495b 90%); - background: linear-gradient(99deg, #875a7b 10%, #62495b 90%); + background: -webkit-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: -moz-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: -ms-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: -o-linear-gradient(99deg, #875A7B 10%, #62495B 90%); + background: linear-gradient(99deg, #875A7B 10%, #62495B 90%); -webkit-animation: fadeIn 1s; -o-animation: fadeIn 1s; animation: fadeIn 1s; @@ -11974,7 +11974,7 @@ main.index.animating .card { } } .card.top.has_banner { - background: #282f33; + background: #282F33; } .card.top.has_banner:before { display: none; @@ -12047,7 +12047,7 @@ main .alert > ul { } main .alert > .alert-title, main .alert > h3 { - color: #4b9eb6; + color: #4B9EB6; } main .alert > .alert-title:before, main .alert > h3:before { @@ -12095,7 +12095,7 @@ main .alert.alert-success { } main .alert.alert-success > .alert-title, main .alert.alert-success > h3 { - color: #50af51; + color: #50AF51; } main .alert.alert-success > .alert-title:before, main .alert.alert-success > h3:before { @@ -12111,7 +12111,7 @@ main .alert.alert-info > .alert-title, main .alert.tip > .alert-title, main .alert.alert-info > h3, main .alert.tip > h3 { - color: #4b9eb6; + color: #4B9EB6; } main .alert.alert-info > .alert-title:before, main .alert.tip > .alert-title:before, @@ -12129,7 +12129,7 @@ main .alert.alert-warning > .alert-title, main .alert.warning > .alert-title, main .alert.alert-warning > h3, main .alert.warning > h3 { - color: #f0ad4e; + color: #F0AD4E; } main .alert.alert-warning > .alert-title:before, main .alert.warning > .alert-title:before, @@ -12144,7 +12144,7 @@ main .alert.alert-danger { } main .alert.alert-danger > .alert-title, main .alert.alert-danger > h3 { - color: #d9534f; + color: #D9534F; } main .alert.alert-danger > .alert-title:before, main .alert.alert-danger > h3:before { @@ -12157,7 +12157,7 @@ main .alert.alert-exercise { } main .alert.alert-exercise > .alert-title, main .alert.alert-exercise > h3 { - color: #938e94; + color: #938E94; } main .alert.alert-exercise > .alert-title:before, main .alert.alert-exercise > h3:before { @@ -12179,7 +12179,7 @@ main .alert.alert-go_to a { } main .alert.alert-go_to > .alert-title, main .alert.alert-go_to > h3 { - color: #4b9eb6; + color: #4B9EB6; } main .alert.alert-go_to > .alert-title:before, main .alert.alert-go_to > h3:before { @@ -12214,6 +12214,7 @@ span.menuselection { } dt { margin: .5em 0 .3em; + font-weight: normal; } blockquote { font-family: Georgia, "Times New Roman", Times, serif; @@ -12283,6 +12284,8 @@ table, .table { font-size: 0.9em; } +.code-namespace, +.code-module, .code-class, .code-staticmethod, .code-classmethod, @@ -12293,6 +12296,8 @@ table, padding-left: 20px; margin-bottom: 2em; } +.code-namespace > h6, +.code-module > h6, .code-class > h6, .code-staticmethod > h6, .code-classmethod > h6, @@ -12303,6 +12308,8 @@ table, margin-left: -20px; margin-bottom: 0.3em; } +.code-namespace > h6 .viewcode-link, +.code-module > h6 .viewcode-link, .code-class > h6 .viewcode-link, .code-staticmethod > h6 .viewcode-link, .code-classmethod > h6 .viewcode-link, @@ -12313,6 +12320,8 @@ table, display: none; float: right; } +.code-namespace > h6:hover .viewcode-link, +.code-module > h6:hover .viewcode-link, .code-class > h6:hover .viewcode-link, .code-staticmethod > h6:hover .viewcode-link, .code-classmethod > h6:hover .viewcode-link, @@ -12322,6 +12331,8 @@ table, .code-data > h6:hover .viewcode-link { display: inline; } +.code-namespace p, +.code-module p, .code-class p, .code-staticmethod p, .code-classmethod p, @@ -12340,7 +12351,7 @@ table, } } article.doc-body { - background: #ffffff; + background: #fff; } article.doc-body section.doc-content:first-of-type > p:first-child { margin-bottom: 21px; diff --git a/doc/_extensions/odoo_ext/static/style.less b/doc/_extensions/odoo_ext/static/style.less index 5fd9fe17804..89d287d487e 100644 --- a/doc/_extensions/odoo_ext/static/style.less +++ b/doc/_extensions/odoo_ext/static/style.less @@ -621,7 +621,7 @@ span.menuselection{ background:transparent; } -dt { margin: .5em 0 .3em;} +dt { margin: .5em 0 .3em; font-weight: normal; } blockquote { font-family: @font-family-serif; @@ -700,7 +700,7 @@ table, .table { font-size: 0.9em; } -.code-class, +.code-namespace, .code-module, .code-class, .code-staticmethod, .code-classmethod, .code-method, .code-function, .code-attribute, .code-data { // indents *all* content diff --git a/doc/_extensions/pyjsparser/LICENSE b/doc/_extensions/pyjsparser/LICENSE new file mode 100644 index 00000000000..5f5aff9f636 --- /dev/null +++ b/doc/_extensions/pyjsparser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Piotr Dabkowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/_extensions/pyjsparser/README b/doc/_extensions/pyjsparser/README new file mode 100644 index 00000000000..574593bbf2e --- /dev/null +++ b/doc/_extensions/pyjsparser/README @@ -0,0 +1,4 @@ +Vendoring of pyjsparser to add comments support as we need them for autojsdoc. + +Alternative: use https://github.com/Kronuz/esprima-python instead, seems to +support comments though may be attaching them differently. diff --git a/doc/_extensions/pyjsparser/__init__.py b/doc/_extensions/pyjsparser/__init__.py new file mode 100644 index 00000000000..19036bddbf1 --- /dev/null +++ b/doc/_extensions/pyjsparser/__init__.py @@ -0,0 +1,4 @@ +s__all__ = ['PyJsParser', 'parse', 'JsSyntaxError'] +__author__ = 'Piotr Dabkowski' +__version__ = '2.2.0' +from .parser import PyJsParser, parse, JsSyntaxError diff --git a/doc/_extensions/pyjsparser/parser.py b/doc/_extensions/pyjsparser/parser.py new file mode 100644 index 00000000000..78ebe14e6ec --- /dev/null +++ b/doc/_extensions/pyjsparser/parser.py @@ -0,0 +1,2928 @@ +# The MIT License +# +# Copyright 2014, 2015 Piotr Dabkowski +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the 'Software'), +# to deal in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, subject +# to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE +from __future__ import unicode_literals +from .pyjsparserdata import * +from .std_nodes import * +from pprint import pprint +import sys + +__all__ = ['PyJsParser', 'parse', 'ENABLE_JS2PY_ERRORS', 'ENABLE_PYIMPORT', 'JsSyntaxError'] +REGEXP_SPECIAL_SINGLE = ('\\', '^', '$', '*', '+', '?', '.', '[', ']', '(', ')', '{', '{', '|', '-') +ENABLE_PYIMPORT = False +ENABLE_JS2PY_ERRORS = False + +PY3 = sys.version_info >= (3,0) + +if PY3: + basestring = str + long = int + xrange = range + unicode = str + +ESPRIMA_VERSION = '2.2.0' +DEBUG = False +# Small naming convention changes +# len -> leng +# id -> d +# type -> typ +# str -> st +true = True +false = False +null = None + + +class PyJsParser: + """ Usage: + parser = PyJsParser() + parser.parse('var JavaScriptCode = 5.1') + """ + + def __init__(self): + self.clean() + + def test(self, code): + pprint(self.parse(code)) + + def clean(self): + self.strict = None + self.sourceType = None + self.index = 0 + self.lineNumber = 1 + self.lineStart = 0 + self.hasLineTerminator = None + self.lastIndex = None + self.lastLineNumber = None + self.lastLineStart = None + self.startIndex = None + self.startLineNumber = None + self.startLineStart = None + self.scanning = None + self.lookahead = None + self.state = None + self.extra = None + self.isBindingElement = None + self.isAssignmentTarget = None + self.firstCoverInitializedNameError = None + + # 7.4 Comments + + def skipSingleLineComment(self, offset): + start = self.index - offset; + while self.index < self.length: + ch = self.source[self.index]; + self.index += 1 + if isLineTerminator(ch): + if (ord(ch) == 13 and ord(self.source[self.index]) == 10): + self.index += 1 + self.lineNumber += 1 + self.hasLineTerminator = True + self.lineStart = self.index + return { + 'type': 'Line', + 'value': self.source[start + offset:self.index-2], + 'leading': True, + 'trailing': False, + 'loc': None, + } + + def skipMultiLineComment(self): + start = self.index + while self.index < self.length: + ch = ord(self.source[self.index]) + if isLineTerminator(ch): + if (ch == 0x0D and ord(self.source[self.index + 1]) == 0x0A): + self.index += 1 + self.lineNumber += 1 + self.index += 1 + self.hasLineTerminator = True + self.lineStart = self.index + elif ch == 0x2A: + # Block comment ends with '*/'. + if ord(self.source[self.index + 1]) == 0x2F: + self.index += 2 + return { + 'type': 'Block', + 'value': self.source[start:self.index-2], + 'leading': True, + 'trailing': False, + 'loc': None, + } + self.index += 1 + else: + self.index += 1 + self.tolerateUnexpectedToken() + + def skipComment(self): + self.hasLineTerminator = False + startIndex = self.index + start = (self.index == 0) + comments = [] + while self.index < self.length: + ch = ord(self.source[self.index]) + if isWhiteSpace(ch): + self.index += 1 + elif isLineTerminator(ch): + self.hasLineTerminator = True + self.index += 1 + if (ch == 0x0D and ord(self.source[self.index]) == 0x0A): + self.index += 1 + self.lineNumber += 1 + self.lineStart = self.index + start = True + elif (ch == 0x2F): # U+002F is '/' + ch = ord(self.source[self.index + 1]) + if (ch == 0x2F): + self.index += 2 + comments.append(self.skipSingleLineComment(2)) + start = True + elif (ch == 0x2A): # U+002A is '*' + self.index += 2 + comments.append(self.skipMultiLineComment()) + else: + break + elif (start and ch == 0x2D): # U+002D is '-' + # U+003E is '>' + if (ord(self.source[self.index + 1]) == 0x2D) and (ord(self.source[self.index + 2]) == 0x3E): + # '-->' is a single-line comment + self.index += 3 + self.skipSingleLineComment(3) + else: + break + elif (ch == 0x3C): # U+003C is '<' + if self.source[self.index + 1: self.index + 4] == '!--': + # - - - -

-
-
-
- -:: - - // test/demo.js - test('templates', {templates: true}, function (instance) { - var s = instance.web.qweb.render('DemoTemplate'); - var texts = $(s).find('p').map(function () { - return $(this).text(); - }).get(); - - deepEqual(texts, ['0', '1', '2', '3', '4']); - }); - -Asynchronous cases ------------------- - -The test case examples so far are all synchronous, they execute from -the first to the last line and once the last line has executed the -test is done. But the web client is full of :ref:`asynchronous code -`, and thus test cases need to be async-aware. - -This is done by returning a :class:`deferred ` from the -case callback:: - - // test/demo.js - test('asynchronous', { - asserts: 1 - }, function () { - var d = $.Deferred(); - setTimeout(function () { - ok(true); - d.resolve(); - }, 100); - return d; - }); - -This example also uses the :class:`options parameter ` -to specify the number of assertions the case should expect, if less or -more assertions are specified the case will count as failed. - -Asynchronous test cases *must* specify the number of assertions they -will run. This allows more easily catching situations where e.g. the -test architecture was not warned about asynchronous operations. - -.. note:: - - Asynchronous test cases also have a 2 seconds timeout: if the test - does not finish within 2 seconds, it will be considered - failed. This pretty much always means the test will not - resolve. This timeout *only* applies to the test itself, not to - the setup and teardown processes. - -.. note:: - - If the returned deferred is rejected, the test will be failed - unless :attr:`~TestOptions.fail_on_rejection` is set to - ``false``. - -RPC ---- - -An important subset of asynchronous test cases is test cases which -need to perform (and chain, to an extent) RPC calls. - -.. note:: - - Because they are a subset of asynchronous cases, RPC cases must - also provide a valid :attr:`assertions count - `. - -To enable mock RPC, set the :attr:`rpc option ` to -``mock``. This will add a third parameter to the test case callback: - -.. function:: mock(rpc_spec, handler) - - Can be used in two different ways depending on the shape of the - first parameter: - - * If it matches the pattern ``model:method`` (if it contains a - colon, essentially) the call will set up the mocking of an RPC - call straight to the Odoo server (through XMLRPC) as - performed via e.g. :func:`odoo.web.Model.call`. - - In that case, ``handler`` should be a function taking two - arguments ``args`` and ``kwargs``, matching the corresponding - arguments on the server side and should simply return the value - as if it were returned by the Python XMLRPC handler:: - - test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { - // set up mocking - mock('people.famous:name_search', function (args, kwargs) { - strictEqual(kwargs.name, 'bob'); - return [ - [1, "Microsoft Bob"], - [2, "Bob the Builder"], - [3, "Silent Bob"] - ]; - }); - - // actual test code - return new instance.web.Model('people.famous') - .call('name_search', {name: 'bob'}).then(function (result) { - strictEqual(result.length, 3, "shoud return 3 people"); - strictEqual(result[0][1], "Microsoft Bob", - "the most famous bob should be Microsoft Bob"); - }); - }); - - * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it - will mock a JSON-RPC call to a web client controller, such as - ``/web/webclient/translations``. In that case, the handler takes - a single ``params`` argument holding all of the parameters - provided over JSON-RPC. - - As previously, the handler should simply return the result value - as if returned by the original JSON-RPC handler:: - - test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) { - var fetched_dbs = false, fetched_langs = false; - mock('/web/database/get_list', function () { - fetched_dbs = true; - return ['foo', 'bar', 'baz']; - }); - mock('/web/session/get_lang_list', function () { - fetched_langs = true; - return [['vo_IS', 'Hopelandic / Vonlenska']]; - }); - - // widget needs that or it blows up - instance.webclient = {toggle_bars: odoo.testing.noop}; - var dbm = new instance.web.DatabaseManager({}); - return dbm.appendTo($s).then(function () { - ok(fetched_dbs, "should have fetched databases"); - ok(fetched_langs, "should have fetched languages"); - deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); - }); - }); - -.. note:: - - Mock handlers can contain assertions, these assertions should be - part of the assertions count (and if multiple calls are made to a - handler containing assertions, it multiplies the effective number - of assertions). - -Testing API ------------ - -.. function:: odoo.testing.section(name[, options], body) - - A test section, serves as shared namespace for related tests (for - constants or values to only set up once). The ``body`` function - should contain the tests themselves. - - Note that the order in which tests are run is essentially - undefined, do *not* rely on it. - - :param String name: - :param TestOptions options: - :param body: - :type body: Function<:func:`~odoo.testing.case`, void> - -.. function:: odoo.testing.case(name[, options], callback) - - Registers a test case callback in the test runner, the callback - will only be run once the runner is started (or maybe not at all, - if the test is filtered out). - - :param String name: - :param TestOptions options: - :param callback: - :type callback: Function> - -.. class:: TestOptions - - the various options which can be passed to - :func:`~odoo.testing.section` or - :func:`~odoo.testing.case`. Except for - :attr:`~TestOptions.setup` and - :attr:`~TestOptions.teardown`, an option on - :func:`~odoo.testing.case` will overwrite the corresponding - option on :func:`~odoo.testing.section` so - e.g. :attr:`~TestOptions.rpc` can be set for a - :func:`~odoo.testing.section` and then differently set for - some :func:`~odoo.testing.case` of that - :func:`~odoo.testing.section` - - .. attribute:: TestOptions.asserts - - An integer, the number of assertions which should run during a - normal execution of the test. Mandatory for asynchronous tests. - - .. attribute:: TestOptions.setup - - Test case setup, run right before each test case. A section's - :func:`~TestOptions.setup` is run before the case's own, if - both are specified. - - .. attribute:: TestOptions.teardown - - Test case teardown, a case's :func:`~TestOptions.teardown` - is run before the corresponding section if both are present. - - .. attribute:: TestOptions.fail_on_rejection - - If the test is asynchronous and its resulting promise is - rejected, fail the test. Defaults to ``true``, set to - ``false`` to not fail the test in case of rejection:: - - // test/demo.js - test('unfail rejection', { - asserts: 1, - fail_on_rejection: false - }, function () { - var d = $.Deferred(); - setTimeout(function () { - ok(true); - d.reject(); - }, 100); - return d; - }); - - .. attribute:: TestOptions.rpc - - RPC method to use during tests, one of ``"mock"`` or - ``"rpc"``. Any other value will disable RPC for the test (if - they were enabled by the suite for instance). - - .. attribute:: TestOptions.templates - - Whether the current module (and its dependencies)'s templates - should be loaded into QWeb before starting the test. A - boolean, ``false`` by default. - -The test runner can also use two global configuration values set -directly on the ``window`` object: - -* ``oe_all_dependencies`` is an ``Array`` of all modules with a web - component, ordered by dependency (for a module ``A`` with - dependencies ``A'``, any module of ``A'`` must come before ``A`` in - the array) - -Running through Python ----------------------- - -The web client includes the means to run these tests on the -command-line (or in a CI system), but while actually running it is -pretty simple the setup of the pre-requisite parts has some -complexities. - -#. Install PhantomJS_. It is a headless - browser which allows automating running and testing web - pages. QUnitSuite_ uses it to actually run the qunit_ test suite. - - The PhantomJS_ website provides pre-built binaries for some - platforms, and your OS's package management probably provides it as - well. - - If you're building PhantomJS_ from source, I recommend preparing - for some knitting time as it's not exactly fast (it needs to - compile both `Qt `_ and `Webkit - `_, both being pretty big projects). - - .. note:: - - Because PhantomJS_ is webkit-based, it will not be able to test - if Firefox, Opera or Internet Explorer can correctly run the - test suite (and it is only an approximation for Safari and - Chrome). It is therefore recommended to *also* run the test - suites in actual browsers once in a while. - - .. note:: - - The version of PhantomJS_ this was build through is 1.7, - previous versions *should* work but are not actually supported - (and tend to just segfault when something goes wrong in - PhantomJS_ itself so they're a pain to debug). - -#. Install a new database with all relevant modules (all modules with - a web component at least), then restart the server - - .. note:: - - For some tests, a source database needs to be duplicated. This - operation requires that there be no connection to the database - being duplicated, but Odoo doesn't currently break - existing/outstanding connections, so restarting the server is - the simplest way to ensure everything is in the right state. - -#. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct - addons-path specified (and replacing ``$DATABASE`` by the source - database you created above) - - .. note:: - - If you leave out ``-mweb``, the runner will attempt to run all - the tests in all the modules, which may or may not work. - -If everything went correctly, you should now see a list of tests with -(hopefully) ``ok`` next to their names, closing with a report of the -number of tests run and the time it took: - -.. literalinclude:: test-report.txt - :language: text - -Congratulation, you have just performed a successful "offline" run of -the OpenERP Web test suite. - -.. note:: - - Note that this runs all the Python tests for the ``web`` module, - but all the web tests for all of Odoo. This can be surprising. - -.. _qunit: http://qunitjs.com/ - -.. _qunit assertions: http://api.qunitjs.com/category/assert/ - -.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/ - -.. _PhantomJS: http://phantomjs.org/ - -.. [#eventsdelegation] not all DOM events are compatible with events delegation - -.. [#terminal] - with a small twist: :py:meth:`sqlalchemy.orm.query.Query.group_by` is not - terminal, it returns a query which can still be altered. - +.. automodule:: * + :members: diff --git a/doc/requirements.txt b/doc/requirements.txt index e3059c049c9..6c2a5df88c2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,4 @@ -sphinx==1.2.3 +sphinx>=1.2 mercurial==3.2.2 sphinx-patchqueue==0.4.0 +PyJSDoc==0.9.1 diff --git a/odoo/api.py b/odoo/api.py index 506c344da30..143f51bc31c 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -692,9 +692,9 @@ def call_kw(model, name, args, kwargs): class Environment(Mapping): """ An environment wraps data for ORM records: - - :attr:`cr`, the current database cursor; - - :attr:`uid`, the current user id; - - :attr:`context`, the current context dictionary. + - :attr:`cr`, the current database cursor; + - :attr:`uid`, the current user id; + - :attr:`context`, the current context dictionary. It provides access to the registry by implementing a mapping from model names to new api models. It also holds a cache for records, and a data