Skip to content

Commit 4d09e3c

Browse files
authored
Use dataclasses more liberally throughout the codebase (#12571)
1 parent 800c124 commit 4d09e3c

File tree

19 files changed

+123
-201
lines changed

19 files changed

+123
-201
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Convert numerous internal classes to dataclasses for readability and stricter
2+
enforcement of immutability across the codebase. A conservative approach was
3+
taken in selecting which classes to convert. Classes which did not convert
4+
cleanly into a dataclass or were "too complex" (e.g. maintains interconnected
5+
state) were left alone.

src/pip/_internal/index/collector.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212
import urllib.parse
1313
import urllib.request
14+
from dataclasses import dataclass
1415
from html.parser import HTMLParser
1516
from optparse import Values
1617
from typing import (
@@ -248,29 +249,22 @@ def parse_links(page: "IndexContent") -> Iterable[Link]:
248249
yield link
249250

250251

252+
@dataclass(frozen=True)
251253
class IndexContent:
252-
"""Represents one response (or page), along with its URL"""
254+
"""Represents one response (or page), along with its URL.
253255
254-
def __init__(
255-
self,
256-
content: bytes,
257-
content_type: str,
258-
encoding: Optional[str],
259-
url: str,
260-
cache_link_parsing: bool = True,
261-
) -> None:
262-
"""
263-
:param encoding: the encoding to decode the given content.
264-
:param url: the URL from which the HTML was downloaded.
265-
:param cache_link_parsing: whether links parsed from this page's url
266-
should be cached. PyPI index urls should
267-
have this set to False, for example.
268-
"""
269-
self.content = content
270-
self.content_type = content_type
271-
self.encoding = encoding
272-
self.url = url
273-
self.cache_link_parsing = cache_link_parsing
256+
:param encoding: the encoding to decode the given content.
257+
:param url: the URL from which the HTML was downloaded.
258+
:param cache_link_parsing: whether links parsed from this page's url
259+
should be cached. PyPI index urls should
260+
have this set to False, for example.
261+
"""
262+
263+
content: bytes
264+
content_type: str
265+
encoding: Optional[str]
266+
url: str
267+
cache_link_parsing: bool = True
274268

275269
def __str__(self) -> str:
276270
return redact_auth_from_url(self.url)

src/pip/_internal/index/package_finder.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import itertools
66
import logging
77
import re
8+
from dataclasses import dataclass
89
from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
910

1011
from pip._vendor.packaging import specifiers
@@ -322,22 +323,15 @@ def filter_unallowed_hashes(
322323
return filtered
323324

324325

326+
@dataclass
325327
class CandidatePreferences:
326328
"""
327329
Encapsulates some of the preferences for filtering and sorting
328330
InstallationCandidate objects.
329331
"""
330332

331-
def __init__(
332-
self,
333-
prefer_binary: bool = False,
334-
allow_all_prereleases: bool = False,
335-
) -> None:
336-
"""
337-
:param allow_all_prereleases: Whether to allow all pre-releases.
338-
"""
339-
self.allow_all_prereleases = allow_all_prereleases
340-
self.prefer_binary = prefer_binary
333+
prefer_binary: bool = False
334+
allow_all_prereleases: bool = False
341335

342336

343337
class BestCandidateResult:

src/pip/_internal/locations/_sysconfig.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,10 @@ def get_scheme(
192192
data=paths["data"],
193193
)
194194
if root is not None:
195+
converted_keys = {}
195196
for key in SCHEME_KEYS:
196-
value = change_root(root, getattr(scheme, key))
197-
setattr(scheme, key, value)
197+
converted_keys[key] = change_root(root, getattr(scheme, key))
198+
scheme = Scheme(**converted_keys)
198199
return scheme
199200

200201

src/pip/_internal/models/candidate.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
1+
from dataclasses import dataclass
2+
3+
from pip._vendor.packaging.version import Version
14
from pip._vendor.packaging.version import parse as parse_version
25

36
from pip._internal.models.link import Link
4-
from pip._internal.utils.models import KeyBasedCompareMixin
57

68

7-
class InstallationCandidate(KeyBasedCompareMixin):
9+
@dataclass(frozen=True)
10+
class InstallationCandidate:
811
"""Represents a potential "candidate" for installation."""
912

1013
__slots__ = ["name", "version", "link"]
1114

15+
name: str
16+
version: Version
17+
link: Link
18+
1219
def __init__(self, name: str, version: str, link: Link) -> None:
13-
self.name = name
14-
self.version = parse_version(version)
15-
self.link = link
16-
17-
super().__init__(
18-
key=(self.name, self.version, self.link),
19-
defining_class=InstallationCandidate,
20-
)
21-
22-
def __repr__(self) -> str:
23-
return (
24-
f"<InstallationCandidate({self.name!r}, {self.version!r}, {self.link!r})>"
25-
)
20+
object.__setattr__(self, "name", name)
21+
object.__setattr__(self, "version", parse_version(version))
22+
object.__setattr__(self, "link", link)
2623

2724
def __str__(self) -> str:
2825
return f"{self.name!r} candidate (version {self.version} at {self.link})"

src/pip/_internal/models/direct_url.py

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import json
44
import re
55
import urllib.parse
6-
from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union
6+
from dataclasses import dataclass
7+
from typing import Any, ClassVar, Dict, Iterable, Optional, Type, TypeVar, Union
78

89
__all__ = [
910
"DirectUrl",
@@ -65,18 +66,13 @@ def _filter_none(**kwargs: Any) -> Dict[str, Any]:
6566
return {k: v for k, v in kwargs.items() if v is not None}
6667

6768

69+
@dataclass
6870
class VcsInfo:
69-
name = "vcs_info"
71+
name: ClassVar = "vcs_info"
7072

71-
def __init__(
72-
self,
73-
vcs: str,
74-
commit_id: str,
75-
requested_revision: Optional[str] = None,
76-
) -> None:
77-
self.vcs = vcs
78-
self.requested_revision = requested_revision
79-
self.commit_id = commit_id
73+
vcs: str
74+
commit_id: str
75+
requested_revision: Optional[str] = None
8076

8177
@classmethod
8278
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
@@ -140,14 +136,11 @@ def _to_dict(self) -> Dict[str, Any]:
140136
return _filter_none(hash=self.hash, hashes=self.hashes)
141137

142138

139+
@dataclass
143140
class DirInfo:
144-
name = "dir_info"
141+
name: ClassVar = "dir_info"
145142

146-
def __init__(
147-
self,
148-
editable: bool = False,
149-
) -> None:
150-
self.editable = editable
143+
editable: bool = False
151144

152145
@classmethod
153146
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
@@ -162,16 +155,11 @@ def _to_dict(self) -> Dict[str, Any]:
162155
InfoType = Union[ArchiveInfo, DirInfo, VcsInfo]
163156

164157

158+
@dataclass
165159
class DirectUrl:
166-
def __init__(
167-
self,
168-
url: str,
169-
info: InfoType,
170-
subdirectory: Optional[str] = None,
171-
) -> None:
172-
self.url = url
173-
self.info = info
174-
self.subdirectory = subdirectory
160+
url: str
161+
info: InfoType
162+
subdirectory: Optional[str] = None
175163

176164
def _remove_auth_from_netloc(self, netloc: str) -> str:
177165
if "@" not in netloc:

src/pip/_internal/models/link.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
split_auth_from_netloc,
2828
splitext,
2929
)
30-
from pip._internal.utils.models import KeyBasedCompareMixin
3130
from pip._internal.utils.urls import path_to_url, url_to_path
3231

3332
if TYPE_CHECKING:
@@ -179,7 +178,8 @@ def _ensure_quoted_url(url: str) -> str:
179178
return urllib.parse.urlunparse(result._replace(path=path))
180179

181180

182-
class Link(KeyBasedCompareMixin):
181+
@functools.total_ordering
182+
class Link:
183183
"""Represents a parsed link from a Package Index's simple URL"""
184184

185185
__slots__ = [
@@ -254,8 +254,6 @@ def __init__(
254254
self.yanked_reason = yanked_reason
255255
self.metadata_file_data = metadata_file_data
256256

257-
super().__init__(key=url, defining_class=Link)
258-
259257
self.cache_link_parsing = cache_link_parsing
260258
self.egg_fragment = self._egg_fragment()
261259

@@ -375,6 +373,19 @@ def __str__(self) -> str:
375373
def __repr__(self) -> str:
376374
return f"<Link {self}>"
377375

376+
def __hash__(self) -> int:
377+
return hash(self.url)
378+
379+
def __eq__(self, other: Any) -> bool:
380+
if not isinstance(other, Link):
381+
return NotImplemented
382+
return self.url == other.url
383+
384+
def __lt__(self, other: Any) -> bool:
385+
if not isinstance(other, Link):
386+
return NotImplemented
387+
return self.url < other.url
388+
378389
@property
379390
def url(self) -> str:
380391
return self._url

src/pip/_internal/models/scheme.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,21 @@
55
https://docs.python.org/3/install/index.html#alternate-installation.
66
"""
77

8+
from dataclasses import dataclass
9+
810
SCHEME_KEYS = ["platlib", "purelib", "headers", "scripts", "data"]
911

1012

13+
@dataclass(frozen=True)
1114
class Scheme:
1215
"""A Scheme holds paths which are used as the base directories for
1316
artifacts associated with a Python package.
1417
"""
1518

1619
__slots__ = SCHEME_KEYS
1720

18-
def __init__(
19-
self,
20-
platlib: str,
21-
purelib: str,
22-
headers: str,
23-
scripts: str,
24-
data: str,
25-
) -> None:
26-
self.platlib = platlib
27-
self.purelib = purelib
28-
self.headers = headers
29-
self.scripts = scripts
30-
self.data = data
21+
platlib: str
22+
purelib: str
23+
headers: str
24+
scripts: str
25+
data: str

src/pip/_internal/models/search_scope.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import posixpath
55
import urllib.parse
6+
from dataclasses import dataclass
67
from typing import List
78

89
from pip._vendor.packaging.utils import canonicalize_name
@@ -14,13 +15,18 @@
1415
logger = logging.getLogger(__name__)
1516

1617

18+
@dataclass(frozen=True)
1719
class SearchScope:
1820
"""
1921
Encapsulates the locations that pip is configured to search.
2022
"""
2123

2224
__slots__ = ["find_links", "index_urls", "no_index"]
2325

26+
find_links: List[str]
27+
index_urls: List[str]
28+
no_index: bool
29+
2430
@classmethod
2531
def create(
2632
cls,
@@ -63,16 +69,6 @@ def create(
6369
no_index=no_index,
6470
)
6571

66-
def __init__(
67-
self,
68-
find_links: List[str],
69-
index_urls: List[str],
70-
no_index: bool,
71-
) -> None:
72-
self.find_links = find_links
73-
self.index_urls = index_urls
74-
self.no_index = no_index
75-
7672
def get_formatted_locations(self) -> str:
7773
lines = []
7874
redacted_index_urls = []

src/pip/_internal/models/selection_prefs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pip._internal.models.format_control import FormatControl
44

55

6+
# TODO: This needs Python 3.10's improved slots support for dataclasses
7+
# to be converted into a dataclass.
68
class SelectionPreferences:
79
"""
810
Encapsulates the candidate selection preferences for downloading

0 commit comments

Comments
 (0)