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:
@versiondispatchdeffoo():
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 versionAt 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:
@versiondispatchdeffoo():
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.
@versiondispatchdefshow_items(list_a, list_b):
# the strict argument for zip was introduced in Python 3.10foritem_a, item_binzip(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 explicitlyiflen(list_a) !=len(list_b):
raiseValueError("zip arguments don't have same length")
foritem_a, item_binzip(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.
@versiondispatchdeffunc():
return"Linux"@func.register("os==win32")def_():
return"Windows"@func.register("os==Darwin")def_():
return"MacOS"Environment variables can be checked by prefixing them with a “$“. Only equality tests are supported for that.
@versiondispatchdeffunc():
return"default"@func.register("$LANG==en_US.UTF-8")def_():
return"English"@func.register("$LANG==de_DE.UTF-8")def_():
return"German"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.
@versiondispatchdeffoo():
# no warning herereturn"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 warningreturn"old behavior"# if, and only if, a user calls foo with an old sklearn version, the DeprecationWarning is shownfoo()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:
importsome_libdeffoo():
ifsome_lib.__version__.startswith("0."):
# do somethingelifsome_lib.__version__=="1.0.0":
# do something elseelse:
# do yet something elseThis 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:
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:
@versiondispatchdeffoo():
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