.. _entry-points: Entry Points ============ ``fleche`` is extensible through Python *entry points* (the standard :mod:`importlib.metadata` plugin mechanism): any installed package can register digest hooks for its types in the ``fleche`` entry point group under the name ``digest``. Installing such a package is all it takes — no imports, no calls to :func:`~fleche.digest.add_hook`. The first time :func:`~fleche.digest.digest` encounters a value it does not know how to handle, it loads all registered entry points and retries. Use entry points whenever digest support for a type should "just work" after a ``pip install``: * as a *plugin package* that adds ``fleche`` support for a third-party library (see :ref:`fleche-ase` below), or * as a *library author* shipping ``fleche`` support for your own types alongside your library. .. _fleche-ase: ``fleche-ase``: ASE Support --------------------------- `fleche-ase `_ is the canonical entry point plugin. It registers digest hooks for the core types of the `Atomic Simulation Environment (ASE) `_: * ``ase.Atoms`` — digested from the cell, the periodic boundary conditions and all per-atom arrays (positions, atomic numbers, ...). * ``ase.vibrations.VibrationsData`` — digested from the underlying atoms, the vibrational indices and the 2D Hessian. * ``ase.calculators.calculator.Calculator`` — digested from the calculator class name and its ``todict()`` parameters; matches all calculator subclasses. With it installed, ``@fleche()``-decorated functions can accept and return ASE objects with no further setup: .. code-block:: bash pip install fleche-ase .. code-block:: python from ase.build import bulk from fleche import fleche @fleche() def relax(structure): # an ase.Atoms — digested via fleche-ase ... relax(bulk("Al")) How Hooks Are Loaded -------------------- Entry points are loaded *lazily*: to avoid import overhead, nothing happens until :func:`~fleche.digest.digest` raises :class:`~fleche.digest.Indigestible` for a value. At that point ``fleche``: 1. loads all entry points registered under the group ``fleche`` with the name ``digest``, and 2. retries the digestion. Hooks match with ``isinstance``, so a hook registered for a base class (like ``fleche-ase``'s ``Calculator`` hook) also covers its subclasses. Precedence rules: * Hooks registered manually with :func:`~fleche.digest.add_hook` always take priority over entry point hooks; an ``INFO`` message is logged when one shadows the other. * If multiple entry points provide hooks for the same type, the first one encountered wins. * An entry point that fails to load is logged as an ``ERROR`` (with traceback) and skipped — it never breaks digestion of other values. Registering Your Own Entry Point -------------------------------- Declare the entry point in the ``fleche`` group with the name ``digest`` in your ``pyproject.toml`` (or equivalent): .. code-block:: toml [project.entry-points."fleche"] digest = "my_package.hooks:digest_hooks" The entry point must resolve to one of: * a single :class:`~fleche.digest.Hook` object, * a tuple of ``(Type, Callable)``, or * a list containing any combination of the above. These must be **module-level objects**, not callables that return hooks. ``fleche`` loads the entry point with ``importlib.metadata.EntryPoint.load()``, which returns the object at the given path directly — it does **not** call it. .. code-block:: pycon >>> from fleche.digest import digest, Digest, Hook >>> class TypeA: ... def __init__(self, field1, field2): ... self.field1 = field1 ... self.field2 = field2 >>> class TypeB: ... def __init__(self, relevant_field): ... self.relevant_field = relevant_field >>> def type_a_digest(obj: TypeA) -> Digest: ... return digest((type(obj).__name__, obj.field1, obj.field2)) >>> def type_b_digest(obj: TypeB) -> Digest: ... return digest((type(obj).__name__, obj.relevant_field)) >>> digest_hooks = [ ... Hook(TypeA, type_a_digest), ... (TypeB, type_b_digest), ... ] For guidance on writing the digest functions themselves (which fields to include, avoiding collisions), see :doc:`../dev/custom_digests`.