Skip to content

Commit 501fe25

Browse files
committed
pylock: improve getters
1 parent b689a02 commit 501fe25

File tree

2 files changed

+69
-72
lines changed

2 files changed

+69
-72
lines changed

src/pip/_internal/models/pylock.py

Lines changed: 67 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
)
1818

1919
from pip._vendor import tomli_w
20-
from pip._vendor.packaging.markers import InvalidMarker, Marker
21-
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
22-
from pip._vendor.packaging.version import InvalidVersion, Version
20+
from pip._vendor.packaging.markers import Marker
21+
from pip._vendor.packaging.specifiers import SpecifierSet
22+
from pip._vendor.packaging.version import Version
2323
from pip._vendor.typing_extensions import Self
2424

2525
from pip._internal.models.direct_url import ArchiveInfo, DirInfo, VcsInfo
@@ -28,15 +28,16 @@
2828
from pip._internal.utils.urls import url_to_path
2929

3030
T = TypeVar("T")
31+
T2 = TypeVar("T2")
3132

3233

33-
class PylockDataClass(Protocol):
34+
class FromDictProtocol(Protocol):
3435
@classmethod
3536
def from_dict(cls, d: Dict[str, Any]) -> Self:
3637
pass
3738

3839

39-
PylockDataClassT = TypeVar("PylockDataClassT", bound=PylockDataClass)
40+
FromDictProtocolT = TypeVar("FromDictProtocolT", bound=FromDictProtocol)
4041

4142
PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
4243

@@ -67,12 +68,12 @@ def _toml_dict_factory(data: List[Tuple[str, Any]]) -> Dict[str, Any]:
6768

6869
def _get(d: Dict[str, Any], expected_type: Type[T], key: str) -> Optional[T]:
6970
"""Get value from dictionary and verify expected type."""
70-
if key not in d:
71+
value = d.get(key)
72+
if value is None:
7173
return None
72-
value = d[key]
7374
if not isinstance(value, expected_type):
7475
raise PylockValidationError(
75-
f"{value!r} has unexpected type for {key} (expected {expected_type})"
76+
f"{key} has unexpected type {type(value)} (expected {expected_type})"
7677
)
7778
return value
7879

@@ -85,99 +86,94 @@ def _get_required(d: Dict[str, Any], expected_type: Type[T], key: str) -> T:
8586
return value
8687

8788

88-
def _get_version(d: Dict[str, Any], key: str) -> Optional[Version]:
89-
value = _get(d, str, key)
89+
def _get_as(
90+
d: Dict[str, Any], expected_type: Type[T], target_type: Type[T2], key: str
91+
) -> Optional[T2]:
92+
"""Get value from dictionary, verify expected type, convert to target type.
93+
94+
This assumes the target_type constructor accepts the value.
95+
"""
96+
value = _get(d, expected_type, key)
9097
if value is None:
9198
return None
9299
try:
93-
return Version(value)
94-
except InvalidVersion:
95-
raise PylockUnsupportedVersionError(f"invalid version {value!r}")
100+
return target_type(value) # type: ignore[call-arg]
101+
except Exception as e:
102+
raise PylockValidationError(f"Error parsing value of {key!r}: {e}") from e
96103

97104

98-
def _get_required_version(d: Dict[str, Any], key: str) -> Version:
99-
value = _get_version(d, key)
105+
def _get_required_as(
106+
d: Dict[str, Any], expected_type: Type[T], target_type: Type[T2], key: str
107+
) -> T2:
108+
"""Get required value from dictionary, verify expected type,
109+
convert to target type."""
110+
value = _get_as(d, expected_type, target_type, key)
100111
if value is None:
101112
raise PylockRequiredKeyError(key)
102113
return value
103114

104115

105-
def _get_marker(d: Dict[str, Any], key: str) -> Optional[Marker]:
106-
value = _get(d, str, key)
107-
if value is None:
108-
return None
109-
try:
110-
return Marker(value)
111-
except InvalidMarker:
112-
raise PylockValidationError(f"invalid marker {value!r}")
113-
114-
115-
def _get_list_of_markers(d: Dict[str, Any], key: str) -> Optional[List[Marker]]:
116+
def _get_list_as(
117+
d: Dict[str, Any], expected_type: Type[T], target_type: Type[T2], key: str
118+
) -> Optional[List[T2]]:
116119
"""Get list value from dictionary and verify expected items type."""
117-
if key not in d:
120+
value = _get(d, list, key)
121+
if value is None:
118122
return None
119-
value = d[key]
120-
if not isinstance(value, list):
121-
raise PylockValidationError(f"{key!r} is not a list")
122123
result = []
123124
for i, item in enumerate(value):
124-
if not isinstance(item, str):
125-
raise PylockValidationError(f"Item {i} in list {key!r} is not a string")
126-
try:
127-
result.append(Marker(item))
128-
except InvalidMarker:
125+
if not isinstance(item, expected_type):
129126
raise PylockValidationError(
130-
f"Item {i} in list {key!r} is not a valid environment marker: {item!r}"
127+
f"Item {i} of {key} has unpexpected type {type(item)} "
128+
f"(expected {expected_type})"
131129
)
130+
try:
131+
result.append(target_type(item)) # type: ignore[call-arg]
132+
except Exception as e:
133+
raise PylockValidationError(
134+
f"Error parsing item {i} of {key!r}: {e}"
135+
) from e
132136
return result
133137

134138

135-
def _get_specifier_set(d: Dict[str, Any], key: str) -> Optional[SpecifierSet]:
136-
value = _get(d, str, key)
137-
if value is None:
138-
return None
139-
try:
140-
return SpecifierSet(value)
141-
except InvalidSpecifier:
142-
raise PylockValidationError(f"invalid version specifier {value!r}")
143-
144-
145139
def _get_object(
146-
d: Dict[str, Any], expected_type: Type[PylockDataClassT], key: str
147-
) -> Optional[PylockDataClassT]:
140+
d: Dict[str, Any], target_type: Type[FromDictProtocolT], key: str
141+
) -> Optional[FromDictProtocolT]:
148142
"""Get dictionary value from dictionary and convert to dataclass."""
149-
if key not in d:
143+
value = _get(d, dict, key)
144+
if value is None:
150145
return None
151-
value = d[key]
152-
if not isinstance(value, dict):
153-
raise PylockValidationError(f"{key!r} is not a dictionary")
154-
return expected_type.from_dict(value)
146+
try:
147+
return target_type.from_dict(value)
148+
except Exception as e:
149+
raise PylockValidationError(f"Error parsing value of {key!r}: {e}") from e
155150

156151

157152
def _get_list_of_objects(
158-
d: Dict[str, Any], expected_type: Type[PylockDataClassT], key: str
159-
) -> Optional[List[PylockDataClassT]]:
153+
d: Dict[str, Any], target_type: Type[FromDictProtocolT], key: str
154+
) -> Optional[List[FromDictProtocolT]]:
160155
"""Get list value from dictionary and convert items to dataclass."""
161-
if key not in d:
156+
value = _get(d, list, key)
157+
if value is None:
162158
return None
163-
value = d[key]
164-
if not isinstance(value, list):
165-
raise PylockValidationError(f"{key!r} is not a list")
166159
result = []
167160
for i, item in enumerate(value):
168161
if not isinstance(item, dict):
162+
raise PylockValidationError(f"Item {i} of {key!r} is not a table")
163+
try:
164+
result.append(target_type.from_dict(item))
165+
except Exception as e:
169166
raise PylockValidationError(
170-
f"Item {i} in table {key!r} is not a dictionary"
171-
)
172-
result.append(expected_type.from_dict(item))
167+
f"Error parsing item {i} of {key!r}: {e}"
168+
) from e
173169
return result
174170

175171

176172
def _get_required_list_of_objects(
177-
d: Dict[str, Any], expected_type: Type[PylockDataClassT], key: str
178-
) -> List[PylockDataClassT]:
173+
d: Dict[str, Any], target_type: Type[FromDictProtocolT], key: str
174+
) -> List[FromDictProtocolT]:
179175
"""Get required list value from dictionary and convert items to dataclass."""
180-
result = _get_list_of_objects(d, expected_type, key)
176+
result = _get_list_of_objects(d, target_type, key)
181177
if result is None:
182178
raise PylockRequiredKeyError(key)
183179
return result
@@ -356,9 +352,9 @@ def __post_init__(self) -> None:
356352
def from_dict(cls, d: Dict[str, Any]) -> Self:
357353
package = cls(
358354
name=_get_required(d, str, "name"),
359-
version=_get_version(d, "version"),
360-
requires_python=_get_specifier_set(d, "requires-python"),
361-
marker=_get_marker(d, "marker"),
355+
version=_get_as(d, str, Version, "version"),
356+
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
357+
marker=_get_as(d, str, Marker, "marker"),
362358
vcs=_get_object(d, PackageVcs, "vcs"),
363359
directory=_get_object(d, PackageDirectory, "directory"),
364360
archive=_get_object(d, PackageArchive, "archive"),
@@ -490,10 +486,10 @@ def to_dict(self) -> Dict[str, Any]:
490486
@classmethod
491487
def from_dict(cls, d: Dict[str, Any]) -> Self:
492488
return cls(
493-
lock_version=_get_required_version(d, "lock-version"),
494-
environments=_get_list_of_markers(d, "environments"),
489+
lock_version=_get_required_as(d, str, Version, "lock-version"),
490+
environments=_get_list_as(d, str, Marker, "environments"),
495491
created_by=_get_required(d, str, "created-by"),
496-
requires_python=_get_specifier_set(d, "requires-python"),
492+
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
497493
packages=_get_required_list_of_objects(d, Package, "packages"),
498494
)
499495

tests/unit/test_pylock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_pylock_invalid_version() -> None:
4848
"created-by": "pip",
4949
"packages": [],
5050
}
51-
with pytest.raises(PylockUnsupportedVersionError):
51+
with pytest.raises(PylockValidationError):
5252
Pylock.from_dict(data)
5353

5454

@@ -91,6 +91,7 @@ def test_pylock_packages_without_dist() -> None:
9191
with pytest.raises(PylockValidationError) as exc_info:
9292
Pylock.from_dict(data)
9393
assert str(exc_info.value) == (
94+
"Error parsing item 0 of 'packages': "
9495
"Exactly one of vcs, directory, archive must be set "
9596
"if sdist and wheels are not set"
9697
)

0 commit comments

Comments
 (0)