Skip to content

Commit 5887878

Browse files
committed
Support for choice between uv, pixi and poetry
1 parent d539efa commit 5887878

File tree

16 files changed

+466
-94
lines changed

16 files changed

+466
-94
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# Changelog
22

3+
## 31.11.2025
4+
5+
Support for choice between uv, pixi and poetry as package manager
6+
37
## 01.11.2020 - Initial release

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This repository contains a [cookiecutter](https://github.com/cookiecutter/cookiecutter) template
44
that can be used for library development. The template contains several well-known "best-practices" for libraries
5-
(poetry, poethepoet, mypy, ruff, nbqa) and also some tools
5+
(configurable package managers [`uv`, `pixi`, `poetry`], `poethepoet`, `mypy`, `ruff`, `nbqa`) and also some tools
66
inspired by projects of ours that we consider generally useful - build and release scripts,
77
auto-generation of documentation files, and others.
88
Earlier versions of this template were used in several industry projects as well as for open source libraries.
@@ -13,7 +13,7 @@ The template includes CI/CD pipelines based on github actions. The documentation
1313
In the documentation links to source code will be created, therefore you will be prompted to give the project's url.
1414

1515
See the resulting repository's [contributing guidelines](docs/04_contributing/04_contributing.rst)
16-
for further details. Some examples for projects following the general style of the template are [tianshou](https://github.com/thu-ml/tianshou)
16+
for further details. Some examples for projects from this template (with poetry) are [tianshou](https://github.com/thu-ml/tianshou)
1717
and [armscan_env](https://github.com/appliedAI-Initiative/armscan_env/)
1818

1919
# Usage
@@ -23,7 +23,9 @@ and [armscan_env](https://github.com/appliedAI-Initiative/armscan_env/)
2323
The template supports python 3.11 and higher. For a smooth project generation you need to have
2424

2525
1) Cookiecutter. Install it e.g. with `pip install cookiecutter`
26-
2) Poetry (for using the new repository)
26+
2) You will need `tomli` and `tomli-w`. Install them e.g. with `pip install tomli tomli-w`.
27+
If you have cloned this repo, you can also install them with `pip install -r requirements.txt`
28+
3) The selected package manager for your project (`uv`, `pixi`, or `poetry`) (not needed for the templating itself)
2729

2830

2931
## Creating a new project
@@ -42,8 +44,8 @@ e.g.,
4244

4345
```shell script
4446
git init
45-
poetry shell
46-
poetry install --with dev
47+
# Setup depends on your chosen package manager (uv, pixi, or poetry)
48+
# The generated README will provide specific instructions
4749
poe format
4850
git add . && git commit -m "Initial commit from pymetrius"
4951
```

cookiecutter.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"github_username": "{{cookiecutter.author}}",
66
"package_name": "{{cookiecutter.project_name}}",
77
"python_version": ["3.11", "3.12"],
8+
"package_manager": [
9+
"uv",
10+
"pixi",
11+
"poetry"
12+
],
813
"project_url": "https://github.com/{{cookiecutter.author}}/{{cookiecutter.project_name}}",
914
"include_accsr_configuration_utils": ["N", "y"],
1015
"include_readthedocs_yaml": ["N", "y"],

hooks/post_gen_project.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
import shutil
3+
import tomli
4+
import tomli_w
35

46

57
def remove(filepath: str):
@@ -19,6 +21,147 @@ def delete_line_in_file(filepath: str, line_starts_with: str):
1921
f.truncate()
2022

2123

24+
def read_toml(filepath: str):
25+
"""Read a TOML file."""
26+
with open(filepath, "rb") as f:
27+
return tomli.load(f)
28+
29+
30+
def update_pyproject_toml():
31+
"""Update pyproject.toml with project config and dependencies."""
32+
project_config = read_toml("project_config.toml")
33+
dependencies = read_toml("dependencies.toml")
34+
35+
# Read the current pyproject.toml
36+
with open("pyproject.toml", "rb") as f:
37+
pyproject = tomli.load(f)
38+
39+
package_manager = "{{cookiecutter.package_manager}}".lower()
40+
41+
# Add project configuration
42+
if package_manager == "poetry":
43+
# Poetry uses tool.poetry for project metadata
44+
pyproject["tool"] = pyproject.get("tool", {})
45+
pyproject["tool"]["poetry"] = {
46+
"name": project_config["project"]["name"],
47+
"version": project_config["project"]["version"],
48+
"description": project_config["project"]["description"],
49+
"authors": project_config["project"]["authors"],
50+
"license": project_config["project"]["license"],
51+
"readme": project_config["project"]["readme"],
52+
"homepage": project_config["project"]["homepage"],
53+
"classifiers": project_config["project"]["classifiers"],
54+
"packages": [project_config["project"]["package_dir"]],
55+
"exclude": project_config["project"]["exclude"],
56+
}
57+
58+
# Add dependencies
59+
pyproject["tool"]["poetry"]["dependencies"] = {"python": f"^{project_config['project']['python_version']}"}
60+
for name, version in dependencies["dependencies"].items():
61+
pyproject["tool"]["poetry"]["dependencies"][name] = f"^{version}"
62+
63+
# Add dev dependencies
64+
pyproject["tool"]["poetry"].setdefault("group", {})
65+
pyproject["tool"]["poetry"]["group"]["dev"] = {"optional": True, "dependencies": {}}
66+
for name, version in dependencies["dev-dependencies"].items():
67+
if isinstance(version, dict):
68+
# Handle complex dependencies like black = {version = "23.7.0", extras = ["jupyter"]}
69+
pyproject["tool"]["poetry"]["group"]["dev"]["dependencies"][name] = {
70+
"version": f"^{version['version']}",
71+
"extras": version.get("extras", [])
72+
}
73+
else:
74+
pyproject["tool"]["poetry"]["group"]["dev"]["dependencies"][name] = "*" if version == "*" else f"^{version}"
75+
76+
# Add poetry-specific tool tasks
77+
pyproject["tool"]["poe"]["tasks"]["_poetry_install_sort_plugin"] = "poetry self add poetry-plugin-sort"
78+
pyproject["tool"]["poe"]["tasks"]["_poetry_sort"] = "poetry sort"
79+
pyproject["tool"]["poe"]["tasks"]["format"] = ["_ruff_format", "_ruff_format_nb", "_black_format", "_poetry_install_sort_plugin", "_poetry_sort"]
80+
81+
elif package_manager == "uv":
82+
# UV uses standard project metadata
83+
pyproject["project"] = {
84+
"name": project_config["project"]["name"],
85+
"version": project_config["project"]["version"],
86+
"description": project_config["project"]["description"],
87+
"authors": [{"name": author.split("<")[0].strip(), "email": author.split("<")[1].strip(">")}
88+
for author in project_config["project"]["authors"]],
89+
"license": {"text": project_config["project"]["license"]},
90+
"readme": project_config["project"]["readme"],
91+
"requires-python": f">={project_config['project']['python_version']}",
92+
"classifiers": project_config["project"]["classifiers"],
93+
}
94+
95+
# Add project URLs
96+
pyproject["project"]["urls"] = {"Homepage": project_config["project"]["homepage"]}
97+
98+
# Add dependencies
99+
pyproject["project"]["dependencies"] = []
100+
for name, version in dependencies["dependencies"].items():
101+
pyproject["project"]["dependencies"].append(f"{name}>={version}")
102+
103+
# Add dev dependencies
104+
pyproject["project"]["optional-dependencies"] = {"dev": []}
105+
for name, version in dependencies["dev-dependencies"].items():
106+
if isinstance(version, dict):
107+
# Handle complex dependencies
108+
ver_str = version.get("version", "*")
109+
if "extras" in version:
110+
extras = ','.join(version["extras"])
111+
pyproject["project"]["optional-dependencies"]["dev"].append(f"{name}[{extras}]>={ver_str}")
112+
else:
113+
pyproject["project"]["optional-dependencies"]["dev"].append(f"{name}>={ver_str}")
114+
else:
115+
if version == "*":
116+
pyproject["project"]["optional-dependencies"]["dev"].append(name)
117+
else:
118+
pyproject["project"]["optional-dependencies"]["dev"].append(f"{name}>={version}")
119+
120+
elif package_manager == "pixi":
121+
# Pixi uses tool.pixi for dependencies
122+
pyproject["project"] = {
123+
"name": project_config["project"]["name"],
124+
"version": project_config["project"]["version"],
125+
"description": project_config["project"]["description"],
126+
"authors": [{"name": author.split("<")[0].strip(), "email": author.split("<")[1].strip(">")}
127+
for author in project_config["project"]["authors"]],
128+
"license": {"text": project_config["project"]["license"]},
129+
"readme": project_config["project"]["readme"],
130+
"requires-python": f">={project_config['project']['python_version']}",
131+
"classifiers": project_config["project"]["classifiers"],
132+
}
133+
134+
# Add project URLs
135+
pyproject["project"]["urls"] = {"Homepage": project_config["project"]["homepage"]}
136+
137+
# Add dependencies
138+
pyproject["tool"] = pyproject.get("tool", {})
139+
pyproject["tool"]["pixi"] = {}
140+
pyproject["tool"]["pixi"]["dependencies"] = {}
141+
for name, version in dependencies["dependencies"].items():
142+
pyproject["tool"]["pixi"]["dependencies"][name] = f">={version}"
143+
144+
# Add dev dependencies
145+
pyproject["tool"]["pixi"]["dev-dependencies"] = {}
146+
for name, version in dependencies["dev-dependencies"].items():
147+
if isinstance(version, dict):
148+
# Handle complex dependencies
149+
pyproject["tool"]["pixi"]["dev-dependencies"][name] = {
150+
"version": f">={version['version']}",
151+
"extras": version.get("extras", [])
152+
}
153+
else:
154+
pyproject["tool"]["pixi"]["dev-dependencies"][name] = "*" if version == "*" else f">={version}"
155+
156+
# Write the updated pyproject.toml
157+
with open("pyproject.toml", "wb") as f:
158+
tomli_w.dump(pyproject, f)
159+
160+
# Clean up the template files
161+
remove("project_config.toml")
162+
remove("dependencies.toml")
163+
164+
22165
if "{{cookiecutter.include_readthedocs_yaml}}".lower() != "y":
23166
remove(".readthedocs.yaml")
24167

@@ -27,9 +170,12 @@ def delete_line_in_file(filepath: str, line_starts_with: str):
27170
remove("config.json")
28171
remove("docs/02_notebooks/02_config_example.ipynb")
29172
remove("data")
30-
delete_line_in_file("pyproject.toml", "accsr")
31173
delete_line_in_file(".gitignore", "config_local.json")
32174

175+
# Update pyproject.toml with project config and dependencies
176+
update_pyproject_toml()
177+
178+
# Initialize git repository
33179
return_code = os.system("""
34180
echo "Initializing your new project in $(pwd)."
35181

hooks/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tomli>=2.0.0
2+
tomli-w>=1.0.0

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tomli
2+
tomli-w
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{% raw %}
2+
# Common step definitions to be included by other workflows
3+
# This way we can centralize the package manager-specific steps
4+
5+
# Poetry setup steps
6+
poetry-setup:
7+
- name: Install poetry
8+
uses: abatilo/actions-poetry@v2
9+
- name: Setup a local virtual environment (if no poetry.toml file)
10+
run: |
11+
poetry config virtualenvs.create true --local
12+
poetry config virtualenvs.in-project true --local
13+
- uses: actions/cache@v3
14+
name: Define a cache for the virtual environment based on the dependencies lock file
15+
with:
16+
path: ./.venv
17+
key: venv-${{ hashFiles('poetry.lock') }}
18+
- name: Install the project dependencies
19+
run: |
20+
poetry install --with dev
21+
22+
# UV setup steps
23+
uv-setup:
24+
- name: Install uv
25+
run: pip install uv
26+
- uses: actions/cache@v3
27+
name: Cache dependencies
28+
with:
29+
path: ~/.cache/uv
30+
key: uv-${{ hashFiles('pyproject.toml') }}
31+
- name: Install dependencies
32+
run: |
33+
uv venv
34+
uv pip install -e ".[dev]"
35+
36+
# Pixi setup steps
37+
pixi-setup:
38+
- name: Install pixi
39+
uses: prefix-dev/[email protected]
40+
with:
41+
pixi-version: v0.7.0
42+
- name: Install dependencies
43+
run: pixi install
44+
{% endraw %}

{{cookiecutter.project_name}}/.github/workflows/lint_and_docs.yaml

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ jobs:
2222
uses: actions/setup-python@v4
2323
with:
2424
python-version: {{cookiecutter.python_version}}
25+
26+
{% if cookiecutter.package_manager == "poetry" %}
2527
{% raw -%}
26-
# use poetry and cache installed packages, see https://github.com/marketplace/actions/python-poetry-action
28+
# use poetry and cache installed packages
2729
- name: Install poetry
2830
uses: abatilo/actions-poetry@v2
2931
- name: Setup a local virtual environment (if no poetry.toml file)
@@ -44,6 +46,47 @@ jobs:
4446
run: poetry run poe type-check
4547
- name: Docs
4648
run: poetry run poe doc-build
49+
{%- endraw %}
50+
{% elif cookiecutter.package_manager == "uv" %}
51+
{% raw -%}
52+
# use uv package manager
53+
- name: Install uv
54+
run: pip install uv
55+
- uses: actions/cache@v3
56+
name: Cache dependencies
57+
with:
58+
path: ~/.cache/uv
59+
key: uv-${{ hashFiles('pyproject.toml') }}
60+
- name: Install dependencies
61+
run: |
62+
uv venv
63+
uv pip install -e ".[dev]"
64+
- name: Lint
65+
run: uv run poe lint
66+
- name: Types
67+
run: uv run poe type-check
68+
- name: Docs
69+
run: uv run poe doc-build
70+
{%- endraw %}
71+
{% elif cookiecutter.package_manager == "pixi" %}
72+
{% raw -%}
73+
# use pixi package manager
74+
- name: Install pixi
75+
uses: prefix-dev/[email protected]
76+
with:
77+
pixi-version: v0.7.0
78+
- name: Install dependencies
79+
run: pixi install
80+
- name: Lint
81+
run: pixi run poe lint
82+
- name: Types
83+
run: pixi run poe type-check
84+
- name: Docs
85+
run: pixi run poe doc-build
86+
{%- endraw %}
87+
{% endif %}
88+
89+
{% raw -%}
4790
- name: Upload artifact
4891
uses: actions/upload-pages-artifact@v2
4992
with:

{{cookiecutter.project_name}}/.github/workflows/publish.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ jobs:
1313
uses: actions/setup-python@v1
1414
with:
1515
python-version: {{cookiecutter.python_version}}
16+
17+
{% if cookiecutter.package_manager == "poetry" %}
1618
- name: Install poetry
1719
uses: abatilo/actions-poetry@v2
1820
- name: Setup a local virtual environment (if no poetry.toml file)
@@ -29,3 +31,32 @@ jobs:
2931
poetry config pypi-token.pypi $PYPI_TOKEN
3032
poetry config repositories.pypi https://pypi.org/legacy
3133
poetry publish --build --repository pypi
34+
{% elif cookiecutter.package_manager == "uv" %}
35+
- name: Install build and twine
36+
run: pip install build twine
37+
- name: Build and publish
38+
env:
39+
{% raw -%}
40+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
41+
{%- endraw %}
42+
run: |
43+
if [ -z "${PYPI_TOKEN}" ]; then echo "Set the PYPI_TOKEN variable in your repository secrets"; exit 1; fi
44+
python -m build
45+
python -m twine upload dist/* --username __token__ --password $PYPI_TOKEN
46+
{% elif cookiecutter.package_manager == "pixi" %}
47+
- name: Install pixi
48+
uses: prefix-dev/[email protected]
49+
with:
50+
pixi-version: v0.7.0
51+
- name: Install build and twine
52+
run: pip install build twine
53+
- name: Build and publish
54+
env:
55+
{% raw -%}
56+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
57+
{%- endraw %}
58+
run: |
59+
if [ -z "${PYPI_TOKEN}" ]; then echo "Set the PYPI_TOKEN variable in your repository secrets"; exit 1; fi
60+
python -m build
61+
python -m twine upload dist/* --username __token__ --password $PYPI_TOKEN
62+
{% endif %}

0 commit comments

Comments
 (0)