Make it easier to adopt your code to use different versions of a package. The API is similar to functools.singledispatch
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.
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.
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)
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"
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()
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”).
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
.
- 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 the unit tests:
pytest --cov --cov-report=term-missing test.py
# run mypy
mypy --strict src.py
# run black
black *.py
# run ruff
ruff .
Under consideration to be implemented:
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")
Add support for the !=
operator (PEP440)
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"
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.
- distribute on PyPI
- set up CI
- tidy up the repo
- badges