Migration Guide⚓︎
Poetry → UV Migration (v6)⚓︎
Calcipy has migrated from Poetry to UV for package management and builds. This migration brings significant improvements in speed, simplicity, and modern Python packaging standards.
What Changed⚓︎
Build System:
- ✅ Migrated from
poetry-coretouv_build - ✅ Updated to modern PEP 621
[project]format - ✅ Using
[dependency-groups]instead of[tool.poetry.group] - ✅ Lock file changed from
poetry.locktouv.lock
CI/CD:
- ✅ All workflows updated to use
uvcommands - ✅ GitHub Actions use
astral-sh/setup-uv@v5 - ✅ Pre-commit hooks configured for
uv.lock
Task Automation:
- ✅ All tasks updated to use
uv run,uv sync,uv build,uv publish - ✅ Noxfile uses
venv_backend='uv'
Migration Path for Users⚓︎
If you’re using Calcipy in your project and want to migrate from Poetry to UV:
1. Update Package Manager⚓︎
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or with brew
brew install uv
2. Convert pyproject.toml⚓︎
Your pyproject.toml needs these changes:
Before (Poetry):
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "my-package"
version = "1.0.0"
dependencies = { python = "^3.10" }
[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
After (UV):
[build-system]
requires = ["uv_build>=0.9.7"]
build-backend = "uv_build"
[project]
name = "my-package"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = []
[dependency-groups]
dev = ["pytest>=7.0"]
3. Migrate Lock File⚓︎
# Remove old poetry files
rm poetry.lock
# Create new uv lock file
uv lock
# Install dependencies
uv sync --all-extras
4. Update CI/CD⚓︎
Replace poetry commands with uv equivalents:
| Poetry | UV |
|---|---|
poetry install |
uv sync --all-extras |
poetry add package |
uv add package |
poetry run command |
uv run command |
poetry build |
uv build |
poetry publish |
uv publish |
5. Update GitHub Actions⚓︎
# Before
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Poetry
run: pipx install poetry
- name: Install dependencies
run: poetry install
# After
- uses: astral-sh/setup-uv@v5
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: uv sync --all-extras
Python Version Management: mise + uv + nox⚓︎
Calcipy uses a powerful combination of tools for managing Python versions:
mise (Version Manager)⚓︎
mise (formerly rtx) is a polyglot version manager that replaces asdf, pyenv, nvm, etc.
Setup mise:
# Install mise
curl https://mise.run | sh
# Or with brew
brew install mise
# Add to your shell (e.g., ~/.bashrc or ~/.zshrc)
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
Configure Python versions:
Create mise.toml:
[tools]
python = ["3.12.5", "3.10.16"]
[env]
_.python.venv = { path = ".venv" }
Use mise:
# Install Python versions from mise.toml
mise install
# Verify installation
mise which python
python --version
Integration with nox⚓︎
Calcipy’s noxfile automatically reads Python versions from mise.toml:
# From src/calcipy/noxfile/_noxfile.py
def _get_pythons() -> List[str]:
"""Return python versions from supported configuration files."""
return [*{str(ver) for ver in get_tool_versions()['python']}]
@nox_session(venv_backend='uv', python=_get_pythons(), reuse_venv=True)
def tests(session: NoxSession) -> None:
"""Run tests for all specified Python versions."""
...
This reads from:
mise.lock(resolved versions)mise.toml(specified versions).tool-versions(legacy asdf format)
Run nox tests:
# List available sessions
uv run nox -l
# Run tests for all Python versions
uv run nox
# Run tests for specific version
uv run nox -s tests-3.12
Benefits of this Setup⚓︎
- Version Consistency: Same Python versions for development, testing, and CI
- Multi-version Testing: nox automatically tests against all specified versions
- Fast Environment Creation: uv’s venv backend is significantly faster than virtualenv
- Per-directory Versions: mise manages Python versions per project
- No Global Pollution: Each project has isolated Python environments
Common Issues⚓︎
Problem: nox can’t find Python version
Solution: Ensure mise installed all versions:
mise install
mise which python3.9
mise which python3.12
Problem: Different versions between local and CI
Solution: Use mise.lock to pin exact versions:
mise lock
git add mise.lock
Tool vs Dependency Usage⚓︎
With v6, Calcipy now has clear modes:
Tool Mode (New in v6):
# Minimal installation for linting and code analysis
uv tool install 'calcipy[tool]'
calcipy-lint lint
calcipy-tags tags --base-dir=./my-project
Dependency Mode (Traditional):
# Full development environment
uv add --dev 'calcipy[dev]'
uv run calcipy test
See README for detailed usage examples.
v5⚓︎
The breaking changes include removing stale and pack.check_license
Speed Test⚓︎
After further reduction of dependencies, the CLI performance has continued to improve:
> poetry run pip freeze | wc -l
79
> hyperfine -m 20 --warmup 5 ./run
Benchmark 1: ./run
Time (mean ± σ): 397.1 ms ± 12.2 ms [User: 268.4 ms, System: 57.0 ms]
Range (min … max): 385.9 ms … 421.5 ms 20 runs
v4⚓︎
The total number of dependencies was reduce even further by replacing flake8, isort, and other tooling with ruff; fewer mkdocs plugins; and fewer steps in the main run task to speed up normal usage.
The only breaking change impacted recipes when write_autoformatted_md_sections was renamed to write_template_formatted_md_sections.
Speed Test⚓︎
Following up on performance checks from the v2 migration. The performance is comparable, but you will see savings in cache size and poetry install and when running main (./run main for Calcipy, currently takes ~20s)
> hyperfine -m 20 --warmup 5 ./run
Benchmark 1: ./run
Time (mean ± σ): 863.9 ms ± 10.0 ms [User: 550.7 ms, System: 102.3 ms]
Range (min … max): 848.5 ms … 885.3 ms 20 runs
> hyperfine -m 20 --warmup 5 "poetry run calcipy-tags"
Benchmark 1: poetry run calcipy-tags
Time (mean ± σ): 770.5 ms ± 5.7 ms [User: 470.6 ms, System: 89.5 ms]
Range (min … max): 760.1 ms … 780.3 ms 20 runs
v3⚓︎
Replaced features from flake8 and plugins with corresponding checks from ruff, however both are still used in parallel.
v2⚓︎
Background⚓︎
calcipy v1 was a complete rewrite to switch from doit to invoke:
- with
invoke, tasks can be run from anywhere without adodo.pyfile - tasks can be loaded lazily, which means that some performance gains are possible
- since there is no shared state file, tasks can be more easily run from pre-commit or generally in parallel
doit excelled at clearly delineated task output and run summary, but invoke isn’t designed that way. I would like to improve the CLI output, but the benefits are worth this tradeoff.
calcipy v0 was built on doit and thus required a dodo.py file. I began adding cement to support a separate CLI for calcipy installed with pipx or uvx, but that required a lot of boilerplate code. With doit, the string command needed to be complete at task evaluation rather than runtime, so globbing files couldn’t be resolved lazily.
Migration⚓︎
While refactoring, the global configuration was mostly removed (DoitGlobals) along with a few tasks, but the main functionality is still present. Any project dependent on calcipy will need substantial changes. The easiest way to start migrating is to run copier copy gh:KyleKing/calcipy_template . for calcipy_template
Speed Test⚓︎
It turns out that switching to invoke appears to have only saved 100ms
> hyperfine -m 20 --warmup 5 ./run
Benchmark 1: ./run
Time (mean ± σ): 863.9 ms ± 10.0 ms [User: 550.7 ms, System: 102.3 ms]
Range (min … max): 848.5 ms … 885.3 ms 20 runs
> hyperfine -m 20 --warmup 5 "poetry run calcipy-tags"
Benchmark 1: poetry run calcipy-tags
Time (mean ± σ): 770.5 ms ± 5.7 ms [User: 470.6 ms, System: 89.5 ms]
Range (min … max): 760.1 ms … 780.3 ms 20 runs
> hyperfine -m 20 --warmup 5 "poetry run python -c 'print(1)'"
Benchmark 1: poetry run python -c 'print(1)'
Time (mean ± σ): 377.9 ms ± 3.1 ms [User: 235.0 ms, System: 61.8 ms]
Range (min … max): 372.7 ms … 384.0 ms 20 runs
> hyperfine -m 20 --warmup 5 ./run
Benchmark 1: ./run
Time (mean ± σ): 936.0 ms ± 26.9 ms [User: 1548.2 ms, System: 1687.7 ms]
Range (min … max): 896.4 ms … 1009.4 ms 20 runs
> hyperfine -m 20 --warmup 5 "poetry run calcipy_tags"
Benchmark 1: poetry run calcipy_tags
Time (mean ± σ): 618.5 ms ± 29.7 ms [User: 1536.8 ms, System: 1066.2 ms]
Range (min … max): 578.2 ms … 694.9 ms 20 runs
> hyperfine -m 20 --warmup 5 "poetry run doit list"
Benchmark 1: poetry run doit list
Time (mean ± σ): 1.002 s ± 0.015 s [User: 1.643 s, System: 1.682 s]
Range (min … max): 0.974 s … 1.023 s 20 runs
Additionally, the major decrease in dependencies will make install and update actions much faster. With the recommended extras installed, calcipy-v1 has 124 dependencies (with all extras, 164) vs. calcipy-v0’s 259. Counted with: cat .calcipy_packaging.lock | jq 'keys' | wc -l
Code Comparison⚓︎
Accounting for code extracted to corallium, the overall number of lines decreased from 1772 to 1550 or only 12%, while increasing the CLI and pre-commit capabilities.
~/calcipy-v0 > cloc calcipy
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Python 26 942 1075 1772
-------------------------------------------------------------------------------
SUM: 26 942 1075 1772
-------------------------------------------------------------------------------
~/calcipy > cloc calcipy
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Python 27 454 438 1185
-------------------------------------------------------------------------------
SUM: 27 454 438 1185
-------------------------------------------------------------------------------
~/corallium > cloc corallium
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Python 7 176 149 365
-------------------------------------------------------------------------------
SUM: 7 176 149 365
-------------------------------------------------------------------------------
~/calcipy > cloc tests
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
YAML 2 0 0 580
Python 19 176 68 578
JSON 2 0 0 60
Markdown 3 9 10 8
Text 1 0 0 2
-------------------------------------------------------------------------------
SUM: 27 185 78 1228
-------------------------------------------------------------------------------
~/calcipy-v0 > cloc tests
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
JSON 30 0 0 762
YAML 2 0 0 580
Python 24 314 186 578
Markdown 3 9 10 8
-------------------------------------------------------------------------------
SUM: 59 323 196 1928
-------------------------------------------------------------------------------
~/corallium > cloc tests
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Python 6 36 15 69
Markdown 1 1 0 2
-------------------------------------------------------------------------------
SUM: 7 37 15 71
-------------------------------------------------------------------------------
doit output⚓︎
I would like to restore the doit task summary, but invoke’s architecture doesn’t really make this possible. The --continue option was extremely useful, but that also might not be achievable.
> poetry run doit run
. format_recipes > [
Python: function format_recipes
]
2023-02-19 10:40:23.954 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/breakfast
2023-02-19 10:40:23.957 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/rice
2023-02-19 10:40:23.959 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/meals
2023-02-19 10:40:23.964 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/seafood
2023-02-19 10:40:23.967 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/pizza
2023-02-19 10:40:23.969 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/poultry
2023-02-19 10:40:23.972 | INFO | recipes.formatter:_write_toc:287 - Creating TOC for: ./recipes/docs/sushi
. collect_code_tags > [
Python: function write_code_tag_file
]
. cl_write > [
Cmd: poetry run cz changelog
Python: function _move_cl
]
. lock > [
Cmd: poetry lock --no-update
]
Resolving dependencies...
. nox_coverage > [
Cmd: poetry run nox --error-on-missing-interpreters --session coverage
]
...
doit> Summary:
doit> format_recipes was successful
doit> collect_code_tags was successful
doit> cl_write was successful
doit> lock was successful
doit> nox_coverage was successful
doit> auto_format was successful
doit> document was successful
doit> check_for_stale_packages was successful
doit> pre_commit_hooks failed (red)
doit> lint_project was not run
doit> static_checks was not run
doit> security_checks was not run
doit> check_types was not run