Skip to content

Commit 6b34d20

Browse files
authored
Dependency management improvements (#188)
* Correctly use a constraints file Turns out I've been using the constraint file incorrectly for years. I love learning moments like this! The `constraints.txt` file **is** the lockfile for the project. It contains all the versions that *should* be used *if* they are needed. Is is not the source of **what** should be installed, only what version. So, instead of having N different `requirements*.in` files with N different `requirements*.txt` generated from `pip-compile`; we have a single `constraints.txt` that all of our `requirements*.txt` files reference. Glorious. This solves for the scaling problem that I ran into when, for example, `requirements-doc.txt` had a version conflicct with something in `requirements-test.txt`. Initially, I thought to somehow cross-reference all the `.txt` files with themselves in some horrible chained constraint system. That worked about as well as it sounds. Note, I have to remove the dynamic dependencies here from the `pyproject.toml`. That's fine. Eventually, I'll be moving to `uv` which simplifies most of this juggling. For now, however, I want to understand the underlying machanics before abstracting them away. * Improve noxfile for dev install and management Moving toward simplicity of use, the `noxfile.txt` now has a session called `dev`. This session creates, if needed, the `.venv` and installs all development requirements. Additionally, the `update` and `upgrade` sessions have been renamed to `update-deps` and `upgrade-deps` respectively. They now use the new, awesome pattern of a single constraint file for dependencies. Finally, added the `--no-annotations` to the `pip-compile` commands and what a difference that makes! I have yet to understand when and why `pip-compile` will expand `requirements/requirements.txt` to `/home/preocts/project-name/requirements/requirements.txt`. In practice, the results are different depending on the OS being used as well. Since it all lead to noisy diffs without much value, removing it seems ideal! * Correct nox sessions for CI actions Okay, this is the ugly part of the path I'm taking. It is now required to install the correct requirement*.txt files for each session. Previously it was a simple `.[test]` reference when installing the package. This too shall pass. Already thinking about pivoting into listing all requirements in the `pyproject.toml`. With dependency groups being supported this should be amazing. It will hindge on `pip-compile` being able to extract from dependency groups. That's the next PR! * Update documenation Adjust the contributing docs with the new expected nox workflows. Add some leading links at the top of the readme pointing to the secondary files. Reading, strangely, isn't the strongest skill of many.
1 parent 768ba1e commit 6b34d20

9 files changed

Lines changed: 148 additions & 143 deletions

File tree

CONTRIBUTING.md

Lines changed: 15 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ prior to submitting a pull request.
3535
- No test should be dependent on another
3636
- No test should be dependent on secrets/tokens
3737

38-
3938
---
4039

4140
# Local developer installation
@@ -52,81 +51,23 @@ git clone https://github.com/[ORG NAME]/[REPO NAME]
5251
cd [REPO NAME]
5352
```
5453

55-
### Virtual Environment
56-
57-
Use a ([`venv`](https://docs.python.org/3/library/venv.html)), or equivalent,
58-
when working with python projects. Leveraging a `venv` will ensure the installed
59-
dependency files will not impact other python projects or any system
60-
dependencies.
61-
62-
**Windows users**: Depending on your python install you will use `py` in place
63-
of `python` to create the `venv`.
64-
65-
**Linux/Mac users**: Replace `python`, if needed, with the appropriate call to
66-
the desired version while creating the `venv`. (e.g. `python3` or `python3.12`)
67-
68-
**All users**: Once inside an active `venv` all systems should allow the use of
69-
`python` for command line instructions. This will ensure you are using the
70-
`venv`'s python and not the system level python.
71-
72-
### Create the `venv`:
73-
74-
```console
75-
python -m venv .venv
76-
```
77-
78-
Activate the `venv`:
79-
80-
```console
81-
# Linux/Mac
82-
. .venv/bin/activate
83-
84-
# Windows
85-
.venv\Scripts\activate
86-
```
54+
### [Install nox](https://nox.thea.codes/en/stable/index.html)
8755

88-
The command prompt should now have a `(venv)` prefix on it. `python` will now
89-
call the version of the interpreter used to create the `venv`
56+
It is recommended to use a tool such as `pipx` or `uvx` to install nox. nox is
57+
needed to run the provided sessions for developer setup, linting, tests, and
58+
dependency management. It is optional, but these instructions will not cover
59+
manually steps outside of nox.
9060

91-
To deactivate (exit) the `venv`:
9261

93-
```console
94-
deactivate
95-
```
96-
97-
---
62+
## Nox Sessions
9863

99-
## Developer Installation Steps
64+
### Developer Install
10065

101-
### Install editable library and development requirements
66+
This builds the `/.venv`, installs the editable
67+
package, and installs all dependency files.
10268

103-
```console
104-
python -m pip install --editable .[dev,test]
105-
```
106-
107-
### Install pre-commit [(see below for details)](#pre-commit)
108-
109-
```console
110-
pre-commit install
111-
```
112-
113-
### Install with nox
114-
115-
If you have `nox` installed with `pipx` or in the current venv you can use the
116-
following session. This is an alternative to the two steps above.
117-
118-
```console
119-
nox -s install
120-
```
121-
122-
---
123-
124-
## Pre-commit and nox tools
125-
126-
### Run pre-commit on all files
127-
128-
```console
129-
pre-commit run --all-files
69+
```bash
70+
nox -s dev
13071
```
13172

13273
### Run tests with coverage (quick)
@@ -151,23 +92,23 @@ nox -e build
15192

15293
## Updating dependencies
15394

154-
New dependencys can be added to the `requirements-*.in` file. It is recommended
95+
New dependencys can be added to the `requirements-*.txt` file. It is recommended
15596
to only use pins when specific versions or upgrades beyond a certain version are
15697
to be avoided. Otherwise, allow `pip-compile` to manage the pins in the
157-
generated `requirements-*.txt` files.
98+
generated `constraints.txt` file.
15899

159100
Once updated following the steps below, the package can be installed if needed.
160101

161102
### Update the generated files with changes
162103

163104
```console
164-
nox -e update
105+
nox -s update-deps
165106
```
166107

167108
### Upgrade all generated dependencies
168109

169110
```console
170-
nox -e upgrade
111+
nox -s upgrade-deps
171112
```
172113

173114
---

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
# python-src-template
99

10+
- [Contributing Guide and Developer Setup Guide](./CONTRIBUTING.md)
11+
- [License: MIT](./LICENSE)
12+
13+
---
14+
1015
A template I use for most projects and is setup to jive with my environment at
1116
the company I work with.
1217

noxfile.py

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@
1111
MODULE_NAME = "module_name"
1212
TESTS_PATH = "tests"
1313
COVERAGE_FAIL_UNDER = 50
14-
DEFAULT_PYTHON_VERSION = "3.12"
14+
DEFAULT_PYTHON = "3.12"
1515
PYTHON_MATRIX = ["3.9", "3.10", "3.11", "3.12", "3.13"]
1616
VENV_BACKEND = "venv"
1717
VENV_PATH = ".venv"
18-
REQUIREMENT_IN_FILES = [
19-
pathlib.Path("requirements/requirements.in"),
20-
]
18+
REQUIREMENTS_PATH = "./requirements"
2119

2220
# What we allowed to clean (delete)
2321
CLEANABLE_TARGETS = [
@@ -43,35 +41,72 @@
4341
]
4442

4543

44+
@nox.session(python=False)
45+
def dev(session: nox.Session) -> None:
46+
"""Setup a development environment by creating the venv and installs dependencies."""
47+
# Use the active environement if it exists, otherwise create a new one
48+
venv_path = os.environ.get("VIRTUAL_ENV", VENV_PATH)
49+
50+
if sys.platform == "win32":
51+
py_command = "py"
52+
venv_path = f"{venv_path}/Scripts"
53+
activate_command = f"{venv_path}/activate"
54+
else:
55+
py_command = f"python{DEFAULT_PYTHON}"
56+
venv_path = f"{venv_path}/bin"
57+
activate_command = f"source {venv_path}/activate"
58+
59+
if not os.path.exists(VENV_PATH):
60+
session.run(py_command, "-m", "venv", VENV_PATH, "--upgrade-deps")
61+
62+
python = f"{venv_path}/python"
63+
requirement_files = get_requirement_files()
64+
65+
session.run(python, "-m", "pip", "install", "-e", ".")
66+
for requirement_file in requirement_files:
67+
session.run(python, "-m", "pip", "install", "-r", requirement_file)
68+
69+
session.run(python, "-m", "pip", "install", "pre-commit")
70+
session.run(f"{venv_path}/pre-commit", "install")
71+
72+
if not os.environ.get("VIRTUAL_ENV"):
73+
session.log(f"\n\nRun '{activate_command}' to enter the virtual environment.\n")
74+
75+
4676
@nox.session(python=PYTHON_MATRIX, venv_backend=VENV_BACKEND)
4777
def version_coverage(session: nox.Session) -> None:
4878
"""Run unit tests with coverage saved to partial file."""
4979
print_standard_logs(session)
5080

51-
session.install(".[test]")
81+
session.install(".")
82+
session.install("-r", "requirements/requirements.txt")
83+
session.install("-r", "requirements/requirements-test.txt")
5284
session.run("coverage", "run", "-p", "-m", "pytest", TESTS_PATH)
5385

5486

55-
@nox.session(python=DEFAULT_PYTHON_VERSION, venv_backend=VENV_BACKEND)
87+
@nox.session(python=DEFAULT_PYTHON, venv_backend=VENV_BACKEND)
5688
def coverage_combine(session: nox.Session) -> None:
5789
"""Combine all coverage partial files and generate JSON report."""
5890
print_standard_logs(session)
5991

6092
fail_under = f"--fail-under={COVERAGE_FAIL_UNDER}"
6193

62-
session.install(".[test]")
94+
session.install(".")
95+
session.install("-r", "requirements/requirements.txt")
96+
session.install("-r", "requirements/requirements-test.txt")
6397
session.run("python", "-m", "coverage", "combine")
6498
session.run("python", "-m", "coverage", "report", "-m", fail_under)
6599
session.run("python", "-m", "coverage", "json")
66100

67101

68-
@nox.session(python=DEFAULT_PYTHON_VERSION, venv_backend=VENV_BACKEND)
102+
@nox.session(python=DEFAULT_PYTHON, venv_backend=VENV_BACKEND)
69103
def mypy(session: nox.Session) -> None:
70104
"""Run mypy against package and all required dependencies."""
71105
print_standard_logs(session)
72106

73107
session.install(".")
74-
session.install("mypy")
108+
session.install("-r", "requirements/requirements.txt")
109+
session.install("-r", "requirements/requirements-dev.txt")
75110
session.run("mypy", "-p", MODULE_NAME, "--no-incremental")
76111

77112

@@ -83,7 +118,7 @@ def coverage(session: nox.Session) -> None:
83118
session.run("coverage", "report", "-m")
84119

85120

86-
@nox.session(python=DEFAULT_PYTHON_VERSION, venv_backend=VENV_BACKEND)
121+
@nox.session(python=DEFAULT_PYTHON, venv_backend=VENV_BACKEND)
87122
def build(session: nox.Session) -> None:
88123
"""Build distribution files."""
89124
print_standard_logs(session)
@@ -92,49 +127,41 @@ def build(session: nox.Session) -> None:
92127
session.run("python", "-m", "build")
93128

94129

95-
@nox.session(python=False, venv_backend=VENV_BACKEND)
96-
def install(session: nox.Session) -> None:
97-
"""Setup a development environment. Uses active venv if available, builds one if not."""
98-
# Use the active environement if it exists, otherwise create a new one
99-
venv_path = os.environ.get("VIRTUAL_ENV", VENV_PATH)
100-
101-
if sys.platform == "win32":
102-
py_command = "py"
103-
venv_path = f"{venv_path}/Scripts"
104-
activate_command = f"{venv_path}/activate"
105-
else:
106-
py_command = f"python{DEFAULT_PYTHON_VERSION}"
107-
venv_path = f"{venv_path}/bin"
108-
activate_command = f"source {venv_path}/activate"
109-
110-
if not os.path.exists(VENV_PATH):
111-
session.run(py_command, "-m", "venv", VENV_PATH, "--upgrade-deps")
112-
113-
session.run(f"{venv_path}/python", "-m", "pip", "install", "-e", ".[dev,test]")
114-
session.run(f"{venv_path}/pre-commit", "install")
115-
116-
if not venv_path:
117-
session.log(f"\n\nRun '{activate_command}' to enter the virtual environment.\n")
118-
119-
120-
@nox.session(python=DEFAULT_PYTHON_VERSION, venv_backend=VENV_BACKEND)
121-
def update(session: nox.Session) -> None:
122-
"""Process requirement*.in files, updating only additions/removals."""
130+
@nox.session(python=DEFAULT_PYTHON, venv_backend=VENV_BACKEND, name="update-deps")
131+
def update_deps(session: nox.Session) -> None:
132+
"""Process requirement*.txt files, updating only additions/removals."""
123133
print_standard_logs(session)
124134

125-
session.install("pip-tools")
126-
for filename in REQUIREMENT_IN_FILES:
127-
session.run("pip-compile", "--no-emit-index-url", str(filename))
128-
135+
requirement_files = get_requirement_files()
129136

130-
@nox.session(python=DEFAULT_PYTHON_VERSION, venv_backend=VENV_BACKEND)
131-
def upgrade(session: nox.Session) -> None:
132-
"""Process requirement*.in files and upgrade all libraries as possible."""
137+
session.install("pip-tools")
138+
session.run(
139+
"pip-compile",
140+
"--no-annotate",
141+
"--no-emit-index-url",
142+
"--output-file",
143+
f"{REQUIREMENTS_PATH}/constraints.txt",
144+
*requirement_files,
145+
)
146+
147+
148+
@nox.session(python=DEFAULT_PYTHON, venv_backend=VENV_BACKEND, name="upgrade-deps")
149+
def upgrade_deps(session: nox.Session) -> None:
150+
"""Process requirement*.txt files and upgrade all libraries as possible."""
133151
print_standard_logs(session)
134152

153+
requirement_files = get_requirement_files()
154+
135155
session.install("pip-tools")
136-
for filename in REQUIREMENT_IN_FILES:
137-
session.run("pip-compile", "--no-emit-index-url", "--upgrade", str(filename))
156+
session.run(
157+
"pip-compile",
158+
"--no-annotate",
159+
"--no-emit-index-url",
160+
"--upgrade",
161+
"--output-file",
162+
f"{REQUIREMENTS_PATH}/constraints.txt",
163+
*requirement_files,
164+
)
138165

139166

140167
@nox.session(python=False, venv_backend=VENV_BACKEND)
@@ -157,3 +184,9 @@ def print_standard_logs(session: nox.Session) -> None:
157184
version = session.run("python", "--version", silent=True)
158185
session.log(f"Running from: {session.bin}")
159186
session.log(f"Running with: {version}")
187+
188+
189+
def get_requirement_files() -> list[pathlib.Path]:
190+
"""Get a list of requirement files matching "requirements*.txt"."""
191+
glob = pathlib.Path(REQUIREMENTS_PATH).glob("requirements*.txt")
192+
return [path for path in glob]

pyproject.toml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ classifiers = [
1919
"Programming Language :: Python :: 3 :: Only",
2020
"Programming Language :: Python :: Implementation :: CPython",
2121
]
22-
dynamic = ["dependencies", "optional-dependencies", "version"]
22+
dynamic = ["version"]
2323

2424
[project.urls]
2525
homepage = "https://github.com/[ORG NAME]/[REPO NAME]"
@@ -30,13 +30,6 @@ homepage = "https://github.com/[ORG NAME]/[REPO NAME]"
3030
[tool.setuptools_scm]
3131
# Purposely left empty
3232

33-
[tool.setuptools.dynamic.dependencies]
34-
file = ["requirements/requirements.txt"]
35-
36-
[tool.setuptools.dynamic.optional-dependencies]
37-
dev = { file = ["requirements/requirements-dev.txt"] }
38-
test = { file = ["requirements/requirements-test.txt"] }
39-
4033
[tool.black]
4134
line-length = 100
4235
target-version = ['py39']

requirements/constraints.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.12
3+
# by the following command:
4+
#
5+
# pip-compile --no-annotate --no-emit-index-url --output-file=./requirements/constraints.txt requirements/requirements-dev.txt requirements/requirements-test.txt requirements/requirements.txt
6+
#
7+
black==25.1.0
8+
certifi==2025.4.26
9+
charset-normalizer==3.4.2
10+
click==8.2.1
11+
coverage==7.8.2
12+
flake8==7.2.0
13+
flake8-builtins==2.5.0
14+
flake8-pep585==0.1.7
15+
idna==3.10
16+
iniconfig==2.1.0
17+
isort==6.0.1
18+
mccabe==0.7.0
19+
mypy==1.15.0
20+
mypy-extensions==1.1.0
21+
packaging==25.0
22+
pathspec==0.12.1
23+
platformdirs==4.3.8
24+
pluggy==1.6.0
25+
pycodestyle==2.13.0
26+
pyflakes==3.3.2
27+
pytest==8.3.5
28+
pytest-randomly==3.16.0
29+
requests==2.32.3
30+
typing-extensions==4.13.2
31+
urllib3==2.4.0

0 commit comments

Comments
 (0)