1- import re
2-
31from packaging .requirements import Requirement
4- from pydantic import TypeAdapter
2+ from pydantic import BaseModel , TypeAdapter
53
64from usethis ._config import usethis_config
75from usethis ._console import tick_print
108from usethis ._integrations .uv .errors import UVDepGroupError , UVSubprocessFailedError
119
1210
13- def get_dep_groups () -> dict [str , list [str ]]:
11+ class Dependency (BaseModel ):
12+ name : str
13+ extras : frozenset [str ] = frozenset ()
14+
15+ def __str__ (self ) -> str :
16+ extras = sorted (self .extras or set ())
17+ return self .name + "" .join (f"[{ extra } ]" for extra in extras )
18+
19+ def __hash__ (self ) -> int :
20+ return hash ((self .__class__ .__name__ , self .name , self .extras ))
21+
22+
23+ def get_dep_groups () -> dict [str , list [Dependency ]]:
1424 pyproject = read_pyproject_toml ()
1525 try :
1626 dep_groups_section = pyproject ["dependency-groups" ]
@@ -23,71 +33,85 @@ def get_dep_groups() -> dict[str, list[str]]:
2333 dep_groups_section
2434 )
2535 reqs_by_group = {
26- group : [Requirement (req_str ). name for req_str in req_strs ]
36+ group : [Requirement (req_str ) for req_str in req_strs ]
2737 for group , req_strs in req_strs_by_group .items ()
2838 }
29- return reqs_by_group
39+ deps_by_group = {
40+ group : [Dependency (name = req .name , extras = frozenset (req .extras )) for req in reqs ]
41+ for group , reqs in reqs_by_group .items ()
42+ }
43+ return deps_by_group
3044
3145
32- def get_deps_from_group (group : str ) -> list [str ]:
46+ def get_deps_from_group (group : str ) -> list [Dependency ]:
3347 dep_groups = get_dep_groups ()
3448 try :
3549 return dep_groups [group ]
3650 except KeyError :
3751 return []
3852
3953
40- def add_deps_to_group (deps : list [str ], group : str ) -> None :
54+ def add_deps_to_group (deps : list [Dependency ], group : str ) -> None :
4155 """Add a package as a non-build dependency using PEP 735 dependency groups."""
4256 existing_group = get_deps_from_group (group )
4357
44- _deps = [dep for dep in deps if _strip_extras (dep ) not in existing_group ]
58+ to_add_deps = [
59+ dep for dep in deps if not is_dep_satisfied_in (dep , in_ = existing_group )
60+ ]
4561
46- if not _deps :
62+ if not to_add_deps :
4763 return
4864
49- deps_str = ", " .join ([f"'{ _strip_extras ( dep ) } '" for dep in _deps ])
50- ies = "y" if len (_deps ) == 1 else "ies"
65+ deps_str = ", " .join ([f"'{ dep } '" for dep in to_add_deps ])
66+ ies = "y" if len (to_add_deps ) == 1 else "ies"
5167 tick_print (
5268 f"Adding dependenc{ ies } { deps_str } to the '{ group } ' group in 'pyproject.toml'."
5369 )
5470
55- for dep in _deps :
71+ for dep in to_add_deps :
5672 try :
5773 if not usethis_config .offline :
58- call_uv_subprocess (["add" , "--group" , group , "--quiet" , dep ])
74+ call_uv_subprocess (["add" , "--group" , group , "--quiet" , str ( dep ) ])
5975 else :
6076 call_uv_subprocess (
61- ["add" , "--group" , group , "--quiet" , "--offline" , dep ]
77+ ["add" , "--group" , group , "--quiet" , "--offline" , str ( dep ) ]
6278 )
6379 except UVSubprocessFailedError as err :
6480 msg = f"Failed to add '{ dep } ' to the '{ group } ' dependency group:\n { err } "
6581 raise UVDepGroupError (msg ) from None
6682
6783
68- def remove_deps_from_group (deps : list [str ], group : str ) -> None :
84+ def is_dep_satisfied_in (dep : Dependency , * , in_ : list [Dependency ]) -> bool :
85+ return any (_is_dep_satisfied_by (dep , by = by ) for by in in_ )
86+
87+
88+ def _is_dep_satisfied_by (dep : Dependency , * , by : Dependency ) -> bool :
89+ # Name is the same and extras are a subset of the extras of the dependency
90+ return dep .name == by .name and (dep .extras or set ()) <= (by .extras or set ())
91+
92+
93+ def remove_deps_from_group (deps : list [Dependency ], group : str ) -> None :
6994 """Remove the tool's development dependencies, if present."""
7095 existing_group = get_deps_from_group (group )
7196
72- _deps = [dep for dep in deps if _strip_extras (dep ) in existing_group ]
97+ _deps = [dep for dep in deps if is_dep_satisfied_in (dep , in_ = existing_group ) ]
7398
7499 if not _deps :
75100 return
76101
77- deps_str = ", " .join ([f"'{ _strip_extras ( dep ) } '" for dep in _deps ])
102+ deps_str = ", " .join ([f"'{ dep } '" for dep in _deps ])
78103 ies = "y" if len (_deps ) == 1 else "ies"
79104 tick_print (
80105 f"Removing dependenc{ ies } { deps_str } from the '{ group } ' group in 'pyproject.toml'."
81106 )
82107
83108 for dep in _deps :
84109 try :
85- se_dep = _strip_extras (dep )
86110 if not usethis_config .offline :
87- call_uv_subprocess (["remove" , "--group" , group , "--quiet" , se_dep ])
111+ call_uv_subprocess (["remove" , "--group" , group , "--quiet" , str ( dep ) ])
88112 else :
89113 call_uv_subprocess (
90- ["remove" , "--group" , group , "--quiet" , "--offline" , se_dep ]
114+ ["remove" , "--group" , group , "--quiet" , "--offline" , str ( dep ) ]
91115 )
92116 except UVSubprocessFailedError as err :
93117 msg = (
@@ -96,12 +120,7 @@ def remove_deps_from_group(deps: list[str], group: str) -> None:
96120 raise UVDepGroupError (msg ) from None
97121
98122
99- def is_dep_in_any_group (dep : str ) -> bool :
100- return _strip_extras (dep ) in {
101- dep for group in get_dep_groups ().values () for dep in group
102- }
103-
104-
105- def _strip_extras (dep : str ) -> str :
106- """Remove extras from a dependency string."""
107- return re .sub (r"\[.*\]" , "" , dep )
123+ def is_dep_in_any_group (dep : Dependency ) -> bool :
124+ return is_dep_satisfied_in (
125+ dep , in_ = [dep for group in get_dep_groups ().values () for dep in group ]
126+ )
0 commit comments