Skip to content

BenjaminBossan/versiondispatch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

versiondispatch

Goal

Make it easier to adopt your code to use different versions of a package. The API is similar to functools.singledispatch

Features

Dispatching on a single package version

Decorate the main (default) function with versiondispatch. Then, if you need specific behavior based on the version of a specific package, write that function and decorate it with .register(<version>) like shown below:

@versiondispatch
def foo():
    return "default behavior"

# the name of the registered function doesn't matter
@foo.register("sklearn>1.2")
def _():
    return "new behavior"

@foo.register("sklearn<1.0")
def _():
    return "old behavior"

foo()  # output depends on installed scikit-learn version

At function definition time, the decorator will check the actual version of (in this case) sklearn and make the correspondingly decorated function the one that will actually be used.

Dispatching on multiple package versions

It is possible to dispatch on the versions of multiple packages by enumerating them, separated by a comma:

@versiondispatch
def foo():
    return "default behavior"

@foo.register("sklearn<1.0")
def _():
    return "only sklearn is old"

@foo.register("numpy<1.0, sklearn<1.0")
def _():
    return "both numpy and sklearn are old"

If multiple conditions are matching, then the last condition takes precedence. So in this case if both sklearn and numpy are below v1.0, we would get “both numpy and sklearn are old”, and not “only sklearn is old”, even though that matches too.

Depending on how you define the functions, the last condition is not necessarily the most specific condition, so take care.

Dispatching on the Python version

It is possible to register different functions based on the Python version being used.

@versiondispatch
def show_items(list_a, list_b):
    # the strict argument for zip was introduced in Python 3.10
    for item_a, item_b in zip(list_a, list_b, strict=True):
        print(item_a, item_b)

@show_items.register("Python<3.10")
def _(list_a, list_b):
    # older versions of Python need to check explicitly
    if len(list_a) != len(list_b):
        raise ValueError("zip arguments don't have same length")
    for item_a, item_b in zip(list_a, list_b, strict=True):
        print(item_a, item_b)

Dispatching based on operating system

The special key "os" is reserved for checking the operating system. Only equality tests are supported for that.

@versiondispatch
def func():
    return "Linux"

@func.register("os==win32")
def _():
    return "Windows"

@func.register("os==Darwin")
def _():
    return "MacOS"

Optional warnings

It is possible to register warnings for specific versions. These warnings are shown to the user in case their version matches with the registerd version.

@versiondispatch
def foo():
    # no warning here
    return "default behavior"

msg = "You are using an old sklearn version, which will not be supported after the next release"

@foo.register("sklearn<1.0", warning=DeprecationWarning(msg))
def _():
    # if this is called, there will be a warning
    return "old behavior"

# if, and only if, a user calls foo with an old sklearn version, the DeprecationWarning is shown
foo()

Installation

There is no PyPI package for now. The best way to use this is to copy the contents of src.py into your own project and use it from there (“vendoring”).

Rationale

In library code, it is often desired to support different verions of its dependencies to make it easy for many users to use that pacakge. In some situations, however, behavior changes depending on the version of a dependency. In that case, code can easily become quite messy, with a lot of code like this:

import some_lib

def foo():
    if some_lib.__version__.startswith("0."):
        # do something
    elif some_lib.__version__ == "1.0.0":
        # do something else
    else:
        # do yet something else

This can become cumbersome quite quickly. The versiondispatch decorator allows to cleanly separate the functions. When, eventually, a version is no longer supported, it’s as easy as deleting the whole function with the corresponding decorator, no surgical extraction from if conditions necessary. Also, versiondispatch handles version comparison for you, which otherwise requires third party packages.

Apart from a lack of readability, a disadvantage of the example above is that the version check is performed each time the function is called, even though at runtime, the version of a package (normally) never changes. Yes, it would be possible to re-write the example to cache the check, but then the code gets even messier and more error prone. versiondispatch checks the version only once, when the function is defined – after that it statically dispatches to the desired function.

In contrast, if you write application code with all dependency versions being pinned, it would not make sense to use versiondispatch.

Development

To help development, follow these steps:

Installation

  • clone and check out the repo
  • create a virtual environment with the tool of your choice
  • install development dependencies:

python -m pip install -r requirements-dev.txt

Run checks

# run the unit tests:
pytest --cov --cov-report=term-missing test.py
# run mypy
mypy --strict src.py
# run black
black *.py
# run ruff
ruff .

TODOs

Under consideration to be implemented:

Special keys

Environment variables

It might be nice to be able to check env vars, even if only for exact equality, like @foo.register("$LANG==en_US.UTF-8")

Version inequality

Add support for the != operator (PEP440)

More checks on indicated versions

It would be nice if version checks that don’t make sense are caught at function definition time, like:

@versiondispatch
def foo():
  return "default behavior"

@foo.register("sklearn<1.0, sklearn>2.0")
def _():
  return "can never be reached"

Coverage

If feasible (probably it’s not), tell coverage which functions should be ignored for line coverage because they are meant for a different package version.

General niceties

  • distribute on PyPI
  • set up CI
  • tidy up the repo
  • badges

About

Dispatch functions based on the version of installed packages

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages