User API#

Users of libraries that integrate spatch should usually refer to the library documentation.

In general, the main interaction with spatch will be to modify dispatching behavior. We expect further inspection and modification API to be added in the future.

Modifying and tracing dispatching#

Libraries will re-expose all of this functionality under their own API/names.

There are currently three global environment variables to modify dispatching behavior at startup time:

  • <SPECIFIC_NAME>_PRIORITIZE: Comma seperated list of backends. This is the same as BackendOpts prioritize= option.

  • <SPECIFIC_NAME>_BLOCK: Comma seperated list of backends. This prevents loading the backends as if they were not installed. No backend code will be executed (its entry-point will not be loaded).

  • <SPECIFIC_NAME>_SET_ORDER: Comma seperated list of backend orders. seperated by >. I.e. name1>name2,name3>name2 means that name1 and name3 are ordered before name2. This is more fine-grained than the above two and the above two take precedence. Useful to fix relative order of backends without affecting the priority of backends not listed (including when backends have issues and loading fails).

Note that unknown backend names are ignored in these variables, so check these carefully.

The main interaction of users should however be via the backend options system:

class spatch.backend_system.BackendOpts(*, prioritize=(), disable=(), type=None, trace=False)#

Customize or query the backend dispatching behavior.

Context manager to allow customizing the dispatching behavior. Instantiating the context manager fetches the current dispatching state, modifies it as requested, and then stores the state. You can use this context multiple times (but not nested). enable_globally() can be used for convenience but should only be used from the main program.

Initializing BackendOpts without arguments can be used to query the current state.

See __call__() for information about use as a function decorator.

Warning

When modifying dispatching behavior you must be aware that this may have side effects on your program. See details in notes.

Parameters:
  • prioritize (str or list of str) – The backends to prioritize, this may also enable a backend that would otherwise never be chosen. Prioritization nests, outer prioritization remain active.

  • disable (str or list of str) – Specific backends to disable. This nests, outer disabled are still disabled (unless prioritized).

  • type (type) –

    A type to dispatch for. Functions will behave as if this type was used when calling (additionally to types passed). This is a way to enforce use of this type (and thus backends using it). But if used for a larger chunk of code it can clearly break type assumptions easily. (The type argument of a previous call is replaced.)

    Note

    If no version of a function exists that supports this type, then dispatching will currently fail. It may try without the type in the future to allow a graceful fallback.

  • trace (bool) –

    If True entering returns a list and this list will contain information for each call to a dispatchable function. (When nesting, an outer existing tracing is currently paused.)

    Note

    Tracing is considered for debugging/human readers and does not guarantee a stable API for the trace result.

backends#

Tuple of active backend names in order of priority. If type is set, not all will be applicable and type specialized backends have typically a lower priority since they will be chosen based on input types.

Type:

tuple of str

prioritized#

Frozenset of currently prioritized backends.

Type:

frozenset

type#

The type to dispatch for within this context.

trace#

The trace object (currenly a list as described in the examples). If used, the trace is also returned when entering the context.

Notes

Both prioritize and type can modify behavior of the contained block in significant ways.

For prioritize= this depends on the backend. A backend may for example result in lower precision results. Assuming no bugs, a backend should return roughly equivalent results.

For type= code behavior will change to work as if you were using this type. This will definitely change behavior. I.e. many functions may return the given type. Sometimes this may be useful to modify behavior of code wholesale.

Especially if you call a third party library, either of these changes may break assumptions in their code and while a third party may ensure the correct type for them locally it is not a bug for them not to do so.

Examples

This example is based on a hypothetical cucim backend for skimage:

>>> with skimage.backend_opts(prioritize="cucim"):
...     ...

Which might use cucim also for NumPy inputs (but return NumPy arrays then). (I.e. cucim would use the fact that it is prioritized here to decide it is OK to convert NumPy arrays – it could still defer for speed reasons.)

On the other hand:

>>> with skimage.backend_opts(type=cupy.ndarray):
...     ...

Would guarantee that we work with CuPy arrays that the a returned array is a CuPy array. Together with prioritize="cucim" it ensure the cucim version is used (otherwise another backend may be preferred if it also supports CuPy arrays) or cucim may choose to require prioritization to accept NumPy arrays.

Backends should simply document their behavior with backend_opts and which usage pattern they see for their users.

Tracing calls can be done using, where trace is a list of informations for each call. This contains a tuple of the function identifier and a list of backends called (typically exactly one, but it will also note if a backend deferred via should_run).

>>> with skimage.backend_opts(trace=True) as trace:
...     ...
__call__(func)#

Decorate a function to freeze its dispatching state.

BackendOpts can be used as a decorator, it means freezing the state early (user context around call is ignored). In other words, the following two patterns are very different, because for the decorator, backend_opts is called outside of the function:

@backend_opts(...)
def func():
    # code

def func():
    with backend_opts(...):
        # code

Note

An option here is to add isolated=False to allow mutating the context at call-time/time of entering (isolated=False would do nothing in a simple context manager use-case).

Parameters:

func (callable) – The function to decorate.

Returns:

func – The decorated function.

Return type:

callable

enable_globally()#

Apply these backend options globally.

Setting this state globally should only be done by the end user and never by a library. Global change of behavior may modify unexpected parts of the code (e.g. in third party code) so that it is safer to use the contextmanager with statement instead.

This method will issue a warning if the dispatching state has been previously modified programatically.