Source code for py2rst

#
##
##  SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.5  (Thu Feb  8 19:11:13 CET 2024)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##

"""Automatic generation of documentation

This module provides the tools to automatically generate the documentation
for a pyFormex module. It is mainly intended for use by the
'pyformex --docmodule' command option.

This command automatically extracts class & function docstrings and argument
list from a module and ships out the information in a format that can be
used by the Sphinx document preprocessor.
"""
import inspect
import importlib

from pyformex.utils import underlineHeader

_debug = False


############# Output formatting ##########################


[docs]def add_indent(s, n): """Indent all lines of a multiline string with n blanks.""" return '\n'.join([' ' * n + si for si in s.split('\n')])
def split_doc(docstring): try: s = docstring.split('\n') shortdoc = s[0] if len(s) > 2: longdoc = '\n'.join(s[2:]) else: longdoc = '' return shortdoc.strip('"'), longdoc.strip('"') except Exception: return '', ''
[docs]def sanitize(s): """Sanitize a string for LaTeX.""" for c in '#&%': s = s.replace('\\' + c, c) s = s.replace(c, '\\' + c) # for c in '{}': # s = s.replace('\\'+c,c) # s = s.replace(c,'\\'+c) return s
out = '' def ship_start(): global out out = '' def ship(s): global out out += s out += '\n' def debug(s): if _debug: ship('.. DEBUG:' + str(s)) def ship_module(name, docstring, members): shortdoc, longdoc = split_doc(docstring) auto = 'auto' # '' if name.endswith('_c') else 'auto' ship(f"""\ .. -*- rst -*- .. pyformex reference manual --- {name} .. CREATED WITH pyformex --docmodule: DO NOT EDIT .. include:: <isonum.txt> .. include:: ../defines.inc .. include:: ../links.inc .. _sec:ref-{name}: :mod:`{name}` --- {shortdoc} {'=' * (12 + len(name) + len(shortdoc))} .. {auto}module:: {name} :synopsis: {shortdoc}""") if members: ship(f" :members: {','.join(members)}") ship('') def ship_end(): ship(""" .. moduleauthor:: pyFormex project (http://pyformex.org) .. End """) def ship_functions(members=[]): if members: for f in members: ship(f".. autofunction:: {f}") def ship_class(name, members=[], special=[], exclude=[]): ship(f" .. autoclass:: {name}") if members: ship(f" :members: {','.join(members)}") else: ship(" :members:") if special: ship(f" :special-members: {','.join(special)}") if exclude: ship(f" :exclude-members: {','.join(exclude)}") def ship_attribute(name): ship(f" .. autodata:: {name}") def ship_section_init(secname, modname): indent = 3 # 0 if modname.endswith('_c') else 3 ship( add_indent( '\n' + underlineHeader( f"{secname} defined in module {modname}", '~' ) + '\n', indent ) ) ############# Info selection ##########################
[docs]def filter_local(name, fullname): """Filter definitions to include in doc We only include names defined in the module itself. """ return name.__module__ == fullname
def filter_names(info): return [i for i in info if not i[0].startswith('_')] def filter_docstrings(info): return [ i for i in info if not (i[1].__doc__ is None or i[1].__doc__.startswith('_')) ] def filter_module(info, modname): return [i for i in info if i[1].__module__ == modname] def function_key(i): obj = i[1] return obj.__code__.co_firstlineno def property_key(i): obj = i[1] if hasattr(obj, 'fget'): val = obj.fget.__code__.co_firstlineno else: val = 0 return val _py2rst_order = [] def class_key(i): try: key = inspect.getsourcelines(i[1])[1] except Exception: key = 999999 return key def class_key_1(i): try: key = _py2rst_order.index(i[0]) except ValueError: key = 99999 return key
[docs]def check_declared_members(obj): """Check if obj has declared members Currently 3 members declarations are acknowledged: _members_ _special_members_ _exclude_members_ """ try: members = getattr(obj, '_members_') except Exception: members = [] try: special = getattr(obj, '_special_members_') except Exception: special = [] try: exclude = getattr(obj, '_exclude_members_') except Exception: exclude = [] return members, special, exclude
def do_class(name, obj): members, special, exclude = check_declared_members(obj) ship_class(name, members=members, special=special, exclude=exclude) return # # get class attributes # try: # attrnames = getattr(obj,'_attributes_') # except Exception: # attrnames = [] # # get class properties# # # props = inspect.getmembers(obj,inspect.isdatadescriptor) # # props = [ p for p in props if isinstance(p[1],property) ] # # props = filter_names(props) # # props = filter_docstrings(props) # # props = sorted(props,key=property_key) # props = [] # # get class methods # # methods = inspect.getmembers(obj,inspect.ismethod) # methods = filter_names(methods) # methods = filter_docstrings(methods) # methods = sorted(methods,key=function_key) # names = [ f[0] for f in props+methods ] # ship_class(name,attrnames+names) def do_list(module): members = inspect.getmembers(module) visible = [m for m in members if not m[0].startswith('_')] attributes = [ m for m in visible if not inspect.isclass(m[1]) and not inspect.isfunction(m[1]) ] for m in attributes: print(m) for name, obj in visible: print(f"{name} docstring: {inspect.getdoc(obj)}")
[docs]def get_py_members(module, fullname): """Select the attributes, classes and functions from module""" global _py2rst_order _py2rst_order = getattr(module, '_py2rst_order_', []) # Attributes # attrnames = getattr(module, '_attributes_', []) # Classes # classes = [ c for c in inspect.getmembers(module, inspect.isclass) if filter_local(c[1], fullname) ] classes = filter_names(classes) classes = filter_docstrings(classes) keyfunc = class_key_1 if _py2rst_order else class_key classes = sorted(classes, key=keyfunc) # Functions # functions = [ c for c in inspect.getmembers(module, inspect.isfunction) if filter_local(c[1], fullname) ] functions = filter_names(functions) functions = filter_docstrings(functions) functions = sorted(functions, key=function_key) funcnames = [f[0] for f in functions] return attrnames, classes, funcnames
[docs]def get_c_members(members): """Select the attributes, classes and functions from module""" attrnames = [] # 'accelerated' ] functions = [m for m in members if callable(m[1])] functions = filter_names(functions) functions = filter_docstrings(functions) # sigs = [inspect.signature(m[1]) for m in functions] # print(sigs) funcnames = [f[0] for f in functions] return attrnames, [], funcnames
[docs]def do_module(modname, outfile=None): """Process a module. Prints the documentation of the module in .rst format. The output has to be processes by sphinx using autodoc to generate the full documentation. This is done with the ``make html`` command in the top directory of the pyFormex source. Parameters ---------- modname: str Name of the module in Python dotted style. The ``pyformex.`` part may be omitted. """ if modname.startswith('pyformex.'): fullname = modname else: fullname = 'pyformex.' + modname module = importlib.import_module(fullname) members = inspect.getmembers(module) visible = [m for m in members if not m[0].startswith('_')] # noqa: F841 if modname.endswith('_c'): attrnames, classes, funcnames = get_c_members(members) else: attrnames, classes, funcnames = get_py_members(module, fullname) # Shipout ship_start() ship_module(modname, module.__doc__, funcnames) if attrnames: ship_section_init('Variables', modname) for n in attrnames: ship_attribute(n) if classes: # If the module only contains 1 single class and nothing else, # omit the classes header if len(classes) > 1 or attrnames or funcnames: ship_section_init('Classes', modname) for c in classes: do_class(*c) if funcnames: ship_section_init('Functions', modname) # if funcnames and modname.endswith('_c'): # ship_functions(funcnames) ship_end() return out
# End