Skip to content

geno_lewm.api

api

Public-API lifetime decorators (RFC-0014 §3.3, §3.6).

Two decorators mark stability on the public surface:

  • :func:experimental — wraps a function or class that may change without notice. Emits :class:FutureWarning once per process per decorated object on first call / instantiation.
  • :func:deprecated — wraps a function or class scheduled for removal. Emits :class:DeprecationWarning once per process per call site (i.e. once per (file, line) of the calling code). Takes a free-text reason describing what replaces the deprecated API.

Both decorators preserve __name__, __doc__, __module__, __qualname__, __wrapped__, and (for classes) the original attribute surface, so mypy and IDE introspection are unaffected.

experimental

experimental(obj: F) -> F
experimental(*, reason: str = '') -> Callable[[F], F]
experimental(obj: Any = None, *, reason: str = '') -> Any

Mark obj (a function or class) as experimental.

Usage::

@experimental
def f(...): ...

@experimental(reason="API shape under review")
class C: ...

Emits :class:FutureWarning once per process when the decorated object is first invoked (function) or instantiated (class). Later calls are silent.

Source code in geno_lewm/api.py
def experimental(obj: Any = None, *, reason: str = "") -> Any:
    """Mark ``obj`` (a function or class) as experimental.

    Usage::

        @experimental
        def f(...): ...

        @experimental(reason="API shape under review")
        class C: ...

    Emits :class:`FutureWarning` once per process when the decorated
    object is first invoked (function) or instantiated (class). Later
    calls are silent.
    """

    def _decorate(target: Any) -> Any:
        msg = f"{target.__module__}.{target.__qualname__} is experimental and may change without notice."
        if reason:
            msg += f" {reason}"

        if inspect.isclass(target):
            original_init = target.__init__
            sentinel = id(target)

            @functools.wraps(original_init)
            def __init__(self: Any, *args: Any, **kwargs: Any) -> None:  # noqa: N807
                _emit_once(sentinel, FutureWarning, msg, stacklevel=2)
                original_init(self, *args, **kwargs)

            setattr(target, "__init__", __init__)  # noqa: B010
            target.__geno_lewm_experimental__ = True
            return target

        sentinel_func = id(target)

        @functools.wraps(target)
        def _wrap(*args: Any, **kwargs: Any) -> Any:
            _emit_once(sentinel_func, FutureWarning, msg, stacklevel=2)
            return target(*args, **kwargs)

        _wrap.__geno_lewm_experimental__ = True  # type: ignore[attr-defined]
        return _wrap

    # Distinguish bare ``@experimental`` from parameterised ``@experimental(reason=…)``.
    if obj is not None and callable(obj):
        return _decorate(obj)
    return _decorate

deprecated

deprecated(reason: str = '') -> Callable[[F], F]

Mark obj as deprecated.

Usage::

@deprecated("use new_thing() instead; removed in v0.3")
def old_thing(...): ...

Emits :class:DeprecationWarning once per process per call site — that is, once per (filename, lineno) of the calling code. Multiple call sites all warn; the same site warns only once.

reason is appended to the warning message; pass a short actionable string ("use X instead").

Source code in geno_lewm/api.py
def deprecated(reason: str = "") -> Callable[[F], F]:
    """Mark ``obj`` as deprecated.

    Usage::

        @deprecated("use new_thing() instead; removed in v0.3")
        def old_thing(...): ...

    Emits :class:`DeprecationWarning` once per process **per call
    site** — that is, once per ``(filename, lineno)`` of the calling
    code. Multiple call sites all warn; the same site warns only once.

    ``reason`` is appended to the warning message; pass a short
    actionable string ("use X instead").
    """
    if not isinstance(reason, str):
        from geno_lewm.errors import InputError  # type: ignore[unreachable]

        raise InputError(
            "deprecated(reason) must be str",
            details={"type": type(reason).__name__},
        )

    def _decorate(target: F) -> F:
        base_msg = f"{target.__module__}.{target.__qualname__} is deprecated."
        if reason:
            base_msg += f" {reason}"

        is_class = inspect.isclass(target)
        if is_class:
            original_init: Callable[..., None] = target.__init__

            @functools.wraps(original_init)
            def __init__(self: Any, *args: Any, **kwargs: Any) -> None:  # noqa: N807
                file, line = _caller_site(depth=2)
                _emit_once(
                    (id(target), file, line),
                    DeprecationWarning,
                    base_msg,
                    stacklevel=3,
                )
                original_init(self, *args, **kwargs)

            setattr(target, "__init__", __init__)  # noqa: B010
            target.__geno_lewm_deprecated__ = True  # type: ignore[attr-defined]
            return target

        @functools.wraps(target)
        def _wrap(*args: Any, **kwargs: Any) -> Any:
            file, line = _caller_site(depth=2)
            _emit_once(
                (id(target), file, line),
                DeprecationWarning,
                base_msg,
                stacklevel=3,
            )
            return target(*args, **kwargs)

        _wrap.__geno_lewm_deprecated__ = True  # type: ignore[attr-defined]
        return cast(F, _wrap)

    return _decorate