Source code for plum.repr

__all__ = [
    "repr_short",
    "repr_type",
    "repr_source_path",
    "repr_pyfunction",
    "rich_repr",
]
import inspect
import os
import sys
import types
import typing
from collections.abc import Callable, Iterable
from functools import partial
from typing import Any, TypeVar, overload
from typing_extensions import TypeAliasType

import rich
from rich.color import Color
from rich.style import Style
from rich.text import Text

from ._alias import _transform_union_alias

T = TypeVar("T")

path_style = Style(color=Color.from_ansi(7))
file_style = Style(bold=True, underline=True)

module_style = Style(color="grey66")
class_style = Style(bold=True)


[docs] def repr_type(x: object, /) -> Text: """Returns a :class:`rich.Text` representation of a type or module. Does some light syntax highlighting mimicking Julia, boldening class names and coloring module names with a lighter color. Args: x (object): Type or module. Returns: :class:`rich.Text`: Representation. """ # Apply union aliasing if `x` is a union. This allows us to have the correct # syntax highlighting for aliased unions. x = _transform_union_alias(x) if isinstance(x, type): if x.__module__ in ["builtins", "typing", "typing_extensions"]: return Text(x.__qualname__, style=class_style) else: res = Text(f"{x.__module__}.", style=module_style) res += Text(x.__qualname__, style=class_style) return res if isinstance(x, types.ModuleType): return Text(x.__name__, style=module_style) return Text(repr(x), style=class_style)
[docs] def repr_short(x: object, /) -> str: """Representation as a string, but in shorter form. This just calls :func:`typing._type_repr`. If the type is a union registered in Plum's alias registry, the alias name is used instead. Args: x (object): Object. Returns: str: Shorter representation of `x`. """ if isinstance(transformed := _transform_union_alias(x), TypeAliasType): # It's an aliased union. Use the alias name. return str(transformed.__name__) # :func:`typing._type_repr` is an internal function, but it should be # available in Python versions 3.9 through 3.14. return typing._type_repr(x)
def _safe_getfile(obj: object, /) -> str: """Safer version of :func:`inspect.getfile`. Classes defined in PyBind, even if they pretend hard to be functions, like `jax.jit(fun)`, will raise an error if passed to `inspect.getfile`. This function will catch those errors and try harder to get the underlying file, and raise an error only if it cannot. This function can only raise an `OSError`. Args: obj (object): An object. Returns: str: Path to the file that defines `obj`. Raises: OSError: File that defines `obj` cannot be found. """ try: return inspect.getfile(obj) # type: ignore[arg-type] except TypeError: # Raised when the function passed is a C-defined class. It might still contain # `__module__`, which can be used to backtrace the file that defines `obj`. if hasattr(obj, "__module__"): module = sys.modules.get(obj.__module__, None) if module is not None and hasattr(module, "__file__"): file_path = module.__file__ if file_path is not None: return file_path raise OSError("Source code not available.") from None
[docs] def repr_source_path(function: Callable[..., Any], /) -> Text | None: """Create a :class:`rich.Text` object with an hyperlink to the function definition. Args: function (Callable[..., Any]): A function. Returns: :class:`rich.Text` or None: Representation with a hyperlink to the source. If the introspection failed, it returns :obj:`None`. """ try: f_path = _safe_getfile(function) f_line = inspect.getsourcelines(function)[1] uri = f"file://{f_path}#{f_line}" # Compress the path. home_path = os.path.expanduser("~") f_path = f_path.replace(home_path, "~") # Underline file name. f_name = os.path.basename(f_path) f_path = ( Text(os.path.dirname(f_path), style=path_style) + Text("/") + Text(f_name, style=file_style) ) f_path.append_text(Text(f":{f_line}")) f_path.stylize(f"link {uri}") return f_path except OSError: # pragma: no cover return None
[docs] def repr_pyfunction(f: Callable[..., Any]) -> Text: """Create a :class:`rich.Text` object representation a function, including a link to the source definition created with :func:`repr_source_path`. Args: f (Callable[..., Any]): A function. Returns: :class:`rich.Text`: Representation of `f`. """ res = Text(repr(f)) source = repr_source_path(f) if source is not None: res.append(" @ ") res.append_text(source) return res
######################## # Rich class decorator # ######################## def __repr_from_rich__(self: object, /) -> str: """Default implementation of `__repr__` that calls :mod:`rich`. Returns: str: Representation of `self.` """ console = rich.get_console() with console.capture() as capture: console.print(self, end="") res: str = capture.get() return res def _repr_mimebundle_from_rich_( self: object, include: Iterable[str], exclude: Iterable[str], **kw_args: Any ) -> dict[str, str]: """Implementation of `_repr_mimebundle_` for better rendering in Jupyter. Args: include (Iterable[str]): Only these MIME types should be included. exclude (Iterable[str]): These MIME types should be excluded. **kw_args (object): Additional keyword arguments. These are ignored. Returns: dict[str, str]: Representation by MIME type. """ from rich.jupyter import _render_segments console = rich.get_console() segments = list(console.render(self, console.options)) html = _render_segments(segments) text = console._render_buffer(segments) data = {"text/plain": text, "text/html": html} if include: data = {k: v for (k, v) in data.items() if k in include} if exclude: data = {k: v for (k, v) in data.items() if k not in exclude} return data @overload def rich_repr(cls: type[T], str: bool = False) -> type[T]: ... @overload def rich_repr(cls: None = None, str: bool = False) -> Callable[[type[T]], type[T]]: ...
[docs] def rich_repr( cls: type[T] | None = None, str: bool = False ) -> type[T] | Callable[[type[T]], type[T]]: """Class decorator defining a `__repr__` method that calls :mod:`rich.` This also sets `_repr_mimebundle_` for better rendering in Jupyter. Args: cls (type, optional): Class to decorate. If `None`, this function returns a decorator. str (bool, optional): Also define `__str__`. Defaults to not defining `__str__` Returns: The decorated class. If cls is None, returns a decorator. """ if cls is None: return partial(rich_repr, str=str) cls.__repr__ = __repr_from_rich__ # type: ignore[method-assign] cls._repr_mimebundle_ = _repr_mimebundle_from_rich_ # type: ignore[attr-defined] if str: cls.__str__ = __repr_from_rich__ # type: ignore[method-assign] return cls