Skip to content

Commit

Permalink
Migrate several classes over to dataclasses
Browse files Browse the repository at this point in the history
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'
```
  • Loading branch information
eli-schwartz committed Dec 3, 2023
1 parent 69fbc1e commit 51fd81c
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 102 deletions.
56 changes: 24 additions & 32 deletions src/installer/destinations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import compileall
import io
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -101,40 +102,31 @@ def finalize_installation(
raise NotImplementedError


@dataclass
class SchemeDictionaryDestination(WheelDestination):
"""Destination, based on a mapping of {scheme: file-system-path}."""
"""Destination, based on a mapping of {scheme: file-system-path}.
:ivar scheme_dict: a mapping of {scheme: file-system-path}
:ivar interpreter: the interpreter to use for generating scripts
:ivar script_kind: the "kind" of launcher script to use
:ivar hash_algorithm: the hashing algorithm to use, which is a member
of :any:`hashlib.algorithms_available` (ideally from
:any:`hashlib.algorithms_guaranteed`).
:ivar bytecode_optimization_levels: Compile cached bytecode for
installed .py files with these optimization levels. The bytecode
is specific to the minor version of Python (e.g. 3.10) used to
generate it.
:ivar destdir: A staging directory in which to write all files. This
is expected to be the filesystem root at runtime, so embedded paths
will be written as though this was the root.
"""

def __init__(
self,
scheme_dict: Dict[str, str],
interpreter: str,
script_kind: "LauncherKind",
hash_algorithm: str = "sha256",
bytecode_optimization_levels: Collection[int] = (),
destdir: Optional[str] = None,
) -> None:
"""Construct a ``SchemeDictionaryDestination`` object.
:param scheme_dict: a mapping of {scheme: file-system-path}
:param interpreter: the interpreter to use for generating scripts
:param script_kind: the "kind" of launcher script to use
:param hash_algorithm: the hashing algorithm to use, which is a member
of :any:`hashlib.algorithms_available` (ideally from
:any:`hashlib.algorithms_guaranteed`).
:param bytecode_optimization_levels: Compile cached bytecode for
installed .py files with these optimization levels. The bytecode
is specific to the minor version of Python (e.g. 3.10) used to
generate it.
:param destdir: A staging directory in which to write all files. This
is expected to be the filesystem root at runtime, so embedded paths
will be written as though this was the root.
"""
self.scheme_dict = scheme_dict
self.interpreter = interpreter
self.script_kind = script_kind
self.hash_algorithm = hash_algorithm
self.bytecode_optimization_levels = bytecode_optimization_levels
self.destdir = destdir
scheme_dict: Dict[str, str]
interpreter: str
script_kind: "LauncherKind"
hash_algorithm: str = "sha256"
bytecode_optimization_levels: Collection[int] = field(default_factory=tuple)
destdir: Optional[str] = None

def _path_with_destdir(self, scheme: Scheme, path: str) -> str:
file = os.path.join(self.scheme_dict[scheme], path)
Expand Down
69 changes: 26 additions & 43 deletions src/installer/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import csv
import hashlib
import os
from dataclasses import dataclass
from typing import BinaryIO, Iterable, Iterator, Optional, Tuple, cast

from installer.utils import copyfileobj_with_hashing, get_stream_length
Expand All @@ -16,48 +17,34 @@
]


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

def __init__(
self, elements: Iterable[str], issues: Iterable[str]
) -> None: # noqa: D107
super().__init__(", ".join(issues))
self.issues = issues
self.elements = elements
elements: Iterable[str]
issues: Iterable[str]

def __repr__(self) -> str:
return "InvalidRecordEntry(elements={!r}, issues={!r})".format(
self.elements, self.issues
)
def __post_init__(self) -> None:
super().__init__(", ".join(self.issues))


@dataclass
class Hash:
"""Represents the "hash" element of a RecordEntry."""
"""Represents the "hash" element of a RecordEntry.
def __init__(self, name: str, value: str) -> None:
"""Construct a ``Hash`` object.
Most consumers should use :py:meth:`Hash.parse` instead, since no
validation or parsing is performed by this constructor.
Most consumers should use :py:meth:`Hash.parse` instead, since no
validation or parsing is performed by this constructor.
:ivar name: name of the hash function
:ivar value: hashed value
"""

:param name: name of the hash function
:param value: hashed value
"""
self.name = name
self.value = value
name: str
value: str

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

def __repr__(self) -> str:
return f"Hash(name={self.name!r}, value={self.value!r})"

def __eq__(self, other: object) -> bool:
if not isinstance(other, Hash):
return NotImplemented
return self.value == other.value and self.name == other.name

def validate(self, data: bytes) -> bool:
"""Validate that ``data`` matches this instance.
Expand Down Expand Up @@ -85,27 +72,23 @@ def parse(cls, h: str) -> "Hash":
return cls(name, value)


@dataclass
class RecordEntry:
"""Represents a single record in a RECORD file.
r"""Represents a single record in a RECORD file.
A list of :py:class:`RecordEntry` objects fully represents a RECORD file.
"""
def __init__(self, path: str, hash_: Optional[Hash], size: Optional[int]) -> None:
r"""Construct a ``RecordEntry`` object.
Most consumers should use :py:meth:`RecordEntry.from_elements`, since no
validation or parsing is performed by this constructor.
Most consumers should use :py:meth:`RecordEntry.from_elements`, since no
validation or parsing is performed by this constructor.
:param path: file's path
:param hash\_: hash of the file's contents
:param size: file's size in bytes
"""
super().__init__()
:ivar path: file's path
:ivar hash\_: hash of the file's contents
:ivar size: file's size in bytes
"""

self.path = path
self.hash_ = hash_
self.size = size
path: str
hash_: Optional[Hash]
size: Optional[int]

def to_row(self, path_prefix: Optional[str] = None) -> Tuple[str, str, str]:
"""Convert this into a 3-element tuple that can be written in a RECORD file.
Expand Down
42 changes: 15 additions & 27 deletions src/installer/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import shlex
import zipfile
from dataclasses import dataclass, field
from importlib.resources import read_binary
from typing import TYPE_CHECKING, Mapping, Optional, Tuple

Expand Down Expand Up @@ -80,35 +81,22 @@ class InvalidScript(ValueError):
"""Raised if the user provides incorrect script section or kind."""


@dataclass
class Script:
"""Describes a script based on an entry point declaration."""

__slots__ = ("name", "module", "attr", "section")

def __init__(
self, name: str, module: str, attr: str, section: "ScriptSection"
) -> None:
"""Construct a Script object.
:param name: name of the script
:param module: module path, to load the entry point from
:param attr: final attribute access, for the entry point
:param section: Denotes the "entry point section" where this was specified.
Valid values are ``"gui"`` and ``"console"``.
:type section: str
"""Describes a script based on an entry point declaration.
:ivar name: name of the script
:ivar module: module path, to load the entry point from
:ivar attr: final attribute access, for the entry point
:ivar section: Denotes the "entry point section" where this was specified.
Valid values are ``"gui"`` and ``"console"``.
:type section: str
"""

"""
self.name = name
self.module = module
self.attr = attr
self.section = section

def __repr__(self) -> str:
return "Script(name={!r}, module={!r}, attr={!r}".format(
self.name,
self.module,
self.attr,
)
name: str
module: str
attr: str
section: "ScriptSection" = field(repr=False)

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

0 comments on commit 51fd81c

Please sign in to comment.