Skip to content

Commit 35d9f40

Browse files
committed
Change 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! Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>
1 parent 5a8acf1 commit 35d9f40

11 files changed

Lines changed: 210 additions & 151 deletions

File tree

docs/source/api/for_backends.rst

Lines changed: 38 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 is ``module:path/to/entrypoint.toml`` rather than
17+
the typical pattern of ``module.submodule:value``.
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 right).
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ content-type = "text/markdown"
6666
path = "README.md"
6767

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

7276
[tool.pytest.ini_options]
7377
minversion = "6.0"

src/spatch/__main__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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(
9+
help='subcommand help', required=True, dest="subcommand")
10+
11+
update_entrypoint_cmd = subparsers.add_parser(
12+
'update-entrypoints',
13+
help='update the entrypoint toml file')
14+
update_entrypoint_cmd.add_argument(
15+
"paths", type=str, nargs="+",
16+
help="paths to the entrypoint toml files to update")
17+
18+
args = parser.parse_args()
19+
20+
assert args.subcommand == "update-entrypoints"
21+
for path in args.paths:
22+
update_entrypoint(path)
23+
24+
25+
if __name__ == "__main__":
26+
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 object that to query
10+
backend = "spatch._spatch_example.backend:backend1"
11+
# Modules to load (including submodules) to ensure all functions are initialized
12+
modules = ["spatch._spatch_example.backend"]
13+
14+
[functions.defaults]
15+
# Options here will be defaults for all functions.
16+
uses_context = true
17+
18+
[functions."spatch._spatch_example.library:divide"]
19+
function = "spatch._spatch_example.backend:divide"
20+
should_run = "spatch._spatch_example.backend:divide._should_run"
21+
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 really needed (already backend does this)
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.
15+
"""

src/spatch/backend_system.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import contextvars
22
import dataclasses
33
import functools
4+
import importlib.util
45
import os
56
import sys
67
import textwrap
78
import warnings
89
from collections.abc import Callable
910
from dataclasses import dataclass
10-
from types import MethodType
11+
from types import MethodType, SimpleNamespace
1112
from typing import Any
1213

1314
import importlib_metadata
15+
import tomllib
1416

1517
from spatch import from_identifier, get_identifier
1618
from spatch.utils import EMPTY_TYPE_IDENTIFIER, TypeIdentifier, valid_backend_name
@@ -588,7 +590,29 @@ def _get_entry_points(group, blocked):
588590
if ep.name in blocked:
589591
continue
590592
try:
591-
namespace = ep.load()
593+
mod, _, filename = ep.value.partition(":")
594+
if "." in mod:
595+
raise RuntimeError(
596+
f"Entrypoint {ep.name} has a module name with a dot: '{mod}'. "
597+
"It must use a top-level module and include submodules as path."
598+
)
599+
600+
spec = importlib.util.find_spec(mod)
601+
reader = spec.loader.get_resource_reader(spec.name)
602+
with reader.open_resource(filename) as f:
603+
backend_info = tomllib.load(f)
604+
605+
# We allow a `functions.defaults` field, apply them here to all functions
606+
# and clean up the special fields (defaults and auto-generation).
607+
backend_info["functions"].pop("auto-generation", None) # no need to propagate
608+
defaults = backend_info["functions"].pop("defaults", None)
609+
if defaults:
610+
functions = backend_info["functions"]
611+
for fun in functions:
612+
functions[fun] = {**defaults, **functions[fun]}
613+
614+
# We use a namespace internally right now (convenient for in-code backends/tests)
615+
namespace = SimpleNamespace(**backend_info)
592616
if ep.name != namespace.name:
593617
raise RuntimeError(
594618
f"Entrypoint name {ep.name!r} and actual name {namespace.name!r} mismatch."

0 commit comments

Comments
 (0)