Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 120 additions & 45 deletions template/.rules/python-pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <name>`:

```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 <extra>`:

```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 <name>`:

```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 <name>` or `--only-group <name>` 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`.

______________________________________________________________________

Expand All @@ -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])

______________________________________________________________________

Expand All @@ -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])

______________________________________________________________________

Expand All @@ -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])

______________________________________________________________________

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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])

______________________________________________________________________

Expand All @@ -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
Expand All @@ -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`,
Expand All @@ -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/)
2 changes: 1 addition & 1 deletion tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, (
Expand Down
Loading