Decorators and Descriptors

The tet.decorators module collects a small set of general-purpose helpers that are useful throughout a Tet application: a decorator for marking APIs as deprecated and a reify-style cached-property descriptor that is aware of the attribute name it is bound to.

Both helpers are importable directly from the package:

from tet.decorators import deprecated, reify_attr

The module is deliberately tiny and dependency-free, so it is safe to use in library code, models, services, and views alike.

deprecated

deprecated() is a function decorator that marks a callable as deprecated. The wrapped function continues to work exactly as before, but every call now emits a DeprecationWarning before delegating to the original implementation.

Use it when you want to retire a function or method but cannot remove it yet because callers still depend on it. The warning gives downstream code a clear signal (and, in test suites that turn warnings into errors, a hard failure) without breaking runtime behaviour.

Signature

deprecated(func)

It takes a single callable and returns a wrapper with the same name, docstring, and __dict__ as the original. The warning message is built from the function’s __qualname__, so it correctly identifies methods nested in classes (e.g. MyService.old_method).

Behaviour

When the wrapped function is called, it issues:

Call to deprecated function <qualname>.

The warning is raised with stacklevel=2, which means the warning is attributed to the caller of the deprecated function rather than to the wrapper inside tet.decorators. That makes the warning point at the line of code that actually needs to change.

Note

Python silences DeprecationWarning by default outside of __main__ and test runners. To see the warnings during development, run Python with -W default::DeprecationWarning or configure the warnings filter explicitly. Most test runners (including pytest) surface these warnings out of the box.

Example

from tet.decorators import deprecated


@deprecated
def render_legacy_template(name):
    """Old rendering path; use render_template() instead."""
    return _legacy_render(name)


# Calling it still works, but emits:
#   DeprecationWarning: Call to deprecated function render_legacy_template.
html = render_legacy_template("home")

It works equally well on methods, where __qualname__ produces a fully qualified name in the warning:

class ReportService:
    @deprecated
    def export_csv(self, report):
        # Warning text: "Call to deprecated function ReportService.export_csv."
        return self._export(report, fmt="csv")

reify_attr

reify_attr is a cached-property descriptor. The first time the attribute is accessed on an instance, the wrapped function is called and its return value is computed; that value is then written back onto the instance so that subsequent accesses read a plain attribute and never call the function again.

It is similar in spirit to Pyramid’s pyramid.decorator.reify, but with one key difference: reify_attr caches under the name the descriptor is bound to on the class, not the name of the decorated method. This matters when the descriptor is assigned to an attribute whose name differs from the wrapped function, or assigned dynamically. It is intended as a building block for descriptors such as autowired in pyramid_di, which need to know their own attribute name in order to cache the resolved value on the instance.

Signature

class reify_attr:
    def __init__(self, wrapped): ...

It is used as a decorator on a method that takes self and returns the value to cache. The descriptor copies the wrapped function’s metadata via functools.update_wrapper().

How caching and name resolution work

  • Name discovery via ``__set_name__``: when the descriptor is created as part of a class body, Python calls __set_name__ and reify_attr records the attribute name (or names, if the same descriptor object is bound to several attributes).

  • Fallback discovery: if __set_name__ was never called (for example, the descriptor was attached to the class dynamically after definition), the name is discovered lazily on first access by scanning the owner class’s __dict__ for the attributes that point at this descriptor.

  • Write-back on access: on first __get__ for an instance, the wrapped function is invoked and the result is stored on the instance under every resolved name via setattr(). Because instance attributes shadow class descriptors for non-data descriptors, later accesses return the cached value directly without invoking the function again.

  • Class access: accessing the attribute on the class itself (inst is None) returns the descriptor object rather than computing a value.

Note

reify_attr is a non-data descriptor (it defines __get__ but not __set__), which is exactly what allows the instance attribute written on first access to take precedence on subsequent reads. If you need to force recomputation, delete the cached instance attribute (del inst.name).

Example

from tet.decorators import reify_attr


class Report:
    def __init__(self, rows):
        self.rows = rows

    @reify_attr
    def summary(self):
        # Computed once, then cached on the instance as `summary`.
        print("computing summary...")
        return {
            "count": len(self.rows),
            "total": sum(r.amount for r in self.rows),
        }


report = Report(load_rows())
report.summary  # prints "computing summary..." and computes the dict
report.summary  # returns the cached dict; no print, no recompute

Because caching uses the bound attribute name, you can rely on the cached value living under the attribute you actually access, which is what makes it suitable for descriptor-composition patterns like dependency-injection autowired fields.

See also

  • Utilities – the broader set of helper utilities in tet.util (cryptography, base64, collections, paths, and JSON helpers).

  • Configuration – configuring a Tet application and including Tet components via config.include(...).