Skip to content

Commit 00f6ac4

Browse files
authored
Use a fully locked test environment (#1349)
* Use dependency groups & a fully locked dev environment * update lockfile * Update contributing guide
1 parent 901bfaa commit 00f6ac4

File tree

6 files changed

+1844
-67
lines changed

6 files changed

+1844
-67
lines changed

.github/CONTRIBUTING.md

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,42 +52,36 @@ First, **fork** the repository on GitHub and **clone** it using one of the alter
5252
You can (and should) run our test suite using [*tox*](https://tox.wiki/).
5353
However, you'll probably want a more traditional environment as well.
5454

55-
We recommend using the Python version from the `.python-version-default` file in the project's root directory, because that's the one that is used in the CI by default, too.
55+
We recommend using the Python version from the `.python-version-default` file in the project's root directory.
5656

57-
If you're using [*direnv*](https://direnv.net), you can automate the creation of the project virtual environment with the correct Python version by adding the following `.envrc` to the project root:
57+
We use a fully-locked development environment using [*uv*](https://docs.astral.sh/uv/) so the easiest way to get started is to [install *uv*](https://docs.astral.sh/uv/getting-started/installation/) and you can run `uv run pytest` to run the tests immediately.
58+
59+
I you'd like a traditional virtual environment, you can run `uv sync` and it will create a virtual environment named `.venv` with the correct Python version and install all the dependencies in the root directory.
60+
61+
If you're using [*direnv*](https://direnv.net), you can automate the creation and activation of the project's virtual environment with the correct Python version by adding the following `.envrc` to the project root:
5862

5963
```bash
60-
layout python python$(cat .python-version-default)
64+
uv sync
65+
. .venv/bin/activate
6166
```
6267

63-
or, if you like [*uv*](https://github.com/astral-sh/uv):
68+
---
69+
70+
If you don't want to use *uv*, you can use Pip 25.1 (that added support for dependency groups) or newer and install the dependencies manually:
6471

6572
```bash
66-
test -d .venv || uv venv --python python$(cat .python-version-default)
67-
. .venv/bin/activate
73+
pip install -e . --group dev
6874
```
6975

76+
---
77+
7078
> [!WARNING]
7179
> - **Before** you start working on a new pull request, use the "*Sync fork*" button in GitHub's web UI to ensure your fork is up to date.
7280
>
7381
> - **Always create a new branch off `main` for each new pull request.**
7482
> Yes, you can work on `main` in your fork and submit pull requests.
7583
> But this will *inevitably* lead to you not being able to synchronize your fork with upstream and having to start over.
7684
77-
Change into the newly created directory and after activating a virtual environment, install an editable version of this project along with its tests requirements:
78-
79-
```console
80-
$ pip install -e .[dev] # or `uv pip install -e .[dev]`
81-
```
82-
83-
Now you can run the test suite:
84-
85-
```console
86-
$ python -Im pytest
87-
```
88-
89-
You can *significantly* speed up the test suite by passing `-n auto` to *pytest* which activates [*pytest-xdist*](https://github.com/pytest-dev/pytest-xdist) and takes advantage of all your CPU cores.
90-
9185
---
9286

9387
When working on the documentation, use:
@@ -123,7 +117,6 @@ $ tox run -e docs-doctests
123117
- If you've changed or added public APIs, please update our type stubs (files ending in `.pyi`).
124118

125119

126-
127120
## Tests
128121

129122
- Write your asserts as `expected == actual` to line them up nicely, and leave an empty line before them:

pyproject.toml

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ name = "attrs"
1010
authors = [{ name = "Hynek Schlawack", email = "[email protected]" }]
1111
license = "MIT"
1212
license-files = ["LICENSE"]
13-
requires-python = ">=3.8"
13+
requires-python = ">=3.9"
1414
description = "Classes Without Boilerplate"
1515
keywords = ["class", "attribute", "boilerplate"]
1616
classifiers = [
1717
"Development Status :: 5 - Production/Stable",
18-
"Programming Language :: Python :: 3.8",
1918
"Programming Language :: Python :: 3.9",
2019
"Programming Language :: Python :: 3.10",
2120
"Programming Language :: Python :: 3.11",
@@ -29,7 +28,15 @@ classifiers = [
2928
dependencies = []
3029
dynamic = ["version", "readme"]
3130

32-
[project.optional-dependencies]
31+
[project.urls]
32+
Documentation = "https://www.attrs.org/"
33+
Changelog = "https://www.attrs.org/en/stable/changelog.html"
34+
GitHub = "https://github.com/python-attrs/attrs"
35+
Funding = "https://github.com/sponsors/hynek"
36+
Tidelift = "https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi"
37+
38+
39+
[dependency-groups]
3340
tests-mypy = [
3441
# A transitive dependency of pytest-mypy-plugins doesn't build on 3.14 yet.
3542
'pytest-mypy-plugins; platform_python_implementation == "CPython" and python_version >= "3.10" and python_version < "3.14"',
@@ -38,21 +45,25 @@ tests-mypy = [
3845
'mypy>=1.11.1; platform_python_implementation == "CPython" and python_version >= "3.10"',
3946
]
4047
tests = [
48+
{ include-group = "tests-mypy" },
4149
# For regression test to ensure cloudpickle compat doesn't break.
4250
'cloudpickle; platform_python_implementation == "CPython"',
4351
"hypothesis",
4452
"pympler",
4553
# 4.3.0 dropped last use of `convert`
4654
"pytest>=4.3.0",
47-
"pytest-xdist[psutil]",
48-
"attrs[tests-mypy]",
4955
]
5056
cov = [
51-
"attrs[tests]",
57+
{ include-group = "tests" },
5258
# Ensure coverage is new enough for `source_pkgs`.
5359
"coverage[toml]>=5.3",
5460
]
55-
benchmark = ["pytest-codspeed", "pytest-xdist[psutil]", "attrs[tests]"]
61+
pyright = ["pyright", { include-group = "tests" }]
62+
benchmark = [
63+
{ include-group = "tests" },
64+
"pytest-codspeed",
65+
"pytest-xdist[psutil]",
66+
]
5667
docs = [
5768
"cogapp",
5869
"furo",
@@ -62,14 +73,8 @@ docs = [
6273
"sphinxcontrib-towncrier",
6374
"towncrier",
6475
]
65-
dev = ["attrs[tests]", "pre-commit-uv"]
66-
67-
[project.urls]
68-
Documentation = "https://www.attrs.org/"
69-
Changelog = "https://www.attrs.org/en/stable/changelog.html"
70-
GitHub = "https://github.com/python-attrs/attrs"
71-
Funding = "https://github.com/sponsors/hynek"
72-
Tidelift = "https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi"
76+
docs-watch = [{ include-group = "docs" }, "watchfiles"]
77+
dev = [{ include-group = "tests" }]
7378

7479

7580
[tool.hatch.version]
@@ -227,6 +232,8 @@ ignore = [
227232
"TD", # we don't follow other people's todo style
228233
"TRY301", # I'm sorry, but this makes not sense for us.
229234
"UP031", # format() is slow as molasses; % and f'' FTW.
235+
"UP006", # replace Dict etc by dict etc later.
236+
"UP035", # replace Dict etc by dict etc later.
230237
]
231238

232239
[tool.ruff.lint.per-file-ignores]
@@ -254,10 +261,10 @@ ignore = [
254261
"src/*/*.pyi" = ["ALL"] # TODO
255262
"tests/test_annotations.py" = ["FA100"]
256263
"tests/typing_example.py" = [
257-
"E741", # ambiguous variable names don't matter in type checks
258-
"B018", # useless expressions aren't useless in type checks
259-
"B015", # pointless comparison in type checks aren't pointless
260-
"UP037", # we test some older syntaxes on purpose
264+
"E741", # ambiguous variable names don't matter in type checks
265+
"B018", # useless expressions aren't useless in type checks
266+
"B015", # pointless comparison in type checks aren't pointless
267+
"UP037", # we test some older syntaxes on purpose
261268
]
262269

263270
[tool.ruff.lint.isort]

src/attr/_compat.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111

1212
PYPY = platform.python_implementation() == "PyPy"
13-
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
1413
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
1514
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
1615
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)

src/attr/_funcs.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import copy
55

6-
from ._compat import PY_3_9_PLUS, get_generic_base
6+
from ._compat import get_generic_base
77
from ._make import _OBJ_SETATTR, NOTHING, fields
88
from .exceptions import AttrsAttributeNotFoundError
99

@@ -450,10 +450,11 @@ class yet.
450450
if getattr(cls, "__attrs_types_resolved__", None) != cls:
451451
import typing
452452

453-
kwargs = {"globalns": globalns, "localns": localns}
454-
455-
if PY_3_9_PLUS:
456-
kwargs["include_extras"] = include_extras
453+
kwargs = {
454+
"globalns": globalns,
455+
"localns": localns,
456+
"include_extras": include_extras,
457+
}
457458

458459
hints = typing.get_type_hints(cls, **kwargs)
459460
for field in fields(cls) if attribs is None else attribs:

tox.ini

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ min_version = 4
33
# Mypy doesn't run on 3.14 yet.
44
env_list =
55
pre-commit,
6-
py3{8,9,10,11,12,13,14}-tests,
6+
py3{9,10,11,12,13,14}-tests,
77
py3{10,11,12,13}-mypy,
88
pypy3-tests,
99
pyright,
@@ -17,45 +17,46 @@ pass_env = SETUPTOOLS_SCM_PRETEND_VERSION
1717

1818

1919
[testenv]
20+
runner = uv-venv-lock-runner
2021
package = wheel
2122
wheel_build_env = .pkg
22-
extras =
23+
dependency_groups =
2324
tests: tests
2425
mypy: tests-mypy
2526
commands =
26-
tests: pytest {posargs:-n auto}
27+
tests: pytest {posargs}
2728
mypy: mypy tests/typing_example.py
2829
mypy: mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi
2930

3031
[testenv:pypy3-tests]
31-
extras = tests
32+
dependency_groups = tests
3233
commands = pytest tests/test_functional.py
3334

34-
[testenv:py3{8,10,13}-tests]
35-
extras = cov
35+
[testenv:py3{9,10,13}-tests]
36+
dependency_groups = cov
3637
# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
3738
# PYTHONWARNINGS=d makes them visible during the tox run.
3839
set_env =
3940
COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
4041
PYTHONWARNINGS=d
4142
commands_pre = python -c 'import pathlib; pathlib.Path("{env_site_packages_dir}/cov.pth").write_text("import coverage; coverage.process_startup()")'
42-
# We group xdist execution by file because otherwise the Mypy tests have race conditions.
43-
commands = coverage run -m pytest {posargs:-n auto --dist loadfile}
43+
commands =
44+
coverage run -m pytest {posargs}
4445

4546
[testenv:coverage-report]
4647
# Keep base_python in-sync with .python-version-default
4748
base_python = py313
48-
# Keep depends in-sync with testenv above that has cov extra.
49-
depends = py3{8,10,13}-tests
49+
# Keep depends in-sync with testenv above that has the cov dependency group.
50+
depends = py3{9,10,13}-tests
5051
skip_install = true
51-
deps = coverage[toml]>=5.3
52+
dependency_groups = cov
5253
commands =
5354
coverage combine
5455
coverage report
5556

5657

5758
[testenv:codspeed]
58-
extras = benchmark
59+
dependency_groups = benchmark
5960
pass_env =
6061
CODSPEED_TOKEN
6162
CODSPEED_ENV
@@ -66,9 +67,10 @@ commands = pytest --codspeed -n auto bench/test_benchmarks.py
6667

6768

6869
[testenv:docs-{build,doctests,linkcheck}]
69-
# Keep base_python in sync with .readthedocs.yaml.
70+
runner = uv-venv-lock-runner
71+
# Keep base_python in-sync with .readthedocs.yaml.
7072
base_python = py313
71-
extras = docs
73+
dependency_groups = docs
7274
commands =
7375
build: sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs {posargs:docs/_build/}html
7476
doctests: sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs {posargs:docs/_build/}html
@@ -77,46 +79,48 @@ commands =
7779
[testenv:docs-watch]
7880
package = editable
7981
base_python = {[testenv:docs-build]base_python}
80-
extras = {[testenv:docs-build]extras}
82+
dependency_groups = docs-watch
8183
deps = watchfiles
8284
commands =
8385
watchfiles \
8486
--ignore-paths docs/_build/ \
8587
'sphinx-build -W -n --jobs auto -b html -d {envtmpdir}/doctrees docs docs/_build/html' \
88+
README.md \
8689
src \
8790
docs
8891

8992
[testenv:docs-sponsors]
93+
runner = uv-venv-runner
94+
skip_install = true
9095
description = Ensure sponsor logos are up to date.
9196
deps = cogapp
9297
commands = cog -rP README.md docs/index.md
9398

9499

95100
[testenv:pre-commit]
101+
runner = uv-venv-runner
96102
skip_install = true
97103
deps = pre-commit-uv
98104
commands = pre-commit run --all-files
99105

100106

101107
[testenv:changelog]
102-
# See https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92
103-
# Pin also present in pyproject.toml
104-
deps = towncrier
108+
dependency_groups = docs
105109
skip_install = true
106110
commands =
107111
towncrier --version
108112
towncrier build --version main --draft
109113

110114

111115
[testenv:pyright]
112-
extras = tests
113-
deps = pyright>=1.1.380
116+
dependency_groups = pyright
114117
commands = pytest tests/test_pyright.py -vv
115118

116119

117120
[testenv:docset]
121+
runner = uv-venv-runner
118122
deps = doc2dash
119-
extras = docs
123+
dependency_groups = docs
120124
allowlist_externals =
121125
rm
122126
cp

0 commit comments

Comments
 (0)