summaryrefslogtreecommitdiff
path: root/doc/_extensions/pyjsdoc/__init__.py
diff options
context:
space:
mode:
authorXavier Morel <xmo@odoo.com>2017-10-10 13:29:11 +0200
committerXavier Morel <xmo@odoo.com>2017-10-10 13:29:11 +0200
commit40ca8557e9e25832afa40afd84124319cb71f87b (patch)
treeb4792e33b8404b75274e33e317de1183bb86e795 /doc/_extensions/pyjsdoc/__init__.py
parent8071742959b03020ff2cf32670bb5495804eae57 (diff)
[FIX] building documentation with P3
* fix P3 incompatibilities in doc/_extensions * vendor pyjsdoc, strip out everything we're not using and make what's left P3-compatible (upstream is not P3 compliant) * drop undeclared dependency on attrs as it turns out not to be much used
Diffstat (limited to 'doc/_extensions/pyjsdoc/__init__.py')
-rw-r--r--doc/_extensions/pyjsdoc/__init__.py408
1 files changed, 408 insertions, 0 deletions
diff --git a/doc/_extensions/pyjsdoc/__init__.py b/doc/_extensions/pyjsdoc/__init__.py
new file mode 100644
index 00000000000..8368f73083e
--- /dev/null
+++ b/doc/_extensions/pyjsdoc/__init__.py
@@ -0,0 +1,408 @@
+from __future__ import division
+import re, json
+
+##### Parsing utilities #####
+
+def split_delimited(delimiters, split_by, text):
+ """
+ Generator that walks the ``text`` and splits it into an array on
+ ``split_by``, being careful not to break inside a delimiter pair.
+ ``delimiters`` should be an even-length string with each pair of matching
+ delimiters listed together, open first.
+
+
+ >>> list(split_delimited('{}[]', ',', ''))
+ ['']
+ >>> list(split_delimited('', ',', 'foo,bar'))
+ ['foo', 'bar']
+ >>> list(split_delimited('[]', ',', 'foo,[bar, baz]'))
+ ['foo', '[bar, baz]']
+ >>> list(split_delimited('{}', ' ', '{Type Name} name Desc'))
+ ['{Type Name}', 'name', 'Desc']
+ >>> list(split_delimited('[]{}', ',', '[{foo,[bar, baz]}]'))
+ ['[{foo,[bar, baz]}]']
+
+ Two adjacent delimiters result in a zero-length string between them:
+
+ >>> list(split_delimited('{}', ' ', '{Type Name} Desc'))
+ ['{Type Name}', '', 'Desc']
+
+ ``split_by`` may be a predicate function instead of a string, in which
+ case it should return true on a character to split.
+
+ >>> list(split_delimited('', lambda c: c in '[]{}, ', '[{foo,[bar, baz]}]'))
+ ['', '', 'foo', '', 'bar', '', 'baz', '', '', '']
+
+ """
+ delims = [0] * (len(delimiters) // 2)
+ actions = {}
+ for i in range(0, len(delimiters), 2):
+ actions[delimiters[i]] = (i // 2, 1)
+ actions[delimiters[i + 1]] = (i // 2, -1)
+
+ if isinstance(split_by, str):
+ def split_fn(c):
+ return c == split_by
+ else:
+ split_fn = split_by
+ last = 0
+
+ for i in range(len(text)):
+ c = text[i]
+ if split_fn(c) and not any(delims):
+ yield text[last:i]
+ last = i + 1
+ try:
+ which, dir = actions[c]
+ delims[which] = delims[which] + dir
+ except KeyError:
+ pass # Normal character
+ yield text[last:]
+
+
+def strip_stars(doc_comment):
+ r"""
+ Strip leading stars from a doc comment.
+
+ >>> strip_stars('/** This is a comment. */')
+ 'This is a comment.'
+ >>> strip_stars('/**\n * This is a\n * multiline comment. */')
+ 'This is a\n multiline comment.'
+ >>> strip_stars('/** \n\t * This is a\n\t * multiline comment. \n*/')
+ 'This is a\n multiline comment.'
+
+ """
+ return re.sub('\n\s*?\*\s*?', '\n', doc_comment[3:-2]).strip()
+
+
+def split_tag(section):
+ """
+ Split the JSDoc tag text (everything following the @) at the first
+ whitespace. Returns a tuple of (tagname, body).
+ """
+ splitval = re.split('\s+', section, 1)
+ tag, body = len(splitval) > 1 and splitval or (splitval[0], '')
+ return tag.strip(), body.strip()
+
+
+FUNCTION_REGEXPS = [
+ 'function (\w+)',
+ '(\w+):\sfunction',
+ '\.(\w+)\s*=\s*function',
+]
+
+
+def guess_function_name(next_line, regexps=FUNCTION_REGEXPS):
+ """
+ Attempt to determine the function name from the first code line
+ following the comment. The patterns recognized are described by
+ `regexps`, which defaults to FUNCTION_REGEXPS. If a match is successful,
+ returns the function name. Otherwise, returns None.
+ """
+ for regexp in regexps:
+ match = re.search(regexp, next_line)
+ if match:
+ return match.group(1)
+ return None
+
+
+def guess_parameters(next_line):
+ """
+ Attempt to guess parameters based on the presence of a parenthesized
+ group of identifiers. If successful, returns a list of parameter names;
+ otherwise, returns None.
+ """
+ match = re.search('\(([\w\s,]+)\)', next_line)
+ if match:
+ return [arg.strip() for arg in match.group(1).split(',')]
+ else:
+ return None
+
+
+def parse_comment(doc_comment, next_line):
+ r"""
+ Split the raw comment text into a dictionary of tags. The main comment
+ body is included as 'doc'.
+
+ >>> comment = get_doc_comments(read_file('examples/module.js'))[4][0]
+ >>> parse_comment(strip_stars(comment), '')['doc']
+ 'This is the documentation for the fourth function.\n\n Since the function being documented is itself generated from another\n function, its name needs to be specified explicitly. using the @function tag'
+ >>> parse_comment(strip_stars(comment), '')['function']
+ 'not_auto_discovered'
+
+ If there are multiple tags with the same name, they're included as a list:
+
+ >>> parse_comment(strip_stars(comment), '')['param']
+ ['{String} arg1 The first argument.', '{Int} arg2 The second argument.']
+
+ """
+ sections = re.split('\n\s*@', doc_comment)
+ tags = {
+ 'doc': sections[0].strip(),
+ 'guessed_function': guess_function_name(next_line),
+ 'guessed_params': guess_parameters(next_line)
+ }
+ for section in sections[1:]:
+ tag, body = split_tag(section)
+ if tag in tags:
+ existing = tags[tag]
+ try:
+ existing.append(body)
+ except AttributeError:
+ tags[tag] = [existing, body]
+ else:
+ tags[tag] = body
+ return tags
+
+
+#### Classes #####
+
+class CommentDoc(object):
+ """
+ Base class for all classes that represent a parsed comment of some sort.
+ """
+
+ def __init__(self, parsed_comment):
+ self.parsed = parsed_comment
+
+ def __str__(self):
+ return "Docs for " + self.name
+
+ def __repr__(self):
+ return str(self)
+
+ def __contains__(self, tag_name):
+ return tag_name in self.parsed
+
+ def __getitem__(self, tag_name):
+ return self.get(tag_name)
+
+ def get(self, tag_name, default=''):
+ """
+ Return the value of a particular tag, or None if that tag doesn't
+ exist. Use 'doc' for the comment body itself.
+ """
+ return self.parsed.get(tag_name, default)
+
+ def get_as_list(self, tag_name):
+ """
+ Return the value of a tag, making sure that it's a list. Absent
+ tags are returned as an empty-list; single tags are returned as a
+ one-element list.
+
+ The returned list is a copy, and modifications do not affect the
+ original object.
+ """
+ val = self.get(tag_name, [])
+ if isinstance(val, list):
+ return val[:]
+ else:
+ return [val]
+
+ @property
+ def doc(self):
+ """
+ Return the comment body.
+ """
+ return self.get('doc')
+
+ @property
+ def url(self):
+ """
+ Return a URL for the comment, within the page.
+ """
+ return '#' + self.name
+
+ @property
+ def see(self):
+ """
+ Return a list of all @see tags on the comment.
+ """
+ return self.get_as_list('see')
+
+ def to_json(self):
+ """
+ Return a JSON representation of the CommentDoc. Keys are as per
+ to_dict.
+ """
+ return json.dumps(self.to_dict())
+
+ def to_dict(self):
+ """
+ Return a dictionary representation of the CommentDoc. The keys of
+ this correspond to the tags in the comment, with the comment body in
+ `doc`.
+ """
+ return self.parsed.copy()
+
+class ParamDoc(object):
+ """
+ Represents a parameter, option, or parameter-like object, basically
+ anything that has a name, a type, and a description, separated by spaces.
+ This is also used for return types and exceptions, which use an empty
+ string for the name.
+
+ >>> param = ParamDoc('{Array<DOM>} elems The elements to act upon')
+ >>> param.name
+ 'elems'
+ >>> param.doc
+ 'The elements to act upon'
+ >>> param.type
+ 'Array<DOM>'
+
+ You can also omit the type: if the first element is not surrounded by
+ curly braces, it's assumed to be the name instead:
+
+ >>> param2 = ParamDoc('param1 The first param')
+ >>> param2.type
+ ''
+ >>> param2.name
+ 'param1'
+ >>> param2.doc
+ 'The first param'
+
+ """
+
+ def __init__(self, text):
+ parsed = list(split_delimited('{}', ' ', text))
+ if parsed[0].startswith('{') and parsed[0].endswith('}'):
+ self.type = parsed[0][1:-1]
+ self.name = parsed[1]
+ self.doc = ' '.join(parsed[2:])
+ else:
+ self.type = ''
+ self.name = parsed[0]
+ self.doc = ' '.join(parsed[1:])
+
+ def to_dict(self):
+ """
+ Convert this to a dict. Keys (all strings) are:
+
+ - **name**: Parameter name
+ - **type**: Parameter type
+ - **doc**: Parameter description
+ """
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'doc': self.doc
+ }
+
+ def to_html(self, css_class=''):
+ """
+ Returns the parameter as a dt/dd pair.
+ """
+ if self.name and self.type:
+ header_text = '%s (%s)' % (self.name, self.type)
+ elif self.type:
+ header_text = self.type
+ else:
+ header_text = self.name
+ return '<dt>%s</dt><dd>%s</dd>' % (header_text, self.doc)
+
+
+##### DEPENDENCIES #####
+
+class CyclicDependency(Exception):
+ """
+ Exception raised if there is a cyclic dependency.
+ """
+
+ def __init__(self, remaining_dependencies):
+ self.values = remaining_dependencies
+
+ def __str__(self):
+ return ('The following dependencies result in a cycle: '
+ + ', '.join(self.values))
+
+
+class MissingDependency(Exception):
+ """
+ Exception raised if a file references a dependency that doesn't exist.
+ """
+
+ def __init__(self, file, dependency):
+ self.file = file
+ self.dependency = dependency
+
+ def __str__(self):
+ return "Couldn't find dependency %s when processing %s" % \
+ (self.dependency, self.file)
+
+
+def build_dependency_graph(start_nodes, js_doc):
+ """
+ Build a graph where nodes are filenames and edges are reverse dependencies
+ (so an edge from jquery.js to jquery.dimensions.js indicates that jquery.js
+ must be included before jquery.dimensions.js). The graph is represented
+ as a dictionary from filename to (in-degree, edges) pair, for ease of
+ topological sorting. Also returns a list of nodes of degree zero.
+ """
+ queue = []
+ dependencies = {}
+ start_sort = []
+
+ def add_vertex(file):
+ in_degree = len(js_doc[file].module.dependencies)
+ dependencies[file] = [in_degree, []]
+ queue.append(file)
+ if in_degree == 0:
+ start_sort.append(file)
+
+ def add_edge(from_file, to_file):
+ dependencies[from_file][1].append(to_file)
+
+ def is_in_graph(file):
+ return file in dependencies
+
+ for file in start_nodes:
+ add_vertex(file)
+ for file in queue:
+ for dependency in js_doc[file].module.dependencies:
+ if dependency not in js_doc:
+ raise MissingDependency(file, dependency)
+ if not is_in_graph(dependency):
+ add_vertex(dependency)
+ add_edge(dependency, file)
+ return dependencies, start_sort
+
+
+def topological_sort(dependencies, start_nodes):
+ """
+ Perform a topological sort on the dependency graph `dependencies`, starting
+ from list `start_nodes`.
+ """
+ retval = []
+
+ def edges(node):
+ return dependencies[node][1]
+
+ def in_degree(node):
+ return dependencies[node][0]
+
+ def remove_incoming(node):
+ dependencies[node][0] = in_degree(node) - 1
+
+ while start_nodes:
+ node = start_nodes.pop()
+ retval.append(node)
+ for child in edges(node):
+ remove_incoming(child)
+ if not in_degree(child):
+ start_nodes.append(child)
+ leftover_nodes = [node for node in dependencies.keys()
+ if in_degree(node) > 0]
+ if leftover_nodes:
+ raise CyclicDependency(leftover_nodes)
+ else:
+ return retval
+
+
+def find_dependencies(start_nodes, js_doc):
+ """
+ Sort the dependency graph, taking in a list of starting module names and a
+ CodeBaseDoc (or equivalent dictionary). Returns an ordered list of
+ transitive dependencies such that no module appears before its
+ dependencies.
+ """
+ return topological_sort(*build_dependency_graph(start_nodes, js_doc))