From da83ee4e07053d62d83fa143df140904db58bdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 14 Apr 2025 13:41:19 +0000 Subject: [PATCH 1/4] Migrate from `poetry` to `uv` --- .github/workflows/test-pr.yml | 20 +++--- {{cookiecutter.project_slug}}/Makefile | 70 +++++++++++--------- {{cookiecutter.project_slug}}/README.md | 5 +- {{cookiecutter.project_slug}}/pyproject.toml | 54 ++++++++------- 4 files changed, 81 insertions(+), 68 deletions(-) diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index 0485608..0b66f32 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -14,19 +14,19 @@ jobs: - name: 'Check out code' uses: actions/checkout@v3 - name: 'Install Python' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' + - name: 'Install uv' + uses: astral-sh/setup-uv@v5 - name: 'Install cookiecutter' run: pip install cookiecutter - - name: 'Install Poetry' - run: curl -sSL https://install.python-poetry.org | python3 - --version 1.3.2 - name: 'Generate template' run: cookiecutter --no-input . project_name='Test Project' - name: 'Generate lock file' - run: poetry -C ./test-project lock + run: uv --project ./test-project lock - name: 'Cache generated project' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./test-project key: test-project-${{ github.run_id }} @@ -37,16 +37,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - name: 'Install Python' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: 'Install Poetry' - run: curl -sSL https://install.python-poetry.org | python3 - --version 1.3.2 + - name: 'Install uv' + uses: astral-sh/setup-uv@v5 - name: 'Restore generated project' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./test-project key: test-project-${{ github.run_id }} diff --git a/{{cookiecutter.project_slug}}/Makefile b/{{cookiecutter.project_slug}}/Makefile index 9f60904..5b27c48 100644 --- a/{{cookiecutter.project_slug}}/Makefile +++ b/{{cookiecutter.project_slug}}/Makefile @@ -1,5 +1,5 @@ -POETRY := poetry -POETRY_RUN := $(POETRY) run +UV := uv +UV_RUN := $(UV) run -- default: check test-unit @@ -13,11 +13,7 @@ clean: .PHONY: build build: - $(POETRY) build - -.PHONY: poetry-install -poetry-install: - $(POETRY) install + $(uv) build # Tests @@ -26,14 +22,17 @@ TEST_ARGS := test: test-all -test-all: poetry-install - $(POETRY_RUN) pytest src/tests --maxfail=1 --verbose --durations=0 --numprocesses=4 --dist=worksteal $(TEST_ARGS) +.PHONY: test-all +test-all: + $(UV_RUN) pytest src/tests --maxfail=1 --verbose --durations=0 --numprocesses=4 --dist=worksteal $(TEST_ARGS) -test-unit: poetry-install - $(POETRY_RUN) pytest src/tests/unit --maxfail=1 --verbose $(TEST_ARGS) +.PHONY: test-unit +test-unit: + $(UV_RUN) pytest src/tests/unit --maxfail=1 --verbose $(TEST_ARGS) -test-integration: poetry-install - $(POETRY_RUN) pytest src/tests/integration --maxfail=1 --verbose --durations=0 --numprocesses=4 --dist=worksteal $(TEST_ARGS) +.PHONY: test-integration +test-integration: + $(UV_RUN) pytest src/tests/integration --maxfail=1 --verbose --durations=0 --numprocesses=4 --dist=worksteal $(TEST_ARGS) # Coverage @@ -59,34 +58,43 @@ cov-integration: test-integration format: autoflake isort black check: check-flake8 check-mypy check-autoflake check-isort check-black -check-flake8: poetry-install - $(POETRY_RUN) flake8 src +.PHONY: check-flake8 +check-flake8: + $(UV_RUN) flake8 src -check-mypy: poetry-install - $(POETRY_RUN) mypy src +.PHONY: check-mypy +check-mypy: + $(UV_RUN) mypy src -autoflake: poetry-install - $(POETRY_RUN) autoflake --quiet --in-place src +.PHONY: autoflake +autoflake: + $(UV_RUN) autoflake --quiet --in-place src -check-autoflake: poetry-install - $(POETRY_RUN) autoflake --quiet --check src +.PHONY: check-autoflake +check-autoflake: + $(UV_RUN) autoflake --quiet --check src -isort: poetry-install - $(POETRY_RUN) isort src +.PHONY: isort +isort: + $(UV_RUN) isort src -check-isort: poetry-install - $(POETRY_RUN) isort --check src +.PHONY: check-isort +check-isort: + $(UV_RUN) isort --check src -black: poetry-install - $(POETRY_RUN) black src +.PHONY: black +black: + $(UV_RUN) black src -check-black: poetry-install - $(POETRY_RUN) black --check src +.PHONY: check-black +check-black: + $(UV_RUN) black --check src # Optional tools SRC_FILES := $(shell find src -type f -name '*.py') -pyupgrade: poetry-install - $(POETRY_RUN) pyupgrade --py310-plus $(SRC_FILES) +.PHONY: pyupgrade +pyupgrade: + $(UV_RUN) pyupgrade --py310-plus $(SRC_FILES) diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index ef9ad50..dd97086 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -3,7 +3,7 @@ ## Installation -Prerequsites: `python >= 3.10`, `pip >= 20.0.2`, `poetry >= 1.3.2`. +Prerequsites: `python >= 3.10`, [`uv`](https://docs.astral.sh/uv/). ```bash make build @@ -19,5 +19,4 @@ Use `make` to run common tasks (see the [Makefile](Makefile) for a complete list * `make check`: Check code style * `make format`: Format code * `make test-unit`: Run unit tests - -For interactive use, spawn a shell with `poetry shell` (after `poetry install`), then run an interpreter. +* `make test-integration`: Run integration tests diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 845d47c..28f2671 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -1,34 +1,40 @@ [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] +[project] name = "{{ cookiecutter.project_slug }}" version = "{{ cookiecutter.version }}" description = "{{ cookiecutter.description }}" -authors = [ - "{{ cookiecutter.author_name }} <{{ cookiecutter.author_email }}>", +readme = "README.md" +requires-python = "~=3.10" +dependencies = [] + +[[project.authors]] +name = "{{ cookiecutter.author_name }}" +email = "{{ cookiecutter.author_email }}" + +[dependency-groups] +dev = [ + "autoflake", + "black", + "flake8", + "flake8-bugbear", + "flake8-comprehensions", + "flake8-quotes", + "flake8-type-checking", + "isort", + "mypy", + "pep8-naming", + "pytest", + "pytest-cov", + "pytest-mock", + "pytest-xdist", + "pyupgrade", ] -[tool.poetry.dependencies] -python = "^3.10" - -[tool.poetry.group.dev.dependencies] -autoflake = "*" -black = "*" -flake8 = "*" -flake8-bugbear = "*" -flake8-comprehensions = "*" -flake8-quotes = "*" -flake8-type-checking = "*" -isort = "*" -mypy = "*" -pep8-naming = "*" -pytest = "*" -pytest-cov = "*" -pytest-mock = "*" -pytest-xdist = "*" -pyupgrade = "*" +[tool.hatch.metadata] +allow-direct-references = true [tool.isort] profile = "black" From f31bc4c07036969a6aafa0a504ed8f997a8d463a Mon Sep 17 00:00:00 2001 From: Julian Kuners Date: Wed, 30 Apr 2025 13:53:56 +0200 Subject: [PATCH 2/4] update nixpkgs to unstable nixpkgs on previously 22.05 changed something in regard to `python.interpreter`. Updating to a version atleast as recent as used in other runtime verification repositories fixes this. --- {{cookiecutter.project_slug}}/flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/flake.nix b/{{cookiecutter.project_slug}}/flake.nix index f0083d7..7702fff 100644 --- a/{{cookiecutter.project_slug}}/flake.nix +++ b/{{cookiecutter.project_slug}}/flake.nix @@ -1,7 +1,7 @@ { description = "{{ cookiecutter.project_slug }} - {{ cookiecutter.description }}"; inputs = { - nixpkgs.url = "nixpkgs/nixos-22.05"; + nixpkgs.url = "nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; poetry2nix.url = "github:nix-community/poetry2nix"; }; From 892494204c06f703771c7f3c82b5e0c339be5ddb Mon Sep 17 00:00:00 2001 From: Julian Kuners Date: Wed, 30 Apr 2025 13:55:59 +0200 Subject: [PATCH 3/4] add CLI example --- {{cookiecutter.project_slug}}/pyproject.toml | 3 +++ .../src/{{cookiecutter.package_name}}/__main__.py | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__main__.py diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 28f2671..bbd3794 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -14,6 +14,9 @@ dependencies = [] name = "{{ cookiecutter.author_name }}" email = "{{ cookiecutter.author_email }}" +[project.scripts] +{{ cookiecutter.project_slug }} = "{{ cookiecutter.project_slug }}.__main__:main" + [dependency-groups] dev = [ "autoflake", diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__main__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__main__.py new file mode 100644 index 0000000..3192708 --- /dev/null +++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__main__.py @@ -0,0 +1,2 @@ +def main() -> None: + print('Hello world!') From c07139837dae32a7cfe6662d5af68a42e6569cfb Mon Sep 17 00:00:00 2001 From: Julian Kuners Date: Wed, 30 Apr 2025 13:56:13 +0200 Subject: [PATCH 4/4] migrate from `poetry2nix` to `uv2nix` --- {{cookiecutter.project_slug}}/flake.nix | 93 +++++++++++++------ .../build-systems-overlay.nix | 22 +++++ .../{{cookiecutter.project_slug}}/default.nix | 44 +++++++++ 3 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/build-systems-overlay.nix create mode 100644 {{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/default.nix diff --git a/{{cookiecutter.project_slug}}/flake.nix b/{{cookiecutter.project_slug}}/flake.nix index 7702fff..818667a 100644 --- a/{{cookiecutter.project_slug}}/flake.nix +++ b/{{cookiecutter.project_slug}}/flake.nix @@ -3,39 +3,80 @@ inputs = { nixpkgs.url = "nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; - poetry2nix.url = "github:nix-community/poetry2nix"; + pyproject-nix = { + url = "github:pyproject-nix/pyproject.nix"; + inputs.nixpkgs.follows = "uv2nix/nixpkgs"; + # inputs.uv2nix.follows = "nixpkgs"; + }; + pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.uv2nix.follows = "uv2nix"; + inputs.nixpkgs.follows = "uv2nix/nixpkgs"; + # inputs.uv2nix.follows = "nixpkgs"; + }; + uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.pyproject-nix.follows = "pyproject-nix"; + # stale nixpkgs is missing the alias `lib.match` -> `builtins.match` + # therefore point uv2nix to a patched nixpkgs, which introduces this alias + # this is a temporary solution until nixpkgs us up-to-date again + inputs.nixpkgs.url = "github:runtimeverification/nixpkgs/libmatch"; + # inputs.uv2nix.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, flake-utils, poetry2nix }: - let - allOverlays = [ - poetry2nix.overlay - (final: prev: { - {{ cookiecutter.project_slug }} = prev.poetry2nix.mkPoetryApplication { - python = prev.python310; - projectDir = ./.; - groups = []; - # We remove `dev` from `checkGroups`, so that poetry2nix does not try to resolve dev dependencies. - checkGroups = []; - }; - }) - ]; - in flake-utils.lib.eachSystem [ + outputs = { self, nixpkgs, flake-utils, pyproject-nix, pyproject-build-systems, uv2nix }: + let + pythonVer = "310"; + in flake-utils.lib.eachSystem [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ] (system: - let - pkgs = import nixpkgs { - inherit system; - overlays = allOverlays; + let + # due to the nixpkgs that we use in this flake being outdated, uv is also heavily outdated + # we can instead use the binary release of uv provided by uv2nix for now + uvOverlay = final: prev: { + uv = uv2nix.packages.${final.system}.uv-bin; + }; + {{ cookiecutter.project_slug }}Overlay = final: prev: { + {{ cookiecutter.project_slug }} = final.callPackage ./nix/{{ cookiecutter.project_slug }} { + inherit pyproject-nix pyproject-build-systems uv2nix; + python = final."python${pythonVer}"; }; - in { - packages = rec { - inherit (pkgs) {{ cookiecutter.project_slug }}; - default = {{ cookiecutter.project_slug }}; + }; + pkgs = import nixpkgs { + inherit system; + overlays = [ + uvOverlay + {{ cookiecutter.project_slug }}Overlay + ]; + }; + python = pkgs."python${pythonVer}"; + in { + devShells.default = pkgs.mkShell { + name = "uv develop shell"; + buildInputs = [ + python + pkgs.uv + ]; + env = { + # prevent uv from managing Python downloads and force use of specific + UV_PYTHON_DOWNLOADS = "never"; + UV_PYTHON = python.interpreter; }; - }) // { - overlay = nixpkgs.lib.composeManyExtensions allOverlays; + shellHook = '' + unset PYTHONPATH + ''; + }; + packages = rec { + inherit (pkgs) {{ cookiecutter.project_slug }}; + default = {{ cookiecutter.project_slug }}; + }; + }) // { + overlays.default = final: prev: { + inherit (self.packages.${final.system}) {{ cookiecutter.project_slug }}; }; + }; } diff --git a/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/build-systems-overlay.nix b/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/build-systems-overlay.nix new file mode 100644 index 0000000..d27c115 --- /dev/null +++ b/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/build-systems-overlay.nix @@ -0,0 +1,22 @@ +final: prev: +let + inherit (final) resolveBuildSystem; + inherit (builtins) mapAttrs; + + # Build system dependencies specified in the shape expected by resolveBuildSystem + # The empty lists below are lists of optional dependencies. + # + # A package `foo` with specification written as: + # `setuptools-scm[toml]` in pyproject.toml would be written as + # `foo.setuptools-scm = [ "toml" ]` in Nix + buildSystemOverrides = { + # add dependencies here, e.g.: + # pyperclip.setuptools = [ ]; + }; +in +mapAttrs ( + name: spec: + prev.${name}.overrideAttrs (old: { + nativeBuildInputs = old.nativeBuildInputs ++ resolveBuildSystem spec; + }) +) buildSystemOverrides diff --git a/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/default.nix b/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/default.nix new file mode 100644 index 0000000..1d1b916 --- /dev/null +++ b/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/default.nix @@ -0,0 +1,44 @@ +{ + lib, + callPackage, + + pyproject-nix, + pyproject-build-systems, + uv2nix, + + python +}: +let + pyproject-util = callPackage pyproject-nix.build.util {}; + pyproject-packages = callPackage pyproject-nix.build.packages { + inherit python; + }; + + # load a uv workspace from a workspace root + workspace = uv2nix.lib.workspace.loadWorkspace { + workspaceRoot = ../..; + }; + + # create overlay + lockFileOverlay = workspace.mkPyprojectOverlay { + # prefer "wheel" over "sdist" due to maintance overhead + # there is no bundled set of overlays for "sdist" in uv2nix, in contrast to poetry2nix + sourcePreference = "wheel"; + }; + + buildSystemsOverlay = import ./build-systems-overlay.nix; + + # construct package set + pythonSet = pyproject-packages.overrideScope (lib.composeManyExtensions [ + # make build tools available by default as these are not necessarily specified in python lock files + pyproject-build-systems.overlays.default + # include all packages from the python lock file + lockFileOverlay + # add build system overrides to certain python packages + buildSystemsOverlay + ]); +in pyproject-util.mkApplication { + # default dependancy group enables no optional dependencies and no dependency-groups + venv = pythonSet.mkVirtualEnv "{{ cookiecutter.project_slug }}-env" workspace.deps.default; + package = pythonSet.{{ cookiecutter.project_slug }}; +}