Skip to content

Commit 49b4035

Browse files
Move shared dep-reading logic to _file/pyproject_toml/deps.py
Extract get_project_deps() and get_dep_groups() into a shared lower-level module at _file/pyproject_toml/deps.py that both _backend and _deps can import from. _deps.py now wraps the shared functions with error translation. _backend/uv/available.py uses the shared helpers directly. Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/c1af1dd1-49e0-443d-866d-ffb972b62b89 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
1 parent 3c92360 commit 49b4035

6 files changed

Lines changed: 116 additions & 141 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ usethis # usethis: Automate Python project setup and d
113113
│ │ ├── errors # Error types for INI file operations.
114114
│ │ └── io_ # INI file I/O manager.
115115
│ ├── pyproject_toml # pyproject.toml file reading and writing.
116+
│ │ ├── deps # Dependency extraction from pyproject.toml.
116117
│ │ ├── errors # Error types for pyproject.toml operations.
117118
│ │ ├── io_ # pyproject.toml file I/O manager.
118119
│ │ ├── name # Project name and description extraction from pyproject.toml.

docs/module-tree.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ usethis # usethis: Automate Python project setup and d
4949
│ │ ├── errors # Error types for INI file operations.
5050
│ │ └── io_ # INI file I/O manager.
5151
│ ├── pyproject_toml # pyproject.toml file reading and writing.
52+
│ │ ├── deps # Dependency extraction from pyproject.toml.
5253
│ │ ├── errors # Error types for pyproject.toml operations.
5354
│ │ ├── io_ # pyproject.toml file I/O manager.
5455
│ │ ├── name # Project name and description extraction from pyproject.toml.
Lines changed: 9 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
"""Check whether the uv CLI is available."""
22

3-
from __future__ import annotations
4-
5-
from typing import TYPE_CHECKING
6-
7-
from packaging.requirements import InvalidRequirement, Requirement
8-
93
from usethis._backend.uv.call import call_uv_subprocess
104
from usethis._backend.uv.errors import UVSubprocessFailedError
11-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
5+
from usethis._file.pyproject_toml.deps import get_dep_groups, get_project_deps
126
from usethis._types.deps import Dependency
137

14-
if TYPE_CHECKING:
15-
from tomlkit import TOMLDocument
16-
178

189
def is_uv_available() -> bool:
1910
"""Check if the `uv` command is available in the current environment."""
@@ -26,76 +17,18 @@ def is_uv_available() -> bool:
2617

2718

2819
def _is_uv_a_dep() -> bool:
29-
"""Check if uv is declared as a project dependency or in a dependency group.
30-
31-
Note: we cannot use the higher-level ``get_project_deps`` /
32-
``get_dep_groups`` helpers from ``usethis._deps`` because the
33-
``_backend`` layer must not import from the ``_deps`` layer
34-
(enforced by import-linter). The logic below mirrors those
35-
functions using only ``_file`` and ``_types`` which are
36-
permitted.
37-
"""
20+
"""Check if uv is declared as a project dependency or in a dependency group."""
3821
uv_dep = Dependency(name="uv")
3922

4023
try:
41-
pyproject = PyprojectTOMLManager().get()
24+
project_deps = get_project_deps()
4225
except Exception:
43-
return False
26+
project_deps = []
4427

45-
all_deps = _get_project_deps(pyproject) + _get_dep_group_deps(pyproject)
46-
return any(dep.name == uv_dep.name for dep in all_deps)
47-
48-
49-
def _get_project_deps(pyproject: TOMLDocument) -> list[Dependency]:
50-
"""Extract dependencies from the [project.dependencies] section."""
5128
try:
52-
project_section = pyproject["project"]
53-
except KeyError:
54-
return []
55-
56-
if not isinstance(project_section, dict):
57-
return []
58-
59-
try:
60-
dep_section = project_section["dependencies"]
61-
except KeyError:
62-
return []
63-
64-
if not isinstance(dep_section, list):
65-
return []
66-
67-
deps: list[Dependency] = []
68-
for item in dep_section:
69-
if not isinstance(item, str):
70-
continue
71-
try:
72-
req = Requirement(item)
73-
except InvalidRequirement:
74-
continue
75-
deps.append(Dependency(name=req.name, extras=frozenset(req.extras)))
76-
return deps
77-
78-
79-
def _get_dep_group_deps(pyproject: TOMLDocument) -> list[Dependency]:
80-
"""Extract dependencies from all [dependency-groups] sections."""
81-
try:
82-
groups_section = pyproject["dependency-groups"]
83-
except KeyError:
84-
return []
85-
86-
if not isinstance(groups_section, dict):
87-
return []
29+
dep_groups = get_dep_groups()
30+
except Exception:
31+
dep_groups = {}
8832

89-
deps: list[Dependency] = []
90-
for group_items in groups_section.values():
91-
if not isinstance(group_items, list):
92-
continue
93-
for item in group_items:
94-
if not isinstance(item, str):
95-
continue
96-
try:
97-
req = Requirement(item)
98-
except InvalidRequirement:
99-
continue
100-
deps.append(Dependency(name=req.name, extras=frozenset(req.extras)))
101-
return deps
33+
all_group_deps = [dep for group in dep_groups.values() for dep in group]
34+
return any(dep.name == uv_dep.name for dep in project_deps + all_group_deps)

src/usethis/_deps.py

Lines changed: 18 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from __future__ import annotations
44

5-
import pydantic
6-
from packaging.requirements import Requirement
7-
from pydantic import TypeAdapter
5+
from typing import TYPE_CHECKING
6+
87
from typing_extensions import assert_never
98

109
from usethis._backend.dispatch import get_backend
@@ -17,11 +16,20 @@
1716
from usethis._backend.uv.errors import UVDepGroupError
1817
from usethis._config import usethis_config
1918
from usethis._console import instruct_print, tick_print
19+
from usethis._file.pyproject_toml.deps import (
20+
get_dep_groups as _get_dep_groups,
21+
)
22+
from usethis._file.pyproject_toml.deps import (
23+
get_project_deps as _get_project_deps,
24+
)
25+
from usethis._file.pyproject_toml.errors import PyprojectTOMLDepsError
2026
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
2127
from usethis._types.backend import BackendEnum
22-
from usethis._types.deps import Dependency
2328
from usethis.errors import DepGroupError
2429

30+
if TYPE_CHECKING:
31+
from usethis._types.deps import Dependency
32+
2533

2634
def get_project_deps() -> list[Dependency]:
2735
"""Get all project dependencies.
@@ -33,71 +41,16 @@ def get_project_deps() -> list[Dependency]:
3341
of the `pyproject.toml` file.
3442
"""
3543
try:
36-
pyproject = PyprojectTOMLManager().get()
37-
except FileNotFoundError:
38-
return []
39-
40-
try:
41-
project_section = pyproject["project"]
42-
except KeyError:
43-
return []
44-
45-
if not isinstance(project_section, dict):
46-
return []
47-
48-
try:
49-
dep_section = project_section["dependencies"]
50-
except KeyError:
51-
return []
52-
53-
try:
54-
req_strs = TypeAdapter(list[str]).validate_python(dep_section)
55-
except pydantic.ValidationError as err:
56-
msg = (
57-
"Failed to parse the 'project.dependencies' section in 'pyproject.toml':\n"
58-
f"{err}\n\n"
59-
"Please check the section and try again."
60-
)
61-
raise UVDepGroupError(msg) from None
62-
63-
reqs = [Requirement(req_str) for req_str in req_strs]
64-
deps = [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
65-
return deps
44+
return _get_project_deps()
45+
except PyprojectTOMLDepsError as err:
46+
raise UVDepGroupError(str(err)) from None
6647

6748

6849
def get_dep_groups() -> dict[str, list[Dependency]]:
6950
try:
70-
pyproject = PyprojectTOMLManager().get()
71-
except FileNotFoundError:
72-
return {}
73-
74-
try:
75-
dep_groups_section = pyproject["dependency-groups"]
76-
except KeyError:
77-
# In the past might have been in [tool.uv.dev-dependencies] section when using
78-
# uv but this will be deprecated, so we don't support it in usethis.
79-
return {}
80-
81-
try:
82-
req_strs_by_group = TypeAdapter(dict[str, list[str]]).validate_python(
83-
dep_groups_section
84-
)
85-
except pydantic.ValidationError as err:
86-
msg = (
87-
"Failed to parse the 'dependency-groups' section in 'pyproject.toml':\n"
88-
f"{err}\n\n"
89-
"Please check the section and try again."
90-
)
91-
raise DepGroupError(msg) from None
92-
reqs_by_group = {
93-
group: [Requirement(req_str) for req_str in req_strs]
94-
for group, req_strs in req_strs_by_group.items()
95-
}
96-
deps_by_group = {
97-
group: [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
98-
for group, reqs in reqs_by_group.items()
99-
}
100-
return deps_by_group
51+
return _get_dep_groups()
52+
except PyprojectTOMLDepsError as err:
53+
raise DepGroupError(str(err)) from None
10154

10255

10356
def get_deps_from_group(group: str) -> list[Dependency]:
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Dependency extraction from pyproject.toml."""
2+
3+
from __future__ import annotations
4+
5+
import pydantic
6+
from packaging.requirements import Requirement
7+
from pydantic import TypeAdapter
8+
9+
from usethis._file.pyproject_toml.errors import PyprojectTOMLDepsError
10+
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
11+
from usethis._types.deps import Dependency
12+
13+
14+
def get_project_deps() -> list[Dependency]:
15+
"""Get all project dependencies from [project.dependencies].
16+
17+
This does not include development dependencies, e.g. not those in the
18+
dependency-groups section, not extras/optional dependencies, not build dependencies.
19+
"""
20+
try:
21+
pyproject = PyprojectTOMLManager().get()
22+
except FileNotFoundError:
23+
return []
24+
25+
try:
26+
project_section = pyproject["project"]
27+
except KeyError:
28+
return []
29+
30+
if not isinstance(project_section, dict):
31+
return []
32+
33+
try:
34+
dep_section = project_section["dependencies"]
35+
except KeyError:
36+
return []
37+
38+
try:
39+
req_strs = TypeAdapter(list[str]).validate_python(dep_section)
40+
except pydantic.ValidationError as err:
41+
msg = (
42+
"Failed to parse the 'project.dependencies' section in 'pyproject.toml':\n"
43+
f"{err}\n\n"
44+
"Please check the section and try again."
45+
)
46+
raise PyprojectTOMLDepsError(msg) from None
47+
48+
reqs = [Requirement(req_str) for req_str in req_strs]
49+
return [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
50+
51+
52+
def get_dep_groups() -> dict[str, list[Dependency]]:
53+
"""Get all dependency groups from [dependency-groups]."""
54+
try:
55+
pyproject = PyprojectTOMLManager().get()
56+
except FileNotFoundError:
57+
return {}
58+
59+
try:
60+
dep_groups_section = pyproject["dependency-groups"]
61+
except KeyError:
62+
return {}
63+
64+
try:
65+
req_strs_by_group = TypeAdapter(dict[str, list[str]]).validate_python(
66+
dep_groups_section
67+
)
68+
except pydantic.ValidationError as err:
69+
msg = (
70+
"Failed to parse the 'dependency-groups' section in 'pyproject.toml':\n"
71+
f"{err}\n\n"
72+
"Please check the section and try again."
73+
)
74+
raise PyprojectTOMLDepsError(msg) from None
75+
76+
reqs_by_group = {
77+
group: [Requirement(req_str) for req_str in req_strs]
78+
for group, req_strs in req_strs_by_group.items()
79+
}
80+
return {
81+
group: [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
82+
for group, reqs in reqs_by_group.items()
83+
}

src/usethis/_file/pyproject_toml/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,7 @@ class PyprojectTOMLValueAlreadySetError(PyprojectTOMLError, TOMLValueAlreadySetE
7070

7171
class PyprojectTOMLValueMissingError(PyprojectTOMLError, TOMLValueMissingError):
7272
"""Raised when a value is unexpectedly missing from the 'pyproject.toml' file."""
73+
74+
75+
class PyprojectTOMLDepsError(PyprojectTOMLError):
76+
"""Raised when dependency sections in 'pyproject.toml' cannot be parsed."""

0 commit comments

Comments
 (0)