From 4ba810220087eaf11f65b8f492141b4a76263a72 Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 28 Jul 2026 13:33:51 +0200 Subject: [PATCH 1/2] Teach PEP 735 development dependency groups (#44) --- template/.rules/python-pyproject.md | 165 ++++++++++++++++++++-------- 1 file changed, 120 insertions(+), 45 deletions(-) diff --git a/template/.rules/python-pyproject.md b/template/.rules/python-pyproject.md index 50b20fc..f44c16c 100644 --- a/template/.rules/python-pyproject.md +++ b/template/.rules/python-pyproject.md @@ -55,7 +55,8 @@ dependencies = [ Reddit[^5]) - **`description` and `readme`:** Although not mandatory, they help with indexing and packaging tools; `readme = "README.md"` tells `uv` (and PyPI) to - include your README as the long description. (Astral Docs[^1], Python Packaging[^4]) + include your README as the long description. (Astral Docs[^1], Python + Packaging[^4]) - **`requires-python`:** Constrains which Python interpreters your package supports (e.g. `>=3.10`). (Python Packaging[^4], Reddit[^5]) - **`license`:** Specify a licence as an SPDX identifier (via @@ -72,32 +73,93 @@ dependencies = [ ______________________________________________________________________ -## 3. Optional and Development Dependencies +## 3. Runtime vs. development dependencies -Modern projects typically distinguish between "production" dependencies (those -needed at runtime) and "development" dependencies (linters, test frameworks, -etc.). In PEP 621, you use `[project.optional-dependencies]` for this: +`uv` (via PEP 621 and PEP 735) exposes three dependency fields. Choosing the +right one decides whether a dependency ships to every end user or only ever +exists on a contributor's machine. + +Table 1. Dependency field selection. + +| Field | Installed for | Use it for | +| --- | --- | --- | +| `project.dependencies` | Everyone who installs the package | Libraries the shipped code imports at runtime | +| `project.optional-dependencies` | End users who opt into an *extra* | Optional runtime *features* (`package[extra]`) | +| `dependency-groups` | Local development only | Test, lint, type-check, docs, and other tooling | + +### Required runtime dependencies — `project.dependencies` + +Packages the shipped code imports unconditionally. They are published in the +wheel metadata and installed for every consumer. Use PEP 508 specifiers with +bounded ranges, and add them with `uv add `: + +```toml +[project] +dependencies = [ + "httpx>=0.27,<1", +] +``` + +### Optional runtime features — `project.optional-dependencies` + +Published "extras" that an *end user* opts into to enable an optional feature +of the package, requested with `package[extra]` syntax (for example, +`pandas[excel]`). Reach for this only when the extra dependency powers +user-facing functionality that not everyone needs — never for development +tooling. Add them with `uv add --optional `: ```toml [project.optional-dependencies] -dev = [ - "pytest>=7.0", # Testing framework - "black", # Code formatter - "flake8>=4.0" # Linter +# Opt-in feature: end users request it with cuprum[feature]. +feature = [ + "some-runtime-lib>=1.2,<2", ] -docs = [ - "sphinx>=5.0", # Documentation builder - "sphinx-rtd-theme" +``` + +### Development-time dependencies — `dependency-groups` + +Tooling only contributors need: test frameworks, linters, type checkers, +documentation builders, and property or mutation testers. These are +**local-only** — PEP 735 dependency groups are *not* included in published +package metadata (they are not part of the wheel), so they must live here rather +than in `project.optional-dependencies`. Add them with `uv add --dev` (the +`dev` group) or `uv add --group `: + +```toml +[dependency-groups] +dev = [ + "pytest<9.1", + "ruff", + "ty", ] ``` -- **`[project.optional-dependencies]`:** Each table key (e.g. `dev`, `docs`) - defines a "dependency group." You can install a group via - `uv add --group dev` or `uv sync --include dev`. (Python Packaging[^4], - DevsJC[^6]) -- **Why use groups?** You keep the lockfile deterministic (via `uv.lock`) while - still separating concerns (test‐only vs. production). (Medium[^7], - DevsJC[^6]) +**`uv` installs the `dev` group automatically by default.** `uv run` and +`uv sync` include the `dev` group with no extra flags, so a bare `uv sync` +gives a contributor the full toolchain. Adjust this with: + +- `--no-dev` or `--no-default-groups` to exclude development dependencies (for + example, when building a wheel or a production install). +- `--group ` or `--only-group ` to include or isolate a + non-default group. +- `[tool.uv].default-groups` to change which groups sync by default: + +```toml +[tool.uv] +default-groups = ["dev", "docs"] # or "all" +``` + +Groups may nest via `{ include-group = "..." }`, and by default `uv` resolves +every group together into a single `uv.lock`, so groups must be mutually +compatible unless you declare incompatible sets explicitly under +`[tool.uv].conflicts`. +(Astral Docs[^6]) + +> **Rule of thumb:** if an end user needs it to *run* the code, it belongs +> in `project.dependencies` (always) or `project.optional-dependencies` +> (an opt-in feature). If only a contributor needs it to *develop, test, +> lint, type-check, or document* the code, it belongs in +> `dependency-groups`. ______________________________________________________________________ @@ -116,12 +178,12 @@ mygui = "my_project.gui:start" ``` - **`[project.scripts]`:** Defines console scripts. When you run `uv run mycli`, - `uv` will invoke the `main` function in `my_project/cli.py`. (Astral Docs[^8]) + `uv` will invoke the `main` function in `my_project/cli.py`. (Astral Docs[^7]) - **`[project.gui-scripts]`:** On Windows, `uv` will wrap these in a GUI executable; on Unix-like systems, they behave like normal console scripts. - (Astral Docs[^8]) + (Astral Docs[^7]) - **Plugin Entry Points:** If your project supports plugins, use - `[project.entry-points.'group.name']` to register them. (Astral Docs[^8]) + `[project.entry-points.'group.name']` to register them. (Astral Docs[^7]) ______________________________________________________________________ @@ -140,14 +202,14 @@ build-backend = "setuptools.build_meta" - **`requires`:** A list of packages needed at build time. For editable installs in `uv`, you need at least `setuptools>=61.0` and `wheel`. (Python - Packaging[^4], Astral Docs[^8]) + Packaging[^4], Astral Docs[^7]) - **`build-backend`:** The entry point for your build backend. `setuptools.build_meta` is the PEP 517-compliant backend for setuptools. - (Python Packaging[^4], Astral Docs[^8]) + (Python Packaging[^4], Astral Docs[^7]) - **Note:** If you omit `[build-system]`, `uv` will assume `setuptools.build_meta:__legacy__` and still install dependencies, but it won't editably install your own project unless you set - `tool.uv.package = true` (see next section). (Astral Docs[^8]) + `tool.uv.package = true` (see next section). (Astral Docs[^7]) ______________________________________________________________________ @@ -164,10 +226,10 @@ package = true - **`tool.uv.package = true`:** Forces `uv` to build and install your project into its virtual environment every time you run `uv sync` or `uv run`. Without this, `uv` only installs dependencies (not your own package) if - `[build-system]` is missing. (Astral Docs[^8]) + `[build-system]` is missing. (Astral Docs[^7]) - You may also set other `uv`-specific keys (e.g., custom indexes, resolver policies) under `[tool.uv]`, but `package` is the most common. (Python - Packaging[^4], Astral Docs[^8]) + Packaging[^4], Astral Docs[^7]) ______________________________________________________________________ @@ -198,11 +260,18 @@ dependencies = [ "numpy>=1.23" ] +# Opt-in runtime feature; end users install it with my_project[fast]. [project.optional-dependencies] +fast = [ + "orjson>=3.9" +] + +# Development-only tooling (PEP 735); never shipped to end users. +[dependency-groups] dev = [ "pytest>=7.0", - "black", - "flake8>=4.0" + "ruff", + "mypy>=1.0" ] docs = [ "sphinx>=5.0", @@ -233,30 +302,33 @@ package = true - `dependencies`: runtime requirements, expressed in PEP 508 syntax. (Astral Docs[^1], RidgeRun.ai[^2]) -2. **Optional Dependencies (`[project.optional-dependencies]`):** +2. **Optional features vs. development tooling:** - - Grouped as `dev` (for testing + linting) and `docs` (for documentation). - Installing them is as simple as `uv add --group dev` or - `uv sync --include dev`. (Python Packaging[^4], DevsJC[^6]) + - `[project.optional-dependencies]` declares an opt-in runtime *extra* + (`fast`), installed by end users via `my_project[fast]`. (Python + Packaging[^4]) + - `[dependency-groups]` (PEP 735) holds development-only tooling (`dev`, + `docs`) that is never published; `uv sync` installs the `dev` group by + default. (Astral Docs[^6]) 3. **Entry Points (`[project.scripts]`):** - Defines a console command `mycli` that maps to `my_project/cli.py:main`. - Invoking `uv run mycli` will run the `main()` function. (Astral Docs[^8]) + Invoking `uv run mycli` will run the `main()` function. (Astral Docs[^7]) 4. **Build System:** - `setuptools>=61.0` plus `wheel` ensures both legacy and editable installs work. ✱ Newer versions of setuptools support PEP 660 editable installs - without a `setup.py` stub. (Python Packaging[^4], Astral Docs[^8]) + without a `setup.py` stub. (Python Packaging[^4], Astral Docs[^7]) - `build-backend = "setuptools.build_meta"` tells `uv` how to compile your - package. (Python Packaging[^4], Astral Docs[^8]) + package. (Python Packaging[^4], Astral Docs[^7]) 5. **`[tool.uv]`:** - `package = true` ensures that `uv sync` will build and install your own project (in editable mode) every time dependencies change. Otherwise, `uv` - treats your project as a collection of scripts only (no package). (Astral Docs[^8]) + treats your project as a collection of scripts only (no package). (Astral Docs[^7]) ______________________________________________________________________ @@ -272,15 +344,16 @@ ______________________________________________________________________ 3. **Semantic Versioning:** Follow [semver](https://semver.org/) for `version` values (e.g., `1.2.3`). Bump patch versions for bug fixes, minor for - backward-compatible changes, and major for breaking changes. (Python Packaging[^4]) + backward-compatible changes, and major for breaking changes. (Python + Packaging[^4]) 4. **Keep Build Constraints Minimal:** If you don't need editable installs, you can omit `[build-system]` (but then `uv` won't build your package; it will only install dependencies). To override, set `tool.uv.package = true`. - (Astral Docs[^8]) + (Astral Docs[^7]) 5. **Use Exact or Bounded Ranges for Dependencies:** Rather than `requests`, use - `requests>=2.25, <3.0` to avoid unexpected major bumps. (DevsJC[^6]) + `requests>=2.25, <3.0` to avoid unexpected major bumps. (DevsJC[^8]) 6. **Consider Dynamic Fields Sparingly:** You can declare fields like `dynamic = ["version"]` if your version is computed at build time (e.g. via @@ -293,8 +366,10 @@ ______________________________________________________________________ A "modern" `pyproject.toml` for an Astral `uv` project should: -- Use the PEP 621 `[project]` table for metadata and `dependencies`. -- Distinguish optional dependencies under `[project.optional-dependencies]`. +- Use the PEP 621 `[project]` table for metadata and runtime `dependencies`. +- Declare opt-in runtime *features* as extras under + `[project.optional-dependencies]`, and development-only tooling under + `[dependency-groups]` (the `dev` group installs by default). - Define any CLI or GUI entry points under `[project.scripts]` or `[project.gui-scripts]`. - Declare a PEP 517 `[build-system]` (e.g. `setuptools>=61.0`, `wheel`, @@ -311,6 +386,6 @@ easy to maintain, and integrates seamlessly with Astral `uv`. [^3]: [Modern Python Development with pyproject.toml and UV](https://levelup.gitconnected.com/modern-python-development-with-pyproject-toml-and-uv-405dfb8b6ec8) [^4]: [Writing your pyproject.toml – Python Packaging User Guide](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) [^5]: [Anyone used UV package manager in production? (Reddit)](https://www.reddit.com/r/Python/comments/1ixryec/anyone_used_uv_package_manager_in_production/) -[^6]: [The Complete Guide to pyproject.toml – devsjc blogs](https://devsjc.github.io/blog/20240627-the-complete-guide-to-pyproject-toml/) -[^7]: [Start Using UV Python Package Manager for Better Dependency Management](https://medium.com/%40gnetkov/start-using-uv-python-package-manager-for-better-dependency-management-183e7e428760) -[^8]: [Configuring projects | uv - Astral Docs](https://docs.astral.sh/uv/concepts/projects/config/) +[^6]: [Managing dependencies | uv - Astral Docs](https://docs.astral.sh/uv/concepts/projects/dependencies/) +[^7]: [Configuring projects | uv - Astral Docs](https://docs.astral.sh/uv/concepts/projects/config/) +[^8]: [The Complete Guide to pyproject.toml – devsjc blogs](https://devsjc.github.io/blog/20240627-the-complete-guide-to-pyproject-toml/) From 0e4e81d609fa4695cab724297f52040cc3fd3217 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 22:59:19 +0200 Subject: [PATCH 2/2] Update Makefile phony contract test --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3bda56d..0c5a669 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -496,7 +496,7 @@ def test_parent_makefile_test_target_uses_requisite_pytest_command() -> None: """ makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8") - assert ".PHONY: help check-fmt lint spelling test typecheck" in makefile, ( + assert ".PHONY: help check-fmt fmt lint spelling test typecheck" in makefile, ( "expected parent Makefile to mark documented gate targets as phony" ) assert "check-fmt: ## Verify template test formatting" in makefile, (