Skip to content

Commit b77e1ec

Browse files
sebergeriknw
andauthored
Change entry-points to be toml files (#28)
Changes entry-points to be toml files This also changes the auto-generation from decorator version to create the entry-point files to use: ``` python -m spatch update-entrypoints file [file ...] ``` if you add the special: ``` [functions.auto-generation] backend = "spatch._spatch_example.backend:backend1" modules = ["spatch._spatch_example.backend"] ``` section to the entry-point file itself. I really like this, plus the old abuse of black to format things nicely is fun. TIL: `tomlkit` is really cool about editing toml files! Note that we assume that `module` is the main `module` and any `.submodule` is just a subdirectory within the same package. I am not 100% sure if this is right, but since some tooling rejects ``/`` in entry-point values (to the extend that they break ALL entry-point loading, not just ours!), we really can't rely on ``/``. I am not 100% sure that this is always correct (i.e. I feel subpackages are a thing now in principle, and those might not follow a directory structure always?). But, it seems practical enough, unless the entry-point validation is changed. (Or unless the packaging people explain why this is horrifying.) * Apply suggestions from code review * More fixes from review Co-authored-by: Erik Welch <[email protected]> --------- Signed-off-by: Sebastian Berg <[email protected]> Co-authored-by: Erik Welch <[email protected]>
1 parent bb15fcd commit b77e1ec

File tree

13 files changed

+235
-164
lines changed

13 files changed

+235
-164
lines changed

docs/source/api/for_backends.rst

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ Backend API
33

44
Backends have to do the heavy lifting of using spatch.
55
At the moment we suggest to check the
6-
`example <https://github.com/scientific-python/spatch/tree/main/spatch/_spatch_example>`_.
6+
`example <https://github.com/scientific-python/spatch/tree/main/src/spatch/_spatch_example>`_.
77

88
Entry point definition
99
----------------------
10-
To extend an existing library with a backend, you need to define a `Python entry-point <https://packaging.python.org/en/latest/specifications/entry-points/>`_.
10+
To extend an existing library with a backend, you need to define a
11+
`Python entry-point <https://packaging.python.org/en/latest/specifications/entry-points/>`_.
1112
This entry point includes the necessary information for spatch to find and
1213
dispatch to your backend.
1314

15+
``spatch`` entry-points are TOML files and *not* Python objects.
16+
The entry-point value must point to the file with
17+
``module.submodule:filename.toml``. [#ep-value-structure]_
18+
1419
Before writing a backend, you need to think about a few things:
1520

1621
* Which types do you accept? This could be NumPy, dask, jax, etc. arrays.
@@ -25,11 +30,11 @@ Before writing a backend, you need to think about a few things:
2530
In that case, your backend should likely only be used if prioritized
2631
by the user.
2732

28-
Please check the example linked above. These example entry-points include
29-
code that means running them modifies them in-place if the ``@implements``
30-
decorator is used (see next section).
33+
Please check the example linked above. ``spatch`` can automatically update
34+
the functions entries in these entry-points if the ``@implements`` decorator
35+
is used (see next section).
3136

32-
Some of the most important things are:
37+
Some of the most important fields are:
3338

3439
``name``
3540
^^^^^^^^
@@ -55,13 +60,14 @@ However, we do support the following, e.g.:
5560
- ``"~numpy:ndarray"`` to match any subclass of NumPy arrays
5661
- ``"@module:qualname"`` to match any subclass of an abstract base class
5762

58-
If you use an abstract base class, note that you must take a lot of care:
63+
.. warning::
64+
If you use an abstract base class, note that you must take additional care:
5965

60-
- The abstract base class must be cheap to import, because we cannot avoid
61-
importing it.
62-
- Since we can't import all classes, ``spatch`` has no ability to order abstract
63-
classes correctly (but we order them last if a primary type, which is typically right).
64-
- ``spatch`` will not guarantee correct behavior if an ABC is mutated at runtime.
66+
- The abstract base class must be *cheap to import*, because we cannot avoid
67+
importing it.
68+
- Since we can't import all classes, ``spatch`` has no ability to order abstract
69+
classes correctly (but we order them last if a primary type, which is typically correct).
70+
- ``spatch`` will not guarantee correct behavior if an ABC is mutated at runtime.
6571

6672
``requires_opt_in``
6773
^^^^^^^^^^^^^^^^^^^
@@ -93,19 +99,36 @@ functions
9399
^^^^^^^^^
94100

95101
A mapping of library functions to your implementations. All fields use
96-
the ``__module__:__qualname__`` identifiers to avoid immediate import.
102+
the ``__module__:__qualname__`` identifiers.
97103
The following fields are supported for each function:
98104

99105
- ``function``: The implementation to dispatch to.
100106
- ``should_run`` (optional): A function that gets all inputs (and context)
101107
and can decide to defer. Unless you know things will error, try to make sure
102108
that this function is light-weight.
103-
- ``uses_context``: Whether the implementation needs a ``DispatchContext``.
109+
- ``uses_context`` (optional): Whether the implementation needs a ``DispatchContext``.
104110
- ``additional_docs`` (optional): Brief text to add to the documentation
105111
of the original function. We suggest keeping this short but including a
106112
link, but the library guidance should be followed.
107113

108-
``spatch`` provides tooling to help create this mapping.
114+
A typical part of the entry-point TOML will look like this::
115+
116+
[functions."skimage.filters:gaussian"]
117+
function = "cucim.skimage.filters:gaussian"
118+
uses_context = true
119+
additional_docs = "CUDA enabled version..."
120+
121+
An additional ``[functions.defaults]`` key can be added to set defaults for all
122+
functions and avoid repeating e.g. ``uses_context``.
123+
124+
``spatch`` provides tooling to help create this mapping. This tooling uses the
125+
additional fields::
126+
127+
[functions.auto-generation]
128+
# where to find the BackendImplementation:
129+
backend = "module.submodule:backend_name"
130+
# Additional modules to be imported (to ensure all functions are found):
131+
modules = ["spatch._spatch_example.backend"]
109132

110133
Manual backend prioritization
111134
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -127,6 +150,13 @@ More?
127150
maybe a version? (I don't think we generally need it, but it may be
128151
interesting.)
129152

153+
.. [#ep-value-structure]
154+
``spatch`` currently assumes submodules follow directory structure so that
155+
the file is located relative to the main ``module`` at
156+
``module/submodule/filename.toml``. As of 2025, do *not* use ``/`` in
157+
the filename, since the entry-point value may be checked for being a
158+
valid Python object ``__qualname__``, which a ``/`` is not.
159+
130160
Implementations for dispatchable functions
131161
------------------------------------------
132162

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import annotations
22

3-
import importlib_metadata
3+
import importlib.metadata
44

55
project = "spatch"
66
copyright = "2025, Spatch authors"
77
author = "Spatch authors"
8-
version = release = importlib_metadata.version("spatch")
8+
version = release = importlib.metadata.version("spatch")
99

1010
extensions = [
1111
"myst_parser",

pyproject.toml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ description = "Coming soon: Python library for enabling dispatching to backends"
1010
readme = "README.md"
1111
license = "BSD-3-Clause"
1212
requires-python = ">=3.10"
13-
dependencies = ["importlib_metadata"]
1413
classifiers = [
1514
"Development Status :: 3 - Alpha",
1615
"Environment :: Console",
@@ -19,7 +18,6 @@ classifiers = [
1918
"Operating System :: OS Independent",
2019
"Programming Language :: Python",
2120
"Programming Language :: Python :: 3",
22-
"Programming Language :: Python :: 3.10",
2321
"Programming Language :: Python :: 3.11",
2422
"Programming Language :: Python :: 3.12",
2523
"Programming Language :: Python :: 3.13",
@@ -28,6 +26,7 @@ classifiers = [
2826
"Topic :: Software Development :: Libraries :: Python Modules",
2927
]
3028
keywords = ["dispatching"]
29+
dependencies = ['tomli ; python_version < "3.11"']
3130

3231
[project.urls]
3332
homepage = "https://github.com/scientific-python/spatch"
@@ -36,8 +35,8 @@ source = "https://github.com/scientific-python/spatch"
3635
changelog = "https://github.com/scientific-python/spatch/releases"
3736

3837
[dependency-groups]
39-
# black is used to format entry-point files
40-
backend_utils = ["black"]
38+
# tomlkit is used to format entry-point files
39+
backend_utils = ["tomlkit"]
4140
test = [
4241
"pytest >=6",
4342
"pytest-cov >=3",
@@ -66,8 +65,12 @@ content-type = "text/markdown"
6665
path = "README.md"
6766

6867
[project.entry-points._spatch_example_backends]
69-
backend1 = 'spatch._spatch_example.entry_point'
70-
backend2 = 'spatch._spatch_example.entry_point2'
68+
backend1 = 'spatch._spatch_example:entry_point.toml'
69+
backend2 = 'spatch._spatch_example:entry_point2.toml'
70+
71+
[[tool.spatch.update_functions]]
72+
backend1 = "spatch._spatch_example.backend"
73+
backend2 = "spatch._spatch_example.backend"
7174

7275
[tool.pixi.workspace]
7376
channels = ["https://prefix.dev/conda-forge"]

src/spatch/__main__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import argparse
2+
3+
from .backend_utils import update_entrypoint
4+
5+
6+
def main():
7+
parser = argparse.ArgumentParser(prog="python -m spatch")
8+
subparsers = parser.add_subparsers(help="subcommand help", required=True, dest="subcommand")
9+
10+
update_entrypoint_cmd = subparsers.add_parser(
11+
"update-entrypoints", help="update the entrypoint toml file"
12+
)
13+
update_entrypoint_cmd.add_argument(
14+
"paths", type=str, nargs="+", help="paths to the entrypoint toml files to update"
15+
)
16+
17+
args = parser.parse_args()
18+
19+
if args.subcommand == "update-entrypoints":
20+
for path in args.paths:
21+
update_entrypoint(path)
22+
else:
23+
raise RuntimeError("unreachable: subcommand not known.")
24+
25+
26+
if __name__ == "__main__":
27+
main()

src/spatch/_spatch_example/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ The "library" contains only:
1414
designed for only `int` inputs.
1515

1616
We then have two backends with their corresponding definitions in `backend.py`.
17-
The entry-points are `entry_point.py` and `entry_point2.py` and these files
18-
can be run to generate their `functions` context (i.e. if you add more functions).
17+
The entry-points are `entry_point.toml` and `entry_point2.toml`. When code changes,
18+
these can be updated via `python -m spin update-entrypoints *.toml`
19+
(the necessary info is in the file itself).
1920

2021
For users we have the following basic capabilities. Starting with normal
2122
type dispatching.

src/spatch/_spatch_example/entry_point.py

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name = "backend1"
2+
primary_types = ["builtins:float", "builtins:int"]
3+
secondary_types = []
4+
requires_opt_in = false
5+
6+
# higher_priority_than = ["default"]
7+
8+
[functions.auto-generation]
9+
# `backend = BackendImplementation` object used for `@backend.implements()`
10+
backend = "spatch._spatch_example.backend:backend1"
11+
# Modules to load (including submodules) to ensure all functions are imported.
12+
# `spatch` will also import all submodules.
13+
modules = ["spatch._spatch_example.backend"]
14+
15+
[functions.defaults]
16+
# Options here will be defaults for all functions.
17+
uses_context = true
18+
19+
[functions."spatch._spatch_example.library:divide"]
20+
function = "spatch._spatch_example.backend:divide"
21+
should_run = "spatch._spatch_example.backend:divide._should_run"
22+
additional_docs = """This implementation works well on floats."""

src/spatch/_spatch_example/entry_point2.py

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name = "backend2"
2+
primary_types = ["builtins:float", "builtins:complex"]
3+
secondary_types = ["builtins:int"]
4+
requires_opt_in = false
5+
6+
[functions.auto-generation]
7+
backend = "spatch._spatch_example.backend:backend2"
8+
# `modules` not needed (backend import loads all functions)
9+
10+
[functions."spatch._spatch_example.library:divide"]
11+
function = "spatch._spatch_example.backend:divide2"
12+
should_run = "spatch._spatch_example.backend:divide2._should_run"
13+
additional_docs = """This is a test backend!
14+
and it has a multi-line docstring which makes this longer than normal."""

src/spatch/backend_system.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import contextvars
22
import dataclasses
33
import functools
4+
import importlib.metadata
5+
import importlib.util
46
import os
7+
import pathlib
58
import sys
69
import textwrap
710
import warnings
811
from collections.abc import Callable
912
from dataclasses import dataclass
10-
from types import MethodType
13+
from types import MethodType, SimpleNamespace
1114
from typing import Any
1215

13-
import importlib_metadata
14-
1516
from spatch import from_identifier, get_identifier
1617
from spatch.utils import EMPTY_TYPE_IDENTIFIER, TypeIdentifier, valid_backend_name
1718

19+
try:
20+
import tomllib
21+
except ImportError:
22+
import tomli as tomllib # for Python 3.10 support
23+
1824
__doctest_skip__ = ["BackendOpts.__init__"]
1925

2026

@@ -583,12 +589,34 @@ def _get_entry_points(group, blocked):
583589
return []
584590

585591
backends = []
586-
eps = importlib_metadata.entry_points(group=group)
592+
eps = importlib.metadata.entry_points(group=group)
587593
for ep in eps:
588594
if ep.name in blocked:
589595
continue
590596
try:
591-
namespace = ep.load()
597+
mod, _, filename = ep.value.partition(":")
598+
# As of writing, entry-points are assumed to be valid Python
599+
# names (e.g. no `/`). But we can presumably assume that the
600+
# submodules follow a directory structure (at least in practice).
601+
# See also https://github.com/python/importlib_metadata/issues/523
602+
mod, *path = mod.split(".")
603+
604+
spec = importlib.util.find_spec(mod)
605+
reader = spec.loader.get_resource_reader(spec.name)
606+
with reader.open_resource(pathlib.Path(*path, filename)) as f:
607+
backend_info = tomllib.load(f)
608+
609+
# We allow a `functions.defaults` field, apply them here to all functions
610+
# and clean up the special fields (defaults and auto-generation).
611+
backend_info["functions"].pop("auto-generation", None) # no need to propagate
612+
defaults = backend_info["functions"].pop("defaults", None)
613+
if defaults:
614+
functions = backend_info["functions"]
615+
for fun in functions:
616+
functions[fun] = {**defaults, **functions[fun]}
617+
618+
# We use a namespace internally right now (convenient for in-code backends/tests)
619+
namespace = SimpleNamespace(**backend_info)
592620
if ep.name != namespace.name:
593621
raise RuntimeError(
594622
f"Entrypoint name {ep.name!r} and actual name {namespace.name!r} mismatch."

0 commit comments

Comments
 (0)