Skip to content

Commit 673086e

Browse files
authored
feat: add initial support for uv (#816)
* refactor(dependency): move `pep621` to `pep621.base` * refactor(dependency): move `pdm` to `pep621.pdm` * refactor(pep621): simplify logic * refactor(pep621): get dev dependencies from main `get` * refactor(pep621): remove useless argument for PEP 508 extraction * style(pdm): move misplaced dataclass attribute * feat(pep621): add uv dependency getter * feat(dependency): detect uv * test(functional): test against uv * docs: mention uv support * docs(uv): better document dev dependencies
1 parent 4e82f7a commit 673086e

File tree

19 files changed

+423
-100
lines changed

19 files changed

+423
-100
lines changed

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@
99
[![PyPI - Downloads](https://img.shields.io/pypi/dm/deptry)](https://pypistats.org/packages/deptry)
1010
[![License](https://img.shields.io/github/license/fpgmaas/deptry)](https://img.shields.io/github/license/fpgmaas/deptry)
1111

12-
_deptry_ is a command line tool to check for issues with dependencies in a Python project, such as unused or missing dependencies. It supports the following types of projects:
12+
_deptry_ is a command line tool to check for issues with dependencies in a Python project, such as unused or missing
13+
dependencies. It supports projects
14+
using [Poetry](https://python-poetry.org/), [pip](https://pip.pypa.io/), [PDM](https://pdm-project.org/), [uv](https://docs.astral.sh/uv/),
15+
and more generally any project supporting [PEP 621](https://peps.python.org/pep-0621/) specification.
1316

14-
- Projects that use [Poetry](https://python-poetry.org/) and a corresponding _pyproject.toml_ file
15-
- Projects that use [PDM](https://pdm.fming.dev/latest/) and a corresponding _pyproject.toml_ file
16-
- Projects that use any package manager that strictly follows [PEP 621](https://peps.python.org/pep-0621/) dependency specification
17-
- Projects that use a _requirements.txt_ file according to the [pip](https://pip.pypa.io/en/stable/user_guide/) standards
18-
19-
Dependency issues are detected by scanning for imported modules within all Python files in a directory and its subdirectories, and comparing those to the dependencies listed in the project's requirements.
17+
Dependency issues are detected by scanning for imported modules within all Python files in a directory and its
18+
subdirectories, and comparing those to the dependencies listed in the project's requirements.
2019

2120
---
2221
<p align="center">

docs/index.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@
1717
[![PyPI - Downloads](https://img.shields.io/pypi/dm/deptry)](https://pypistats.org/packages/deptry)
1818
[![License](https://img.shields.io/github/license/fpgmaas/deptry)](https://img.shields.io/github/license/fpgmaas/deptry)
1919

20-
_deptry_ is a command line tool to check for issues with dependencies in a Python project, such as unused or missing dependencies. It supports the following types of projects:
21-
22-
- Projects that use [Poetry](https://python-poetry.org/) and a corresponding `pyproject.toml` file
23-
- Projects that use [PDM](https://pdm.fming.dev/latest/) and a corresponding `pyproject.toml` file
24-
- Projects that use any package manager that strictly follows [PEP 621](https://peps.python.org/pep-0621/) dependency specification
25-
- Projects that use a `requirements.txt` file according to the [pip](https://pip.pypa.io/en/stable/user_guide/) standards
20+
_deptry_ is a command line tool to check for issues with dependencies in a Python project, such as unused or missing
21+
dependencies. It supports projects
22+
using [Poetry](https://python-poetry.org/), [pip](https://pip.pypa.io/), [PDM](https://pdm-project.org/), [uv](https://docs.astral.sh/uv/),
23+
and more generally any project supporting [PEP 621](https://peps.python.org/pep-0621/) specification.
2624

2725
Dependency issues are detected by scanning for imported modules within all Python files in a directory and its subdirectories, and comparing those to the dependencies listed in the project's requirements.
2826

docs/usage.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ To determine the project's dependencies, _deptry_ will scan the directory it is
3434
1. If a `pyproject.toml` file with a `[tool.poetry.dependencies]` section is found, _deptry_ will assume it uses Poetry and extract:
3535
- dependencies from `[tool.poetry.dependencies]` section
3636
- development dependencies from `[tool.poetry.group.dev.dependencies]` or `[tool.poetry.dev-dependencies]` section
37-
2. If a `pyproject.toml` file with a `[tool.pdm.dev-dependencies]` section is found, _deptry_ will assume it uses PDM and extract:
37+
1. If a `pyproject.toml` file with a `[tool.pdm.dev-dependencies]` section is found, _deptry_ will assume it uses PDM and extract:
3838
- dependencies from `[project.dependencies]` and `[project.optional-dependencies]` sections
3939
- development dependencies from `[tool.pdm.dev-dependencies]` section and from the groups under `[project.optional-dependencies]` passed via the [`--pep621-dev-dependency-groups`](#pep-621-dev-dependency-groups) argument.
40-
3. If a `pyproject.toml` file with a `[project]` section is found, _deptry_ will assume it uses [PEP 621](https://peps.python.org/pep-0621/) for dependency specification and extract:
40+
1. If a `pyproject.toml` file with a `[tool.uv.dev-dependencies]` section is found, _deptry_ will assume it uses uv and extract:
41+
- dependencies from `[project.dependencies]` and `[project.optional-dependencies]` sections
42+
- development dependencies from `[tool.uv.dev-dependencies]` section and from the groups under `[project.optional-dependencies]` passed via the [`--pep621-dev-dependency-groups`](#pep-621-dev-dependency-groups) argument.
43+
1. If a `pyproject.toml` file with a `[project]` section is found, _deptry_ will assume it uses [PEP 621](https://peps.python.org/pep-0621/) for dependency specification and extract:
4144
- dependencies from `[project.dependencies]` and `[project.optional-dependencies]`.
4245
- development dependencies from the groups under `[project.optional-dependencies]` passed via the [`--pep621-dev-dependency-groups`](#pep-621-dev-dependency-groups) argument.
43-
4. If a `requirements.in` or `requirements.txt` file is found, _deptry_ will:
46+
1. If a `requirements.in` or `requirements.txt` file is found, _deptry_ will:
4447
- extract dependencies from that file.
4548
- extract development dependencies from `dev-dependencies.txt` and `dependencies-dev.txt`, if any exist
4649

python/deptry/dependency_getter/builder.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from pathlib import Path
66
from typing import TYPE_CHECKING, Mapping
77

8-
from deptry.dependency_getter.pdm import PDMDependencyGetter
9-
from deptry.dependency_getter.pep_621 import PEP621DependencyGetter
8+
from deptry.dependency_getter.pep621.base import PEP621DependencyGetter
9+
from deptry.dependency_getter.pep621.pdm import PDMDependencyGetter
10+
from deptry.dependency_getter.pep621.uv import UvDependencyGetter
1011
from deptry.dependency_getter.poetry import PoetryDependencyGetter
1112
from deptry.dependency_getter.requirements_files import RequirementsTxtDependencyGetter
1213
from deptry.exceptions import DependencySpecificationNotFoundError
@@ -47,6 +48,9 @@ def build(self) -> DependencyGetter:
4748
if self._project_uses_pdm(pyproject_toml):
4849
return PDMDependencyGetter(self.config, self.package_module_name_map, self.pep621_dev_dependency_groups)
4950

51+
if self._project_uses_uv(pyproject_toml):
52+
return UvDependencyGetter(self.config, self.package_module_name_map, self.pep621_dev_dependency_groups)
53+
5054
if self._project_uses_pep_621(pyproject_toml):
5155
return PEP621DependencyGetter(
5256
self.config, self.package_module_name_map, self.pep621_dev_dependency_groups
@@ -102,6 +106,23 @@ def _project_uses_pdm(pyproject_toml: dict[str, Any]) -> bool:
102106
else:
103107
return True
104108

109+
@staticmethod
110+
def _project_uses_uv(pyproject_toml: dict[str, Any]) -> bool:
111+
try:
112+
pyproject_toml["tool"]["uv"]["dev-dependencies"]
113+
logging.debug(
114+
"pyproject.toml contains a [tool.uv.dev-dependencies] section, so uv is used to specify the project's"
115+
" dependencies."
116+
)
117+
except KeyError:
118+
logging.debug(
119+
"pyproject.toml does not contain a [tool.uv.dev-dependencies] section, so uv is not used to specify the"
120+
" project's dependencies."
121+
)
122+
return False
123+
else:
124+
return True
125+
105126
@staticmethod
106127
def _project_uses_pep_621(pyproject_toml: dict[str, Any]) -> bool:
107128
if pyproject_toml.get("project"):

python/deptry/dependency_getter/pep621/__init__.py

Whitespace-only changes.

python/deptry/dependency_getter/pep_621.py renamed to python/deptry/dependency_getter/pep621/base.py

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
from __future__ import annotations
22

3-
import itertools
43
import logging
54
import re
65
from dataclasses import dataclass
7-
from typing import TYPE_CHECKING
86

97
from deptry.dependency import Dependency
108
from deptry.dependency_getter.base import DependenciesExtract, DependencyGetter
119
from deptry.utils import load_pyproject_toml
1210

13-
if TYPE_CHECKING:
14-
from collections.abc import Mapping, Sequence
15-
1611

1712
@dataclass
1813
class PEP621DependencyGetter(DependencyGetter):
19-
pep621_dev_dependency_groups: tuple[str, ...] = ()
2014
"""
2115
Class to extract dependencies from a pyproject.toml file in which dependencies are specified according to PEP 621. For example:
2216
@@ -41,34 +35,36 @@ class PEP621DependencyGetter(DependencyGetter):
4135
`pep621_dev_dependency_groups=(test,)`, both `pytest` and `pytest-cov` are returned as development dependencies.
4236
"""
4337

38+
pep621_dev_dependency_groups: tuple[str, ...] = ()
39+
4440
def get(self) -> DependenciesExtract:
4541
dependencies = self._get_dependencies()
4642
optional_dependencies = self._get_optional_dependencies()
4743

48-
if self.pep621_dev_dependency_groups:
49-
self._check_for_invalid_group_names(optional_dependencies)
50-
dev_dependencies, leftover_optional_dependencies = (
51-
self._split_development_dependencies_from_optional_dependencies(optional_dependencies)
52-
)
53-
dependencies = [*dependencies, *leftover_optional_dependencies]
54-
return DependenciesExtract(dependencies, dev_dependencies)
55-
56-
dependencies = [*dependencies, *itertools.chain(*optional_dependencies.values())]
57-
return DependenciesExtract(dependencies, [])
44+
dev_dependencies_from_optional, remaining_optional_dependencies = (
45+
self._split_development_dependencies_from_optional_dependencies(optional_dependencies)
46+
)
47+
return DependenciesExtract(
48+
[*dependencies, *remaining_optional_dependencies],
49+
self._get_dev_dependencies(dev_dependencies_from_optional),
50+
)
5851

5952
def _get_dependencies(self) -> list[Dependency]:
6053
pyproject_data = load_pyproject_toml(self.config)
6154
dependency_strings: list[str] = pyproject_data["project"].get("dependencies", [])
62-
return self._extract_pep_508_dependencies(dependency_strings, self.package_module_name_map)
55+
return self._extract_pep_508_dependencies(dependency_strings)
6356

6457
def _get_optional_dependencies(self) -> dict[str, list[Dependency]]:
6558
pyproject_data = load_pyproject_toml(self.config)
6659

6760
return {
68-
group: self._extract_pep_508_dependencies(dependencies, self.package_module_name_map)
61+
group: self._extract_pep_508_dependencies(dependencies)
6962
for group, dependencies in pyproject_data["project"].get("optional-dependencies", {}).items()
7063
}
7164

65+
def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]) -> list[Dependency]:
66+
return dev_dependencies_from_optional
67+
7268
def _check_for_invalid_group_names(self, optional_dependencies: dict[str, list[Dependency]]) -> None:
7369
missing_groups = set(self.pep621_dev_dependency_groups) - set(optional_dependencies.keys())
7470
if missing_groups:
@@ -86,21 +82,21 @@ def _split_development_dependencies_from_optional_dependencies(
8682
Split the optional dependencies into optional dependencies and development dependencies based on the `pep621_dev_dependency_groups`
8783
parameter. Return a tuple with two values: a list of the development dependencies and a list of the remaining 'true' optional dependencies.
8884
"""
89-
dev_dependencies = list(
90-
itertools.chain.from_iterable(
91-
deps for group, deps in optional_dependencies.items() if group in self.pep621_dev_dependency_groups
92-
)
93-
)
94-
regular_dependencies = list(
95-
itertools.chain.from_iterable(
96-
deps for group, deps in optional_dependencies.items() if group not in self.pep621_dev_dependency_groups
97-
)
98-
)
85+
dev_dependencies: list[Dependency] = []
86+
regular_dependencies: list[Dependency] = []
87+
88+
if self.pep621_dev_dependency_groups:
89+
self._check_for_invalid_group_names(optional_dependencies)
90+
91+
for group, dependencies in optional_dependencies.items():
92+
if group in self.pep621_dev_dependency_groups:
93+
dev_dependencies.extend(dependencies)
94+
else:
95+
regular_dependencies.extend(dependencies)
96+
9997
return dev_dependencies, regular_dependencies
10098

101-
def _extract_pep_508_dependencies(
102-
self, dependencies: list[str], package_module_name_map: Mapping[str, Sequence[str]]
103-
) -> list[Dependency]:
99+
def _extract_pep_508_dependencies(self, dependencies: list[str]) -> list[Dependency]:
104100
"""
105101
Given a list of dependency specifications (e.g. "django>2.1; os_name != 'nt'"), convert them to Dependency objects.
106102
"""
@@ -114,7 +110,7 @@ def _extract_pep_508_dependencies(
114110
Dependency(
115111
name,
116112
self.config,
117-
module_names=package_module_name_map.get(name),
113+
module_names=self.package_module_name_map.get(name),
118114
)
119115
)
120116

python/deptry/dependency_getter/pdm.py renamed to python/deptry/dependency_getter/pep621/pdm.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
from dataclasses import dataclass
55
from typing import TYPE_CHECKING
66

7-
from deptry.dependency_getter.base import DependenciesExtract
8-
from deptry.dependency_getter.pep_621 import PEP621DependencyGetter
7+
from deptry.dependency_getter.pep621.base import PEP621DependencyGetter
98
from deptry.utils import load_pyproject_toml
109

1110
if TYPE_CHECKING:
@@ -15,20 +14,13 @@
1514
@dataclass
1615
class PDMDependencyGetter(PEP621DependencyGetter):
1716
"""
18-
Class to get dependencies that are specified according to PEP 621 from a `pyproject.toml` file for a project that uses PDM for its dependency management.
17+
Class to get dependencies that are specified according to PEP 621 from a `pyproject.toml` file for a project that
18+
uses PDM for its dependency management.
1919
"""
2020

21-
def get(self) -> DependenciesExtract:
22-
pep_621_dependencies_extract = super().get()
23-
24-
return DependenciesExtract(
25-
pep_621_dependencies_extract.dependencies,
26-
[*pep_621_dependencies_extract.dev_dependencies, *self._get_pdm_dev_dependencies()],
27-
)
28-
29-
def _get_pdm_dev_dependencies(self) -> list[Dependency]:
21+
def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]) -> list[Dependency]:
3022
"""
31-
Try to get development dependencies from pyproject.toml, which with PDM are specified as:
23+
Retrieve dev dependencies from pyproject.toml, which in PDM are specified as:
3224
3325
[tool.pdm.dev-dependencies]
3426
test = [
@@ -40,6 +32,8 @@ def _get_pdm_dev_dependencies(self) -> list[Dependency]:
4032
"tox-pdm>=0.5",
4133
]
4234
"""
35+
dev_dependencies = super()._get_dev_dependencies(dev_dependencies_from_optional)
36+
4337
pyproject_data = load_pyproject_toml(self.config)
4438

4539
dev_dependency_strings: list[str] = []
@@ -50,4 +44,4 @@ def _get_pdm_dev_dependencies(self) -> list[Dependency]:
5044
except KeyError:
5145
logging.debug("No section [tool.pdm.dev-dependencies] found in pyproject.toml")
5246

53-
return self._extract_pep_508_dependencies(dev_dependency_strings, self.package_module_name_map)
47+
return [*dev_dependencies, *self._extract_pep_508_dependencies(dev_dependency_strings)]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from dataclasses import dataclass
5+
from typing import TYPE_CHECKING
6+
7+
from deptry.dependency_getter.pep621.base import PEP621DependencyGetter
8+
from deptry.utils import load_pyproject_toml
9+
10+
if TYPE_CHECKING:
11+
from deptry.dependency import Dependency
12+
13+
14+
@dataclass
15+
class UvDependencyGetter(PEP621DependencyGetter):
16+
"""
17+
Class to get dependencies that are specified according to PEP 621 from a `pyproject.toml` file for a project that
18+
uses uv for its dependency management.
19+
"""
20+
21+
def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]) -> list[Dependency]:
22+
"""
23+
Retrieve dev dependencies from pyproject.toml, which in uv are specified as:
24+
25+
[tool.uv]
26+
dev-dependencies = [
27+
"pytest==8.3.2",
28+
"pytest-cov==5.0.0",
29+
"tox",
30+
]
31+
32+
Dev dependencies marked as such from optional dependencies are also added to the list of dev dependencies found.
33+
"""
34+
dev_dependencies = super()._get_dev_dependencies(dev_dependencies_from_optional)
35+
36+
pyproject_data = load_pyproject_toml(self.config)
37+
38+
dev_dependency_strings: list[str] = []
39+
try:
40+
dev_dependency_strings = pyproject_data["tool"]["uv"]["dev-dependencies"]
41+
except KeyError:
42+
logging.debug("No section [tool.uv.dev-dependencies] found in pyproject.toml")
43+
44+
return [*dev_dependencies, *self._extract_pep_508_dependencies(dev_dependency_strings)]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[project]
2+
# PEP 621 project metadata
3+
# See https://www.python.org/dev/peps/pep-0621/
4+
name = "foo"
5+
version = "0.0.1"
6+
requires-python = ">=3.8"
7+
dependencies = [
8+
"pkginfo==1.11.1",
9+
"tomli==2.0.1",
10+
"urllib3==2.2.2",
11+
]
12+
13+
[project.optional-dependencies]
14+
foo = [
15+
"click==8.1.7",
16+
"isort==5.13.2",
17+
]
18+
bar = ["requests==2.32.3"]
19+
20+
[tool.uv]
21+
dev-dependencies = [
22+
"black==24.8.0",
23+
"mypy==1.11.1",
24+
"pytest==8.2.0",
25+
"pytest-cov==5.0.0",
26+
]
27+
28+
[tool.deptry]
29+
pep621_dev_dependency_groups = ["bar"]
30+
31+
[tool.deptry.per_rule_ignores]
32+
DEP002 = ["pkginfo"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from os import chdir, walk
2+
from pathlib import Path
3+
4+
import black
5+
import click
6+
import mypy
7+
import pytest
8+
import pytest_cov
9+
import white as w
10+
from urllib3 import contrib
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 2,
6+
"id": "9f4924ec-2200-4801-9d49-d4833651cbc4",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"import click\n",
11+
"from urllib3 import contrib\n",
12+
"import tomli"
13+
]
14+
}
15+
],
16+
"metadata": {
17+
"kernelspec": {
18+
"display_name": "Python 3 (ipykernel)",
19+
"language": "python",
20+
"name": "python3"
21+
},
22+
"language_info": {
23+
"codemirror_mode": {
24+
"name": "ipython",
25+
"version": 3
26+
},
27+
"file_extension": ".py",
28+
"mimetype": "text/x-python",
29+
"name": "python",
30+
"nbconvert_exporter": "python",
31+
"pygments_lexer": "ipython3",
32+
"version": "3.9.11"
33+
}
34+
},
35+
"nbformat": 4,
36+
"nbformat_minor": 5
37+
}

0 commit comments

Comments
 (0)