"""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)