handbook/docs/environments.md
disqualifier c53d67da2f add uv as a parallel option alongside pip
uv is presented as the recommended faster, standards-compliant drop-in;
pip stays the baseline/fallback. uv command syntax verified against
docs.astral.sh/uv before writing.

environments.md:
- new 'uv (optional, faster)' section: install from pyproject
  (uv pip install . / -e . / '.[dev]'), the uv sync managed-venv flow,
  and a note that uv.lock is local-only/gitignored (not committed)
- pinning subsection: pin direct deps in pyproject via == or git @ref,
  with a warning that this pins direct deps only — transitive deps still
  float at build time (documented tradeoff)
- uv equivalents added beside pip in the Local .venv and Docker tabs

deploy.md:
- 'Faster builds with uv' tip: uv-from-ghcr COPY, ENV UV_COMPILE_BYTECODE=1,
  uv pip install --system . (reads pyproject, no lock)
- layer-caching shows the uv variant beside the pip one
- checklist notes uv pip install + UV_COMPILE_BYTECODE; fix stray 'configs'
  plural -> 'config'

.gitignore: ignore uv.lock (local-only, never committed).

Verified in-browser; mkdocs build --strict clean (anchors resolve).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-30 04:40:10 -04:00

199 lines
7.0 KiB
Markdown

# Virtual environments
How we keep a project's Python isolated — from the system Python and from every
other project. The rule underneath all of it: **never install into, or upgrade,
the system Python.** The OS depends on it; a project must never touch it.
!!! danger "Leave the system Python alone"
No `sudo pip install`, no upgrading the system interpreter for a project. If a
project needs a different version or a package, that goes in a **virtual
environment** — never the system one. Breaking system Python can break the OS.
## Project-based isolation
Every project runs against its own isolated environment. There are three ways that
happens, depending on where the project runs:
=== "Local `.venv`"
A virtualenv living in the repo. Simplest for day-to-day local work.
```bash
python -m venv .venv # create it (once)
source .venv/bin/activate # activate for this shell
pip install -e . # install the project from pyproject.toml
```
Or with [uv](#uv-optional-faster) — same `pyproject.toml`, much faster:
```bash
uv venv # create .venv
uv pip install -e . # install from pyproject.toml
```
Keep `.venv/` **gitignored** — it's per-machine, never committed.
=== "Makefile-driven"
Wrap the venv in a `make` target so every dev (and CI) sets up identically —
no "did you activate it?" drift.
```makefile
VENV := .venv
PY := $(VENV)/bin/python
$(VENV): requirements.txt
python -m venv $(VENV)
$(PY) -m pip install -r requirements.txt
.PHONY: run
run: $(VENV)
$(PY) -m yourapp
```
`make run` creates the venv if missing, installs deps, and runs — all against
the isolated interpreter, no manual activation.
=== "Docker"
The container **is** the isolation — its own filesystem, its own interpreter,
nothing shared with the host. You don't need a `.venv` inside an image; install
straight into the container's Python.
```dockerfile
FROM python:3.12-slim
COPY pyproject.toml .
RUN pip install --no-cache-dir . # or: uv pip install .
COPY . .
```
This is how things run in production — see the [Deploy guide](deploy.md) for the
full container standard (uid 1337, mounts, layer caching, and the uv image
setup).
!!! tip "Which one?"
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable
setup across the team, **Docker** for anything that ships. They're not
exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for
deploy.
## uv (optional, faster)
!!! tip "uv is the recommended fast path; pip stays the baseline"
[uv](https://docs.astral.sh/uv/) is a faster, standards-compliant drop-in for
pip. It reads the **same `pyproject.toml`** — no workflow change required, and
`pip` keeps working exactly as before. Use it wherever you'd reach for pip; the
rest of this handbook shows the pip command with the uv equivalent beside it.
Install deps straight from `pyproject.toml` (no `requirements.txt` needed):
```bash
uv pip install . # install the project + its deps
uv pip install -e . # editable (dev) install
uv pip install '.[dev]' # with an extras group, e.g. dev
```
If you'd rather have uv manage the venv for you, use the managed-venv flow:
```bash
uv sync # create/refresh .venv from pyproject.toml + uv.lock
uv run python -m yourapp # run inside the managed env, no manual activate
```
!!! note "`uv.lock` is local-only — never committed"
`uv sync` writes a `uv.lock` for your machine's resolved environment. It is
**gitignored**, not committed — we don't ship a lockfile. Pinning happens in
`pyproject.toml` (below), not the lock.
### Pinning deps
Pin **direct** dependencies in `pyproject.toml` with `==` or a git `@ref`:
```toml
[project]
dependencies = [
"requests==2.31.0",
"mylib @ git+https://git.rethinkstudios.io/rethink-public/mylib.git@<sha>",
]
```
!!! warning "This pins direct deps only — transitive deps still float"
`==` / `@ref` pins the packages **you** list. Their dependencies still resolve
fresh at build time. That's an accepted tradeoff — documented on purpose — not
an oversight: we pin what we depend on directly and let the rest float.
## Local dev with pyenv
For local work you also need the right **Python version**, not just isolated deps.
We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and
switches versions per-project without touching the system Python.
- **Per-project pinning:** a `.python-version` file in a repo makes pyenv
auto-select that interpreter when you `cd` in — everyone on the project runs the
same one.
- **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an
isolated version *and* isolated deps.
### Install
Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation)
for the installer and build dependencies — no point reproducing it here. Then add
the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell.
### Shell init
Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and
auto-version-switching won't work:
```bash
# pyenv — Python version management
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)" # (1)!
eval "$(pyenv init -)" # (2)!
eval "$(pyenv virtualenv-init -)" # (3)!
```
1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected
version instead of the system one.
2. Shell integration — command rehashing and completion.
3. Auto-activates a project's virtualenv on `cd`**only** if you use
`pyenv-virtualenv`. Drop this line if you don't.
### Everyday use
```bash
pyenv install 3.10.14 # install a version (one-time)
pyenv install --list # see available versions
cd <project>
pyenv local 3.10.14 # writes .python-version -> auto-selects here
python --version # confirms the pinned version
```
- **`pyenv local <ver>`** per project — commit the `.python-version` so the team
matches.
- **`pyenv global <ver>`** for your default outside any project.
## Shell quality-of-life extras
A couple of optional lines worth having alongside pyenv:
```bash
# flake8 with our shared config (max line 120)
alias flake8='flake8 --config ~/.config/flake8'
# local bins on PATH (pip --user installs, npm globals)
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
- The **flake8 alias** keeps everyone linting with the same config (our 120
max-line, etc. — see [Standards](standards.md#files-and-style)).
- The **`.local/bin` / npm-global** PATH lines stop "command not found" after a
`pip install --user` or a global npm install.
!!! note "More shell setup"
This is the Python-env slice. The fuller dev shell setup — the WSL/Windows
clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on
the [Workflow](workflow.md#handy-shell-setup) page.