Package pdoc

Python package pdoc provides types, functions, and a command-line interface for accessing public documentation of Python modules, and for presenting it in a user-friendly, industry-standard open format. It is best suited for small- to medium-sized projects with tidy, hierarchical APIs.

pdoc extracts documentation of:

  • modules (including submodules),
  • functions (including methods, properties, coroutines …),
  • classes, and
  • variables (including globals, class variables, and instance variables).

Documentation is extracted from live objects' docstrings using Python's __doc__ attribute1. Documentation for variables is found by examining objects' abstract syntax trees.

What objects are documented?

pdoc only extracts public API documentation.2 Code objects (modules, variables, functions, classes, methods) are considered public in the modules where they are defined (vs. imported from somewhere else) as long as their identifiers don't begin with an underscore ( _ ).3 If a module defines __all__, then only the identifiers contained in this list are considered public, regardless of where they were defined.

This can be fine-tuned through __pdoc__ dict.

Where does pdoc get documentation from?

In Python, objects like modules, functions, classes, and methods have a special attribute __doc__ which contains that object's documentation string (docstring). For example, the following code defines a function with a docstring and shows how to access its contents:

>>> def test():
...     """This is a docstring."""
...     pass
...
>>> test.__doc__
'This is a docstring.'

It's pretty much the same with classes and modules. See PEP-257 for Python docstring conventions.

These docstrings are set as descriptions for each module, class, function, and method listed in the documentation produced by pdoc.

pdoc extends the standard use of docstrings in Python in two important ways: by allowing methods to inherit docstrings, and by introducing syntax for docstrings for variables.

Docstrings inheritance

pdoc considers methods' docstrings inherited from superclass methods', following the normal class inheritance patterns. Consider the following code example:

>>> class A:
...     def test(self):
...         """Docstring for A."""
...         pass
...
>>> class B(A):
...     def test(self):
...         pass
...
>>> A.test.__doc__
'Docstring for A.'
>>> B.test.__doc__
None

In Python, the docstring for B.test doesn't exist, even though a docstring was defined for A.test. Contrary, when pdoc generates documentation for code such as above, it will automatically attach the docstring for A.test to B.test if the latter doesn't define its own. In the default HTML template, such inherited docstrings are greyed out.

Docstrings for variables

Python by itself doesn't allow docstrings attached to variables. However, pdoc supports documenting module (or global) variables, class variables, and object instance variables via two different mechanisms: PEP-224 and #: doc-comments.

For example:

module_variable = 1
"""PEP 224 docstring for module_variable."""

class C:
    #: Documentation comment for class_variable
    #: spanning over three lines.
    class_variable = 2  #: Assignment line is included.

    def __init__(self):
        #: Instance variable's doc-comment
        self.variable = 3
        """But note, PEP 224 docstrings take precedence."""

While the resulting variables have no __doc__ attribute, pdoc compensates by reading the source code (when available) and parsing the syntax tree.

By convention, variables defined in a class' __init__ method and attached to self are considered and documented as instance variables.

Class and instance variables can also inherit docstrings.

Overriding docstrings with __pdoc__

Docstrings for objects can be disabled, overridden, or whitelisted with a special module-level dictionary __pdoc__. The keys should be string identifiers within the scope of the module or, alternatively, fully-qualified reference names. E.g. for instance variable self.variable of class C, its module-level identifier is 'C.variable', and some_package.module.C.variable its refname.

If __pdoc__[key] = False, then key (and its members) will be excluded from the documentation of the module.

Conversely, if __pdoc__[key] = True, then key (and its public members) will be included in the documentation of the module. This can be used to include documentation of private objects, including special functions such as __call__, which are ignored by default.

Alternatively, the values of __pdoc__ can be the overriding docstrings. This feature is useful when there's no feasible way of attaching a docstring to something. A good example is a namedtuple:

__pdoc__ = {}

Table = namedtuple('Table', ['types', 'names', 'rows'])
__pdoc__['Table.types'] = 'Types for each column in the table.'
__pdoc__['Table.names'] = 'The names of each column in the table.'
__pdoc__['Table.rows'] = 'Lists corresponding to each row in the table.'

pdoc will then show Table as a class with documentation for the types, names and rows members.

Note

The assignments to __pdoc__ need to be placed where they'll be executed when the module is imported. For example, at the top level of a module or in the definition of a class.

Supported Docstring Formats

Currently, pure Markdown (with extensions), numpydoc, and Google-style docstrings formats are supported, along with some reST directives.

Additionally, if latex_math template config option is enabled, LaTeX math syntax is supported when placed between recognized delimiters: \(...\) for inline equations and \[...\] or $$...$$ for block equations. Note, you need to escape your backslashes in Python docstrings (\\(, \\frac{}{}, …) or, alternatively, use raw string literals.

Supported reST directives

The following reST directives should work:

  • specific and generic admonitions (attention, caution, danger, error, hint, important, note, tip, warning, admonition),
  • .. image:: or .. figure:: (without options),
  • .. include::, with support for the options: :start-line:, :end-line:, :start-after: and :end-before:.
  • .. math::
  • .. versionadded::
  • .. versionchanged::
  • .. deprecated::
  • .. todo::

Linking To Other Identifiers

In your documentation, you may refer to other identifiers in your modules. When exporting to HTML, linking is automatically done whenever you surround an identifier with backticks ( ` ). Unless within the current module, the identifier name must be fully qualified, for example `pdoc.Doc.docstring` is correct (and will link to Doc.docstring) while `Doc.docstring` only works within pdoc module.

Command-line interface

pdoc includes a feature-rich "binary" program for producing HTML and plain text documentation of your modules. For example, to produce HTML documentation of your whole package in subdirectory 'build' of the current directory, using the default HTML template, run:

$ pdoc --html --output-dir build my_package

If you want to omit the source code preview, run:

$ pdoc --html --config show_source_code=False my_package

Find additional template configuration tunables in custom templates section below.

To run a local HTTP server while developing your package or writing docstrings for it, run:

$ pdoc --http : my_package

To re-build documentation as part of your continuous integration (CI) best practice, i.e. ensuring all reference links are correct and up-to-date, make warnings error loudly by settings the environment variable PYTHONWARNINGS before running pdoc:

$ export PYTHONWARNINGS='error::UserWarning'

For brief usage instructions, type:

$ pdoc --help

Even more usage examples can be found in the FAQ.

Programmatic Usage

The main entry point is Module which wraps a module object and recursively imports and wraps any submodules and their members.

After all related modules are wrapped (related modules are those that share the same Context), you need to call link_inheritance() with the used Context instance to establish class inheritance links.

Afterwards, you can use Module.html() and Module.text() methods to output documentation in the desired format. For example:

import pdoc

modules = ['a', 'b']  # Public submodules are auto-imported
context = pdoc.Context()

modules = [pdoc.Module(mod, context=context)
           for mod in modules]
pdoc.link_inheritance(context)

def recursive_htmls(mod):
    yield mod.name, mod.html()
    for submod in mod.submodules():
        yield from recursive_htmls(submod)

for mod in modules:
    for module_name, html in recursive_htmls(mod):
        ...  # Process

When documenting a single module, you might find functions html() and text() handy. For importing arbitrary modules/files, use import_module().

Alternatively, use the runnable script included with this package.

Custom Templates

To override the built-in HTML/CSS and plain text templates, copy the relevant templates from pdoc/templates directory into a directory of your choosing and edit them. When you run pdoc command afterwards, pass the directory path as a parameter to the --template-dir switch.

Tip

If you find you only need to apply minor alterations to the HTML template, see if you can do so by overriding just some of the following, placeholder sub-templates:

  • config.mako: Basic template configuration, affects the way templates are rendered.
  • head.mako: Included just before </head>. Best for adding resources and styles.
  • logo.mako: Included at the very top of the navigation sidebar. Empty by default.
  • credits.mako: Included in the footer, right before pdoc version string.

See default template files for reference.

Tip

You can also alter individual config.mako preferences using the --config command-line switch.

If working with pdoc programmatically, prepend the directory with modified templates into the directories list of the tpl_lookup object.

Compatibility

pdoc requires Python 3.6+. The last version to support Python 2.x is pdoc3 0.3.x.

Contributing

pdoc is on GitHub. Bug reports and pull requests are welcome.

License

pdoc is licensed under the terms of GNU AGPL-3.0 or later, meaning you can use it for any reasonable purpose and remain in complete ownership of all the documentation you produce, but you are also encouraged to make sure any upgrades to pdoc itself find their way back to the community.


  1. Documented modules are executed in order to provide __doc__ attributes. Any non-fenced global code in imported modules will affect the current runtime environment

  2. Here, public API refers to the API that is made available to your project end-users, not the public API e.g. of a private class that can be reasonably extended elsewhere by your project developers. 

  3. Prefixing private, implementation-specific objects with an underscore is a common convention

Expand source code Browse git
"""
Python package `pdoc` provides types, functions, and a command-line
interface for accessing public documentation of Python modules, and
for presenting it in a user-friendly, industry-standard open format.
It is best suited for small- to medium-sized projects with tidy,
hierarchical APIs.

.. include:: ./documentation.md
"""
import ast
import enum
import importlib.machinery
import importlib.util
import inspect
import os
import os.path as path
import re
import sys
import typing
from contextlib import contextmanager
from copy import copy
from functools import lru_cache, reduce, partial, wraps
from itertools import tee, groupby
from types import ModuleType
from typing import (  # noqa: F401
    cast, Any, Callable, Dict, Generator, Iterable, List, Mapping, NewType,
    Optional, Set, Tuple, Type, TypeVar, Union,
)
from warnings import warn

from mako.lookup import TemplateLookup
from mako.exceptions import TopLevelLookupException
from mako.template import Template

try:
    from pdoc._version import version as __version__  # noqa: F401
except ImportError:
    __version__ = '???'  # Package not installed


_get_type_hints = lru_cache()(typing.get_type_hints)

_URL_MODULE_SUFFIX = '.html'
_URL_INDEX_MODULE_SUFFIX = '.m.html'  # For modules named literal 'index'
_URL_PACKAGE_SUFFIX = '/index.html'

# type.__module__ can be None by the Python spec. In those cases, use this value
_UNKNOWN_MODULE = '?'

T = TypeVar('T', 'Module', 'Class', 'Function', 'Variable')

__pdoc__: Dict[str, Union[bool, str]] = {}

tpl_lookup = TemplateLookup(
    cache_args=dict(cached=True,
                    cache_type='memory'),
    input_encoding='utf-8',
    directories=[path.join(path.dirname(__file__), "templates")],
)
"""
A `mako.lookup.TemplateLookup` object that knows how to load templates
from the file system. You may add additional paths by modifying the
object's `directories` attribute.
"""
if os.getenv("XDG_CONFIG_HOME"):
    tpl_lookup.directories.insert(0, path.join(os.getenv("XDG_CONFIG_HOME", ''), "pdoc"))


class Context(dict):
    """
    The context object that maps all documented identifiers
    (`pdoc.Doc.refname`) to their respective `pdoc.Doc` objects.

    You can pass an instance of `pdoc.Context` to `pdoc.Module` constructor.
    All `pdoc.Module` objects that share the same `pdoc.Context` will see
    (and be able to link in HTML to) each other's identifiers.

    If you don't pass your own `Context` instance to `Module` constructor,
    a global context object will be used.
    """
    __pdoc__['Context.__init__'] = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A surrogate so that the check in Module._link_inheritance()
        # "__pdoc__-overriden key {!r} does not exist" can see the object
        # (and not warn).
        self.blacklisted = getattr(args[0], 'blacklisted', set()) if args else set()


_global_context = Context()


def reset():
    """Resets the global `pdoc.Context` to the initial (empty) state."""
    global _global_context
    _global_context.clear()

    # Clear LRU caches
    for func in (_get_type_hints,
                 _is_blacklisted,
                 _is_whitelisted):
        func.cache_clear()
    for cls in (Doc, Module, Class, Function, Variable, External):
        for _, method in inspect.getmembers(cls):
            if isinstance(method, property):
                method = method.fget
            if hasattr(method, 'cache_clear'):
                method.cache_clear()


def _get_config(**kwargs):
    # Apply config.mako configuration
    MAKO_INTERNALS = Template('').module.__dict__.keys()
    DEFAULT_CONFIG = path.join(path.dirname(__file__), 'templates', 'config.mako')
    config = {}
    for config_module in (Template(filename=DEFAULT_CONFIG).module,
                          tpl_lookup.get_template('/config.mako').module):
        config.update((var, getattr(config_module, var, None))
                      for var in config_module.__dict__
                      if var not in MAKO_INTERNALS)

    known_keys = (set(config)
                  | {'docformat'}  # Feature. https://github.com/pdoc3/pdoc/issues/169
                  # deprecated
                  | {'module', 'modules', 'http_server', 'external_links', 'search_query'})
    invalid_keys = {k: v for k, v in kwargs.items() if k not in known_keys}
    if invalid_keys:
        warn(f'Unknown configuration variables (not in config.mako): {invalid_keys}')
    config.update(kwargs)

    if 'search_query' in config:
        warn('Option `search_query` has been deprecated. Use `google_search_query` instead',
             DeprecationWarning, stacklevel=2)
        config['google_search_query'] = config['search_query']
        del config['search_query']

    return config


def _render_template(template_name, **kwargs):
    """
    Returns the Mako template with the given name.  If the template
    cannot be found, a nicer error message is displayed.
    """
    config = _get_config(**kwargs)

    try:
        t = tpl_lookup.get_template(template_name)
    except TopLevelLookupException:
        paths = [path.join(p, template_name.lstrip('/')) for p in tpl_lookup.directories]
        raise OSError(f"No template found at any of: {', '.join(paths)}")

    try:
        return t.render(**config).strip()
    except Exception:
        from mako import exceptions
        print(exceptions.text_error_template().render(),
              file=sys.stderr)
        raise


def html(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) -> str:
    """
    Returns the documentation for the module `module_name` in HTML
    format. The module must be a module or an importable string.

    `docfilter` is an optional predicate that controls which
    documentation objects are shown in the output. It is a function
    that takes a single argument (a documentation object) and returns
    `True` or `False`. If `False`, that object will not be documented.
    """
    mod = Module(import_module(module_name, reload=reload),
                 docfilter=docfilter, skip_errors=skip_errors)
    link_inheritance()
    return mod.html(**kwargs)


def text(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) -> str:
    """
    Returns the documentation for the module `module_name` in plain
    text format suitable for viewing on a terminal.
    The module must be a module or an importable string.

    `docfilter` is an optional predicate that controls which
    documentation objects are shown in the output. It is a function
    that takes a single argument (a documentation object) and returns
    `True` or `False`. If `False`, that object will not be documented.
    """
    mod = Module(import_module(module_name, reload=reload),
                 docfilter=docfilter, skip_errors=skip_errors)
    link_inheritance()
    return mod.text(**kwargs)


def import_module(module: Union[str, ModuleType],
                  *, reload: bool = False) -> ModuleType:
    """
    Return module object matching `module` specification (either a python
    module path or a filesystem path to file/directory).
    """
    @contextmanager
    def _module_path(module):
        from os.path import abspath, dirname, isfile, isdir, split
        path = '_pdoc_dummy_nonexistent'
        module_name = inspect.getmodulename(module)
        if isdir(module):
            path, module = split(abspath(module))
        elif isfile(module) and module_name:
            path, module = dirname(abspath(module)), module_name
        try:
            sys.path.insert(0, path)
            yield module
        finally:
            sys.path.remove(path)

    if isinstance(module, Module):
        module = module.obj
    if isinstance(module, str):
        with _module_path(module) as module_path:
            try:
                module = importlib.import_module(module_path)
            except Exception as e:
                raise ImportError(f'Error importing {module!r}: {e.__class__.__name__}: {e}')

    assert inspect.ismodule(module)
    # If this is pdoc itself, return without reloading. Otherwise later
    # `isinstance(..., pdoc.Doc)` calls won't work correctly.
    if reload and not module.__name__.startswith(__name__):
        module = importlib.reload(module)
        # We recursively reload all submodules, in case __all_ is used - cf. issue #264
        for mod_key, mod in list(sys.modules.items()):
            if mod_key.startswith(module.__name__):
                importlib.reload(mod)
    return module


def _pairwise(iterable):
    """s -> (s0,s1), (s1,s2), (s2, s3), ..."""
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)


def _pep224_docstrings(doc_obj: Union['Module', 'Class'], *,
                       _init_tree=None) -> Tuple[Dict[str, str],
                                                 Dict[str, str]]:
    """
    Extracts PEP-224 docstrings and doc-comments (`#: ...`) for variables of `doc_obj`
    (either a `pdoc.Module` or `pdoc.Class`).

    Returns a tuple of two dicts mapping variable names to their docstrings.
    The second dict contains instance variables and is non-empty only in case
    `doc_obj` is a `pdoc.Class` which has `__init__` method.
    """
    # No variables in namespace packages
    if isinstance(doc_obj, Module) and doc_obj.is_namespace:
        return {}, {}

    vars: Dict[str, str] = {}
    instance_vars: Dict[str, str] = {}

    if _init_tree:
        tree = _init_tree
    else:
        try:
            # Maybe raise exceptions with appropriate message
            # before using cleaned doc_obj.source
            _ = inspect.findsource(doc_obj.obj)
            tree = ast.parse(doc_obj.source)  # type: ignore
        except (OSError, TypeError, SyntaxError) as exc:
            # Don't emit a warning for builtins that don't have source available
            is_builtin = getattr(doc_obj.obj, '__module__', None) == 'builtins'
            if not is_builtin:
                warn(f"Couldn't read PEP-224 variable docstrings from {doc_obj!r}: {exc}",
                     stacklevel=3 + int(isinstance(doc_obj, Class)))
            return {}, {}

        if isinstance(doc_obj, Class):
            tree = tree.body[0]  # ast.parse creates a dummy ast.Module wrapper

            # For classes, maybe add instance variables defined in __init__
            # Get the *last* __init__ node in case it is preceded by @overloads.
            for node in reversed(tree.body):
                if isinstance(node, ast.FunctionDef) and node.name == '__init__':
                    instance_vars, _ = _pep224_docstrings(doc_obj, _init_tree=node)
                    break

    def get_name(assign_node):
        if isinstance(assign_node, ast.Assign) and len(assign_node.targets) == 1:
            target = assign_node.targets[0]
        elif isinstance(assign_node, ast.AnnAssign):
            target = assign_node.target
            # Skip the annotation. PEP 526 says:
            # > Putting the instance variable annotations together in the class
            # > makes it easier to find them, and helps a first-time reader of the code.
        else:
            return None

        if not _init_tree and isinstance(target, ast.Name):
            name = target.id
        elif (_init_tree and
              isinstance(target, ast.Attribute) and
              isinstance(target.value, ast.Name) and
              target.value.id == 'self'):
            name = target.attr
        else:
            return None

        if not _is_public(name) and not _is_whitelisted(name, doc_obj):
            return None

        return name

    # For handling PEP-224 docstrings for variables
    for assign_node, str_node in _pairwise(ast.iter_child_nodes(tree)):
        if not (isinstance(assign_node, (ast.Assign, ast.AnnAssign)) and
                isinstance(str_node, ast.Expr) and
                isinstance(str_node.value, ast.Str)):
            continue

        name = get_name(assign_node)
        if not name:
            continue

        docstring = inspect.cleandoc(str_node.value.s).strip()
        if not docstring:
            continue

        vars[name] = docstring

    # For handling '#:' docstrings for variables
    for assign_node in ast.iter_child_nodes(tree):
        if not isinstance(assign_node, (ast.Assign, ast.AnnAssign)):
            continue

        name = get_name(assign_node)
        if not name:
            continue

        # Already documented. PEP-224 method above takes precedence.
        if name in vars:
            continue

        def get_indent(line):
            return len(line) - len(line.lstrip())

        source_lines = doc_obj.source.splitlines()  # type: ignore
        assign_line = source_lines[assign_node.lineno - 1]
        assign_indent = get_indent(assign_line)
        comment_lines = []
        MARKER = '#: '
        for line in reversed(source_lines[:assign_node.lineno - 1]):
            if get_indent(line) == assign_indent and line.lstrip().startswith(MARKER):
                comment_lines.append(line.split(MARKER, maxsplit=1)[1])
            else:
                break

        # Since we went 'up' need to reverse lines to be in correct order
        comment_lines = comment_lines[::-1]

        # Finally: check for a '#: ' comment at the end of the assignment line itself.
        if MARKER in assign_line:
            comment_lines.append(assign_line.rsplit(MARKER, maxsplit=1)[1])

        if comment_lines:
            vars[name] = '\n'.join(comment_lines)

    return vars, instance_vars


@lru_cache()
def _is_whitelisted(name: str, doc_obj: Union['Module', 'Class']):
    """
    Returns `True` if `name` (relative or absolute refname) is
    contained in some module's __pdoc__ with a truish value.
    """
    refname = f'{doc_obj.refname}.{name}'
    module: Optional[Module] = doc_obj.module
    while module:
        qualname = refname[len(module.refname) + 1:]
        if module.__pdoc__.get(qualname) or module.__pdoc__.get(refname):
            return True
        module = module.supermodule
    return False


@lru_cache()
def _is_blacklisted(name: str, doc_obj: Union['Module', 'Class']):
    """
    Returns `True` if `name` (relative or absolute refname) is
    contained in some module's __pdoc__ with value False.
    """
    refname = f'{doc_obj.refname}.{name}'
    module: Optional[Module] = doc_obj.module
    while module:
        qualname = refname[len(module.refname) + 1:]
        if module.__pdoc__.get(qualname) is False or module.__pdoc__.get(refname) is False:
            return True
        module = module.supermodule
    return False


def _is_public(ident_name):
    """
    Returns `True` if `ident_name` matches the export criteria for an
    identifier name.
    """
    return not ident_name.startswith("_")


def _is_function(obj):
    return inspect.isroutine(obj) and callable(obj)


def _is_descriptor(obj):
    return (inspect.isdatadescriptor(obj) or
            inspect.ismethoddescriptor(obj) or
            inspect.isgetsetdescriptor(obj) or
            inspect.ismemberdescriptor(obj))


def _filter_type(type: Type[T],
                 values: Union[Iterable['Doc'], Mapping[str, 'Doc']]) -> List[T]:
    """
    Return a list of values from `values` of type `type`.
    """
    if isinstance(values, dict):
        values = values.values()
    return [i for i in values if isinstance(i, type)]


def _toposort(graph: Mapping[T, Set[T]]) -> Generator[T, None, None]:
    """
    Return items of `graph` sorted in topological order.
    Source: https://rosettacode.org/wiki/Topological_sort#Python
    """
    items_without_deps = reduce(set.union, graph.values(), set()) - set(graph.keys())  # type: ignore  # noqa: E501
    yield from items_without_deps
    ordered = items_without_deps
    while True:
        graph = {item: (deps - ordered)
                 for item, deps in graph.items()
                 if item not in ordered}
        ordered = {item
                   for item, deps in graph.items()
                   if not deps}
        yield from ordered
        if not ordered:
            break
    assert not graph, f"A cyclic dependency exists amongst {graph!r}"


def link_inheritance(context: Context = None):
    """
    Link inheritance relationsships between `pdoc.Class` objects
    (and between their members) of all `pdoc.Module` objects that
    share the provided `context` (`pdoc.Context`).

    You need to call this if you expect `pdoc.Doc.inherits` and
    inherited `pdoc.Doc.docstring` to be set correctly.
    """
    if context is None:
        context = _global_context

    graph = {cls: set(cls.mro(only_documented=True))
             for cls in _filter_type(Class, context)}

    for cls in _toposort(graph):
        cls._fill_inheritance()

    for module in _filter_type(Module, context):
        module._link_inheritance()


class Doc:
    """
    A base class for all documentation objects.

    A documentation object corresponds to *something* in a Python module
    that has a docstring associated with it. Typically, this includes
    modules, classes, functions, and methods. However, `pdoc` adds support
    for extracting some docstrings from abstract syntax trees, making
    (module, class or instance) variables supported too.

    A special type of documentation object `pdoc.External` is used to
    represent identifiers that are not part of the public interface of
    a module. (The name "External" is a bit of a misnomer, since it can
    also correspond to unexported members of the module, particularly in
    a class's ancestor list.)
    """
    __slots__ = ('module', 'name', 'obj', 'docstring', 'inherits')

    def __init__(self, name: str, module, obj, docstring: str = None):
        """
        Initializes a documentation object, where `name` is the public
        identifier name, `module` is a `pdoc.Module` object where raw
        Python object `obj` is defined, and `docstring` is its
        documentation string. If `docstring` is left empty, it will be
        read with `inspect.getdoc()`.
        """
        self.module = module
        """
        The module documentation object that this object is defined in.
        """

        self.name = name
        """
        The identifier name for this object.
        """

        self.obj = obj
        """
        The raw python object.
        """

        docstring = (docstring or inspect.getdoc(obj) or '').strip()
        if '.. include::' in docstring:
            from pdoc.html_helpers import _ToMarkdown
            docstring = _ToMarkdown.admonitions(docstring, self.module, ('include',))
        self.docstring = docstring
        """
        The cleaned docstring for this object with any `.. include::`
        directives resolved (i.e. content included).
        """

        self.inherits: Optional[Union[Class, Function, Variable]] = None
        """
        The Doc object (Class, Function, or Variable) this object inherits from,
        if any.
        """

    def __repr__(self):
        return f'<{self.__class__.__name__} {self.refname!r}>'

    @property  # type: ignore
    @lru_cache()
    def source(self) -> str:
        """
        Cleaned (dedented) source code of the Python object. If not
        available, an empty string.
        """
        try:
            lines, _ = inspect.getsourcelines(self.obj)
        except (ValueError, TypeError, OSError):
            return ''
        return inspect.cleandoc(''.join(['\n'] + lines))

    @property
    def refname(self) -> str:
        """
        Reference name of this documentation
        object, usually its fully qualified path
        (e.g. <code>pdoc.Doc.refname</code>). Every
        documentation object provides this property.
        """
        # Ok for Module and External, the rest need it overriden
        return self.name

    @property
    def qualname(self) -> str:
        """
        Module-relative "qualified" name of this documentation
        object, used for show (e.g. <code>Doc.qualname</code>).
        """
        return getattr(self.obj, '__qualname__', self.name)

    @lru_cache()
    def url(self, relative_to: 'Module' = None, *, link_prefix: str = '',
            top_ancestor: bool = False) -> str:
        """
        Canonical relative URL (including page fragment) for this
        documentation object.

        Specify `relative_to` (a `pdoc.Module` object) to obtain a
        relative URL.

        For usage of `link_prefix` see `pdoc.html()`.

        If `top_ancestor` is `True`, the returned URL instead points to
        the top ancestor in the object's `pdoc.Doc.inherits` chain.
        """
        if top_ancestor:
            self = self._inherits_top()

        if relative_to is None or link_prefix:
            return link_prefix + self._url()

        if self.module.name == relative_to.name:
            return f'#{self.refname}'

        # Otherwise, compute relative path from current module to link target
        url = os.path.relpath(self._url(), relative_to.url()).replace(path.sep, '/')
        # We have one set of '..' too many
        if url.startswith('../'):
            url = url[3:]
        return url

    def _url(self):
        return f'{self.module._url()}#{self.refname}'

    def _inherits_top(self):
        """
        Follow the `pdoc.Doc.inherits` chain and return the top object.
        """
        top = self
        while top.inherits:
            top = top.inherits
        return top

    def __lt__(self, other):
        return self.refname < other.refname


class Module(Doc):
    """
    Representation of a module's documentation.
    """
    __pdoc__["Module.name"] = """
        The name of this module with respect to the context/path in which
        it was imported from. It is always an absolute import path.
        """

    __slots__ = ('supermodule', 'doc', '_context', '_is_inheritance_linked',
                 '_skipped_submodules')

    def __init__(self, module: Union[ModuleType, str], *, docfilter: Callable[[Doc], bool] = None,
                 supermodule: 'Module' = None, context: Context = None,
                 skip_errors: bool = False):
        """
        Creates a `Module` documentation object given the actual
        module Python object.

        `docfilter` is an optional predicate that controls which
        sub-objects are documentated (see also: `pdoc.html()`).

        `supermodule` is the parent `pdoc.Module` this module is
        a submodule of.

        `context` is an instance of `pdoc.Context`. If `None` a
        global context object will be used.

        If `skip_errors` is `True` and an unimportable, erroneous
        submodule is encountered, a warning will be issued instead
        of raising an exception.
        """
        if isinstance(module, str):
            module = import_module(module)

        super().__init__(module.__name__, self, module)
        if self.name.endswith('.__init__') and not self.is_package:
            self.name = self.name[:-len('.__init__')]

        self._context = _global_context if context is None else context
        """
        A lookup table for ALL doc objects of all modules that share this context,
        mainly used in `Module.find_ident()`.
        """
        assert isinstance(self._context, Context), \
            'pdoc.Module(context=) should be a pdoc.Context instance'

        self.supermodule = supermodule
        """
        The parent `pdoc.Module` this module is a submodule of, or `None`.
        """

        self.doc: Dict[str, Union[Module, Class, Function, Variable]] = {}
        """A mapping from identifier name to a documentation object."""

        self._is_inheritance_linked = False
        """Re-entry guard for `pdoc.Module._link_inheritance()`."""

        self._skipped_submodules = set()

        var_docstrings, _ = _pep224_docstrings(self)

        # Populate self.doc with this module's public members
        public_objs = []
        if hasattr(self.obj, '__all__'):
            for name in self.obj.__all__:
                try:
                    obj = getattr(self.obj, name)
                except AttributeError:
                    warn(f"Module {self.module!r} doesn't contain identifier `{name}` "
                         "exported in `__all__`")
                if not _is_blacklisted(name, self):
                    obj = inspect.unwrap(obj)
                public_objs.append((name, obj))
        else:
            def is_from_this_module(obj):
                mod = inspect.getmodule(inspect.unwrap(obj))
                return mod is None or mod.__name__ == self.obj.__name__

            for name, obj in inspect.getmembers(self.obj):
                if ((_is_public(name) or
                     _is_whitelisted(name, self)) and
                        (_is_blacklisted(name, self) or  # skips unwrapping that follows
                         is_from_this_module(obj) or
                         name in var_docstrings)):

                    if _is_blacklisted(name, self):
                        self._context.blacklisted.add(f'{self.refname}.{name}')
                        continue

                    obj = inspect.unwrap(obj)
                    public_objs.append((name, obj))

            index = list(self.obj.__dict__).index
            public_objs.sort(key=lambda i: index(i[0]))

        for name, obj in public_objs:
            if _is_function(obj):
                self.doc[name] = Function(name, self, obj)
            elif inspect.isclass(obj):
                self.doc[name] = Class(name, self, obj)
            elif name in var_docstrings:
                self.doc[name] = Variable(name, self, var_docstrings[name], obj=obj)

        # If the module is a package, scan the directory for submodules
        if self.is_package:

            def iter_modules(paths):
                """
                Custom implementation of `pkgutil.iter_modules()`
                because that one doesn't play well with namespace packages.
                See: https://github.com/pypa/setuptools/issues/83
                """
                from os.path import isdir, join
                for pth in paths:
                    for file in os.listdir(pth):
                        if file.startswith(('.', '__pycache__', '__init__.py')):
                            continue
                        module_name = inspect.getmodulename(file)
                        if module_name:
                            yield module_name
                        if isdir(join(pth, file)) and '.' not in file:
                            yield file

            for root in iter_modules(self.obj.__path__):
                # Ignore if this module was already doc'd.
                if root in self.doc:
                    continue

                # Ignore if it isn't exported
                if not _is_public(root) and not _is_whitelisted(root, self):
                    continue
                if _is_blacklisted(root, self):
                    self._skipped_submodules.add(root)
                    continue

                assert self.refname == self.name
                fullname = f"{self.name}.{root}"
                try:
                    m = Module(import_module(fullname),
                               docfilter=docfilter, supermodule=self,
                               context=self._context, skip_errors=skip_errors)
                except Exception as ex:
                    if skip_errors:
                        warn(str(ex), Module.ImportWarning)
                        continue
                    raise

                self.doc[root] = m
                # Skip empty namespace packages because they may
                # as well be other auxiliary directories
                if m.is_namespace and not m.doc:
                    del self.doc[root]
                    self._context.pop(m.refname)

        # Apply docfilter
        if docfilter:
            for name, dobj in self.doc.copy().items():
                if not docfilter(dobj):
                    self.doc.pop(name)
                    self._context.pop(dobj.refname, None)

        # Build the reference name dictionary of the module
        self._context[self.refname] = self
        for docobj in self.doc.values():
            self._context[docobj.refname] = docobj
            if isinstance(docobj, Class):
                self._context.update((obj.refname, obj)
                                     for obj in docobj.doc.values())

    class ImportWarning(UserWarning):
        """
        Our custom import warning because the builtin is ignored by default.
        https://docs.python.org/3/library/warnings.html#default-warning-filter
        """

    __pdoc__['Module.ImportWarning'] = False

    @property
    def __pdoc__(self) -> dict:
        """This module's __pdoc__ dict, or an empty dict if none."""
        return getattr(self.obj, '__pdoc__', {})

    def _link_inheritance(self):
        # Inherited members are already in place since
        # `Class._fill_inheritance()` has been called from
        # `pdoc.fill_inheritance()`.
        # Now look for docstrings in the module's __pdoc__ override.

        if self._is_inheritance_linked:
            # Prevent re-linking inheritance for modules which have already
            # had done so. Otherwise, this would raise "does not exist"
            # errors if `pdoc.link_inheritance()` is called multiple times.
            return

        # Apply __pdoc__ overrides
        for name, docstring in self.__pdoc__.items():
            # In case of whitelisting with "True", there's nothing to do
            if docstring is True:
                continue

            refname = f"{self.refname}.{name}"
            if docstring in (False, None):
                if docstring is None:
                    warn('Setting `__pdoc__[key] = None` is deprecated; '
                         'use `__pdoc__[key] = False` '
                         f'(key: {name!r}, module: {self.name!r}).')

                if name in self._skipped_submodules:
                    continue

                if (not name.endswith('.__init__') and
                        name not in self.doc and
                        refname not in self._context and
                        refname not in self._context.blacklisted):
                    warn(f'__pdoc__-overriden key {name!r} does not exist '
                         f'in module {self.name!r}')

                obj = self.find_ident(name)
                cls = getattr(obj, 'cls', None)
                if cls:
                    del cls.doc[obj.name]
                self.doc.pop(name, None)
                self._context.pop(refname, None)

                # Pop also all that startwith refname
                for key in list(self._context.keys()):
                    if key.startswith(refname + '.'):
                        del self._context[key]

                continue

            dobj = self.find_ident(refname)
            if isinstance(dobj, External):
                continue
            if not isinstance(docstring, str):
                raise ValueError('__pdoc__ dict values must be strings; '
                                 f'__pdoc__[{name!r}] is of type {type(docstring)}')
            dobj.docstring = inspect.cleandoc(docstring)

        # Now after docstrings are set correctly, continue the
        # inheritance routine, marking members inherited or not
        for c in _filter_type(Class, self.doc):
            c._link_inheritance()

        self._is_inheritance_linked = True

    def text(self, **kwargs) -> str:
        """
        Returns the documentation for this module as plain text.
        """
        txt = _render_template('/text.mako', module=self, **kwargs)
        return re.sub("\n\n\n+", "\n\n", txt)

    def html(self, minify=True, **kwargs) -> str:
        """
        Returns the documentation for this module as
        self-contained HTML.

        If `minify` is `True`, the resulting HTML is minified.

        For explanation of other arguments, see `pdoc.html()`.

        `kwargs` is passed to the `mako` render function.
        """
        html = _render_template('/html.mako', module=self, **kwargs)
        if minify:
            from pdoc.html_helpers import minify_html
            html = minify_html(html)
        return html

    @property
    def is_package(self) -> bool:
        """
        `True` if this module is a package.

        Works by checking whether the module has a `__path__` attribute.
        """
        return hasattr(self.obj, "__path__")

    @property
    def is_namespace(self) -> bool:
        """
        `True` if this module is a namespace package.
        """
        try:
            return self.obj.__spec__.origin in (None, 'namespace')  # None in Py3.7+
        except AttributeError:
            return False

    def find_class(self, cls: type) -> Doc:
        """
        Given a Python `cls` object, try to find it in this module
        or in any of the exported identifiers of the submodules.
        """
        # XXX: Is this corrent? Does it always match
        # `Class.module.name + Class.qualname`?. Especially now?
        # If not, see what was here before.
        return self.find_ident(f'{cls.__module__ or _UNKNOWN_MODULE}.{cls.__qualname__}')

    def find_ident(self, name: str) -> Doc:
        """
        Searches this module and **all** other public modules
        for an identifier with name `name` in its list of
        exported identifiers.

        The documentation object corresponding to the identifier is
        returned. If one cannot be found, then an instance of
        `External` is returned populated with the given identifier.
        """
        _name = name.rstrip('()')  # Function specified with parentheses

        if _name.endswith('.__init__'):  # Ref to class' init is ref to class itself
            _name = _name[:-len('.__init__')]

        return (self.doc.get(_name) or
                self._context.get(_name) or
                self._context.get(f'{self.name}.{_name}') or
                External(name))

    def _filter_doc_objs(self, type: Type[T], sort=True) -> List[T]:
        result = _filter_type(type, self.doc)
        return sorted(result) if sort else result

    def variables(self, sort=True) -> List['Variable']:
        """
        Returns all documented module-level variables in the module,
        optionally sorted alphabetically, as a list of `pdoc.Variable`.
        """
        return self._filter_doc_objs(Variable, sort)

    def classes(self, sort=True) -> List['Class']:
        """
        Returns all documented module-level classes in the module,
        optionally sorted alphabetically, as a list of `pdoc.Class`.
        """
        return self._filter_doc_objs(Class, sort)

    def functions(self, sort=True) -> List['Function']:
        """
        Returns all documented module-level functions in the module,
        optionally sorted alphabetically, as a list of `pdoc.Function`.
        """
        return self._filter_doc_objs(Function, sort)

    def submodules(self) -> List['Module']:
        """
        Returns all documented sub-modules of the module sorted
        alphabetically as a list of `pdoc.Module`.
        """
        return self._filter_doc_objs(Module)

    def _url(self):
        url = self.module.name.replace('.', '/')
        if self.is_package:
            return url + _URL_PACKAGE_SUFFIX
        elif url.endswith('/index'):
            return url + _URL_INDEX_MODULE_SUFFIX
        return url + _URL_MODULE_SUFFIX


def _getmembers_all(obj: type) -> List[Tuple[str, Any]]:
    # The following code based on inspect.getmembers() @ 5b23f7618d43
    mro = obj.__mro__[:-1]  # Skip object
    names = set(dir(obj))
    # Add keys from bases
    for base in mro:
        names.update(base.__dict__.keys())
        # Add members for which type annotations exist
        names.update(getattr(obj, '__annotations__', {}).keys())

    results = []
    for name in names:
        try:
            value = getattr(obj, name)
        except AttributeError:
            for base in mro:
                if name in base.__dict__:
                    value = base.__dict__[name]
                    break
            else:
                # Missing slot member or a buggy __dir__;
                # In out case likely a type-annotated member
                # which we'll interpret as a variable
                value = None
        results.append((name, value))
    return results


class Class(Doc):
    """
    Representation of a class' documentation.
    """
    __slots__ = ('doc', '_super_members')

    def __init__(self, name: str, module: Module, obj, *, docstring: str = None):
        assert inspect.isclass(obj)

        if docstring is None:
            init_doc = inspect.getdoc(obj.__init__) or ''
            if init_doc == object.__init__.__doc__:
                init_doc = ''
            docstring = f'{inspect.getdoc(obj) or ""}\n\n{init_doc}'.strip()

        super().__init__(name, module, obj, docstring=docstring)

        self.doc: Dict[str, Union[Function, Variable]] = {}
        """A mapping from identifier name to a `pdoc.Doc` objects."""

        # Annotations for filtering.
        # Use only own, non-inherited annotations (the rest will be inherited)
        annotations = getattr(self.obj, '__annotations__', {})

        public_objs = []
        for _name, obj in _getmembers_all(self.obj):
            # Filter only *own* members. The rest are inherited
            # in Class._fill_inheritance()
            if ((_name in self.obj.__dict__ or
                 _name in annotations) and
                    (_is_public(_name) or
                     _is_whitelisted(_name, self))):

                if _is_blacklisted(_name, self):
                    self.module._context.blacklisted.add(f'{self.refname}.{_name}')
                    continue

                obj = inspect.unwrap(obj)
                public_objs.append((_name, obj))

        def definition_order_index(
                name,
                _annot_index=list(annotations).index,
                _dict_index=list(self.obj.__dict__).index):
            try:
                return _dict_index(name)
            except ValueError:
                pass
            try:
                return _annot_index(name) - len(annotations)  # sort annotated first
            except ValueError:
                return 9e9

        public_objs.sort(key=lambda i: definition_order_index(i[0]))

        var_docstrings, instance_var_docstrings = _pep224_docstrings(self)

        # Convert the public Python objects to documentation objects.
        for name, obj in public_objs:
            if _is_function(obj):
                self.doc[name] = Function(
                    name, self.module, obj, cls=self)
            else:
                self.doc[name] = Variable(
                    name, self.module,
                    docstring=(
                        var_docstrings.get(name) or
                        (inspect.isclass(obj) or _is_descriptor(obj)) and inspect.getdoc(obj)),
                    cls=self,
                    obj=getattr(obj, 'fget', getattr(obj, '__get__', None)),
                    instance_var=(_is_descriptor(obj) or
                                  name in getattr(self.obj, '__slots__', ())))

        for name, docstring in instance_var_docstrings.items():
            self.doc[name] = Variable(
                name, self.module, docstring, cls=self,
                obj=getattr(self.obj, name, None),
                instance_var=True)

    @staticmethod
    def _method_type(cls: type, name: str):
        """
        Returns `None` if the method `name` of class `cls`
        is a regular method. Otherwise, it returns
        `classmethod` or `staticmethod`, as appropriate.
        """
        func = getattr(cls, name, None)
        if inspect.ismethod(func):
            # If the function is already bound, it's a classmethod.
            # Regular methods are not bound before initialization.
            return classmethod
        for c in inspect.getmro(cls):
            if name in c.__dict__:
                if isinstance(c.__dict__[name], staticmethod):
                    return staticmethod
                return None
        raise RuntimeError(f"{cls}.{name} not found")

    @property
    def refname(self) -> str:
        return f'{self.module.name}.{self.qualname}'

    def mro(self, only_documented=False) -> List['Class']:
        """
        Returns a list of ancestor (superclass) documentation objects
        in method resolution order.

        The list will contain objects of type `pdoc.Class`
        if the types are documented, and `pdoc.External` otherwise.
        """
        classes = [cast(Class, self.module.find_class(c))
                   for c in inspect.getmro(self.obj)
                   if c not in (self.obj, object)]
        if self in classes:
            # This can contain self in case of a class inheriting from
            # a class with (previously) the same name. E.g.
            #
            #     class Loc(namedtuple('Loc', 'lat lon')): ...
            #
            # We remove it from ancestors so that toposort doesn't break.
            classes.remove(self)
        if only_documented:
            classes = _filter_type(Class, classes)
        return classes

    def subclasses(self) -> List['Class']:
        """
        Returns a list of subclasses of this class that are visible to the
        Python interpreter (obtained from `type.__subclasses__()`).

        The objects in the list are of type `pdoc.Class` if available,
        and `pdoc.External` otherwise.
        """
        return sorted(cast(Class, self.module.find_class(c))
                      for c in type.__subclasses__(self.obj))

    def params(self, *, annotate=False, link=None) -> List[str]:
        """
        Return a list of formatted parameters accepted by the
        class constructor (method `__init__`). See `pdoc.Function.params`.
        """
        name = self.name + '.__init__'
        qualname = self.qualname + '.__init__'
        refname = self.refname + '.__init__'
        exclusions = self.module.__pdoc__
        if name in exclusions or qualname in exclusions or refname in exclusions:
            return []

        return Function._params(self, annotate=annotate, link=link, module=self.module)

    def _filter_doc_objs(self, type: Type[T], include_inherited=True,
                         filter_func: Callable[[T], bool] = lambda x: True,
                         sort=True) -> List[T]:
        result = [obj for obj in _filter_type(type, self.doc)
                  if (include_inherited or not obj.inherits) and filter_func(obj)]
        return sorted(result) if sort else result

    def class_variables(self, include_inherited=True, sort=True) -> List['Variable']:
        """
        Returns an optionally-sorted list of `pdoc.Variable` objects that
        represent this class' class variables.
        """
        return self._filter_doc_objs(
            Variable, include_inherited, lambda dobj: not dobj.instance_var,
            sort)

    def instance_variables(self, include_inherited=True, sort=True) -> List['Variable']:
        """
        Returns an optionally-sorted list of `pdoc.Variable` objects that
        represent this class' instance variables. Instance variables
        are those defined in a class's `__init__` as `self.variable = ...`.
        """
        return self._filter_doc_objs(
            Variable, include_inherited, lambda dobj: dobj.instance_var,
            sort)

    def methods(self, include_inherited=True, sort=True) -> List['Function']:
        """
        Returns an optionally-sorted list of `pdoc.Function` objects that
        represent this class' methods.
        """
        return self._filter_doc_objs(
            Function, include_inherited, lambda dobj: dobj.is_method,
            sort)

    def functions(self, include_inherited=True, sort=True) -> List['Function']:
        """
        Returns an optionally-sorted list of `pdoc.Function` objects that
        represent this class' static functions.
        """
        return self._filter_doc_objs(
            Function, include_inherited, lambda dobj: not dobj.is_method,
            sort)

    def inherited_members(self) -> List[Tuple['Class', List[Doc]]]:
        """
        Returns all inherited members as a list of tuples
        (ancestor class, list of ancestor class' members sorted by name),
        sorted by MRO.
        """
        return sorted(((cast(Class, k), sorted(g))
                       for k, g in groupby((i.inherits
                                            for i in self.doc.values() if i.inherits),
                                           key=lambda i: i.cls)),                   # type: ignore
                      key=lambda x, _mro_index=self.mro().index: _mro_index(x[0]))  # type: ignore

    def _fill_inheritance(self):
        """
        Traverses this class's ancestor list and attempts to fill in
        missing documentation objects from its ancestors.

        Afterwards, call to `pdoc.Class._link_inheritance()` to also
        set `pdoc.Doc.inherits` pointers.
        """
        super_members = self._super_members = {}
        for cls in self.mro(only_documented=True):
            for name, dobj in cls.doc.items():
                if name not in super_members and dobj.docstring:
                    super_members[name] = dobj
                    if name not in self.doc:
                        dobj = copy(dobj)
                        dobj.cls = self

                        self.doc[name] = dobj
                        self.module._context[dobj.refname] = dobj

    def _link_inheritance(self):
        """
        Set `pdoc.Doc.inherits` pointers to inherited ancestors' members,
        as appropriate. This must be called after
        `pdoc.Class._fill_inheritance()`.

        The reason this is split in two parts is that in-between
        the `__pdoc__` overrides are applied.
        """
        if not hasattr(self, '_super_members'):
            return

        for name, parent_dobj in self._super_members.items():
            try:
                dobj = self.doc[name]
            except KeyError:
                # There is a key in some __pdoc__ dict blocking this member
                continue
            if (dobj.obj is parent_dobj.obj or
                    (dobj.docstring or parent_dobj.docstring) == parent_dobj.docstring):
                dobj.inherits = parent_dobj
                dobj.docstring = parent_dobj.docstring
        del self._super_members


def maybe_lru_cache(func):
    cached_func = lru_cache()(func)

    @wraps(func)
    def wrapper(*args):
        try:
            return cached_func(*args)
        except TypeError:
            return func(*args)

    return wrapper


@maybe_lru_cache
def _formatannotation(annot):
    """
    Format typing annotation with better handling of `typing.NewType`,
    `typing.Optional`, `nptyping.NDArray` and other types.

    >>> _formatannotation(NewType('MyType', str))
    'MyType'
    >>> _formatannotation(Optional[Tuple[Optional[int], None]])
    'Optional[Tuple[Optional[int], None]]'
    """
    class force_repr(str):
        __repr__ = str.__str__

    def maybe_replace_reprs(a):
        # NoneType -> None
        if a is type(None):  # noqa: E721
            return force_repr('None')
        # Union[T, None] -> Optional[T]
        if (getattr(a, '__origin__', None) is typing.Union and
                len(a.__args__) == 2 and
                type(None) in a.__args__):
            t = inspect.formatannotation(
                maybe_replace_reprs(next(filter(None, a.__args__))))
            return force_repr(f'Optional[{t}]')
        # typing.NewType('T', foo) -> T
        module = getattr(a, '__module__', '')
        if module == 'typing' and getattr(a, '__qualname__', '').startswith('NewType.'):
            return force_repr(a.__name__)
        # nptyping.types._ndarray.NDArray -> NDArray[(Any,), Int[64]]  # GH-231
        if module.startswith('nptyping.'):
            return force_repr(repr(a))
        # Recurse into typing.Callable/etc. args
        if hasattr(a, 'copy_with') and hasattr(a, '__args__'):
            if a is typing.Callable:
                # Bug on Python < 3.9, https://bugs.python.org/issue42195
                return a
            a = a.copy_with(tuple([maybe_replace_reprs(arg) for arg in a.__args__]))
        return a

    return str(inspect.formatannotation(maybe_replace_reprs(annot)))


class Function(Doc):
    """
    Representation of documentation for a function or method.
    """
    __slots__ = ('cls',)

    def __init__(self, name: str, module: Module, obj, *, cls: Class = None):
        """
        Same as `pdoc.Doc`, except `obj` must be a
        Python function object. The docstring is gathered automatically.

        `cls` should be set when this is a method or a static function
        beloing to a class. `cls` should be a `pdoc.Class` object.

        `method` should be `True` when the function is a method. In
        all other cases, it should be `False`.
        """
        assert callable(obj), (name, module, obj)
        super().__init__(name, module, obj)

        self.cls = cls
        """
        The `pdoc.Class` documentation object if the function is a method.
        If not, this is None.
        """

    @property
    def is_method(self) -> bool:
        """
        Whether this function is a normal bound method.

        In particular, static and class methods have this set to False.
        """
        assert self.cls
        return not Class._method_type(self.cls.obj, self.name)

    @property
    def method(self):
        warn('`Function.method` is deprecated. Use: `Function.is_method`', DeprecationWarning,
             stacklevel=2)
        return self.is_method

    __pdoc__['Function.method'] = False

    def funcdef(self) -> str:
        """
        Generates the string of keywords used to define the function,
        for example `def` or `async def`.
        """
        return 'async def' if self._is_async else 'def'

    @property
    def _is_async(self):
        """
        Returns whether is function is asynchronous, either as a coroutine or an async
        generator.
        """
        try:
            # Both of these are required because coroutines aren't classified as async
            # generators and vice versa.
            obj = inspect.unwrap(self.obj)
            return (inspect.iscoroutinefunction(obj) or
                    inspect.isasyncgenfunction(obj))
        except AttributeError:
            return False

    def return_annotation(self, *, link=None) -> str:
        """Formatted function return type annotation or empty string if none."""
        annot = ''
        for method in (
                lambda: _get_type_hints(self.obj)['return'],
                # Mainly for non-property variables
                lambda: _get_type_hints(cast(Class, self.cls).obj)[self.name],
                # global variables
                lambda: _get_type_hints(not self.cls and self.module.obj)[self.name],
                lambda: inspect.signature(self.obj).return_annotation,
                # Use raw annotation strings in unmatched forward declarations
                lambda: cast(Class, self.cls).obj.__annotations__[self.name],
                # Extract annotation from the docstring for C builtin function
                lambda: Function._signature_from_string(self).return_annotation,
        ):
            try:
                annot = method()
            except Exception:
                continue
            else:
                break
        else:
            # Don't warn on variables. The annotation just isn't available.
            if not isinstance(self, Variable):
                warn(f"Error handling return annotation for {self!r}", stacklevel=3)

        if annot is inspect.Parameter.empty or not annot:
            return ''

        if isinstance(annot, str):
            s = annot
        else:
            s = _formatannotation(annot)
            s = re.sub(r'\bForwardRef\((?P<quot>[\"\'])(?P<str>.*?)(?P=quot)\)',
                       r'\g<str>', s)
        s = s.replace(' ', '\N{NBSP}')  # Better line breaks in html signatures

        if link:
            from pdoc.html_helpers import _linkify
            s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=self.module), s)
        return s

    def params(self, *, annotate: bool = False, link: Callable[[Doc], str] = None) -> List[str]:
        """
        Returns a list where each element is a nicely formatted
        parameter of this function. This includes argument lists,
        keyword arguments and default values, and it doesn't include any
        optional arguments whose names begin with an underscore.

        If `annotate` is True, the parameter strings include [PEP 484]
        type hint annotations.

        [PEP 484]: https://www.python.org/dev/peps/pep-0484/
        """
        return self._params(self, annotate=annotate, link=link, module=self.module)

    @staticmethod
    def _params(doc_obj, annotate=False, link=None, module=None):
        try:
            # We want __init__ to actually be implemented somewhere in the
            # MRO to still satisfy https://github.com/pdoc3/pdoc/issues/124
            if (
                inspect.isclass(doc_obj.obj)
                and doc_obj.obj.__init__ is not object.__init__
            ):
                # Remove the first argument (self) from __init__ signature
                init_sig = inspect.signature(doc_obj.obj.__init__)
                init_params = list(init_sig.parameters.values())
                signature = init_sig.replace(parameters=init_params[1:])
            else:
                signature = inspect.signature(doc_obj.obj)
        except ValueError:
            signature = Function._signature_from_string(doc_obj)
            if not signature:
                return ['...']

        def safe_default_value(p: inspect.Parameter):
            value = p.default
            if value is inspect.Parameter.empty:
                return p

            replacement = next((i for i in ('os.environ',
                                            'sys.stdin',
                                            'sys.stdout',
                                            'sys.stderr',)
                                if value is eval(i)), None)
            if not replacement:
                if isinstance(value, enum.Enum):
                    replacement = str(value)
                elif inspect.isclass(value):
                    replacement = f'{value.__module__ or _UNKNOWN_MODULE}.{value.__qualname__}'
                elif ' at 0x' in repr(value):
                    replacement = re.sub(r' at 0x\w+', '', repr(value))

                nonlocal link
                if link and ('<' in repr(value) or '>' in repr(value)):
                    import html
                    replacement = html.escape(replacement or repr(value))

            if replacement:
                class mock:
                    def __repr__(self):
                        return replacement
                return p.replace(default=mock())
            return p

        params = []
        kw_only = False
        pos_only = False
        EMPTY = inspect.Parameter.empty

        if link:
            from pdoc.html_helpers import _linkify
            _linkify = partial(_linkify, link=link, module=module)

        for p in signature.parameters.values():  # type: inspect.Parameter
            if not _is_public(p.name) and p.default is not EMPTY:
                continue

            if p.kind == p.POSITIONAL_ONLY:
                pos_only = True
            elif pos_only:
                params.append("/")
                pos_only = False

            if p.kind == p.VAR_POSITIONAL:
                kw_only = True
            if p.kind == p.KEYWORD_ONLY and not kw_only:
                kw_only = True
                params.append('*')

            p = safe_default_value(p)

            if not annotate:
                p = p.replace(annotation=EMPTY)

            formatted = p.name
            if p.annotation is not EMPTY:
                annotation = _formatannotation(p.annotation).replace(' ', '\N{NBSP}')
                # "Eval" forward-declarations (typing string literals)
                if isinstance(p.annotation, str):
                    annotation = annotation.strip("'")
                if link:
                    annotation = re.sub(r'[\w\.]+', _linkify, annotation)
                formatted += f':\N{NBSP}{annotation}'
            if p.default is not EMPTY:
                if p.annotation is not EMPTY:
                    formatted += f'\N{NBSP}=\N{NBSP}{repr(p.default)}'
                else:
                    formatted += f'={repr(p.default)}'
            if p.kind == p.VAR_POSITIONAL:
                formatted = f'*{formatted}'
            elif p.kind == p.VAR_KEYWORD:
                formatted = f'**{formatted}'

            params.append(formatted)

        if pos_only:
            params.append("/")

        return params

    @staticmethod
    @lru_cache()
    def _signature_from_string(self):
        signature = None
        for expr, cleanup_docstring, filter in (
                # Full proper typed signature, such as one from pybind11
                (r'^{}\(.*\)(?: -> .*)?$', True, lambda s: s),
                # Human-readable, usage-like signature from some Python builtins
                # (e.g. `range` or `slice` or `itertools.repeat` or `numpy.arange`)
                (r'^{}\(.*\)(?= -|$)', False, lambda s: s.replace('[', '').replace(']', '')),
        ):
            strings = sorted(re.findall(expr.format(self.name),
                                        self.docstring, re.MULTILINE),
                             key=len, reverse=True)
            if strings:
                string = filter(strings[0])
                _locals, _globals = {}, {}
                _globals.update({'capsule': None})  # pybind11 capsule data type
                _globals.update(typing.__dict__)
                _globals.update(self.module.obj.__dict__)
                # Trim binding module basename from type annotations
                # See: https://github.com/pdoc3/pdoc/pull/148#discussion_r407114141
                module_basename = self.module.name.rsplit('.', maxsplit=1)[-1]
                if module_basename in string and module_basename not in _globals:
                    string = re.sub(fr'(?<!\.)\b{module_basename}\.\b', '', string)

                try:
                    exec(f'def {string}: pass', _globals, _locals)
                except SyntaxError:
                    continue
                signature = inspect.signature(_locals[self.name])
                if cleanup_docstring and len(strings) == 1:
                    # Remove signature from docstring variable
                    self.docstring = self.docstring.replace(strings[0], '')
                break
        return signature

    @property
    def refname(self) -> str:
        return f'{self.cls.refname if self.cls else self.module.refname}.{self.name}'


class Variable(Doc):
    """
    Representation of a variable's documentation. This includes
    module, class, and instance variables.
    """
    __slots__ = ('cls', 'instance_var')

    def __init__(self, name: str, module: Module, docstring, *,
                 obj=None, cls: Class = None, instance_var: bool = False):
        """
        Same as `pdoc.Doc`, except `cls` should be provided
        as a `pdoc.Class` object when this is a class or instance
        variable.
        """
        super().__init__(name, module, obj, docstring)

        self.cls = cls
        """
        The `pdoc.Class` object if this is a class or instance
        variable. If not (i.e. it is a global variable), this is None.
        """

        self.instance_var = instance_var
        """
        True if variable is some class' instance variable (as
        opposed to class variable).
        """

    @property
    def qualname(self) -> str:
        if self.cls:
            return f'{self.cls.qualname}.{self.name}'
        return self.name

    @property
    def refname(self) -> str:
        return f'{self.cls.refname if self.cls else self.module.refname}.{self.name}'

    def type_annotation(self, *, link=None) -> str:
        """Formatted variable type annotation or empty string if none."""
        return Function.return_annotation(cast(Function, self), link=link)


class External(Doc):
    """
    A representation of an external identifier. The textual
    representation is the same as an internal identifier.

    External identifiers are also used to represent something that is
    not documented but appears somewhere in the public interface (like
    the ancestor list of a class).
    """

    __pdoc__["External.docstring"] = """
        An empty string. External identifiers do not have
        docstrings.
        """
    __pdoc__["External.module"] = """
        Always `None`. External identifiers have no associated
        `pdoc.Module`.
        """
    __pdoc__["External.name"] = """
        Always equivalent to `pdoc.External.refname` since external
        identifiers are always expressed in their fully qualified
        form.
        """

    def __init__(self, name: str):
        """
        Initializes an external identifier with `name`, where `name`
        should be a fully qualified name.
        """
        super().__init__(name, None, None)

    def url(self, *args, **kwargs):
        """
        `External` objects return absolute urls matching `/{name}.ext`.
        """
        return f'/{self.name}.ext'

Sub-modules

pdoc.cli

pdoc's CLI interface and helper functions.

pdoc.html_helpers

Helper functions for HTML output.

pdoc.test

Unit tests for pdoc package.

Global variables

var tpl_lookup

A mako.lookup.TemplateLookup object that knows how to load templates from the file system. You may add additional paths by modifying the object's directories attribute.

Functions

def html(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) ‑> str

Returns the documentation for the module module_name in HTML format. The module must be a module or an importable string.

docfilter is an optional predicate that controls which documentation objects are shown in the output. It is a function that takes a single argument (a documentation object) and returns True or False. If False, that object will not be documented.

Expand source code Browse git
def html(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) -> str:
    """
    Returns the documentation for the module `module_name` in HTML
    format. The module must be a module or an importable string.

    `docfilter` is an optional predicate that controls which
    documentation objects are shown in the output. It is a function
    that takes a single argument (a documentation object) and returns
    `True` or `False`. If `False`, that object will not be documented.
    """
    mod = Module(import_module(module_name, reload=reload),
                 docfilter=docfilter, skip_errors=skip_errors)
    link_inheritance()
    return mod.html(**kwargs)
def import_module(module: Union[str, module], *, reload: bool = False) ‑> module

Return module object matching module specification (either a python module path or a filesystem path to file/directory).

Expand source code Browse git
def import_module(module: Union[str, ModuleType],
                  *, reload: bool = False) -> ModuleType:
    """
    Return module object matching `module` specification (either a python
    module path or a filesystem path to file/directory).
    """
    @contextmanager
    def _module_path(module):
        from os.path import abspath, dirname, isfile, isdir, split
        path = '_pdoc_dummy_nonexistent'
        module_name = inspect.getmodulename(module)
        if isdir(module):
            path, module = split(abspath(module))
        elif isfile(module) and module_name:
            path, module = dirname(abspath(module)), module_name
        try:
            sys.path.insert(0, path)
            yield module
        finally:
            sys.path.remove(path)

    if isinstance(module, Module):
        module = module.obj
    if isinstance(module, str):
        with _module_path(module) as module_path:
            try:
                module = importlib.import_module(module_path)
            except Exception as e:
                raise ImportError(f'Error importing {module!r}: {e.__class__.__name__}: {e}')

    assert inspect.ismodule(module)
    # If this is pdoc itself, return without reloading. Otherwise later
    # `isinstance(..., pdoc.Doc)` calls won't work correctly.
    if reload and not module.__name__.startswith(__name__):
        module = importlib.reload(module)
        # We recursively reload all submodules, in case __all_ is used - cf. issue #264
        for mod_key, mod in list(sys.modules.items()):
            if mod_key.startswith(module.__name__):
                importlib.reload(mod)
    return module

Link inheritance relationsships between Class objects (and between their members) of all Module objects that share the provided context (Context).

You need to call this if you expect Doc.inherits and inherited Doc.docstring to be set correctly.

Expand source code Browse git
def link_inheritance(context: Context = None):
    """
    Link inheritance relationsships between `pdoc.Class` objects
    (and between their members) of all `pdoc.Module` objects that
    share the provided `context` (`pdoc.Context`).

    You need to call this if you expect `pdoc.Doc.inherits` and
    inherited `pdoc.Doc.docstring` to be set correctly.
    """
    if context is None:
        context = _global_context

    graph = {cls: set(cls.mro(only_documented=True))
             for cls in _filter_type(Class, context)}

    for cls in _toposort(graph):
        cls._fill_inheritance()

    for module in _filter_type(Module, context):
        module._link_inheritance()
def maybe_lru_cache(func)
Expand source code Browse git
def maybe_lru_cache(func):
    cached_func = lru_cache()(func)

    @wraps(func)
    def wrapper(*args):
        try:
            return cached_func(*args)
        except TypeError:
            return func(*args)

    return wrapper
def reset()

Resets the global Context to the initial (empty) state.

Expand source code Browse git
def reset():
    """Resets the global `pdoc.Context` to the initial (empty) state."""
    global _global_context
    _global_context.clear()

    # Clear LRU caches
    for func in (_get_type_hints,
                 _is_blacklisted,
                 _is_whitelisted):
        func.cache_clear()
    for cls in (Doc, Module, Class, Function, Variable, External):
        for _, method in inspect.getmembers(cls):
            if isinstance(method, property):
                method = method.fget
            if hasattr(method, 'cache_clear'):
                method.cache_clear()
def text(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) ‑> str

Returns the documentation for the module module_name in plain text format suitable for viewing on a terminal. The module must be a module or an importable string.

docfilter is an optional predicate that controls which documentation objects are shown in the output. It is a function that takes a single argument (a documentation object) and returns True or False. If False, that object will not be documented.

Expand source code Browse git
def text(module_name, docfilter=None, reload=False, skip_errors=False, **kwargs) -> str:
    """
    Returns the documentation for the module `module_name` in plain
    text format suitable for viewing on a terminal.
    The module must be a module or an importable string.

    `docfilter` is an optional predicate that controls which
    documentation objects are shown in the output. It is a function
    that takes a single argument (a documentation object) and returns
    `True` or `False`. If `False`, that object will not be documented.
    """
    mod = Module(import_module(module_name, reload=reload),
                 docfilter=docfilter, skip_errors=skip_errors)
    link_inheritance()
    return mod.text(**kwargs)

Classes

class Class (name: str, module: Module, obj, *, docstring: str = None)

Representation of a class' documentation.

Initializes a documentation object, where name is the public identifier name, module is a Module object where raw Python object obj is defined, and docstring is its documentation string. If docstring is left empty, it will be read with inspect.getdoc().

Expand source code Browse git
class Class(Doc):
    """
    Representation of a class' documentation.
    """
    __slots__ = ('doc', '_super_members')

    def __init__(self, name: str, module: Module, obj, *, docstring: str = None):
        assert inspect.isclass(obj)

        if docstring is None:
            init_doc = inspect.getdoc(obj.__init__) or ''
            if init_doc == object.__init__.__doc__:
                init_doc = ''
            docstring = f'{inspect.getdoc(obj) or ""}\n\n{init_doc}'.strip()

        super().__init__(name, module, obj, docstring=docstring)

        self.doc: Dict[str, Union[Function, Variable]] = {}
        """A mapping from identifier name to a `pdoc.Doc` objects."""

        # Annotations for filtering.
        # Use only own, non-inherited annotations (the rest will be inherited)
        annotations = getattr(self.obj, '__annotations__', {})

        public_objs = []
        for _name, obj in _getmembers_all(self.obj):
            # Filter only *own* members. The rest are inherited
            # in Class._fill_inheritance()
            if ((_name in self.obj.__dict__ or
                 _name in annotations) and
                    (_is_public(_name) or
                     _is_whitelisted(_name, self))):

                if _is_blacklisted(_name, self):
                    self.module._context.blacklisted.add(f'{self.refname}.{_name}')
                    continue

                obj = inspect.unwrap(obj)
                public_objs.append((_name, obj))

        def definition_order_index(
                name,
                _annot_index=list(annotations).index,
                _dict_index=list(self.obj.__dict__).index):
            try:
                return _dict_index(name)
            except ValueError:
                pass
            try:
                return _annot_index(name) - len(annotations)  # sort annotated first
            except ValueError:
                return 9e9

        public_objs.sort(key=lambda i: definition_order_index(i[0]))

        var_docstrings, instance_var_docstrings = _pep224_docstrings(self)

        # Convert the public Python objects to documentation objects.
        for name, obj in public_objs:
            if _is_function(obj):
                self.doc[name] = Function(
                    name, self.module, obj, cls=self)
            else:
                self.doc[name] = Variable(
                    name, self.module,
                    docstring=(
                        var_docstrings.get(name) or
                        (inspect.isclass(obj) or _is_descriptor(obj)) and inspect.getdoc(obj)),
                    cls=self,
                    obj=getattr(obj, 'fget', getattr(obj, '__get__', None)),
                    instance_var=(_is_descriptor(obj) or
                                  name in getattr(self.obj, '__slots__', ())))

        for name, docstring in instance_var_docstrings.items():
            self.doc[name] = Variable(
                name, self.module, docstring, cls=self,
                obj=getattr(self.obj, name, None),
                instance_var=True)

    @staticmethod
    def _method_type(cls: type, name: str):
        """
        Returns `None` if the method `name` of class `cls`
        is a regular method. Otherwise, it returns
        `classmethod` or `staticmethod`, as appropriate.
        """
        func = getattr(cls, name, None)
        if inspect.ismethod(func):
            # If the function is already bound, it's a classmethod.
            # Regular methods are not bound before initialization.
            return classmethod
        for c in inspect.getmro(cls):
            if name in c.__dict__:
                if isinstance(c.__dict__[name], staticmethod):
                    return staticmethod
                return None
        raise RuntimeError(f"{cls}.{name} not found")

    @property
    def refname(self) -> str:
        return f'{self.module.name}.{self.qualname}'

    def mro(self, only_documented=False) -> List['Class']:
        """
        Returns a list of ancestor (superclass) documentation objects
        in method resolution order.

        The list will contain objects of type `pdoc.Class`
        if the types are documented, and `pdoc.External` otherwise.
        """
        classes = [cast(Class, self.module.find_class(c))
                   for c in inspect.getmro(self.obj)
                   if c not in (self.obj, object)]
        if self in classes:
            # This can contain self in case of a class inheriting from
            # a class with (previously) the same name. E.g.
            #
            #     class Loc(namedtuple('Loc', 'lat lon')): ...
            #
            # We remove it from ancestors so that toposort doesn't break.
            classes.remove(self)
        if only_documented:
            classes = _filter_type(Class, classes)
        return classes

    def subclasses(self) -> List['Class']:
        """
        Returns a list of subclasses of this class that are visible to the
        Python interpreter (obtained from `type.__subclasses__()`).

        The objects in the list are of type `pdoc.Class` if available,
        and `pdoc.External` otherwise.
        """
        return sorted(cast(Class, self.module.find_class(c))
                      for c in type.__subclasses__(self.obj))

    def params(self, *, annotate=False, link=None) -> List[str]:
        """
        Return a list of formatted parameters accepted by the
        class constructor (method `__init__`). See `pdoc.Function.params`.
        """
        name = self.name + '.__init__'
        qualname = self.qualname + '.__init__'
        refname = self.refname + '.__init__'
        exclusions = self.module.__pdoc__
        if name in exclusions or qualname in exclusions or refname in exclusions:
            return []

        return Function._params(self, annotate=annotate, link=link, module=self.module)

    def _filter_doc_objs(self, type: Type[T], include_inherited=True,
                         filter_func: Callable[[T], bool] = lambda x: True,
                         sort=True) -> List[T]:
        result = [obj for obj in _filter_type(type, self.doc)
                  if (include_inherited or not obj.inherits) and filter_func(obj)]
        return sorted(result) if sort else result

    def class_variables(self, include_inherited=True, sort=True) -> List['Variable']:
        """
        Returns an optionally-sorted list of `pdoc.Variable` objects that
        represent this class' class variables.
        """
        return self._filter_doc_objs(
            Variable, include_inherited, lambda dobj: not dobj.instance_var,
            sort)

    def instance_variables(self, include_inherited=True, sort=True) -> List['Variable']:
        """
        Returns an optionally-sorted list of `pdoc.Variable` objects that
        represent this class' instance variables. Instance variables
        are those defined in a class's `__init__` as `self.variable = ...`.
        """
        return self._filter_doc_objs(
            Variable, include_inherited, lambda dobj: dobj.instance_var,
            sort)

    def methods(self, include_inherited=True, sort=True) -> List['Function']:
        """
        Returns an optionally-sorted list of `pdoc.Function` objects that
        represent this class' methods.
        """
        return self._filter_doc_objs(
            Function, include_inherited, lambda dobj: dobj.is_method,
            sort)

    def functions(self, include_inherited=True, sort=True) -> List['Function']:
        """
        Returns an optionally-sorted list of `pdoc.Function` objects that
        represent this class' static functions.
        """
        return self._filter_doc_objs(
            Function, include_inherited, lambda dobj: not dobj.is_method,
            sort)

    def inherited_members(self) -> List[Tuple['Class', List[Doc]]]:
        """
        Returns all inherited members as a list of tuples
        (ancestor class, list of ancestor class' members sorted by name),
        sorted by MRO.
        """
        return sorted(((cast(Class, k), sorted(g))
                       for k, g in groupby((i.inherits
                                            for i in self.doc.values() if i.inherits),
                                           key=lambda i: i.cls)),                   # type: ignore
                      key=lambda x, _mro_index=self.mro().index: _mro_index(x[0]))  # type: ignore

    def _fill_inheritance(self):
        """
        Traverses this class's ancestor list and attempts to fill in
        missing documentation objects from its ancestors.

        Afterwards, call to `pdoc.Class._link_inheritance()` to also
        set `pdoc.Doc.inherits` pointers.
        """
        super_members = self._super_members = {}
        for cls in self.mro(only_documented=True):
            for name, dobj in cls.doc.items():
                if name not in super_members and dobj.docstring:
                    super_members[name] = dobj
                    if name not in self.doc:
                        dobj = copy(dobj)
                        dobj.cls = self

                        self.doc[name] = dobj
                        self.module._context[dobj.refname] = dobj

    def _link_inheritance(self):
        """
        Set `pdoc.Doc.inherits` pointers to inherited ancestors' members,
        as appropriate. This must be called after
        `pdoc.Class._fill_inheritance()`.

        The reason this is split in two parts is that in-between
        the `__pdoc__` overrides are applied.
        """
        if not hasattr(self, '_super_members'):
            return

        for name, parent_dobj in self._super_members.items():
            try:
                dobj = self.doc[name]
            except KeyError:
                # There is a key in some __pdoc__ dict blocking this member
                continue
            if (dobj.obj is parent_dobj.obj or
                    (dobj.docstring or parent_dobj.docstring) == parent_dobj.docstring):
                dobj.inherits = parent_dobj
                dobj.docstring = parent_dobj.docstring
        del self._super_members

Ancestors

Instance variables

var doc

A mapping from identifier name to a Doc objects.

Methods

def class_variables(self, include_inherited=True, sort=True) ‑> List[Variable]

Returns an optionally-sorted list of Variable objects that represent this class' class variables.

Expand source code Browse git
def class_variables(self, include_inherited=True, sort=True) -> List['Variable']:
    """
    Returns an optionally-sorted list of `pdoc.Variable` objects that
    represent this class' class variables.
    """
    return self._filter_doc_objs(
        Variable, include_inherited, lambda dobj: not dobj.instance_var,
        sort)
def functions(self, include_inherited=True, sort=True) ‑> List[Function]

Returns an optionally-sorted list of Function objects that represent this class' static functions.

Expand source code Browse git
def functions(self, include_inherited=True, sort=True) -> List['Function']:
    """
    Returns an optionally-sorted list of `pdoc.Function` objects that
    represent this class' static functions.
    """
    return self._filter_doc_objs(
        Function, include_inherited, lambda dobj: not dobj.is_method,
        sort)
def inherited_members(self) ‑> List[Tuple[Class, List[Doc]]]

Returns all inherited members as a list of tuples (ancestor class, list of ancestor class' members sorted by name), sorted by MRO.

Expand source code Browse git
def inherited_members(self) -> List[Tuple['Class', List[Doc]]]:
    """
    Returns all inherited members as a list of tuples
    (ancestor class, list of ancestor class' members sorted by name),
    sorted by MRO.
    """
    return sorted(((cast(Class, k), sorted(g))
                   for k, g in groupby((i.inherits
                                        for i in self.doc.values() if i.inherits),
                                       key=lambda i: i.cls)),                   # type: ignore
                  key=lambda x, _mro_index=self.mro().index: _mro_index(x[0]))  # type: ignore
def instance_variables(self, include_inherited=True, sort=True) ‑> List[Variable]

Returns an optionally-sorted list of Variable objects that represent this class' instance variables. Instance variables are those defined in a class's __init__ as self.variable = ....

Expand source code Browse git
def instance_variables(self, include_inherited=True, sort=True) -> List['Variable']:
    """
    Returns an optionally-sorted list of `pdoc.Variable` objects that
    represent this class' instance variables. Instance variables
    are those defined in a class's `__init__` as `self.variable = ...`.
    """
    return self._filter_doc_objs(
        Variable, include_inherited, lambda dobj: dobj.instance_var,
        sort)
def methods(self, include_inherited=True, sort=True) ‑> List[Function]

Returns an optionally-sorted list of Function objects that represent this class' methods.

Expand source code Browse git
def methods(self, include_inherited=True, sort=True) -> List['Function']:
    """
    Returns an optionally-sorted list of `pdoc.Function` objects that
    represent this class' methods.
    """
    return self._filter_doc_objs(
        Function, include_inherited, lambda dobj: dobj.is_method,
        sort)
def mro(self, only_documented=False) ‑> List[Class]

Returns a list of ancestor (superclass) documentation objects in method resolution order.

The list will contain objects of type Class if the types are documented, and External otherwise.

Expand source code Browse git
def mro(self, only_documented=False) -> List['Class']:
    """
    Returns a list of ancestor (superclass) documentation objects
    in method resolution order.

    The list will contain objects of type `pdoc.Class`
    if the types are documented, and `pdoc.External` otherwise.
    """
    classes = [cast(Class, self.module.find_class(c))
               for c in inspect.getmro(self.obj)
               if c not in (self.obj, object)]
    if self in classes:
        # This can contain self in case of a class inheriting from
        # a class with (previously) the same name. E.g.
        #
        #     class Loc(namedtuple('Loc', 'lat lon')): ...
        #
        # We remove it from ancestors so that toposort doesn't break.
        classes.remove(self)
    if only_documented:
        classes = _filter_type(Class, classes)
    return classes
def params(self, *, annotate=False, link=None) ‑> List[str]

Return a list of formatted parameters accepted by the class constructor (method __init__). See Function.params().

Expand source code Browse git
def params(self, *, annotate=False, link=None) -> List[str]:
    """
    Return a list of formatted parameters accepted by the
    class constructor (method `__init__`). See `pdoc.Function.params`.
    """
    name = self.name + '.__init__'
    qualname = self.qualname + '.__init__'
    refname = self.refname + '.__init__'
    exclusions = self.module.__pdoc__
    if name in exclusions or qualname in exclusions or refname in exclusions:
        return []

    return Function._params(self, annotate=annotate, link=link, module=self.module)
def subclasses(self) ‑> List[Class]

Returns a list of subclasses of this class that are visible to the Python interpreter (obtained from type.__subclasses__()).

The objects in the list are of type Class if available, and External otherwise.

Expand source code Browse git
def subclasses(self) -> List['Class']:
    """
    Returns a list of subclasses of this class that are visible to the
    Python interpreter (obtained from `type.__subclasses__()`).

    The objects in the list are of type `pdoc.Class` if available,
    and `pdoc.External` otherwise.
    """
    return sorted(cast(Class, self.module.find_class(c))
                  for c in type.__subclasses__(self.obj))

Inherited members

class Context

The context object that maps all documented identifiers (Doc.refname) to their respective Doc objects.

You can pass an instance of Context to Module constructor. All Module objects that share the same Context will see (and be able to link in HTML to) each other's identifiers.

If you don't pass your own Context instance to Module constructor, a global context object will be used.

Expand source code Browse git
class Context(dict):
    """
    The context object that maps all documented identifiers
    (`pdoc.Doc.refname`) to their respective `pdoc.Doc` objects.

    You can pass an instance of `pdoc.Context` to `pdoc.Module` constructor.
    All `pdoc.Module` objects that share the same `pdoc.Context` will see
    (and be able to link in HTML to) each other's identifiers.

    If you don't pass your own `Context` instance to `Module` constructor,
    a global context object will be used.
    """
    __pdoc__['Context.__init__'] = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A surrogate so that the check in Module._link_inheritance()
        # "__pdoc__-overriden key {!r} does not exist" can see the object
        # (and not warn).
        self.blacklisted = getattr(args[0], 'blacklisted', set()) if args else set()

Ancestors

  • builtins.dict
class Doc (name: str, module, obj, docstring: str = None)

A base class for all documentation objects.

A documentation object corresponds to something in a Python module that has a docstring associated with it. Typically, this includes modules, classes, functions, and methods. However, pdoc adds support for extracting some docstrings from abstract syntax trees, making (module, class or instance) variables supported too.

A special type of documentation object External is used to represent identifiers that are not part of the public interface of a module. (The name "External" is a bit of a misnomer, since it can also correspond to unexported members of the module, particularly in a class's ancestor list.)

Initializes a documentation object, where name is the public identifier name, module is a Module object where raw Python object obj is defined, and docstring is its documentation string. If docstring is left empty, it will be read with inspect.getdoc().

Expand source code Browse git
class Doc:
    """
    A base class for all documentation objects.

    A documentation object corresponds to *something* in a Python module
    that has a docstring associated with it. Typically, this includes
    modules, classes, functions, and methods. However, `pdoc` adds support
    for extracting some docstrings from abstract syntax trees, making
    (module, class or instance) variables supported too.

    A special type of documentation object `pdoc.External` is used to
    represent identifiers that are not part of the public interface of
    a module. (The name "External" is a bit of a misnomer, since it can
    also correspond to unexported members of the module, particularly in
    a class's ancestor list.)
    """
    __slots__ = ('module', 'name', 'obj', 'docstring', 'inherits')

    def __init__(self, name: str, module, obj, docstring: str = None):
        """
        Initializes a documentation object, where `name` is the public
        identifier name, `module` is a `pdoc.Module` object where raw
        Python object `obj` is defined, and `docstring` is its
        documentation string. If `docstring` is left empty, it will be
        read with `inspect.getdoc()`.
        """
        self.module = module
        """
        The module documentation object that this object is defined in.
        """

        self.name = name
        """
        The identifier name for this object.
        """

        self.obj = obj
        """
        The raw python object.
        """

        docstring = (docstring or inspect.getdoc(obj) or '').strip()
        if '.. include::' in docstring:
            from pdoc.html_helpers import _ToMarkdown
            docstring = _ToMarkdown.admonitions(docstring, self.module, ('include',))
        self.docstring = docstring
        """
        The cleaned docstring for this object with any `.. include::`
        directives resolved (i.e. content included).
        """

        self.inherits: Optional[Union[Class, Function, Variable]] = None
        """
        The Doc object (Class, Function, or Variable) this object inherits from,
        if any.
        """

    def __repr__(self):
        return f'<{self.__class__.__name__} {self.refname!r}>'

    @property  # type: ignore
    @lru_cache()
    def source(self) -> str:
        """
        Cleaned (dedented) source code of the Python object. If not
        available, an empty string.
        """
        try:
            lines, _ = inspect.getsourcelines(self.obj)
        except (ValueError, TypeError, OSError):
            return ''
        return inspect.cleandoc(''.join(['\n'] + lines))

    @property
    def refname(self) -> str:
        """
        Reference name of this documentation
        object, usually its fully qualified path
        (e.g. <code>pdoc.Doc.refname</code>). Every
        documentation object provides this property.
        """
        # Ok for Module and External, the rest need it overriden
        return self.name

    @property
    def qualname(self) -> str:
        """
        Module-relative "qualified" name of this documentation
        object, used for show (e.g. <code>Doc.qualname</code>).
        """
        return getattr(self.obj, '__qualname__', self.name)

    @lru_cache()
    def url(self, relative_to: 'Module' = None, *, link_prefix: str = '',
            top_ancestor: bool = False) -> str:
        """
        Canonical relative URL (including page fragment) for this
        documentation object.

        Specify `relative_to` (a `pdoc.Module` object) to obtain a
        relative URL.

        For usage of `link_prefix` see `pdoc.html()`.

        If `top_ancestor` is `True`, the returned URL instead points to
        the top ancestor in the object's `pdoc.Doc.inherits` chain.
        """
        if top_ancestor:
            self = self._inherits_top()

        if relative_to is None or link_prefix:
            return link_prefix + self._url()

        if self.module.name == relative_to.name:
            return f'#{self.refname}'

        # Otherwise, compute relative path from current module to link target
        url = os.path.relpath(self._url(), relative_to.url()).replace(path.sep, '/')
        # We have one set of '..' too many
        if url.startswith('../'):
            url = url[3:]
        return url

    def _url(self):
        return f'{self.module._url()}#{self.refname}'

    def _inherits_top(self):
        """
        Follow the `pdoc.Doc.inherits` chain and return the top object.
        """
        top = self
        while top.inherits:
            top = top.inherits
        return top

    def __lt__(self, other):
        return self.refname < other.refname

Subclasses

Instance variables

var docstring

The cleaned docstring for this object with any .. include:: directives resolved (i.e. content included).

var inherits

The Doc object (Class, Function, or Variable) this object inherits from, if any.

var module

The module documentation object that this object is defined in.

var name

The identifier name for this object.

var obj

The raw python object.

var qualname : str

Module-relative "qualified" name of this documentation object, used for show (e.g. Doc.qualname).

Expand source code Browse git
@property
def qualname(self) -> str:
    """
    Module-relative "qualified" name of this documentation
    object, used for show (e.g. <code>Doc.qualname</code>).
    """
    return getattr(self.obj, '__qualname__', self.name)
var refname : str

Reference name of this documentation object, usually its fully qualified path (e.g. pdoc.Doc.refname). Every documentation object provides this property.

Expand source code Browse git
@property
def refname(self) -> str:
    """
    Reference name of this documentation
    object, usually its fully qualified path
    (e.g. <code>pdoc.Doc.refname</code>). Every
    documentation object provides this property.
    """
    # Ok for Module and External, the rest need it overriden
    return self.name
var source : str

Cleaned (dedented) source code of the Python object. If not available, an empty string.

Expand source code Browse git
@property  # type: ignore
@lru_cache()
def source(self) -> str:
    """
    Cleaned (dedented) source code of the Python object. If not
    available, an empty string.
    """
    try:
        lines, _ = inspect.getsourcelines(self.obj)
    except (ValueError, TypeError, OSError):
        return ''
    return inspect.cleandoc(''.join(['\n'] + lines))

Methods

def url(self, relative_to: Module = None, *, link_prefix: str = '', top_ancestor: bool = False) ‑> str

Canonical relative URL (including page fragment) for this documentation object.

Specify relative_to (a Module object) to obtain a relative URL.

For usage of link_prefix see html().

If top_ancestor is True, the returned URL instead points to the top ancestor in the object's Doc.inherits chain.

Expand source code Browse git
@lru_cache()
def url(self, relative_to: 'Module' = None, *, link_prefix: str = '',
        top_ancestor: bool = False) -> str:
    """
    Canonical relative URL (including page fragment) for this
    documentation object.

    Specify `relative_to` (a `pdoc.Module` object) to obtain a
    relative URL.

    For usage of `link_prefix` see `pdoc.html()`.

    If `top_ancestor` is `True`, the returned URL instead points to
    the top ancestor in the object's `pdoc.Doc.inherits` chain.
    """
    if top_ancestor:
        self = self._inherits_top()

    if relative_to is None or link_prefix:
        return link_prefix + self._url()

    if self.module.name == relative_to.name:
        return f'#{self.refname}'

    # Otherwise, compute relative path from current module to link target
    url = os.path.relpath(self._url(), relative_to.url()).replace(path.sep, '/')
    # We have one set of '..' too many
    if url.startswith('../'):
        url = url[3:]
    return url
class External (name: str)

A representation of an external identifier. The textual representation is the same as an internal identifier.

External identifiers are also used to represent something that is not documented but appears somewhere in the public interface (like the ancestor list of a class).

Initializes an external identifier with name, where name should be a fully qualified name.

Expand source code Browse git
class External(Doc):
    """
    A representation of an external identifier. The textual
    representation is the same as an internal identifier.

    External identifiers are also used to represent something that is
    not documented but appears somewhere in the public interface (like
    the ancestor list of a class).
    """

    __pdoc__["External.docstring"] = """
        An empty string. External identifiers do not have
        docstrings.
        """
    __pdoc__["External.module"] = """
        Always `None`. External identifiers have no associated
        `pdoc.Module`.
        """
    __pdoc__["External.name"] = """
        Always equivalent to `pdoc.External.refname` since external
        identifiers are always expressed in their fully qualified
        form.
        """

    def __init__(self, name: str):
        """
        Initializes an external identifier with `name`, where `name`
        should be a fully qualified name.
        """
        super().__init__(name, None, None)

    def url(self, *args, **kwargs):
        """
        `External` objects return absolute urls matching `/{name}.ext`.
        """
        return f'/{self.name}.ext'

Ancestors

Methods

def url(self, *args, **kwargs)

External objects return absolute urls matching /{name}.ext.

Expand source code Browse git
def url(self, *args, **kwargs):
    """
    `External` objects return absolute urls matching `/{name}.ext`.
    """
    return f'/{self.name}.ext'

Inherited members

class Function (name: str, module: Module, obj, *, cls: Class = None)

Representation of documentation for a function or method.

Same as Doc, except obj must be a Python function object. The docstring is gathered automatically.

cls should be set when this is a method or a static function beloing to a class. cls should be a Class object.

method should be True when the function is a method. In all other cases, it should be False.

Expand source code Browse git
class Function(Doc):
    """
    Representation of documentation for a function or method.
    """
    __slots__ = ('cls',)

    def __init__(self, name: str, module: Module, obj, *, cls: Class = None):
        """
        Same as `pdoc.Doc`, except `obj` must be a
        Python function object. The docstring is gathered automatically.

        `cls` should be set when this is a method or a static function
        beloing to a class. `cls` should be a `pdoc.Class` object.

        `method` should be `True` when the function is a method. In
        all other cases, it should be `False`.
        """
        assert callable(obj), (name, module, obj)
        super().__init__(name, module, obj)

        self.cls = cls
        """
        The `pdoc.Class` documentation object if the function is a method.
        If not, this is None.
        """

    @property
    def is_method(self) -> bool:
        """
        Whether this function is a normal bound method.

        In particular, static and class methods have this set to False.
        """
        assert self.cls
        return not Class._method_type(self.cls.obj, self.name)

    @property
    def method(self):
        warn('`Function.method` is deprecated. Use: `Function.is_method`', DeprecationWarning,
             stacklevel=2)
        return self.is_method

    __pdoc__['Function.method'] = False

    def funcdef(self) -> str:
        """
        Generates the string of keywords used to define the function,
        for example `def` or `async def`.
        """
        return 'async def' if self._is_async else 'def'

    @property
    def _is_async(self):
        """
        Returns whether is function is asynchronous, either as a coroutine or an async
        generator.
        """
        try:
            # Both of these are required because coroutines aren't classified as async
            # generators and vice versa.
            obj = inspect.unwrap(self.obj)
            return (inspect.iscoroutinefunction(obj) or
                    inspect.isasyncgenfunction(obj))
        except AttributeError:
            return False

    def return_annotation(self, *, link=None) -> str:
        """Formatted function return type annotation or empty string if none."""
        annot = ''
        for method in (
                lambda: _get_type_hints(self.obj)['return'],
                # Mainly for non-property variables
                lambda: _get_type_hints(cast(Class, self.cls).obj)[self.name],
                # global variables
                lambda: _get_type_hints(not self.cls and self.module.obj)[self.name],
                lambda: inspect.signature(self.obj).return_annotation,
                # Use raw annotation strings in unmatched forward declarations
                lambda: cast(Class, self.cls).obj.__annotations__[self.name],
                # Extract annotation from the docstring for C builtin function
                lambda: Function._signature_from_string(self).return_annotation,
        ):
            try:
                annot = method()
            except Exception:
                continue
            else:
                break
        else:
            # Don't warn on variables. The annotation just isn't available.
            if not isinstance(self, Variable):
                warn(f"Error handling return annotation for {self!r}", stacklevel=3)

        if annot is inspect.Parameter.empty or not annot:
            return ''

        if isinstance(annot, str):
            s = annot
        else:
            s = _formatannotation(annot)
            s = re.sub(r'\bForwardRef\((?P<quot>[\"\'])(?P<str>.*?)(?P=quot)\)',
                       r'\g<str>', s)
        s = s.replace(' ', '\N{NBSP}')  # Better line breaks in html signatures

        if link:
            from pdoc.html_helpers import _linkify
            s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=self.module), s)
        return s

    def params(self, *, annotate: bool = False, link: Callable[[Doc], str] = None) -> List[str]:
        """
        Returns a list where each element is a nicely formatted
        parameter of this function. This includes argument lists,
        keyword arguments and default values, and it doesn't include any
        optional arguments whose names begin with an underscore.

        If `annotate` is True, the parameter strings include [PEP 484]
        type hint annotations.

        [PEP 484]: https://www.python.org/dev/peps/pep-0484/
        """
        return self._params(self, annotate=annotate, link=link, module=self.module)

    @staticmethod
    def _params(doc_obj, annotate=False, link=None, module=None):
        try:
            # We want __init__ to actually be implemented somewhere in the
            # MRO to still satisfy https://github.com/pdoc3/pdoc/issues/124
            if (
                inspect.isclass(doc_obj.obj)
                and doc_obj.obj.__init__ is not object.__init__
            ):
                # Remove the first argument (self) from __init__ signature
                init_sig = inspect.signature(doc_obj.obj.__init__)
                init_params = list(init_sig.parameters.values())
                signature = init_sig.replace(parameters=init_params[1:])
            else:
                signature = inspect.signature(doc_obj.obj)
        except ValueError:
            signature = Function._signature_from_string(doc_obj)
            if not signature:
                return ['...']

        def safe_default_value(p: inspect.Parameter):
            value = p.default
            if value is inspect.Parameter.empty:
                return p

            replacement = next((i for i in ('os.environ',
                                            'sys.stdin',
                                            'sys.stdout',
                                            'sys.stderr',)
                                if value is eval(i)), None)
            if not replacement:
                if isinstance(value, enum.Enum):
                    replacement = str(value)
                elif inspect.isclass(value):
                    replacement = f'{value.__module__ or _UNKNOWN_MODULE}.{value.__qualname__}'
                elif ' at 0x' in repr(value):
                    replacement = re.sub(r' at 0x\w+', '', repr(value))

                nonlocal link
                if link and ('<' in repr(value) or '>' in repr(value)):
                    import html
                    replacement = html.escape(replacement or repr(value))

            if replacement:
                class mock:
                    def __repr__(self):
                        return replacement
                return p.replace(default=mock())
            return p

        params = []
        kw_only = False
        pos_only = False
        EMPTY = inspect.Parameter.empty

        if link:
            from pdoc.html_helpers import _linkify
            _linkify = partial(_linkify, link=link, module=module)

        for p in signature.parameters.values():  # type: inspect.Parameter
            if not _is_public(p.name) and p.default is not EMPTY:
                continue

            if p.kind == p.POSITIONAL_ONLY:
                pos_only = True
            elif pos_only:
                params.append("/")
                pos_only = False

            if p.kind == p.VAR_POSITIONAL:
                kw_only = True
            if p.kind == p.KEYWORD_ONLY and not kw_only:
                kw_only = True
                params.append('*')

            p = safe_default_value(p)

            if not annotate:
                p = p.replace(annotation=EMPTY)

            formatted = p.name
            if p.annotation is not EMPTY:
                annotation = _formatannotation(p.annotation).replace(' ', '\N{NBSP}')
                # "Eval" forward-declarations (typing string literals)
                if isinstance(p.annotation, str):
                    annotation = annotation.strip("'")
                if link:
                    annotation = re.sub(r'[\w\.]+', _linkify, annotation)
                formatted += f':\N{NBSP}{annotation}'
            if p.default is not EMPTY:
                if p.annotation is not EMPTY:
                    formatted += f'\N{NBSP}=\N{NBSP}{repr(p.default)}'
                else:
                    formatted += f'={repr(p.default)}'
            if p.kind == p.VAR_POSITIONAL:
                formatted = f'*{formatted}'
            elif p.kind == p.VAR_KEYWORD:
                formatted = f'**{formatted}'

            params.append(formatted)

        if pos_only:
            params.append("/")

        return params

    @staticmethod
    @lru_cache()
    def _signature_from_string(self):
        signature = None
        for expr, cleanup_docstring, filter in (
                # Full proper typed signature, such as one from pybind11
                (r'^{}\(.*\)(?: -> .*)?$', True, lambda s: s),
                # Human-readable, usage-like signature from some Python builtins
                # (e.g. `range` or `slice` or `itertools.repeat` or `numpy.arange`)
                (r'^{}\(.*\)(?= -|$)', False, lambda s: s.replace('[', '').replace(']', '')),
        ):
            strings = sorted(re.findall(expr.format(self.name),
                                        self.docstring, re.MULTILINE),
                             key=len, reverse=True)
            if strings:
                string = filter(strings[0])
                _locals, _globals = {}, {}
                _globals.update({'capsule': None})  # pybind11 capsule data type
                _globals.update(typing.__dict__)
                _globals.update(self.module.obj.__dict__)
                # Trim binding module basename from type annotations
                # See: https://github.com/pdoc3/pdoc/pull/148#discussion_r407114141
                module_basename = self.module.name.rsplit('.', maxsplit=1)[-1]
                if module_basename in string and module_basename not in _globals:
                    string = re.sub(fr'(?<!\.)\b{module_basename}\.\b', '', string)

                try:
                    exec(f'def {string}: pass', _globals, _locals)
                except SyntaxError:
                    continue
                signature = inspect.signature(_locals[self.name])
                if cleanup_docstring and len(strings) == 1:
                    # Remove signature from docstring variable
                    self.docstring = self.docstring.replace(strings[0], '')
                break
        return signature

    @property
    def refname(self) -> str:
        return f'{self.cls.refname if self.cls else self.module.refname}.{self.name}'

Ancestors

Instance variables

var cls

The Class documentation object if the function is a method. If not, this is None.

var is_method : bool

Whether this function is a normal bound method.

In particular, static and class methods have this set to False.

Expand source code Browse git
@property
def is_method(self) -> bool:
    """
    Whether this function is a normal bound method.

    In particular, static and class methods have this set to False.
    """
    assert self.cls
    return not Class._method_type(self.cls.obj, self.name)

Methods

def funcdef(self) ‑> str

Generates the string of keywords used to define the function, for example def or async def.

Expand source code Browse git
def funcdef(self) -> str:
    """
    Generates the string of keywords used to define the function,
    for example `def` or `async def`.
    """
    return 'async def' if self._is_async else 'def'
def params(self, *, annotate: bool = False, link: Callable[[Doc], str] = None) ‑> List[str]

Returns a list where each element is a nicely formatted parameter of this function. This includes argument lists, keyword arguments and default values, and it doesn't include any optional arguments whose names begin with an underscore.

If annotate is True, the parameter strings include PEP 484 type hint annotations.

Expand source code Browse git
def params(self, *, annotate: bool = False, link: Callable[[Doc], str] = None) -> List[str]:
    """
    Returns a list where each element is a nicely formatted
    parameter of this function. This includes argument lists,
    keyword arguments and default values, and it doesn't include any
    optional arguments whose names begin with an underscore.

    If `annotate` is True, the parameter strings include [PEP 484]
    type hint annotations.

    [PEP 484]: https://www.python.org/dev/peps/pep-0484/
    """
    return self._params(self, annotate=annotate, link=link, module=self.module)
def return_annotation(self, *, link=None) ‑> str

Formatted function return type annotation or empty string if none.

Expand source code Browse git
def return_annotation(self, *, link=None) -> str:
    """Formatted function return type annotation or empty string if none."""
    annot = ''
    for method in (
            lambda: _get_type_hints(self.obj)['return'],
            # Mainly for non-property variables
            lambda: _get_type_hints(cast(Class, self.cls).obj)[self.name],
            # global variables
            lambda: _get_type_hints(not self.cls and self.module.obj)[self.name],
            lambda: inspect.signature(self.obj).return_annotation,
            # Use raw annotation strings in unmatched forward declarations
            lambda: cast(Class, self.cls).obj.__annotations__[self.name],
            # Extract annotation from the docstring for C builtin function
            lambda: Function._signature_from_string(self).return_annotation,
    ):
        try:
            annot = method()
        except Exception:
            continue
        else:
            break
    else:
        # Don't warn on variables. The annotation just isn't available.
        if not isinstance(self, Variable):
            warn(f"Error handling return annotation for {self!r}", stacklevel=3)

    if annot is inspect.Parameter.empty or not annot:
        return ''

    if isinstance(annot, str):
        s = annot
    else:
        s = _formatannotation(annot)
        s = re.sub(r'\bForwardRef\((?P<quot>[\"\'])(?P<str>.*?)(?P=quot)\)',
                   r'\g<str>', s)
    s = s.replace(' ', '\N{NBSP}')  # Better line breaks in html signatures

    if link:
        from pdoc.html_helpers import _linkify
        s = re.sub(r'[\w\.]+', partial(_linkify, link=link, module=self.module), s)
    return s

Inherited members

class Module (module: Union[str, module], *, docfilter: Callable[[Doc], bool] = None, supermodule: Module = None, context: Context = None, skip_errors: bool = False)

Representation of a module's documentation.

Creates a Module documentation object given the actual module Python object.

docfilter is an optional predicate that controls which sub-objects are documentated (see also: html()).

supermodule is the parent Module this module is a submodule of.

context is an instance of Context. If None a global context object will be used.

If skip_errors is True and an unimportable, erroneous submodule is encountered, a warning will be issued instead of raising an exception.

Expand source code Browse git
class Module(Doc):
    """
    Representation of a module's documentation.
    """
    __pdoc__["Module.name"] = """
        The name of this module with respect to the context/path in which
        it was imported from. It is always an absolute import path.
        """

    __slots__ = ('supermodule', 'doc', '_context', '_is_inheritance_linked',
                 '_skipped_submodules')

    def __init__(self, module: Union[ModuleType, str], *, docfilter: Callable[[Doc], bool] = None,
                 supermodule: 'Module' = None, context: Context = None,
                 skip_errors: bool = False):
        """
        Creates a `Module` documentation object given the actual
        module Python object.

        `docfilter` is an optional predicate that controls which
        sub-objects are documentated (see also: `pdoc.html()`).

        `supermodule` is the parent `pdoc.Module` this module is
        a submodule of.

        `context` is an instance of `pdoc.Context`. If `None` a
        global context object will be used.

        If `skip_errors` is `True` and an unimportable, erroneous
        submodule is encountered, a warning will be issued instead
        of raising an exception.
        """
        if isinstance(module, str):
            module = import_module(module)

        super().__init__(module.__name__, self, module)
        if self.name.endswith('.__init__') and not self.is_package:
            self.name = self.name[:-len('.__init__')]

        self._context = _global_context if context is None else context
        """
        A lookup table for ALL doc objects of all modules that share this context,
        mainly used in `Module.find_ident()`.
        """
        assert isinstance(self._context, Context), \
            'pdoc.Module(context=) should be a pdoc.Context instance'

        self.supermodule = supermodule
        """
        The parent `pdoc.Module` this module is a submodule of, or `None`.
        """

        self.doc: Dict[str, Union[Module, Class, Function, Variable]] = {}
        """A mapping from identifier name to a documentation object."""

        self._is_inheritance_linked = False
        """Re-entry guard for `pdoc.Module._link_inheritance()`."""

        self._skipped_submodules = set()

        var_docstrings, _ = _pep224_docstrings(self)

        # Populate self.doc with this module's public members
        public_objs = []
        if hasattr(self.obj, '__all__'):
            for name in self.obj.__all__:
                try:
                    obj = getattr(self.obj, name)
                except AttributeError:
                    warn(f"Module {self.module!r} doesn't contain identifier `{name}` "
                         "exported in `__all__`")
                if not _is_blacklisted(name, self):
                    obj = inspect.unwrap(obj)
                public_objs.append((name, obj))
        else:
            def is_from_this_module(obj):
                mod = inspect.getmodule(inspect.unwrap(obj))
                return mod is None or mod.__name__ == self.obj.__name__

            for name, obj in inspect.getmembers(self.obj):
                if ((_is_public(name) or
                     _is_whitelisted(name, self)) and
                        (_is_blacklisted(name, self) or  # skips unwrapping that follows
                         is_from_this_module(obj) or
                         name in var_docstrings)):

                    if _is_blacklisted(name, self):
                        self._context.blacklisted.add(f'{self.refname}.{name}')
                        continue

                    obj = inspect.unwrap(obj)
                    public_objs.append((name, obj))

            index = list(self.obj.__dict__).index
            public_objs.sort(key=lambda i: index(i[0]))

        for name, obj in public_objs:
            if _is_function(obj):
                self.doc[name] = Function(name, self, obj)
            elif inspect.isclass(obj):
                self.doc[name] = Class(name, self, obj)
            elif name in var_docstrings:
                self.doc[name] = Variable(name, self, var_docstrings[name], obj=obj)

        # If the module is a package, scan the directory for submodules
        if self.is_package:

            def iter_modules(paths):
                """
                Custom implementation of `pkgutil.iter_modules()`
                because that one doesn't play well with namespace packages.
                See: https://github.com/pypa/setuptools/issues/83
                """
                from os.path import isdir, join
                for pth in paths:
                    for file in os.listdir(pth):
                        if file.startswith(('.', '__pycache__', '__init__.py')):
                            continue
                        module_name = inspect.getmodulename(file)
                        if module_name:
                            yield module_name
                        if isdir(join(pth, file)) and '.' not in file:
                            yield file

            for root in iter_modules(self.obj.__path__):
                # Ignore if this module was already doc'd.
                if root in self.doc:
                    continue

                # Ignore if it isn't exported
                if not _is_public(root) and not _is_whitelisted(root, self):
                    continue
                if _is_blacklisted(root, self):
                    self._skipped_submodules.add(root)
                    continue

                assert self.refname == self.name
                fullname = f"{self.name}.{root}"
                try:
                    m = Module(import_module(fullname),
                               docfilter=docfilter, supermodule=self,
                               context=self._context, skip_errors=skip_errors)
                except Exception as ex:
                    if skip_errors:
                        warn(str(ex), Module.ImportWarning)
                        continue
                    raise

                self.doc[root] = m
                # Skip empty namespace packages because they may
                # as well be other auxiliary directories
                if m.is_namespace and not m.doc:
                    del self.doc[root]
                    self._context.pop(m.refname)

        # Apply docfilter
        if docfilter:
            for name, dobj in self.doc.copy().items():
                if not docfilter(dobj):
                    self.doc.pop(name)
                    self._context.pop(dobj.refname, None)

        # Build the reference name dictionary of the module
        self._context[self.refname] = self
        for docobj in self.doc.values():
            self._context[docobj.refname] = docobj
            if isinstance(docobj, Class):
                self._context.update((obj.refname, obj)
                                     for obj in docobj.doc.values())

    class ImportWarning(UserWarning):
        """
        Our custom import warning because the builtin is ignored by default.
        https://docs.python.org/3/library/warnings.html#default-warning-filter
        """

    __pdoc__['Module.ImportWarning'] = False

    @property
    def __pdoc__(self) -> dict:
        """This module's __pdoc__ dict, or an empty dict if none."""
        return getattr(self.obj, '__pdoc__', {})

    def _link_inheritance(self):
        # Inherited members are already in place since
        # `Class._fill_inheritance()` has been called from
        # `pdoc.fill_inheritance()`.
        # Now look for docstrings in the module's __pdoc__ override.

        if self._is_inheritance_linked:
            # Prevent re-linking inheritance for modules which have already
            # had done so. Otherwise, this would raise "does not exist"
            # errors if `pdoc.link_inheritance()` is called multiple times.
            return

        # Apply __pdoc__ overrides
        for name, docstring in self.__pdoc__.items():
            # In case of whitelisting with "True", there's nothing to do
            if docstring is True:
                continue

            refname = f"{self.refname}.{name}"
            if docstring in (False, None):
                if docstring is None:
                    warn('Setting `__pdoc__[key] = None` is deprecated; '
                         'use `__pdoc__[key] = False` '
                         f'(key: {name!r}, module: {self.name!r}).')

                if name in self._skipped_submodules:
                    continue

                if (not name.endswith('.__init__') and
                        name not in self.doc and
                        refname not in self._context and
                        refname not in self._context.blacklisted):
                    warn(f'__pdoc__-overriden key {name!r} does not exist '
                         f'in module {self.name!r}')

                obj = self.find_ident(name)
                cls = getattr(obj, 'cls', None)
                if cls:
                    del cls.doc[obj.name]
                self.doc.pop(name, None)
                self._context.pop(refname, None)

                # Pop also all that startwith refname
                for key in list(self._context.keys()):
                    if key.startswith(refname + '.'):
                        del self._context[key]

                continue

            dobj = self.find_ident(refname)
            if isinstance(dobj, External):
                continue
            if not isinstance(docstring, str):
                raise ValueError('__pdoc__ dict values must be strings; '
                                 f'__pdoc__[{name!r}] is of type {type(docstring)}')
            dobj.docstring = inspect.cleandoc(docstring)

        # Now after docstrings are set correctly, continue the
        # inheritance routine, marking members inherited or not
        for c in _filter_type(Class, self.doc):
            c._link_inheritance()

        self._is_inheritance_linked = True

    def text(self, **kwargs) -> str:
        """
        Returns the documentation for this module as plain text.
        """
        txt = _render_template('/text.mako', module=self, **kwargs)
        return re.sub("\n\n\n+", "\n\n", txt)

    def html(self, minify=True, **kwargs) -> str:
        """
        Returns the documentation for this module as
        self-contained HTML.

        If `minify` is `True`, the resulting HTML is minified.

        For explanation of other arguments, see `pdoc.html()`.

        `kwargs` is passed to the `mako` render function.
        """
        html = _render_template('/html.mako', module=self, **kwargs)
        if minify:
            from pdoc.html_helpers import minify_html
            html = minify_html(html)
        return html

    @property
    def is_package(self) -> bool:
        """
        `True` if this module is a package.

        Works by checking whether the module has a `__path__` attribute.
        """
        return hasattr(self.obj, "__path__")

    @property
    def is_namespace(self) -> bool:
        """
        `True` if this module is a namespace package.
        """
        try:
            return self.obj.__spec__.origin in (None, 'namespace')  # None in Py3.7+
        except AttributeError:
            return False

    def find_class(self, cls: type) -> Doc:
        """
        Given a Python `cls` object, try to find it in this module
        or in any of the exported identifiers of the submodules.
        """
        # XXX: Is this corrent? Does it always match
        # `Class.module.name + Class.qualname`?. Especially now?
        # If not, see what was here before.
        return self.find_ident(f'{cls.__module__ or _UNKNOWN_MODULE}.{cls.__qualname__}')

    def find_ident(self, name: str) -> Doc:
        """
        Searches this module and **all** other public modules
        for an identifier with name `name` in its list of
        exported identifiers.

        The documentation object corresponding to the identifier is
        returned. If one cannot be found, then an instance of
        `External` is returned populated with the given identifier.
        """
        _name = name.rstrip('()')  # Function specified with parentheses

        if _name.endswith('.__init__'):  # Ref to class' init is ref to class itself
            _name = _name[:-len('.__init__')]

        return (self.doc.get(_name) or
                self._context.get(_name) or
                self._context.get(f'{self.name}.{_name}') or
                External(name))

    def _filter_doc_objs(self, type: Type[T], sort=True) -> List[T]:
        result = _filter_type(type, self.doc)
        return sorted(result) if sort else result

    def variables(self, sort=True) -> List['Variable']:
        """
        Returns all documented module-level variables in the module,
        optionally sorted alphabetically, as a list of `pdoc.Variable`.
        """
        return self._filter_doc_objs(Variable, sort)

    def classes(self, sort=True) -> List['Class']:
        """
        Returns all documented module-level classes in the module,
        optionally sorted alphabetically, as a list of `pdoc.Class`.
        """
        return self._filter_doc_objs(Class, sort)

    def functions(self, sort=True) -> List['Function']:
        """
        Returns all documented module-level functions in the module,
        optionally sorted alphabetically, as a list of `pdoc.Function`.
        """
        return self._filter_doc_objs(Function, sort)

    def submodules(self) -> List['Module']:
        """
        Returns all documented sub-modules of the module sorted
        alphabetically as a list of `pdoc.Module`.
        """
        return self._filter_doc_objs(Module)

    def _url(self):
        url = self.module.name.replace('.', '/')
        if self.is_package:
            return url + _URL_PACKAGE_SUFFIX
        elif url.endswith('/index'):
            return url + _URL_INDEX_MODULE_SUFFIX
        return url + _URL_MODULE_SUFFIX

Ancestors

Instance variables

var doc

A mapping from identifier name to a documentation object.

var is_namespace : bool

True if this module is a namespace package.

Expand source code Browse git
@property
def is_namespace(self) -> bool:
    """
    `True` if this module is a namespace package.
    """
    try:
        return self.obj.__spec__.origin in (None, 'namespace')  # None in Py3.7+
    except AttributeError:
        return False
var is_package : bool

True if this module is a package.

Works by checking whether the module has a __path__ attribute.

Expand source code Browse git
@property
def is_package(self) -> bool:
    """
    `True` if this module is a package.

    Works by checking whether the module has a `__path__` attribute.
    """
    return hasattr(self.obj, "__path__")
var supermodule

The parent Module this module is a submodule of, or None.

Methods

def classes(self, sort=True) ‑> List[Class]

Returns all documented module-level classes in the module, optionally sorted alphabetically, as a list of Class.

Expand source code Browse git
def classes(self, sort=True) -> List['Class']:
    """
    Returns all documented module-level classes in the module,
    optionally sorted alphabetically, as a list of `pdoc.Class`.
    """
    return self._filter_doc_objs(Class, sort)
def find_class(self, cls: type) ‑> Doc

Given a Python cls object, try to find it in this module or in any of the exported identifiers of the submodules.

Expand source code Browse git
def find_class(self, cls: type) -> Doc:
    """
    Given a Python `cls` object, try to find it in this module
    or in any of the exported identifiers of the submodules.
    """
    # XXX: Is this corrent? Does it always match
    # `Class.module.name + Class.qualname`?. Especially now?
    # If not, see what was here before.
    return self.find_ident(f'{cls.__module__ or _UNKNOWN_MODULE}.{cls.__qualname__}')
def find_ident(self, name: str) ‑> Doc

Searches this module and all other public modules for an identifier with name name in its list of exported identifiers.

The documentation object corresponding to the identifier is returned. If one cannot be found, then an instance of External is returned populated with the given identifier.

Expand source code Browse git
def find_ident(self, name: str) -> Doc:
    """
    Searches this module and **all** other public modules
    for an identifier with name `name` in its list of
    exported identifiers.

    The documentation object corresponding to the identifier is
    returned. If one cannot be found, then an instance of
    `External` is returned populated with the given identifier.
    """
    _name = name.rstrip('()')  # Function specified with parentheses

    if _name.endswith('.__init__'):  # Ref to class' init is ref to class itself
        _name = _name[:-len('.__init__')]

    return (self.doc.get(_name) or
            self._context.get(_name) or
            self._context.get(f'{self.name}.{_name}') or
            External(name))
def functions(self, sort=True) ‑> List[Function]

Returns all documented module-level functions in the module, optionally sorted alphabetically, as a list of Function.

Expand source code Browse git
def functions(self, sort=True) -> List['Function']:
    """
    Returns all documented module-level functions in the module,
    optionally sorted alphabetically, as a list of `pdoc.Function`.
    """
    return self._filter_doc_objs(Function, sort)
def html(self, minify=True, **kwargs) ‑> str

Returns the documentation for this module as self-contained HTML.

If minify is True, the resulting HTML is minified.

For explanation of other arguments, see html().

kwargs is passed to the mako render function.

Expand source code Browse git
def html(self, minify=True, **kwargs) -> str:
    """
    Returns the documentation for this module as
    self-contained HTML.

    If `minify` is `True`, the resulting HTML is minified.

    For explanation of other arguments, see `pdoc.html()`.

    `kwargs` is passed to the `mako` render function.
    """
    html = _render_template('/html.mako', module=self, **kwargs)
    if minify:
        from pdoc.html_helpers import minify_html
        html = minify_html(html)
    return html
def submodules(self) ‑> List[Module]

Returns all documented sub-modules of the module sorted alphabetically as a list of Module.

Expand source code Browse git
def submodules(self) -> List['Module']:
    """
    Returns all documented sub-modules of the module sorted
    alphabetically as a list of `pdoc.Module`.
    """
    return self._filter_doc_objs(Module)
def text(self, **kwargs) ‑> str

Returns the documentation for this module as plain text.

Expand source code Browse git
def text(self, **kwargs) -> str:
    """
    Returns the documentation for this module as plain text.
    """
    txt = _render_template('/text.mako', module=self, **kwargs)
    return re.sub("\n\n\n+", "\n\n", txt)
def variables(self, sort=True) ‑> List[Variable]

Returns all documented module-level variables in the module, optionally sorted alphabetically, as a list of Variable.

Expand source code Browse git
def variables(self, sort=True) -> List['Variable']:
    """
    Returns all documented module-level variables in the module,
    optionally sorted alphabetically, as a list of `pdoc.Variable`.
    """
    return self._filter_doc_objs(Variable, sort)

Inherited members

class Variable (name: str, module: Module, docstring, *, obj=None, cls: Class = None, instance_var: bool = False)

Representation of a variable's documentation. This includes module, class, and instance variables.

Same as Doc, except cls should be provided as a Class object when this is a class or instance variable.

Expand source code Browse git
class Variable(Doc):
    """
    Representation of a variable's documentation. This includes
    module, class, and instance variables.
    """
    __slots__ = ('cls', 'instance_var')

    def __init__(self, name: str, module: Module, docstring, *,
                 obj=None, cls: Class = None, instance_var: bool = False):
        """
        Same as `pdoc.Doc`, except `cls` should be provided
        as a `pdoc.Class` object when this is a class or instance
        variable.
        """
        super().__init__(name, module, obj, docstring)

        self.cls = cls
        """
        The `pdoc.Class` object if this is a class or instance
        variable. If not (i.e. it is a global variable), this is None.
        """

        self.instance_var = instance_var
        """
        True if variable is some class' instance variable (as
        opposed to class variable).
        """

    @property
    def qualname(self) -> str:
        if self.cls:
            return f'{self.cls.qualname}.{self.name}'
        return self.name

    @property
    def refname(self) -> str:
        return f'{self.cls.refname if self.cls else self.module.refname}.{self.name}'

    def type_annotation(self, *, link=None) -> str:
        """Formatted variable type annotation or empty string if none."""
        return Function.return_annotation(cast(Function, self), link=link)

Ancestors

Instance variables

var cls

The Class object if this is a class or instance variable. If not (i.e. it is a global variable), this is None.

var instance_var

True if variable is some class' instance variable (as opposed to class variable).

Methods

def type_annotation(self, *, link=None) ‑> str

Formatted variable type annotation or empty string if none.

Expand source code Browse git
def type_annotation(self, *, link=None) -> str:
    """Formatted variable type annotation or empty string if none."""
    return Function.return_annotation(cast(Function, self), link=link)

Inherited members