Source code for torx.decorators.autodoc_decorators_m

"""Contains decorators to add automatically created nice documentation."""
from rich import inspect, get_console
import builtins
import sys
import types
import enum
import inspect as _inspect
import functools
from rich._inspect import Inspect
from rich.console import Group
from rich.highlighter import ReprHighlighter
from rich.panel import Panel
from rich.pretty import Pretty
from rich.table import Table
from rich.text import Text
from abc import ABCMeta
from enum import EnumMeta

# The display of all AutoDoc objects is driven by rich's display hooks
# (installed by rich.pretty.install in torx/__init__.py): instances,
# functions and modules provide __rich__, classes use _ipython_display_
# in IPython/Jupyter and _displayhook in the plain REPL. All __repr__
# methods are pure and return short strings.

def _path(obj):
    """Return the full dotted path of a class or function."""
    return f"{obj.__module__}.{obj.__qualname__}"

def _with_header(panel, title, path):
    """
    Set the title of panel and prepend the full path as a dim first line.

    The title is highlighted the way rich highlights a repr.
    """
    panel.title = ReprHighlighter()(Text(title))
    panel.renderable = Group(Text(path, style="dim"), panel.renderable)
    return panel

def _inspect_panel(obj, title, path, **options):
    """
    Return the panel printed by rich.inspect with title and path line.

    rich only offers inspect() for printing; Inspect is the object it
    prints, so the panel follows the width and colors of the target console.
    Unlike inspect(), Inspect defaults to all=True, hence all=False here.
    """
    panel = Inspect(obj, all=False, **options).__rich__()
    return _with_header(panel, title, path)

def _enum_panel(cls, current=None):
    """Return a panel listing all members of an enum, current one marked."""
    table = Table.grid(padding=(0, 2))
    table.add_column(justify="right", style="bold cyan")
    table.add_column(style="green")
    for member in cls:
        name = member.name
        if current is not None:
            name = f"{'>' if member is current else ' '} {name}"
        table.add_row(name, f"= {member.value!r}")
    grid = Table.grid(padding=(0, 0))
    grid.add_column()
    doc = _inspect.getdoc(cls) or ""
    if doc:
        grid.add_row(Text(doc))
        grid.add_row(Text(""))
    grid.add_row(table)
    if current is None:
        title = f"<enum '{cls.__qualname__}'>"
    else:
        title = repr(current)
    return _with_header(Panel(grid, expand=False), title, _path(cls))

def _class_panel(cls):
    """Return the panel displayed for an AutoDoc class."""
    if issubclass(cls, enum.Enum):
        return _enum_panel(cls)
    return _inspect_panel(cls, f"<class '{cls.__qualname__}'>", _path(cls),
                          methods=True)

class AutoDocModule(types.ModuleType):
    """Module subclass providing automatic rich documentation."""

    def __repr__(self):
        """Return standard Python representation."""
        return f"<module '{self.__name__}'>"

    def __rich__(self):
        """Return the rich panel shown when the module is displayed."""
        name = self.__name__.rpartition(".")[2]
        return _inspect_panel(self, f"<module '{name}'>", self.__name__,
                              methods=True)

    def help(self, full=False):
        """
        Display rich documentation for this module.

        Parameters
        ----------
        full : bool, optional
            If True, show all documentation including private members.
            Default is False.

        Examples
        --------
            >>> import torx
            >>> torx.help()           # Standard module help
            >>> torx.help(full=True)  # Full module help
        """
        if not full:
            inspect(self, help=True, methods=True)
        else:
            inspect(self, all=True)

class AutoDocFunction:
    """Wrapper providing automatic rich documentation for functions."""

    def __init__(self, func, kind='plain', owner=None):
        # Detect and unwrap descriptor kinds before stripping wrappers
        if isinstance(func, classmethod):
            kind = 'classmethod'
            func = func.__func__
        elif isinstance(func, staticmethod):
            kind = 'staticmethod'
            func = func.__func__

        # Unwrap any remaining AutoDocFunction wrappers
        while isinstance(func, AutoDocFunction):
            func = func._func

        self._func = func
        self._kind = kind
        self._owner = owner
        self.__doc__ = func.__doc__
        self.__name__ = func.__name__
        self.__qualname__ = func.__qualname__
        self.__annotations__ = getattr(func, '__annotations__', {})
        self.__wrapped__ = func

    def __set_name__(self, owner, name):
        """Set name of owner."""
        self._owner = owner

    def __get__(self, obj, objtype=None):
        """Handle descriptor protocol for all method kinds."""
        if self._kind == 'classmethod':
            @functools.wraps(self._func)
            def bound(*args, **kwargs):
                return self._func(objtype, *args, **kwargs)
            return bound

        elif self._kind == 'staticmethod':
            return self._func

        else:
            if obj is None:
                # Accessed on the class: return self so __repr__ works
                return self

            @functools.wraps(self._func)
            def bound(*args, **kwargs):
                return self._func(obj, *args, **kwargs)
            return bound

    def __call__(self, *args, **kwargs):
        """
        Call the wrapped function.

        For plain functions this works directly.
        For classmethods/staticmethods accessed without descriptor protocol
        (e.g. direct call on AutoDocFunction instance), handle gracefully.
        """
        if self._kind == 'classmethod':
            raise TypeError(
                f"{self.__qualname__} is a classmethod. "
                f"Access it via the class: ClassName.{self.__name__}()"
            )
        return self._func(*args, **kwargs)

    def __repr__(self):
        """Return standard Python representation."""
        return f"<function '{self.__qualname__}'>"

    def __rich__(self):
        """Return the rich panel shown when the function is displayed."""
        return _inspect_panel(self._func, f"<function '{self.__qualname__}'>",
                              _path(self._func), help=True)

    def help(self, full=False):
        """Return help of the function."""
        if not full:
            inspect(self._func, help=True)
        else:
            inspect(self._func, all=True)

def _wrap_class_methods(cls):
    """
    Wrap all public methods of a class in AutoDocFunction.

    Handles plain methods, classmethods, static methods, and properties.
    Skips private members and already-wrapped members.
    """
    is_enum = issubclass(cls, enum.Enum)

    for attr, value in list(cls.__dict__.items()):
        if attr.startswith('_') or attr in ('help', 'class_help', 'mro'):
            continue
        if is_enum and isinstance(value, cls):
            continue
        if isinstance(value, (classmethod, staticmethod)):
            setattr(cls, attr, AutoDocFunction(value))
        elif isinstance(value, property):
            new_fget = AutoDocFunction(value.fget) if value.fget else None
            setattr(cls, attr, property(new_fget, value.fset, value.fdel, value.__doc__))
        elif callable(value) and not isinstance(value, AutoDocFunction):
            setattr(cls, attr, AutoDocFunction(value))

class AutoDocMeta(type):
    """
    Metaclass providing automatic rich documentation for classes.

    Automatically wraps all public methods in AutoDocFunction at class
    creation time, and provides rich display for both the class itself and
    its instances.

    Used via _make_autodoc_base() or AutoDocClass.
    """

    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if name == 'AutoDocBase':
            return
        _wrap_class_methods(cls)

    def __repr__(cls):
        """Return standard Python representation."""
        return f"<class '{cls.__module__}.{cls.__qualname__}'>"

    def _ipython_display_(cls):
        """Display the class panel in IPython and Jupyter."""
        get_console().print(_class_panel(cls))

def _displayhook(value, _previous=sys.displayhook):
    """
    Display AutoDoc classes with their panel in the plain Python REPL.

    rich renders only instances, so classes would show their short repr.
    Everything else is passed on to the previous (rich) display hook.
    """
    if not isinstance(value, AutoDocMeta):
        _previous(value)
        return
    builtins._ = None
    get_console().print(_class_panel(value))
    builtins._ = value

def _install_displayhook():
    """
    Install _displayhook as sys.displayhook outside of IPython.

    torx/__init__.py installs rich's display hook before this module is
    imported. IPython uses AutoDocMeta._ipython_display_ instead.
    """
    if not hasattr(builtins, "get_ipython"):
        sys.displayhook = _displayhook

_install_displayhook()

def _make_autodoc_base(base_meta=type):
    """
    Create an AutoDocClass base compatible with any metaclass.

    Parameters
    ----------
    base_meta : type
        The metaclass to combine with AutoDocMeta.
        Default is plain type.

    Examples
    --------
        # With ABCMeta:
        from abc import ABCMeta
        AutoDocABCBase = _make_autodoc_base(ABCMeta)

        class MyClass(AutoDocABCBase):
            ...

        # Combined with ABC in the class definition:
        from abc import ABC, ABCMeta
        AutoDocClass = _make_autodoc_base(ABCMeta)

        class MyClass(ABC, AutoDocClass):
            ...
    """
    if base_meta is type:
        Meta = AutoDocMeta
    else:
        # Combine AutoDocMeta with the given metaclass to avoid conflicts
        Meta = type(f'AutoDoc{base_meta.__name__}', (AutoDocMeta, base_meta), {})

    class AutoDocBase(metaclass=Meta):
        """Base class providing automatic rich documentation."""

        def __repr__(self):
            return f"<{self.__class__.__qualname__}>"

        def __rich__(self):
            """Return the rich panel shown when the object is displayed."""
            if isinstance(self, enum.Enum):
                return _enum_panel(type(self), self)
            if type(self).__repr__ is not AutoDocBase.__repr__:
                # Subclasses with their own repr (e.g. dataclasses) keep it
                return Pretty(self)
            return _inspect_panel(self, repr(self), _path(type(self)),
                                  methods=True)

        def help(self, full=False):
            if not full:
                inspect(self, help=True, methods=True)
            else:
                inspect(self, all=True)

        @classmethod
        def class_help(cls, full=False):
            if not full:
                inspect(cls, help=True, methods=True)
            else:
                inspect(cls, all=True)

    return AutoDocBase

# Convenience wrappers, AutoDocClass is the standard base for use with ABC.
AutoDocClass = _make_autodoc_base(ABCMeta)
AutoDocEnum = _make_autodoc_base(EnumMeta)

[docs] def autodoc_module(mod_name): """ Automatically create nice documentation for modules. Must be called at the end of the module file after all definitions. Does NOT print anything on import. Parameters ---------- mod_name : str The name of the module to autodoc. Should always be passed as __name__ from within the module itself. Examples -------- # At the bottom of your module file: from torx.decorators import autodoc_module autodoc_module(__name__) # After importing: >>> import torx >>> torx # Shows rich documentation >>> torx.help() # Explicit help """ module = sys.modules[mod_name] module.__class__ = AutoDocModule
[docs] def autodoc_function(func): """ Automatically create nice documentation for functions. Used as a decorator. Parameters ---------- func : callable The function to autodoc. Examples -------- @autodoc_function def my_function(): pass >>> my_function # Shows rich documentation >>> my_function.help() # Shows rich help >>> my_function() # Still callable normally """ if isinstance(func, AutoDocFunction): return func return AutoDocFunction(func)