Skip to content

Commit f0f0da0

Browse files
committed
Migrate several classes over to dataclasses
Python 3.7 has been the minimum requirement for a while now, which brings dataclasses into the standard library. This reduces the boilerplate necessary to write nice classes with init and repr methods that simply do the right thing. As a side effect, fix the repr of installer.scripts.Script, which was missing the trailing close-parenthesis: ``` >>> import installer >>> installer.scripts.Script('name', 'module', 'attr', 'section') Script(name='name', module='module', attr='attr' ```
1 parent 4b27290 commit f0f0da0

File tree

3 files changed

+69
-89
lines changed

3 files changed

+69
-89
lines changed

src/installer/destinations.py

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import compileall
44
import io
55
import os
6+
from dataclasses import dataclass
67
from pathlib import Path
78
from typing import (
89
TYPE_CHECKING,
@@ -101,40 +102,39 @@ def finalize_installation(
101102
raise NotImplementedError
102103

103104

105+
@dataclass
104106
class SchemeDictionaryDestination(WheelDestination):
105107
"""Destination, based on a mapping of {scheme: file-system-path}."""
106108

107-
def __init__(
108-
self,
109-
scheme_dict: Dict[str, str],
110-
interpreter: str,
111-
script_kind: "LauncherKind",
112-
hash_algorithm: str = "sha256",
113-
bytecode_optimization_levels: Collection[int] = (),
114-
destdir: Optional[str] = None,
115-
) -> None:
116-
"""Construct a ``SchemeDictionaryDestination`` object.
117-
118-
:param scheme_dict: a mapping of {scheme: file-system-path}
119-
:param interpreter: the interpreter to use for generating scripts
120-
:param script_kind: the "kind" of launcher script to use
121-
:param hash_algorithm: the hashing algorithm to use, which is a member
122-
of :any:`hashlib.algorithms_available` (ideally from
123-
:any:`hashlib.algorithms_guaranteed`).
124-
:param bytecode_optimization_levels: Compile cached bytecode for
125-
installed .py files with these optimization levels. The bytecode
126-
is specific to the minor version of Python (e.g. 3.10) used to
127-
generate it.
128-
:param destdir: A staging directory in which to write all files. This
129-
is expected to be the filesystem root at runtime, so embedded paths
130-
will be written as though this was the root.
131-
"""
132-
self.scheme_dict = scheme_dict
133-
self.interpreter = interpreter
134-
self.script_kind = script_kind
135-
self.hash_algorithm = hash_algorithm
136-
self.bytecode_optimization_levels = bytecode_optimization_levels
137-
self.destdir = destdir
109+
scheme_dict: Dict[str, str]
110+
"""A mapping of {scheme: file-system-path}"""
111+
112+
interpreter: str
113+
"""The interpreter to use for generating scripts."""
114+
115+
script_kind: "LauncherKind"
116+
"""The "kind" of launcher script to use."""
117+
118+
hash_algorithm: str = "sha256"
119+
"""
120+
The hashing algorithm to use, which is a member of
121+
:any:`hashlib.algorithms_available` (ideally from
122+
:any:`hashlib.algorithms_guaranteed`).
123+
"""
124+
125+
bytecode_optimization_levels: Collection[int] = ()
126+
"""
127+
Compile cached bytecode for installed .py files with these optimization
128+
levels. The bytecode is specific to the minor version of Python (e.g. 3.10)
129+
used to generate it.
130+
"""
131+
132+
destdir: Optional[str] = None
133+
"""
134+
A staging directory in which to write all files. This is expected to be the
135+
filesystem root at runtime, so embedded paths will be written as though
136+
this was the root.
137+
"""
138138

139139
def _path_with_destdir(self, scheme: Scheme, path: str) -> str:
140140
file = os.path.join(self.scheme_dict[scheme], path)

src/installer/records.py

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import csv
55
import hashlib
66
import os
7+
from dataclasses import dataclass
78
from typing import BinaryIO, Iterable, Iterator, Optional, Tuple, cast
89

910
from installer.utils import copyfileobj_with_hashing, get_stream_length
@@ -16,46 +17,34 @@
1617
]
1718

1819

20+
@dataclass
1921
class InvalidRecordEntry(Exception):
2022
"""Raised when a RecordEntry is not valid, due to improper element values or count."""
2123

22-
def __init__(
23-
self, elements: Iterable[str], issues: Iterable[str]
24-
) -> None: # noqa: D107
25-
super().__init__(", ".join(issues))
26-
self.issues = issues
27-
self.elements = elements
24+
elements: Iterable[str]
25+
issues: Iterable[str]
2826

29-
def __repr__(self) -> str:
30-
return f"InvalidRecordEntry(elements={self.elements!r}, issues={self.issues!r})"
27+
def __post_init__(self) -> None:
28+
super().__init__(", ".join(self.issues))
3129

3230

31+
@dataclass
3332
class Hash:
34-
"""Represents the "hash" element of a RecordEntry."""
33+
"""Represents the "hash" element of a RecordEntry.
3534
36-
def __init__(self, name: str, value: str) -> None:
37-
"""Construct a ``Hash`` object.
35+
Most consumers should use :py:meth:`Hash.parse` instead, since no
36+
validation or parsing is performed by this constructor.
37+
"""
3838

39-
Most consumers should use :py:meth:`Hash.parse` instead, since no
40-
validation or parsing is performed by this constructor.
39+
name: str
40+
"""Name of the hash function."""
4141

42-
:param name: name of the hash function
43-
:param value: hashed value
44-
"""
45-
self.name = name
46-
self.value = value
42+
value: str
43+
"""Hashed value."""
4744

4845
def __str__(self) -> str:
4946
return f"{self.name}={self.value}"
5047

51-
def __repr__(self) -> str:
52-
return f"Hash(name={self.name!r}, value={self.value!r})"
53-
54-
def __eq__(self, other: object) -> bool:
55-
if not isinstance(other, Hash):
56-
return NotImplemented
57-
return self.value == other.value and self.name == other.name
58-
5948
def validate(self, data: bytes) -> bool:
6049
"""Validate that ``data`` matches this instance.
6150
@@ -83,27 +72,24 @@ def parse(cls, h: str) -> "Hash":
8372
return cls(name, value)
8473

8574

75+
@dataclass
8676
class RecordEntry:
8777
"""Represents a single record in a RECORD file.
8878
8979
A list of :py:class:`RecordEntry` objects fully represents a RECORD file.
90-
"""
9180
92-
def __init__(self, path: str, hash_: Optional[Hash], size: Optional[int]) -> None:
93-
r"""Construct a ``RecordEntry`` object.
81+
Most consumers should use :py:meth:`RecordEntry.from_elements`, since no
82+
validation or parsing is performed by this constructor.
83+
"""
9484

95-
Most consumers should use :py:meth:`RecordEntry.from_elements`, since no
96-
validation or parsing is performed by this constructor.
85+
path: str
86+
"""File's path."""
9787

98-
:param path: file's path
99-
:param hash\_: hash of the file's contents
100-
:param size: file's size in bytes
101-
"""
102-
super().__init__()
88+
hash_: Optional[Hash]
89+
"""Hash of the file's contents."""
10390

104-
self.path = path
105-
self.hash_ = hash_
106-
self.size = size
91+
size: Optional[int]
92+
"""File's size in bytes."""
10793

10894
def to_row(self, path_prefix: Optional[str] = None) -> Tuple[str, str, str]:
10995
"""Convert this into a 3-element tuple that can be written in a RECORD file.

src/installer/scripts.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shlex
66
import sys
77
import zipfile
8+
from dataclasses import dataclass, field
89
from types import ModuleType
910
from typing import TYPE_CHECKING, Mapping, Optional, Tuple, Union
1011

@@ -90,31 +91,24 @@ class InvalidScript(ValueError):
9091
"""Raised if the user provides incorrect script section or kind."""
9192

9293

94+
@dataclass
9395
class Script:
9496
"""Describes a script based on an entry point declaration."""
9597

96-
__slots__ = ("name", "module", "attr", "section")
98+
name: str
99+
"""Name of the script."""
97100

98-
def __init__(
99-
self, name: str, module: str, attr: str, section: "ScriptSection"
100-
) -> None:
101-
"""Construct a Script object.
101+
module: str
102+
"""Module path, to load the entry point from."""
102103

103-
:param name: name of the script
104-
:param module: module path, to load the entry point from
105-
:param attr: final attribute access, for the entry point
106-
:param section: Denotes the "entry point section" where this was specified.
107-
Valid values are ``"gui"`` and ``"console"``.
108-
:type section: str
104+
attr: str
105+
"""Final attribute access, for the entry point."""
109106

110-
"""
111-
self.name = name
112-
self.module = module
113-
self.attr = attr
114-
self.section = section
115-
116-
def __repr__(self) -> str:
117-
return f"Script(name={self.name!r}, module={self.module!r}, attr={self.attr!r}"
107+
section: "ScriptSection" = field(repr=False)
108+
"""
109+
Denotes the "entry point section" where this was specified. Valid values
110+
are ``"gui"`` and ``"console"``.
111+
"""
118112

119113
def _get_launcher_data(self, kind: "LauncherKind") -> Optional[bytes]:
120114
if kind == "posix":

0 commit comments

Comments
 (0)