Python
Builds and packaging tools
In the Python Packaging User Guide1:
- Tool recommendations: mentions poetry but not hatch
- Packaging Python Projects: uses hatchling as an example (hatch's build backend, equivalent to poetry-core) but mentions poetry in a footnote
- Key projects: mentions that poetry uses its own dependency resolver instead of using pip's (i think we would actually prefer to use the pip dependency resolver?)
- The Packaging Flow: mentions both but uses hatchling in the examples
- Managing Application Dependencies: just mentions both
Hatch
Hatch is maintained by PyPA (Python Packing Authority), same as other standard tools like pip, setuptools, virtualenv, and twine.
Installing
You are probably going to want to install Hatch from PyPI rather than your distros package manager, since Hatch is under active development and new versions are released somewhat freqnely.
$ python3 -m pip install --user pipx
$ pipx install hatch
You may of course also install it directly with pip install
, though
using pipx
is a much better approach in general.
Switching project to hatch
Moving a project from poetry
to hatch
is easy, but not trivial.
Poetry
Poetry is mainly used for managing an application and its dependencies whereas Hatch is more agnostic to the project type and offers plugin-based functionality for the entire workflow (versioning, tox-like environments, publishing) so you can easily build things other than wheel/sdist, test in a Docker container, etc.2
Versioning
Use poetry-bumpversion
with Poetry to make managing the project's version easier.
Its a plugin for Poetry itself, and thus not tied to the project (which would have been
nice) or it's pyproject.toml
file.
$ poetry self add poetry-bumpversion
And in your pyproject.toml
, configure as needed:
[tool.poetry_bumpversion.file."${module_name}/__init__.py"]
[tool.poetry_bumpversion.file."tests/test_version.py"]
With this example it will update __version__
in ${module_name}/__init__.py
to the version in pyproject.toml
, and also keep the version number in
tests/test_version.py
up to date.
Decorators
Creating a decorator3 that accepts a named argument foo
and
passes it to the decorated function:
def bar_decorator(foo):
def inner(f):
def wrapper(*args, **kwargs):
return f(*args, foo=foo, **kwargs)
return wrapper
return inner
@bar_decorator("bar")
def foobar(foo):
return foo
Since "bar"
was passed to the foobar
function as foo
:
>>> foobar()
'bar'
Decorators that without arguments can also be created:
def decorator(f):
def wrapper(*args, **kwargs):
return f(*args, bar="baz", **kwargs)
return wrapper
@decorator
def foobar(bar):
return bar
Now the foobar
function always gets passed "baz"
for its bar
parameter,
it is "hardcoded" in the decorator since it does not accept paramaters:
>>> foobar()
'baz'
The functools
library provies
the update_wrapper
along with the convenience function wraps
4
for invoking it.
from functools import update_wrapper, wraps
def some_decorator(foo):
def inner(f):
def wrapper(*args, **kwargs):
return f(*args, foo=foo, **kwargs)
return update_wrapper(wrapper, f)
return inner
def another_decorator(foo):
def inner(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, bar="bar", **kwargs)
return wrapper
return inner
Both of these decorators work the same way as the first bar_decorator
.