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..45917bd 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,5 @@ 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 +* `make test-integration`: Run integration tests -For interactive use, spawn a shell with `poetry shell` (after `poetry install`), then run an interpreter. diff --git a/{{cookiecutter.project_slug}}/flake.nix b/{{cookiecutter.project_slug}}/flake.nix index f0083d7..4e86036 100644 --- a/{{cookiecutter.project_slug}}/flake.nix +++ b/{{cookiecutter.project_slug}}/flake.nix @@ -1,41 +1,74 @@ { description = "{{ cookiecutter.project_slug }} - {{ cookiecutter.description }}"; inputs = { - nixpkgs.url = "nixpkgs/nixos-22.05"; + nixpkgs.url = "nixpkgs/nixos-25.05"; flake-utils.url = "github:numtide/flake-utils"; - poetry2nix.url = "github:nix-community/poetry2nix"; + uv2nix.url = "github:pyproject-nix/uv2nix/680e2f8e637bc79b84268949d2f2b2f5e5f1d81c"; + # 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 + uv2nix.inputs.nixpkgs.url = "github:runtimeverification/nixpkgs/libmatch"; + # inputs.nixpkgs.follows = "nixpkgs"; + pyproject-build-systems.url = "github:pyproject-nix/build-system-pkgs/7dba6dbc73120e15b558754c26024f6c93015dd7"; + pyproject-build-systems = { + inputs.nixpkgs.follows = "uv2nix/nixpkgs"; + inputs.uv2nix.follows = "uv2nix"; + inputs.pyproject-nix.follows = "uv2nix/pyproject-nix"; + }; + pyproject-nix.follows = "uv2nix/pyproject-nix"; }; - 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/README.md b/{{cookiecutter.project_slug}}/nix/README.md new file mode 100644 index 0000000..bfdf99c --- /dev/null +++ b/{{cookiecutter.project_slug}}/nix/README.md @@ -0,0 +1,3 @@ +## Notes +#### git submodules +If you use git submodules that are not required for building the project with `nix`, then you should add these directories to the `gitignoreSourcePure` list in `nix/{{ cookiecutter.project_slug }}/default.nix`, see the respective left-over `TODO` in the nix file. Otherwise, the nix build that is uploaded to the nix binary cache by CI might not match the version that is requested by `kup`, in case this project is offered as a package by `kup`. This is due to weird behaviour in regards to git submodules by `git` and `nix`, where a submodule directory might exist and be empty or instead not exist, which impacts the resulting nix hash, which is of impartance when offering/downloading cached nix derivations. See [runtimeverification/k/pull/4804](https://github.com/runtimeverification/k/pull/4804) for more information. \ No newline at end of file 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..1b37c52 --- /dev/null +++ b/{{cookiecutter.project_slug}}/nix/{{cookiecutter.project_slug}}/default.nix @@ -0,0 +1,55 @@ +{ + lib, + callPackage, + nix-gitignore, + + 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 = lib.cleanSource (nix-gitignore.gitignoreSourcePure [ + ../../.gitignore + ".github/" + "result*" + # do not include submodule directories that might be initilized empty or non-existent due to nix/git + # otherwise cachix build might not match the version that is requested by `kup` + # TODO: for new projects, add your submodule directories that are not required for nix builds here! + # e.g., `"/docs/external-computation"` with `external-computation` being a git submodule directory + # "/docs/external-computation" + ] ../.. + ); + }; + + # 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 }}; +} diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 845d47c..bbd3794 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -1,34 +1,43 @@ [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 }}" + +[project.scripts] +{{ cookiecutter.project_slug }} = "{{ cookiecutter.project_slug }}.__main__:main" + +[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" 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!')