Skip to content

Commit af63a46

Browse files
authored
Manage dependencies with uv (#4241)
* tox.ini is outdated and likely not used * No need for extra pip-install Dependencies are already installed by the setup script in the venv, and the pip install would install into the user env if not already in the venv. * Removed traces of isort, black and flake8 since using ruff instead * Moved requirements into dev dependency-group * Added pre-commit hook to check that uv.lock is up to date * Updated README with dependency mangement using uv * Created dependency-group `ci` * Fixed wording in README * Updated dependency management in README
1 parent ae06bdc commit af63a46

File tree

8 files changed

+1517
-147
lines changed

8 files changed

+1517
-147
lines changed

.pre-commit-config.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@ repos:
1515
rev: v1.14.1
1616
hooks:
1717
- id: mypy
18-
18+
1919
- repo: https://github.com/astral-sh/ruff-pre-commit
20-
rev: v0.9.4 # Keep this in sync with requirements_test.txt
20+
rev: v0.9.4 # Keep this in sync with pyproject.toml
2121
hooks:
2222
- id: ruff
2323
args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"]
2424
- id: ruff-format
2525
args: ["--config", "pyproject.toml"]
26+
27+
- repo: https://github.com/astral-sh/uv-pre-commit
28+
# uv version.
29+
rev: 0.8.6
30+
hooks:
31+
- id: uv-lock
32+
args: ["--check"]

.vscode/settings.default.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"python.formatting.provider": "black",
32
// Added --no-cov to work around TypeError: message must be set
43
// https://github.com/microsoft/vscode-python/issues/14067
54
"python.testing.pytestArgs": ["--no-cov"],

README.md

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ Now lets put this all together. If you examine the device definition above you w
404404

405405
# Contribution Guidelines
406406

407-
- All code is formatted with black. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with black. Instructions for installing black in many editors can be found here: <https://github.com/psf/black#editor-integration>
407+
- All code is formatted with ruff. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with ruff. Instructions for integrating ruff in many editors can be found here: <https://docs.astral.sh/ruff/editors/>
408408

409409
- Capture the SimpleDescriptor log entries for each endpoint on the device. These can be found in the HA logs after joining a device and they look like this: `<SimpleDescriptor endpoint=1 profile=260 device_type=1026 device_version=0 input_clusters=[0, 1, 3, 32, 1026, 1280, 2821] output_clusters=[25]>`. This information can also be obtained from the zigbee.db if you want to take the time to query the tables and reconstitute the log entry. I find it easier to just remove and rejoin the device. ZHA entity ids are stable for the most part so it _shouldn't_ disrupt anything you have configured. These need to match what the device reports EXACTLY or zigpy will not match them when a device joins and the handler will not be used for the device. You can also obtain this information from the device screen in HA for the device. The `Zigbee Device Signature` button will launch a dialog that contains all of the information necessary to create quirks.
410410

@@ -449,15 +449,9 @@ You can see a pattern that illustrates how to match a more complex event. In thi
449449

450450
Open a terminal at the root of the project and run the setup script: `script/setup` This script will install all necessary dependencies and it will install the precommit hook.
451451

452-
The tests use the [pytest](https://docs.pytest.org/en/latest/) framework.
453-
454-
### Getting started
455-
456-
To get set up, you need install the test dependencies:
457-
458-
```bash
459-
pip install -r requirements_test.txt
460-
```
452+
All the dependencies are installed using the locked versions in the `uv.lock` file.
453+
To keep the environment up to date with the locked versions of the dependencies, run `uv sync` make sure the
454+
environment matches the `uv.lock` file.
461455

462456
### Running the tests
463457

@@ -560,6 +554,51 @@ def test_ts0121_signature(assert_signature_matches_quirk):
560554
assert_signature_matches_quirk(zhaquirks.tuya.ts0121_plug.Plug, signature)
561555
```
562556

557+
## Managing dependencies
558+
559+
This project uses [uv] to manage dependencies and ensure reproducible environments.
560+
561+
[uv]: https://docs.astral.sh/uv/
562+
563+
### Sync the virtual environment
564+
565+
Running `uv sync` will install all packages using the locked version from the `uv.lock` file into the virtual environment.
566+
It will also *remove* any package that is not listed in the `uv.lock` file.
567+
568+
If/when the `uv.lock` file has been changed, which can be the case when for example switching branch or after merging `dev`
569+
branch into the current branch, you will usually need to re-sync the virtual environment using `uv sync` to ensure that the
570+
environment is up to date with the locked versions of the dependencies.
571+
The `script/setup` script will do this for you (and some more), so use the script once and then use `uv sync` to keep the
572+
environment up to date.
573+
574+
The `dev` dependency group is installed by default by `uv sync`.
575+
576+
### Updating locked dependencies
577+
578+
After modifying a constraint for a dependency in the `pyproject.toml` file, for example bumping the minimum version
579+
of `zigpy`, use `uv lock` to update the locked versions for the dependency and all its dependencies so that the new
580+
constraint is fulfilled.
581+
Note that the `lock` command will not update packages already in the lockfile that already fulfills the constraints,
582+
even if there are newer versions available.
583+
584+
To update a *single* package to its latest available version that fulfills the constraints use `uv lock --upgrade-package <package>`.
585+
586+
To update *all* packages to their latest version that fulfills the constraints use `uv lock --upgrade`.
587+
588+
### Adding new dependencies
589+
590+
Use `uv add <package>` to add a project dependency, `uv add --dev <package>` to add to the `dev` group,
591+
or `uv add --group ci <package>` to add to the `ci` group.
592+
Alternatively just add it to the appropriate field in the `pyproject.toml` file and run `uv lock`
593+
to update the lockfile. Only the new package and its dependencies will be added/updated in the lockfile.
594+
595+
In short, project dependencies, packages that should be installed when
596+
installing the project, go into `dependencies`. Packages used only for development go into the `dev`
597+
group in `dependency-groups` and packages used only for CI go into the `ci` group.
598+
See [uv dependency fields] for details.
599+
600+
[uv dependency fields]: https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-fields
601+
563602
# Thanks
564603

565604
- Special thanks to damarco for the majority of the device tracker code

pyproject.toml

Lines changed: 88 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,36 @@ build-backend = "setuptools.build_meta"
66
name = "zha-quirks"
77
dynamic = ["version"]
88
description = "Library implementing Zigpy quirks for ZHA in Home Assistant"
9-
urls = {repository = "https://github.com/zigpy/zha-device-handlers"}
10-
authors = [
11-
{name = "David F. Mulcahey", email = "[email protected]"}
12-
]
9+
urls = { repository = "https://github.com/zigpy/zha-device-handlers" }
10+
authors = [{ name = "David F. Mulcahey", email = "[email protected]" }]
1311
readme = "README.md"
14-
license = {text = "Apache-2.0"}
12+
license = { text = "Apache-2.0" }
1513
requires-python = ">=3.12"
16-
dependencies = [
17-
"zigpy>=0.82.2",
18-
]
14+
dependencies = ["zigpy>=0.82.2"]
1915

2016
[tool.setuptools.packages.find]
2117
exclude = ["tests", "tests.*"]
2218

23-
[project.optional-dependencies]
24-
testing = [
25-
"pytest",
19+
[dependency-groups]
20+
ci = [
21+
"pytest-github-actions-annotate-failures>=0.3.0",
22+
"pytest-xdist>=3.8.0",
23+
]
24+
dev = [
25+
"codecov",
26+
"codespell>=2.3.0",
27+
"colorlog",
28+
"coveralls",
29+
"mypy==0.942",
30+
"pre-commit",
31+
"pylint",
32+
"pytest>=7.1.3",
33+
"pytest-asyncio",
34+
"pytest-cov",
35+
"pytest-sugar",
36+
"pytest-timeout",
37+
"ruff==0.9.4", # Keep this in sync with .pre-commit-config.yaml
38+
"time-machine>=2,<3",
2639
]
2740

2841
[tool.setuptools-git-versioning]
@@ -78,7 +91,7 @@ testpaths = "tests"
7891
norecursedirs = ".git testing_config"
7992

8093
[tool.codespell]
81-
skip = "Contributors.md"
94+
skip = "Contributors.md,uv.lock"
8295
ignore-words-list = "hass, dout, potentiels, checkin"
8396
quiet-level = 2
8497

@@ -87,70 +100,70 @@ target-version = "py312"
87100

88101
[tool.ruff.lint]
89102
select = [
90-
"B002", # Python does not support the unary prefix increment
91-
"B007", # Loop control variable {name} not used within loop body
92-
"B014", # Exception handler with duplicate exception
93-
"B023", # Function definition does not bind loop variable {name}
94-
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
95-
"B904", # Use raise from to specify exception cause
96-
"C", # complexity
97-
"COM818", # Trailing comma on bare tuple prohibited
98-
"D", # docstrings
99-
"DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow()
100-
"DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts)
101-
"E", # pycodestyle
102-
"F", # pyflakes/autoflake
103-
"G", # flake8-logging-format
104-
"I", # isort
105-
"ICN001", # import concentions; {name} should be imported as {asname}
106-
"N804", # First argument of a class method should be named cls
107-
"N805", # First argument of a method should be named self
108-
"N815", # Variable {name} in class scope should not be mixedCase
103+
"B002", # Python does not support the unary prefix increment
104+
"B007", # Loop control variable {name} not used within loop body
105+
"B014", # Exception handler with duplicate exception
106+
"B023", # Function definition does not bind loop variable {name}
107+
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
108+
"B904", # Use raise from to specify exception cause
109+
"C", # complexity
110+
"COM818", # Trailing comma on bare tuple prohibited
111+
"D", # docstrings
112+
"DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow()
113+
"DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts)
114+
"E", # pycodestyle
115+
"F", # pyflakes/autoflake
116+
"G", # flake8-logging-format
117+
"I", # isort
118+
"ICN001", # import concentions; {name} should be imported as {asname}
119+
"N804", # First argument of a class method should be named cls
120+
"N805", # First argument of a method should be named self
121+
"N815", # Variable {name} in class scope should not be mixedCase
109122
"PERF101", # Do not cast an iterable to list before iterating over it
110123
"PERF102", # When using only the {subset} of a dict use the {subset}() method
111124
"PERF203", # try-except within a loop incurs performance overhead
112-
"PGH004", # Use specific rule codes when using noqa
125+
"PGH004", # Use specific rule codes when using noqa
113126
"PLC0414", # Useless import alias. Import alias does not rename original package.
114-
"PLC", # pylint
115-
"PLE", # pylint
116-
"PLR", # pylint
117-
"PLW", # pylint
118-
"Q000", # Double quotes found but single quotes preferred
119-
"RUF006", # Store a reference to the return value of asyncio.create_task
120-
"S102", # Use of exec detected
121-
"S103", # bad-file-permissions
122-
"S108", # hardcoded-temp-file
123-
"S306", # suspicious-mktemp-usage
124-
"S307", # suspicious-eval-usage
125-
"S313", # suspicious-xmlc-element-tree-usage
126-
"S314", # suspicious-xml-element-tree-usage
127-
"S315", # suspicious-xml-expat-reader-usage
128-
"S316", # suspicious-xml-expat-builder-usage
129-
"S317", # suspicious-xml-sax-usage
130-
"S318", # suspicious-xml-mini-dom-usage
131-
"S319", # suspicious-xml-pull-dom-usage
132-
"S320", # suspicious-xmle-tree-usage
133-
"S601", # paramiko-call
134-
"S602", # subprocess-popen-with-shell-equals-true
135-
"S604", # call-with-shell-equals-true
136-
"S608", # hardcoded-sql-expression
137-
"S609", # unix-command-wildcard-injection
138-
"SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
139-
"SIM114", # Combine if branches using logical or operator
140-
"SIM117", # Merge with-statements that use the same scope
141-
"SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
142-
"SIM201", # Use {left} != {right} instead of not {left} == {right}
143-
"SIM208", # Use {expr} instead of not (not {expr})
144-
"SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
145-
"SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
146-
"SIM401", # Use get from dict with default instead of an if block
147-
"T100", # Trace found: {name} used
148-
"T20", # flake8-print
149-
"TID251", # Banned imports
150-
"TRY004", # Prefer TypeError exception for invalid type
151-
"TRY302", # Remove exception handler; error is immediately re-raised
152-
"UP", # pyupgrade
153-
"W", # pycodestyle
127+
"PLC", # pylint
128+
"PLE", # pylint
129+
"PLR", # pylint
130+
"PLW", # pylint
131+
"Q000", # Double quotes found but single quotes preferred
132+
"RUF006", # Store a reference to the return value of asyncio.create_task
133+
"S102", # Use of exec detected
134+
"S103", # bad-file-permissions
135+
"S108", # hardcoded-temp-file
136+
"S306", # suspicious-mktemp-usage
137+
"S307", # suspicious-eval-usage
138+
"S313", # suspicious-xmlc-element-tree-usage
139+
"S314", # suspicious-xml-element-tree-usage
140+
"S315", # suspicious-xml-expat-reader-usage
141+
"S316", # suspicious-xml-expat-builder-usage
142+
"S317", # suspicious-xml-sax-usage
143+
"S318", # suspicious-xml-mini-dom-usage
144+
"S319", # suspicious-xml-pull-dom-usage
145+
"S320", # suspicious-xmle-tree-usage
146+
"S601", # paramiko-call
147+
"S602", # subprocess-popen-with-shell-equals-true
148+
"S604", # call-with-shell-equals-true
149+
"S608", # hardcoded-sql-expression
150+
"S609", # unix-command-wildcard-injection
151+
"SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
152+
"SIM114", # Combine if branches using logical or operator
153+
"SIM117", # Merge with-statements that use the same scope
154+
"SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
155+
"SIM201", # Use {left} != {right} instead of not {left} == {right}
156+
"SIM208", # Use {expr} instead of not (not {expr})
157+
"SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
158+
"SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
159+
"SIM401", # Use get from dict with default instead of an if block
160+
"T100", # Trace found: {name} used
161+
"T20", # flake8-print
162+
"TID251", # Banned imports
163+
"TRY004", # Prefer TypeError exception for invalid type
164+
"TRY302", # Remove exception handler; error is immediately re-raised
165+
"UP", # pyupgrade
166+
"W", # pycodestyle
154167
]
155168

156169
ignore = [
@@ -175,8 +188,8 @@ ignore = [
175188
"PLR0915", # Too many statements ({statements} > {max_statements})
176189
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
177190
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
178-
"UP006", # keep type annotation style as is
179-
"UP007", # keep type annotation style as is
191+
"UP006", # keep type annotation style as is
192+
"UP007", # keep type annotation style as is
180193
# Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923
181194
"UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)`
182195

@@ -205,10 +218,7 @@ fixture-parentheses = false
205218

206219
[tool.ruff.lint.isort]
207220
force-sort-within-sections = true
208-
known-first-party = [
209-
"zhaquirks",
210-
"tests",
211-
]
221+
known-first-party = ["zhaquirks", "tests"]
212222
combine-as-imports = true
213223
split-on-trailing-comma = false
214224

requirements_test.txt

Lines changed: 0 additions & 18 deletions
This file was deleted.

script/setup

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@ fi
1717

1818
if [ ! -n "$VIRTUAL_ENV" ]; then
1919
if [ -x "$(command -v uv)" ]; then
20-
uv venv venv
20+
uv venv
2121
else
22-
python3 -m venv venv
22+
python3 -m venv .venv
2323
fi
24-
source venv/bin/activate
24+
source .venv/bin/activate
2525
fi
2626

2727
if ! [ -x "$(command -v uv)" ]; then
2828
python3 -m pip install uv
2929
fi
3030

31-
uv pip install -r requirements_test.txt
32-
uv pip install -e .
31+
uv sync
3332

3433
pre-commit install

tox.ini

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)