diff --git a/mainsite_hidden/source/conf.py b/mainsite_hidden/source/conf.py index ec4bd77be..c313c97b8 100644 --- a/mainsite_hidden/source/conf.py +++ b/mainsite_hidden/source/conf.py @@ -203,4 +203,10 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"http://docs.python.org/": None} +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), + "matplotlib": ("https://matplotlib.org", None), + "pandas": ("https://pandas.pydata.org/docs/", None), +} diff --git a/mainsite_hidden/source_plugins/conf.py b/mainsite_hidden/source_plugins/conf.py index 33084f2af..98ac675a0 100644 --- a/mainsite_hidden/source_plugins/conf.py +++ b/mainsite_hidden/source_plugins/conf.py @@ -388,7 +388,9 @@ intersphinx_mapping = { - "python": ("https://docs.python.org/3.6", None), - "numpy": ("https://docs.scipy.org/doc/numpy", None), - "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), + "matplotlib": ("https://matplotlib.org", None), + "pandas": ("https://pandas.pydata.org/docs/", None), } diff --git a/mainsite_hidden/source_plugins/sphinxext/__init__.py b/mainsite_hidden/source_plugins/sphinxext/__init__.py index e69de29bb..5992b818c 100644 --- a/mainsite_hidden/source_plugins/sphinxext/__init__.py +++ b/mainsite_hidden/source_plugins/sphinxext/__init__.py @@ -0,0 +1,3 @@ +__version__ = "0.7.0" + +from .numpydoc import setup diff --git a/mainsite_hidden/source_plugins/sphinxext/docscrape.py b/mainsite_hidden/source_plugins/sphinxext/docscrape.py index bedbd0753..d56352216 100644 --- a/mainsite_hidden/source_plugins/sphinxext/docscrape.py +++ b/mainsite_hidden/source_plugins/sphinxext/docscrape.py @@ -1,19 +1,18 @@ -"""Extract reference documentation from the NumPy source tree. - -""" +"""Extract reference documentation from the NumPy source tree.""" import inspect import textwrap import re import pydoc -from io import StringIO -#from StringIO import StringIO from warnings import warn -4 -class Reader(object): - """A line-based string reader. +import collections +import copy +import sys + + +class Reader: + """A line-based string reader.""" - """ def __init__(self, data): """ Parameters @@ -22,10 +21,10 @@ def __init__(self, data): String with lines separated by '\n'. """ - if isinstance(data,list): + if isinstance(data, list): self._str = data else: - self._str = data.split('\n') # store string as list of lines + self._str = data.split("\n") # store string as list of lines self.reset() @@ -33,7 +32,7 @@ def __getitem__(self, n): return self._str[n] def reset(self): - self._l = 0 # current line nr + self._l = 0 # current line nr def read(self): if not self.eof(): @@ -41,10 +40,10 @@ def read(self): self._l += 1 return out else: - return '' + return "" def seek_next_non_empty_line(self): - for l in self[self._l:]: + for l in self[self._l :]: if l.strip(): break else: @@ -57,68 +56,93 @@ def read_to_condition(self, condition_func): start = self._l for line in self[start:]: if condition_func(line): - return self[start:self._l] + return self[start : self._l] self._l += 1 if self.eof(): - return self[start:self._l+1] + return self[start : self._l + 1] return [] def read_to_next_empty_line(self): self.seek_next_non_empty_line() + def is_empty(line): return not line.strip() + return self.read_to_condition(is_empty) def read_to_next_unindented_line(self): def is_unindented(line): - return (line.strip() and (len(line.lstrip()) == len(line))) + return line.strip() and (len(line.lstrip()) == len(line)) + return self.read_to_condition(is_unindented) - def peek(self,n=0): + def peek(self, n=0): if self._l + n < len(self._str): return self[self._l + n] else: - return '' + return "" def is_empty(self): - return not ''.join(self._str).strip() + return not "".join(self._str).strip() -class NumpyDocString(object): - def __init__(self,docstring): - docstring = textwrap.dedent(docstring).split('\n') +class ParseError(Exception): + def __str__(self): + message = self.args[0] + if hasattr(self, "docstring"): + message = f"{message} in {self.docstring!r}" + return message + + +class NumpyDocString(collections.abc.Mapping): + sections = { + "Signature": "", + "Summary": [""], + "Extended Summary": [], + "Parameters": [], + "Returns": [], + "Yields": [], + "Raises": [], + "Warns": [], + "Other Parameters": [], + "Attributes": [], + "Methods": [], + "See Also": [], + "Notes": [], + "Warnings": [], + "References": "", + "Examples": "", + "index": {}, + } + + def __init__(self, docstring, config={}): + orig_docstring = docstring + docstring = textwrap.dedent(docstring).split("\n") self._doc = Reader(docstring) - self._parsed_data = { - 'Signature': '', - 'Summary': [''], - 'Extended Summary': [], - 'Parameters': [], - 'Returns': [], - 'Raises': [], - 'Warns': [], - 'Other Parameters': [], - 'Attributes': [], - 'Methods': [], - 'See Also': [], - 'Notes': [], - 'Warnings': [], - 'References': '', - 'Examples': '', - 'index': {} - } - - self._parse() - - def __getitem__(self,key): + self._parsed_data = copy.deepcopy(self.sections) + + try: + self._parse() + except ParseError as e: + e.docstring = orig_docstring + raise + + def __getitem__(self, key): return self._parsed_data[key] - def __setitem__(self,key,val): - if not (key in self._parsed_data): + def __setitem__(self, key, val): + if key not in self._parsed_data: warn("Unknown section %s" % key) else: self._parsed_data[key] = val + def __iter__(self): + return iter(self._parsed_data) + + def __len__(self): + return len(self._parsed_data) + def _is_at_section(self): self._doc.seek_next_non_empty_line() @@ -127,29 +151,31 @@ def _is_at_section(self): l1 = self._doc.peek().strip() # e.g. Parameters - if l1.startswith('.. index::'): + if l1.startswith(".. index::"): return True - l2 = self._doc.peek(1).strip() # ---------- or ========== - return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) + l2 = self._doc.peek(1).strip() # ---------- or ========== + return l2.startswith("-" * len(l1)) or l2.startswith("=" * len(l1)) - def _strip(self,doc): + def _strip(self, doc): i = 0 j = 0 - for i,line in enumerate(doc): - if line.strip(): break + for i, line in enumerate(doc): + if line.strip(): + break - for j,line in enumerate(doc[::-1]): - if line.strip(): break + for j, line in enumerate(doc[::-1]): + if line.strip(): + break - return doc[i:len(doc)-j] + return doc[i : len(doc) - j] def _read_to_next_section(self): section = self._doc.read_to_next_empty_line() while not self._is_at_section() and not self._doc.eof(): - if not self._doc.peek(-1).strip(): # previous line was empty - section += [''] + if not self._doc.peek(-1).strip(): # previous line was empty + section += [""] section += self._doc.read_to_next_empty_line() @@ -160,33 +186,36 @@ def _read_sections(self): data = self._read_to_next_section() name = data[0].strip() - if name.startswith('..'): # index section + if name.startswith(".."): # index section yield name, data[1:] elif len(data) < 2: yield StopIteration else: yield name, self._strip(data[2:]) - def _parse_param_list(self,content): + def _parse_param_list(self, content): r = Reader(content) params = [] while not r.eof(): header = r.read().strip() - if ' : ' in header: - arg_name, arg_type = header.split(' : ')[:2] + if " : " in header: + arg_name, arg_type = header.split(" : ")[:2] else: - arg_name, arg_type = header, '' + arg_name, arg_type = header, "" desc = r.read_to_next_unindented_line() desc = dedent_lines(desc) - params.append((arg_name,arg_type,desc)) + params.append((arg_name, arg_type, desc)) return params - - _name_rgx = re.compile(r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" - r" (?P[a-zA-Z0-9_.-]+))\s*", re.X) + _name_rgx = re.compile( + r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" + r" (?P[a-zA-Z0-9_.-]+))\s*", + re.X, + ) + def _parse_see_also(self, content): """ func_name : Descriptive text @@ -206,7 +235,7 @@ def parse_item_name(text): return g[3], None else: return g[2], g[1] - raise ValueError("%s is not a item name" % text) + raise ParseError("%s is not a item name" % text) def push_item(name, rest): if not name: @@ -217,23 +246,25 @@ def push_item(name, rest): current_func = None rest = [] - + for line in content: - if not line.strip(): continue + if not line.strip(): + continue m = self._name_rgx.match(line) - if m and line[m.end():].strip().startswith(':'): + if m and line[m.end() :].strip().startswith(":"): push_item(current_func, rest) - current_func, line = line[:m.end()], line[m.end():] - rest = [line.split(':', 1)[1].strip()] + current_func, line = line[: m.end()], line[m.end() :] + rest = [line.split(":", 1)[1].strip()] if not rest[0]: rest = [] - elif not line.startswith(' '): + elif not line.startswith(" "): push_item(current_func, rest) current_func = None - if ',' in line: - for func in line.split(','): - push_item(func, []) + if "," in line: + for func in line.split(","): + if func.strip(): + push_item(func, []) elif line.strip(): current_func = line elif current_func is not None: @@ -247,79 +278,120 @@ def _parse_index(self, section, content): :refguide: something, else, and more """ + def strip_each_in(lst): return [s.strip() for s in lst] out = {} - section = section.split('::') + section = section.split("::") if len(section) > 1: - out['default'] = strip_each_in(section[1].split(','))[0] + out["default"] = strip_each_in(section[1].split(","))[0] for line in content: - line = line.split(':') + line = line.split(":") if len(line) > 2: - out[line[1]] = strip_each_in(line[2].split(',')) + out[line[1]] = strip_each_in(line[2].split(",")) return out - + def _parse_summary(self): """Grab signature (if given) and summary""" if self._is_at_section(): return - summary = self._doc.read_to_next_empty_line() - summary_str = " ".join([s.strip() for s in summary]).strip() - if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): - self['Signature'] = summary_str - if not self._is_at_section(): - self['Summary'] = self._doc.read_to_next_empty_line() - else: - self['Summary'] = summary + # If several signatures present, take the last one + while True: + summary = self._doc.read_to_next_empty_line() + summary_str = " ".join([s.strip() for s in summary]).strip() + if re.compile(r"^([\w., ]+=)?\s*[\w\.]+\(.*\)$").match(summary_str): + self["Signature"] = summary_str + if not self._is_at_section(): + continue + break + + if summary is not None: + self["Summary"] = summary if not self._is_at_section(): - self['Extended Summary'] = self._read_to_next_section() - + self["Extended Summary"] = self._read_to_next_section() + def _parse(self): self._doc.reset() self._parse_summary() - for (section,content) in self._read_sections(): - if not section.startswith('..'): - section = ' '.join([s.capitalize() for s in section.split(' ')]) - if section in ('Parameters', 'Attributes', 'Methods', - 'Returns', 'Raises', 'Warns'): + sections = list(self._read_sections()) + section_names = {section for section, content in sections} + + has_returns = "Returns" in section_names + has_yields = "Yields" in section_names + # We could do more tests, but we are not. Arbitrarily. + if has_returns and has_yields: + msg = "Docstring contains both a Returns and Yields section." + raise ValueError(msg) + + for section, content in sections: + if not section.startswith(".."): + section = (s.capitalize() for s in section.split(" ")) + section = " ".join(section) + if self.get(section): + if hasattr(self, "_obj"): + # we know where the docs came from: + try: + filename = inspect.getsourcefile(self._obj) + except TypeError: + filename = None + msg = ( + "The section %s appears twice in " + "the docstring of %s in %s." + % (section, self._obj, filename) + ) + raise ValueError(msg) + else: + msg = "The section %s appears twice" % section + raise ValueError(msg) + + if section in ( + "Parameters", + "Returns", + "Yields", + "Raises", + "Warns", + "Other Parameters", + "Attributes", + "Methods", + ): self[section] = self._parse_param_list(content) - elif section.startswith('.. index::'): - self['index'] = self._parse_index(section, content) - elif section == 'See Also': - self['See Also'] = self._parse_see_also(content) + elif section.startswith(".. index::"): + self["index"] = self._parse_index(section, content) + elif section == "See Also": + self["See Also"] = self._parse_see_also(content) else: self[section] = content # string conversion routines - def _str_header(self, name, symbol='-'): - return [name, len(name)*symbol] + def _str_header(self, name, symbol="-"): + return [name, len(name) * symbol] def _str_indent(self, doc, indent=4): out = [] for line in doc: - out += [' '*indent + line] + out += [" " * indent + line] return out def _str_signature(self): - if self['Signature']: - return [self['Signature'].replace('*','\*')] + [''] + if self["Signature"]: + return [self["Signature"].replace("*", r"\*")] + [""] else: - return [''] + return [""] def _str_summary(self): - if self['Summary']: - return self['Summary'] + [''] + if self["Summary"]: + return self["Summary"] + [""] else: return [] def _str_extended_summary(self): - if self['Extended Summary']: - return self['Extended Summary'] + [''] + if self["Extended Summary"]: + return self["Extended Summary"] + [""] else: return [] @@ -327,10 +399,13 @@ def _str_param_list(self, name): out = [] if self[name]: out += self._str_header(name) - for param,param_type,desc in self[name]: - out += ['%s : %s' % (param, param_type)] + for param, param_type, desc in self[name]: + if param_type: + out += [f"{param} : {param_type}"] + else: + out += [param] out += self._str_indent(desc) - out += [''] + out += [""] return out def _str_section(self, name): @@ -338,161 +413,217 @@ def _str_section(self, name): if self[name]: out += self._str_header(name) out += self[name] - out += [''] + out += [""] return out def _str_see_also(self, func_role): - if not self['See Also']: return [] + if not self["See Also"]: + return [] out = [] out += self._str_header("See Also") last_had_desc = True - for func, desc, role in self['See Also']: + for func, desc, role in self["See Also"]: if role: - link = ':%s:`%s`' % (role, func) + link = f":{role}:`{func}`" elif func_role: - link = ':%s:`%s`' % (func_role, func) + link = f":{func_role}:`{func}`" else: link = "`%s`_" % func if desc or last_had_desc: - out += [''] + out += [""] out += [link] else: out[-1] += ", %s" % link if desc: - out += self._str_indent([' '.join(desc)]) + out += self._str_indent([" ".join(desc)]) last_had_desc = True else: last_had_desc = False - out += [''] + out += [""] return out def _str_index(self): - idx = self['index'] + idx = self["index"] out = [] - out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): - if section == 'default': + out += [".. index:: %s" % idx.get("default", "")] + for section, references in idx.items(): + if section == "default": continue - out += [' :%s: %s' % (section, ', '.join(references))] + out += [" :{}: {}".format(section, ", ".join(references))] return out - def __str__(self, func_role=''): + def __str__(self, func_role=""): out = [] out += self._str_signature() out += self._str_summary() out += self._str_extended_summary() - for param_list in ('Parameters','Returns','Raises'): + for param_list in ( + "Parameters", + "Returns", + "Yields", + "Other Parameters", + "Raises", + "Warns", + ): out += self._str_param_list(param_list) - out += self._str_section('Warnings') + out += self._str_section("Warnings") out += self._str_see_also(func_role) - for s in ('Notes','References','Examples'): + for s in ("Notes", "References", "Examples"): out += self._str_section(s) + for param_list in ("Attributes", "Methods"): + out += self._str_param_list(param_list) out += self._str_index() - return '\n'.join(out) + return "\n".join(out) -def indent(str,indent=4): - indent_str = ' '*indent +def indent(str, indent=4): + indent_str = " " * indent if str is None: return indent_str - lines = str.split('\n') - return '\n'.join(indent_str + l for l in lines) + lines = str.split("\n") + return "\n".join(indent_str + l for l in lines) + def dedent_lines(lines): """Deindent a list of lines maximally""" return textwrap.dedent("\n".join(lines)).split("\n") -def header(text, style='-'): - return text + '\n' + style*len(text) + '\n' + +def header(text, style="-"): + return text + "\n" + style * len(text) + "\n" class FunctionDoc(NumpyDocString): - def __init__(self, func, role='func', doc=None): + def __init__(self, func, role="func", doc=None, config={}): self._f = func - self._role = role # e.g. "func" or "meth" + self._role = role # e.g. "func" or "meth" + if doc is None: - doc = inspect.getdoc(func) or '' - try: - NumpyDocString.__init__(self, doc) - except ValueError as e: - print('*'*78) - print("ERROR: '%s' while parsing `%s`" % (e, self._f)) - print('*'*78) - #print "Docstring follows:" - #print doclines - #print '='*78 - - if not self['Signature']: + if func is None: + raise ValueError("No function or docstring given") + doc = inspect.getdoc(func) or "" + NumpyDocString.__init__(self, doc) + + if not self["Signature"] and func is not None: func, func_name = self.get_func() try: - # try to read signature - argspec = inspect.getargspec(func) - argspec = inspect.formatargspec(*argspec) - argspec = argspec.replace('*','\*') - signature = '%s%s' % (func_name, argspec) - except TypeError as e: - signature = '%s()' % func_name - self['Signature'] = signature + try: + signature = str(inspect.signature(func)) + except (AttributeError, ValueError): + # try to read signature, backward compat for older Python + if sys.version_info[0] >= 3: + argspec = inspect.getfullargspec(func) + else: + argspec = inspect.getargspec(func) + signature = inspect.formatargspec(*argspec) + signature = "{}{}".format(func_name, signature.replace("*", r"\*")) + except TypeError: + signature = "%s()" % func_name + self["Signature"] = signature def get_func(self): - func_name = getattr(self._f, '__name__', self.__class__.__name__) + func_name = getattr(self._f, "__name__", self.__class__.__name__) if inspect.isclass(self._f): - func = getattr(self._f, '__call__', self._f.__init__) + func = getattr(self._f, "__call__", self._f.__init__) else: func = self._f return func, func_name - + def __str__(self): - out = '' + out = "" func, func_name = self.get_func() - signature = self['Signature'].replace('*', '\*') + signature = self["Signature"].replace("*", r"\*") - roles = {'func': 'function', - 'meth': 'method'} + roles = {"func": "function", "meth": "method"} if self._role: - if not (self._role in roles): + if self._role not in roles: print("Warning: invalid role %s" % self._role) - out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), - func_name) + out += ".. {}:: {}\n \n\n".format(roles.get(self._role, ""), func_name) - out += super(FunctionDoc, self).__str__(func_role=self._role) + out += super().__str__(func_role=self._role) return out class ClassDoc(NumpyDocString): - def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None): - if not inspect.isclass(cls): - raise ValueError("Initialise using a class. Got %r" % cls) + extra_public_methods = ["__call__"] + + def __init__(self, cls, doc=None, modulename="", func_doc=FunctionDoc, config={}): + if not inspect.isclass(cls) and cls is not None: + raise ValueError("Expected a class or None, but got %r" % cls) self._cls = cls - if modulename and not modulename.endswith('.'): - modulename += '.' + self.show_inherited_members = config.get("show_inherited_class_members", True) + + if modulename and not modulename.endswith("."): + modulename += "." self._mod = modulename - self._name = cls.__name__ - self._func_doc = func_doc if doc is None: + if cls is None: + raise ValueError("No class or documentation string given") doc = pydoc.getdoc(cls) NumpyDocString.__init__(self, doc) - @property - def methods(self): - return [name for name,func in inspect.getmembers(self._cls) - if not name.startswith('_') and callable(func)] - - def __str__(self): - out = '' - out += super(ClassDoc, self).__str__() - out += "\n\n" + if config.get("show_class_members", True): - #for m in self.methods: - # print "Parsing `%s`" % m - # out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n' - # out += '.. index::\n single: %s; %s\n\n' % (self._name, m) - - return out + def splitlines_x(s): + if not s: + return [] + else: + return s.splitlines() + + for field, items in [ + ("Methods", self.methods), + ("Attributes", self.properties), + ]: + if not self[field]: + doc_list = [] + for name in sorted(items): + try: + doc_item = pydoc.getdoc(getattr(self._cls, name)) + doc_list.append((name, "", splitlines_x(doc_item))) + except AttributeError: + pass # method doesn't exist + self[field] = doc_list + @property + def methods(self): + if self._cls is None: + return [] + return [ + name + for name, func in inspect.getmembers(self._cls) + if ( + (not name.startswith("_") or name in self.extra_public_methods) + and isinstance(func, collections.abc.Callable) + and self._is_show_member(name) + ) + ] + @property + def properties(self): + if self._cls is None: + return [] + return [ + name + for name, func in inspect.getmembers(self._cls) + if ( + not name.startswith("_") + and ( + func is None + or isinstance(func, property) + or inspect.isgetsetdescriptor(func) + ) + and self._is_show_member(name) + ) + ] + + def _is_show_member(self, name): + if self.show_inherited_members: + return True # show all class members + if name not in self._cls.__dict__: + return False # class member is inherited, we do not show it + return True diff --git a/mainsite_hidden/source_plugins/sphinxext/docscrape_sphinx.py b/mainsite_hidden/source_plugins/sphinxext/docscrape_sphinx.py index 77ed271b0..c46182a91 100644 --- a/mainsite_hidden/source_plugins/sphinxext/docscrape_sphinx.py +++ b/mainsite_hidden/source_plugins/sphinxext/docscrape_sphinx.py @@ -1,136 +1,313 @@ -import re, inspect, textwrap, pydoc -from docscrape import NumpyDocString, FunctionDoc, ClassDoc +import sys +import re +import inspect +import textwrap +import pydoc +import collections +import os + +from jinja2 import FileSystemLoader +from jinja2.sandbox import SandboxedEnvironment +import sphinx +from sphinx.jinja2glue import BuiltinTemplateLoader + +from .docscrape import NumpyDocString, FunctionDoc, ClassDoc + +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, "unicode_escape") + class SphinxDocString(NumpyDocString): + def __init__(self, docstring, config={}): + NumpyDocString.__init__(self, docstring, config=config) + self.load_config(config) + + def load_config(self, config): + self.use_plots = config.get("use_plots", False) + self.class_members_toctree = config.get("class_members_toctree", True) + self.template = config.get("template", None) + if self.template is None: + template_dirs = [os.path.join(os.path.dirname(__file__), "templates")] + template_loader = FileSystemLoader(template_dirs) + template_env = SandboxedEnvironment(loader=template_loader) + self.template = template_env.get_template("numpydoc_docstring.rst") + # string conversion routines - def _str_header(self, name, symbol='`'): - return ['.. rubric:: ' + name, ''] + def _str_header(self, name, symbol="`"): + return [".. rubric:: " + name, ""] def _str_field_list(self, name): - return [':' + name + ':'] + return [":" + name + ":"] def _str_indent(self, doc, indent=4): out = [] for line in doc: - out += [' '*indent + line] + out += [" " * indent + line] return out def _str_signature(self): - return [''] - if self['Signature']: - return ['``%s``' % self['Signature']] + [''] + return [""] + if self["Signature"]: + return ["``%s``" % self["Signature"]] + [""] else: - return [''] + return [""] def _str_summary(self): - return self['Summary'] + [''] + return self["Summary"] + [""] def _str_extended_summary(self): - return self['Extended Summary'] + [''] + return self["Extended Summary"] + [""] + + def _str_returns(self, name="Returns"): + out = [] + if self[name]: + out += self._str_field_list(name) + out += [""] + for param, param_type, desc in self[name]: + if param_type: + out += self._str_indent( + [f"**{param.strip()}** : {param_type}"] + ) + else: + out += self._str_indent([param.strip()]) + if desc: + out += [""] + out += self._str_indent(desc, 8) + out += [""] + return out def _str_param_list(self, name): out = [] if self[name]: out += self._str_field_list(name) - out += [''] - for param,param_type,desc in self[name]: - out += self._str_indent(['**%s** : %s' % (param.strip(), - param_type)]) - out += [''] - out += self._str_indent(desc,8) - out += [''] + out += [""] + for param, param_type, desc in self[name]: + if param_type: + out += self._str_indent( + [f"**{param.strip()}** : {param_type}"] + ) + else: + out += self._str_indent(["**%s**" % param.strip()]) + if desc: + out += [""] + out += self._str_indent(desc, 8) + out += [""] + return out + + @property + def _obj(self): + if hasattr(self, "_cls"): + return self._cls + elif hasattr(self, "_f"): + return self._f + return None + + def _str_member_list(self, name): + """ + Generate a member listing, autosummary:: table where possible, + and a table where not. + + """ + out = [] + if self[name]: + out += [".. rubric:: %s" % name, ""] + prefix = getattr(self, "_name", "") + + if prefix: + prefix = "~%s." % prefix + + autosum = [] + others = [] + for param, param_type, desc in self[name]: + param = param.strip() + + # Check if the referenced member can have a docstring or not + param_obj = getattr(self._obj, param, None) + if not ( + callable(param_obj) + or isinstance(param_obj, property) + or inspect.isgetsetdescriptor(param_obj) + ): + param_obj = None + + if param_obj and (pydoc.getdoc(param_obj) or not desc): + # Referenced object has a docstring + autosum += [f" {prefix}{param}"] + else: + others.append((param, param_type, desc)) + + if autosum: + out += [".. autosummary::"] + if self.class_members_toctree: + out += [" :toctree:"] + out += [""] + autosum + + if others: + maxlen_0 = max(3, max([len(x[0]) + 4 for x in others])) + hdr = sixu("=") * maxlen_0 + sixu(" ") + sixu("=") * 10 + fmt = sixu("%%%ds %%s ") % (maxlen_0,) + out += ["", "", hdr] + for param, param_type, desc in others: + desc = sixu(" ").join(x.strip() for x in desc).strip() + if param_type: + desc = f"({param_type}) {desc}" + out += [fmt % ("**" + param.strip() + "**", desc)] + out += [hdr] + out += [""] return out def _str_section(self, name): out = [] if self[name]: out += self._str_header(name) - out += [''] + out += [""] content = textwrap.dedent("\n".join(self[name])).split("\n") out += content - out += [''] + out += [""] return out def _str_see_also(self, func_role): out = [] - if self['See Also']: - see_also = super(SphinxDocString, self)._str_see_also(func_role) - out = ['.. seealso::', ''] + if self["See Also"]: + see_also = super()._str_see_also(func_role) + out = [".. seealso::", ""] out += self._str_indent(see_also[2:]) return out def _str_warnings(self): out = [] - if self['Warnings']: - out = ['.. warning::', ''] - out += self._str_indent(self['Warnings']) + if self["Warnings"]: + out = [".. warning::", ""] + out += self._str_indent(self["Warnings"]) return out def _str_index(self): - idx = self['index'] + idx = self["index"] out = [] if len(idx) == 0: return out - out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): - if section == 'default': + out += [".. index:: %s" % idx.get("default", "")] + for section, references in idx.items(): + if section == "default": continue - elif section == 'refguide': - out += [' single: %s' % (', '.join(references))] + elif section == "refguide": + out += [" single: %s" % (", ".join(references))] else: - out += [' %s: %s' % (section, ','.join(references))] + out += [" {}: {}".format(section, ",".join(references))] return out def _str_references(self): out = [] - if self['References']: - out += self._str_header('References') - if isinstance(self['References'], str): - self['References'] = [self['References']] - out.extend(self['References']) - out += [''] + if self["References"]: + out += self._str_header("References") + if isinstance(self["References"], str): + self["References"] = [self["References"]] + out.extend(self["References"]) + out += [""] + # Latex collects all references to a separate bibliography, + # so we need to insert links to it + if sphinx.__version__ >= "0.6": + out += [".. only:: latex", ""] + else: + out += [".. latexonly::", ""] + items = [] + for line in self["References"]: + m = re.match(r".. \[([a-z0-9._-]+)\]", line, re.I) + if m: + items.append(m.group(1)) + out += [" " + ", ".join(["[%s]_" % item for item in items]), ""] return out + def _str_examples(self): + examples_str = "\n".join(self["Examples"]) + + if ( + self.use_plots + and "import matplotlib" in examples_str + and "plot::" not in examples_str + ): + out = [] + out += self._str_header("Examples") + out += [".. plot::", ""] + out += self._str_indent(self["Examples"]) + out += [""] + return out + else: + return self._str_section("Examples") + def __str__(self, indent=0, func_role="obj"): - out = [] - out += self._str_signature() - out += self._str_index() + [''] - out += self._str_summary() - out += self._str_extended_summary() - for param_list in ('Parameters', 'Attributes', 'Methods', - 'Returns','Raises'): - out += self._str_param_list(param_list) - out += self._str_warnings() - out += self._str_see_also(func_role) - out += self._str_section('Notes') - out += self._str_references() - out += self._str_section('Examples') - out = self._str_indent(out,indent) - return '\n'.join(out) + ns = { + "signature": self._str_signature(), + "index": self._str_index(), + "summary": self._str_summary(), + "extended_summary": self._str_extended_summary(), + "parameters": self._str_param_list("Parameters"), + "returns": self._str_returns("Returns"), + "yields": self._str_returns("Yields"), + "other_parameters": self._str_param_list("Other Parameters"), + "raises": self._str_param_list("Raises"), + "warns": self._str_param_list("Warns"), + "warnings": self._str_warnings(), + "see_also": self._str_see_also(func_role), + "notes": self._str_section("Notes"), + "references": self._str_references(), + "examples": self._str_examples(), + "attributes": self._str_member_list("Attributes"), + "methods": self._str_member_list("Methods"), + } + ns = {k: "\n".join(v) for k, v in ns.items()} + + rendered = self.template.render(**ns) + return "\n".join(self._str_indent(rendered.split("\n"), indent)) + class SphinxFunctionDoc(SphinxDocString, FunctionDoc): - pass + def __init__(self, obj, doc=None, config={}): + self.load_config(config) + FunctionDoc.__init__(self, obj, doc=doc, config=config) + class SphinxClassDoc(SphinxDocString, ClassDoc): - pass + def __init__(self, obj, doc=None, func_doc=None, config={}): + self.load_config(config) + ClassDoc.__init__(self, obj, doc=doc, func_doc=None, config=config) + + +class SphinxObjDoc(SphinxDocString): + def __init__(self, obj, doc=None, config={}): + self._f = obj + self.load_config(config) + SphinxDocString.__init__(self, doc, config=config) + -def get_doc_object(obj, what=None, doc=None): +def get_doc_object(obj, what=None, doc=None, config={}, builder=None): if what is None: if inspect.isclass(obj): - what = 'class' + what = "class" elif inspect.ismodule(obj): - what = 'module' - elif callable(obj): - what = 'function' + what = "module" + elif isinstance(obj, collections.abc.Callable): + what = "function" else: - what = 'object' - if what == 'class': - return SphinxClassDoc(obj, '', func_doc=SphinxFunctionDoc, doc=doc) - elif what in ('function', 'method'): - return SphinxFunctionDoc(obj, '', doc=doc) + what = "object" + + template_dirs = [os.path.join(os.path.dirname(__file__), "templates")] + if builder is not None: + template_loader = BuiltinTemplateLoader() + template_loader.init(builder, dirs=template_dirs) + else: + template_loader = FileSystemLoader(template_dirs) + template_env = SandboxedEnvironment(loader=template_loader) + config["template"] = template_env.get_template("numpydoc_docstring.rst") + + if what == "class": + return SphinxClassDoc(obj, func_doc=SphinxFunctionDoc, doc=doc, config=config) + elif what in ("function", "method"): + return SphinxFunctionDoc(obj, doc=doc, config=config) else: if doc is None: doc = pydoc.getdoc(obj) - return SphinxDocString(doc) - + return SphinxObjDoc(obj, doc, config=config) diff --git a/mainsite_hidden/source_plugins/sphinxext/generate_modules.py b/mainsite_hidden/source_plugins/sphinxext/generate_modules.py index f85e1ffdc..c925bc879 100644 --- a/mainsite_hidden/source_plugins/sphinxext/generate_modules.py +++ b/mainsite_hidden/source_plugins/sphinxext/generate_modules.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Miville # Copyright (C) 2008 Société des arts technologiques (SAT) @@ -22,7 +21,7 @@ """ This script parse a directory tree looking for python modules and packages and create ReST files appropriately to create code documentation with Sphinx. -It also create a modules index. +It also create a modules index. """ import os @@ -30,113 +29,129 @@ # automodule options -OPTIONS = ['members', - 'undoc-members', -# 'inherited-members', # disable because there's a bug in sphinx - 'show-inheritance'] +OPTIONS = [ + "members", + "undoc-members", + # 'inherited-members', # disable because there's a bug in sphinx + "show-inheritance", +] def create_file_name(base, opts): """Create file name from base name, path and suffix""" return os.path.join(opts.destdir, "%s.%s" % (base, opts.suffix)) + def write_directive(module, package=None): """Create the automodule directive and add the options""" if package: - directive = '.. automodule:: %s.%s\n' % (package, module) + directive = ".. automodule:: %s.%s\n" % (package, module) else: - directive = '.. automodule:: %s\n' % module + directive = ".. automodule:: %s\n" % module for option in OPTIONS: - directive += ' :%s:\n' % option + directive += " :%s:\n" % option return directive -def write_heading(module, kind='Module'): + +def write_heading(module, kind="Module"): """Create the page heading.""" -# module = module.title() - heading = title_line(':mod:`%s` %s Documentation' % (module, kind), '=') - heading += 'This page contains the %s %s documentation.\n\n' % (module, kind) + # module = module.title() + heading = title_line(":mod:`%s` %s Documentation" % (module, kind), "=") + heading += "This page contains the %s %s documentation.\n\n" % (module, kind) return heading -def write_sub(module, kind='Module'): + +def write_sub(module, kind="Module"): """Create the module subtitle""" - sub = title_line('The :mod:`%s` %s' % (module, kind), '-') + sub = title_line("The :mod:`%s` %s" % (module, kind), "-") return sub + def title_line(title, char): """ Underline the title with the character pass, with the right length.""" - return '%s\n%s\n\n' % (title, len(title) * char) + return "%s\n%s\n\n" % (title, len(title) * char) + def create_module_file(package, module, opts): """Build the text of the file and write the file.""" name = create_file_name(module, opts) if not opts.force and os.path.isfile(name): - print 'File %s already exists.' % name + print "File %s already exists." % name else: - print 'Creating file %s (module).' % name + print "Creating file %s (module)." % name text = write_heading(module) text += write_sub(module) text += write_directive(module, package) # write the file - if not opts.dryrun: - fd = open(name, 'w') + if not opts.dryrun: + fd = open(name, "w") fd.write(text) fd.close() + def create_package_file(root, master_package, subroot, py_files, opts, subs=None): """Build the text of the file and write the file.""" - package = os.path.split(root)[-1] # .lower() + package = os.path.split(root)[-1] # .lower() name = create_file_name(subroot, opts) if not opts.force and os.path.isfile(name): - print 'File %s already exists.' % name + print "File %s already exists." % name else: - print 'Creating file %s (package).' % name - text = write_heading(package, 'Package') + print "Creating file %s (package)." % name + text = write_heading(package, "Package") if subs == None: subs = [] else: # build a list of directories that are package (they contain an __init_.py file) - subs = [sub for sub in subs if os.path.isfile(os.path.join(root, sub, '__init__.py'))] - # if there's some package directories, add a TOC for theses subpackages + subs = [ + sub + for sub in subs + if os.path.isfile(os.path.join(root, sub, "__init__.py")) + ] + # if there's some package directories, add a TOC for these subpackages if subs: -# text += title_line('Subpackages', '-') - text += '.. toctree::\n\n' + # text += title_line('Subpackages', '-') + text += ".. toctree::\n\n" for sub in subs: - text += ' %s.%s\n' % (subroot, sub) - text += '\n' - + text += " %s.%s\n" % (subroot, sub) + text += "\n" + # add each package's module for py_file in py_files: if not check_for_code(os.path.join(root, py_file)): # don't build the file if there's no code in it continue py_file = os.path.splitext(py_file)[0] - py_path = '%s.%s' % (subroot, py_file) + py_path = "%s.%s" % (subroot, py_file) kind = "Module" - if py_file == '__init__': + if py_file == "__init__": kind = "Package" - text += write_sub(kind == 'Package' and package or py_file, kind) - text += write_directive(kind == "Package" and subroot or py_path, master_package) - text += '\n' + text += write_sub(kind == "Package" and package or py_file, kind) + text += write_directive( + kind == "Package" and subroot or py_path, master_package + ) + text += "\n" # write the file - if not opts.dryrun: - fd = open(name, 'w') + if not opts.dryrun: + fd = open(name, "w") fd.write(text) fd.close() + def check_for_code(module): """ Check if there's at least one class or one function in the module. """ - fd = open(module, 'r') + fd = open(module, "r") for line in fd: - if line.startswith('def ') or line.startswith('class '): + if line.startswith("def ") or line.startswith("class "): fd.close() return True fd.close() return False - + + def recurse_tree(path, excludes, opts): """ Look for every file in the directory tree and create the corresponding @@ -144,9 +159,9 @@ def recurse_tree(path, excludes, opts): """ package_name = None # check if the base directory is a package and get is name - if '__init__.py' in os.listdir(path): + if "__init__.py" in os.listdir(path): package_name = os.path.abspath(path).split(os.path.sep)[-1] - + toc = [] excludes = format_excludes(path, excludes) tree = os.walk(path, False) @@ -154,20 +169,23 @@ def recurse_tree(path, excludes, opts): # keep only the Python script files py_files = check_py_file(files) # remove hidden ('.') and private ('_') directories - subs = [sub for sub in subs if sub[0] not in ['.', '_']] + subs = [sub for sub in subs if sub[0] not in [".", "_"]] # check if there's valid files to process # TODO: could add check for windows hidden files - if "/." in root or "/_" in root \ - or not py_files \ - or check_excludes(root, excludes): + if ( + "/." in root + or "/_" in root + or not py_files + or check_excludes(root, excludes) + ): continue - subroot = root[len(path):].lstrip(os.path.sep).replace(os.path.sep, '.') + subroot = root[len(path) :].lstrip(os.path.sep).replace(os.path.sep, ".") if root == path: # we are at the root level so we create only modules for py_file in py_files: module = os.path.splitext(py_file)[0] # add the module if it contains code - if check_for_code(os.path.join(path, '%s.py' % module)): + if check_for_code(os.path.join(path, "%s.py" % module)): create_module_file(package_name, module, opts) toc.append(module) elif not subs and "__init__.py" in py_files: @@ -175,8 +193,10 @@ def recurse_tree(path, excludes, opts): # check if there's only an __init__.py file if len(py_files) == 1: # check if there's code in the __init__.py file - if check_for_code(os.path.join(root, '__init__.py')): - create_package_file(root, package_name, subroot, py_files, opts=opts) + if check_for_code(os.path.join(root, "__init__.py")): + create_package_file( + root, package_name, subroot, py_files, opts=opts + ) toc.append(subroot) else: create_package_file(root, package_name, subroot, py_files, opts=opts) @@ -185,41 +205,43 @@ def recurse_tree(path, excludes, opts): # we are in package with subpackage(s) create_package_file(root, package_name, subroot, py_files, opts, subs) toc.append(subroot) - + # create the module's index if not opts.notoc: modules_toc(toc, opts) -def modules_toc(modules, opts, name='modules'): + +def modules_toc(modules, opts, name="modules"): """ Create the module's index. """ - fname = create_file_name(name, opts) + fname = create_file_name(name, opts) if not opts.force and os.path.exists(fname): print "File %s already exists." % name return print "Creating module's index modules.txt." - text = write_heading(opts.header, 'Modules') -# text += title_line('Modules:', '-') - text += '.. toctree::\n' - text += ' :maxdepth: %s\n\n' % opts.maxdepth - + text = write_heading(opts.header, "Modules") + # text += title_line('Modules:', '-') + text += ".. toctree::\n" + text += " :maxdepth: %s\n\n" % opts.maxdepth + modules.sort() - prev_module = '' + prev_module = "" for module in modules: # look if the module is a subpackage and, if yes, ignore it - if module.startswith(prev_module + '.'): + if module.startswith(prev_module + "."): continue prev_module = module - text += ' %s\n' % module - + text += " %s\n" % module + # write the file - if not opts.dryrun: - fd = open(fname, 'w') + if not opts.dryrun: + fd = open(fname, "w") fd.write(text) fd.close() + def format_excludes(path, excludes): """ Format the excluded directory list. @@ -228,26 +250,28 @@ def format_excludes(path, excludes): """ f_excludes = [] for exclude in excludes: - if not os.path.isabs(exclude) and exclude[:len(path)] != path: + if not os.path.isabs(exclude) and exclude[: len(path)] != path: exclude = os.path.join(path, exclude) # remove trailing slash f_excludes.append(exclude.rstrip(os.path.sep)) return f_excludes + def check_excludes(root, excludes): """ Check if the directory is in the exclude list. """ for exclude in excludes: - if root[:len(exclude)] == exclude: + if root[: len(exclude)] == exclude: return True return False + def check_py_file(files): """ - Return a list with only the python scripts (remove all other files). + Return a list with only the python scripts (remove all other files). """ - py_files = [fich for fich in files if os.path.splitext(fich)[1] == '.py'] + py_files = [fich for fich in files if os.path.splitext(fich)[1] == ".py"] return py_files @@ -255,16 +279,65 @@ def main(): """ Parse and check the command line arguments """ - parser = optparse.OptionParser(usage="""usage: %prog [options] [exclude paths, ...] - -Note: By default this script will not overwrite already created files.""") - parser.add_option("-n", "--doc-header", action="store", dest="header", help="Documentation Header (default=Project)", default="Project") - parser.add_option("-d", "--dest-dir", action="store", dest="destdir", help="Output destination directory", default="") - parser.add_option("-s", "--suffix", action="store", dest="suffix", help="module suffix (default=txt)", default="txt") - parser.add_option("-m", "--maxdepth", action="store", dest="maxdepth", help="Maximum depth of submodules to show in the TOC (default=4)", type="int", default=4) - parser.add_option("-r", "--dry-run", action="store_true", dest="dryrun", help="Run the script without creating the files") - parser.add_option("-f", "--force", action="store_true", dest="force", help="Overwrite all the files") - parser.add_option("-t", "--no-toc", action="store_true", dest="notoc", help="Don't create the table of content file") + parser = optparse.OptionParser( + usage="""usage: %prog [options] [exclude paths, ...] + +Note: By default this script will not overwrite already created files.""" + ) + parser.add_option( + "-n", + "--doc-header", + action="store", + dest="header", + help="Documentation Header (default=Project)", + default="Project", + ) + parser.add_option( + "-d", + "--dest-dir", + action="store", + dest="destdir", + help="Output destination directory", + default="", + ) + parser.add_option( + "-s", + "--suffix", + action="store", + dest="suffix", + help="module suffix (default=txt)", + default="txt", + ) + parser.add_option( + "-m", + "--maxdepth", + action="store", + dest="maxdepth", + help="Maximum depth of submodules to show in the TOC (default=4)", + type="int", + default=4, + ) + parser.add_option( + "-r", + "--dry-run", + action="store_true", + dest="dryrun", + help="Run the script without creating the files", + ) + parser.add_option( + "-f", + "--force", + action="store_true", + dest="force", + help="Overwrite all the files", + ) + parser.add_option( + "-t", + "--no-toc", + action="store_true", + dest="notoc", + help="Don't create the table of content file", + ) (opts, args) = parser.parse_args() if len(args) < 1: parser.error("package path is required.") @@ -276,13 +349,10 @@ def main(): excludes = args[1:] recurse_tree(args[0], excludes, opts) else: - print '%s is not a valid output destination directory.' % opts.destdir + print "%s is not a valid output destination directory." % opts.destdir else: - print '%s is not a valid directory.' % args - - + print "%s is not a valid directory." % args -if __name__ == '__main__': +if __name__ == "__main__": main() - \ No newline at end of file diff --git a/mainsite_hidden/source_plugins/sphinxext/numpydoc.py b/mainsite_hidden/source_plugins/sphinxext/numpydoc.py index eea482dc6..b4b60390b 100644 --- a/mainsite_hidden/source_plugins/sphinxext/numpydoc.py +++ b/mainsite_hidden/source_plugins/sphinxext/numpydoc.py @@ -10,107 +10,266 @@ - Convert Parameters etc. sections to field lists. - Convert See Also section to a See also entry. - Renumber references. -- Extract the signature from the docstring, if it can't be determined otherwise. +- Extract the signature from the docstring, if it can't be determined + otherwise. -.. [1] http://projects.scipy.org/scipy/numpy/wiki/CodingStyleGuidelines#docstring-standard +.. [1] https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt """ -import os, re, pydoc -from docscrape_sphinx import get_doc_object, SphinxDocString +import sys +import re +import pydoc +import sphinx import inspect +import collections -def mangle_docstrings(app, what, name, obj, options, lines, - reference_offset=[0]): - if what == 'module': - # Strip top title - title_re = re.compile(r'^\s*[#*=]{4,}\n[a-z0-9 -]+\n[#*=]{4,}\s*', - re.I|re.S) - lines[:] = title_re.sub('', "\n".join(lines)).split("\n") - else: - doc = get_doc_object(obj, what, "\n".join(lines)) - lines[:] = str(doc).split("\n") +if sphinx.__version__ < "1.0.1": + raise RuntimeError("Sphinx 1.0.1 or newer is required") + +from .docscrape_sphinx import get_doc_object, SphinxDocString + +if sys.version_info[0] >= 3: + sixu = lambda s: s +else: + sixu = lambda s: unicode(s, "unicode_escape") - if app.config.numpydoc_edit_link and hasattr(obj, '__name__') and \ - obj.__name__: - if hasattr(obj, '__module__'): - v = dict(full_name="%s.%s" % (obj.__module__, obj.__name__)) - else: - v = dict(full_name=obj.__name__) - lines += ['', '.. htmlonly::', ''] - lines += [' %s' % x for x in - (app.config.numpydoc_edit_link % v).split("\n")] +def rename_references(app, what, name, obj, options, lines, reference_offset=[0]): # replace reference numbers so that there are no duplicates references = [] - for l in lines: - l = l.strip() - if l.startswith('.. ['): - try: - references.append(int(l[len('.. ['):l.index(']')])) - except ValueError: - print("WARNING: invalid reference in %s docstring" % name) - - # Start renaming from the biggest number, otherwise we may - # overwrite references. - references.sort() + for line in lines: + line = line.strip() + m = re.match( + sixu("^.. \\[(%s)\\]") % app.config.numpydoc_citation_re, line, re.I + ) + if m: + references.append(m.group(1)) + if references: for i, line in enumerate(lines): for r in references: - new_r = reference_offset[0] + r - lines[i] = lines[i].replace('[%d]_' % r, - '[%d]_' % new_r) - lines[i] = lines[i].replace('.. [%d]' % r, - '.. [%d]' % new_r) + if re.match(sixu("^\\d+$"), r): + new_r = sixu("R%d") % (reference_offset[0] + int(r)) + else: + new_r = sixu("%s%d") % (r, reference_offset[0]) + lines[i] = lines[i].replace(sixu("[%s]_") % r, sixu("[%s]_") % new_r) + lines[i] = lines[i].replace( + sixu(".. [%s]") % r, sixu(".. [%s]") % new_r + ) reference_offset[0] += len(references) + +def mangle_docstrings(app, what, name, obj, options, lines): + cfg = { + "use_plots": app.config.numpydoc_use_plots, + "show_class_members": app.config.numpydoc_show_class_members, + "show_inherited_class_members": app.config.numpydoc_show_inherited_class_members, + "class_members_toctree": app.config.numpydoc_class_members_toctree, + } + + u_NL = sixu("\n") + if what == "module": + # Strip top title + pattern = "^\\s*[#*=]{4,}\\n[a-z0-9 -]+\\n[#*=]{4,}\\s*" + title_re = re.compile(sixu(pattern), re.I | re.S) + lines[:] = title_re.sub(sixu(""), u_NL.join(lines)).split(u_NL) + else: + doc = get_doc_object( + obj, what, u_NL.join(lines), config=cfg, builder=app.builder + ) + if sys.version_info[0] >= 3: + doc = str(doc) + else: + doc = unicode(doc) + lines[:] = doc.split(u_NL) + + if app.config.numpydoc_edit_link and hasattr(obj, "__name__") and obj.__name__: + if hasattr(obj, "__module__"): + v = dict(full_name=sixu("%s.%s") % (obj.__module__, obj.__name__)) + else: + v = dict(full_name=obj.__name__) + lines += [sixu(""), sixu(".. htmlonly::"), sixu("")] + lines += [ + sixu(" %s") % x for x in (app.config.numpydoc_edit_link % v).split("\n") + ] + + # call function to replace reference numbers so that there are no + # duplicates + rename_references(app, what, name, obj, options, lines) + + def mangle_signature(app, what, name, obj, options, sig, retann): # Do not try to inspect classes that don't define `__init__` - if (inspect.isclass(obj) and - 'initializes x; see ' in pydoc.getdoc(obj.__init__)): - return '', '' + if inspect.isclass(obj) and ( + not hasattr(obj, "__init__") + or "initializes x; see " in pydoc.getdoc(obj.__init__) + ): + return "", "" - if not (callable(obj) or hasattr(obj, '__argspec_is_invalid_')): return - if not hasattr(obj, '__doc__'): return + if not ( + isinstance(obj, collections.abc.Callable) + or hasattr(obj, "__argspec_is_invalid_") + ): + return + if not hasattr(obj, "__doc__"): + return doc = SphinxDocString(pydoc.getdoc(obj)) - if doc['Signature']: - sig = re.sub("^[^(]*", "", doc['Signature']) - return sig, '' + sig = doc["Signature"] or getattr(obj, "__text_signature__", None) + if sig: + sig = re.sub(sixu("^[^(]*"), sixu(""), sig) + return sig, sixu("") -def initialize(app): - try: - app.connect('autodoc-process-signature', mangle_signature) - except: - monkeypatch_sphinx_ext_autodoc() def setup(app, get_doc_object_=get_doc_object): + if not hasattr(app, "add_config_value"): + return # probably called by nose, better bail out + global get_doc_object get_doc_object = get_doc_object_ - - app.connect('autodoc-process-docstring', mangle_docstrings) - app.connect('builder-inited', initialize) - app.add_config_value('numpydoc_edit_link', None, True) -#------------------------------------------------------------------------------ -# Monkeypatch sphinx.ext.autodoc to accept argspecless autodocs (Sphinx < 0.5) -#------------------------------------------------------------------------------ + app.connect("autodoc-process-docstring", mangle_docstrings) + app.connect("autodoc-process-signature", mangle_signature) + app.add_config_value("numpydoc_edit_link", None, False) + app.add_config_value("numpydoc_use_plots", None, False) + app.add_config_value("numpydoc_show_class_members", True, True) + app.add_config_value("numpydoc_show_inherited_class_members", True, True) + app.add_config_value("numpydoc_class_members_toctree", True, True) + app.add_config_value("numpydoc_citation_re", "[a-z0-9_.-]+", True) -def monkeypatch_sphinx_ext_autodoc(): - global _original_format_signature - import sphinx.ext.autodoc + # Extra mangling domains + app.add_domain(NumpyPythonDomain) + app.add_domain(NumpyCDomain) - if sphinx.ext.autodoc.format_signature is our_format_signature: - return + metadata = {"parallel_read_safe": True} + return metadata - print("[numpydoc] Monkeypatching sphinx.ext.autodoc ...") - _original_format_signature = sphinx.ext.autodoc.format_signature - sphinx.ext.autodoc.format_signature = our_format_signature -def our_format_signature(what, obj): - r = mangle_signature(None, what, None, obj, None, None, None) - if r is not None: - return r[0] - else: - return _original_format_signature(what, obj) +# ------------------------------------------------------------------------------ +# Docstring-mangling domains +# ------------------------------------------------------------------------------ + +from docutils.statemachine import ViewList +from sphinx.domains.c import CDomain +from sphinx.domains.python import PythonDomain + + +class ManglingDomainBase: + directive_mangling_map = {} + + def __init__(self, *a, **kw): + super().__init__(*a, **kw) + self.wrap_mangling_directives() + + def wrap_mangling_directives(self): + for name, objtype in list(self.directive_mangling_map.items()): + self.directives[name] = wrap_mangling_directive( + self.directives[name], objtype + ) + + +class NumpyPythonDomain(ManglingDomainBase, PythonDomain): + name = "np" + directive_mangling_map = { + "function": "function", + "class": "class", + "exception": "class", + "method": "function", + "classmethod": "function", + "staticmethod": "function", + "attribute": "attribute", + } + indices = [] + + +class NumpyCDomain(ManglingDomainBase, CDomain): + name = "np-c" + directive_mangling_map = { + "function": "function", + "member": "attribute", + "macro": "function", + "type": "class", + "var": "object", + } + + +def match_items(lines, content_old): + """Create items for mangled lines. + + This function tries to match the lines in ``lines`` with the items (source + file references and line numbers) in ``content_old``. The + ``mangle_docstrings`` function changes the actual docstrings, but doesn't + keep track of where each line came from. The mangling does many operations + on the original lines, which are hard to track afterwards. + + Many of the line changes come from deleting or inserting blank lines. This + function tries to match lines by ignoring blank lines. All other changes + (such as inserting figures or changes in the references) are completely + ignored, so the generated line numbers will be off if ``mangle_docstrings`` + does anything non-trivial. + + This is a best-effort function and the real fix would be to make + ``mangle_docstrings`` actually keep track of the ``items`` together with + the ``lines``. + + Examples + -------- + >>> lines = ['', 'A', '', 'B', ' ', '', 'C', 'D'] + >>> lines_old = ['a', '', '', 'b', '', 'c'] + >>> items_old = [('file1.py', 0), ('file1.py', 1), ('file1.py', 2), + ... ('file2.py', 0), ('file2.py', 1), ('file2.py', 2)] + >>> content_old = ViewList(lines_old, items=items_old) + >>> match_items(lines, content_old) # doctest: +NORMALIZE_WHITESPACE + [('file1.py', 0), ('file1.py', 0), ('file2.py', 0), ('file2.py', 0), + ('file2.py', 2), ('file2.py', 2), ('file2.py', 2), ('file2.py', 2)] + >>> # first 2 ``lines`` are matched to 'a', second 2 to 'b', rest to 'c' + >>> # actual content is completely ignored. + + Notes + ----- + The algorithm tries to match any line in ``lines`` with one in + ``lines_old``. It skips over all empty lines in ``lines_old`` and assigns + this line number to all lines in ``lines``, unless a non-empty line is + found in ``lines`` in which case it goes to the next line in ``lines_old``. + + """ + items_new = [] + lines_old = content_old.data + items_old = content_old.items + j = 0 + for i, line in enumerate(lines): + # go to next non-empty line in old: + # line.strip() checks whether the string is all whitespace + while j < len(lines_old) - 1 and not lines_old[j].strip(): + j += 1 + items_new.append(items_old[j]) + if line.strip() and j < len(lines_old) - 1: + j += 1 + assert len(items_new) == len(lines) + return items_new + + +def wrap_mangling_directive(base_directive, objtype): + class directive(base_directive): + def run(self): + env = self.state.document.settings.env + + name = None + if self.arguments: + m = re.match(r"^(.*\s+)?(.*?)(\(.*)?", self.arguments[0]) + name = m.group(2).strip() + + if not name: + name = self.arguments[0] + + lines = list(self.content) + mangle_docstrings(env.app, objtype, name, None, None, lines) + if self.content: + items = match_items(lines, self.content) + self.content = ViewList(lines, items=items, parent=self.content.parent) + + return base_directive.run(self) + + return directive