Skip to content

Commit

Permalink
Migrate several classes over to dataclasses (#200)
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 authored Aug 30, 2024
1 parent 08900dd commit da12e58
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 92 deletions.
68 changes: 34 additions & 34 deletions src/installer/destinations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io
import os
from dataclasses import dataclass
from pathlib import Path
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -100,43 +101,42 @@ def finalize_installation(
raise NotImplementedError


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

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,
overwrite_existing: bool = False,
) -> 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.
:param overwrite_existing: silently overwrite existing files.
"""
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
self.overwrite_existing = overwrite_existing
scheme_dict: Dict[str, str]
"""A mapping of {scheme: file-system-path}"""

interpreter: str
"""The interpreter to use for generating scripts."""

script_kind: "LauncherKind"
"""The "kind" of launcher script to use."""

hash_algorithm: str = "sha256"
"""
The hashing algorithm to use, which is a member of
:any:`hashlib.algorithms_available` (ideally from
:any:`hashlib.algorithms_guaranteed`).
"""

bytecode_optimization_levels: Collection[int] = ()
"""
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.
"""

destdir: Optional[str] = None
"""
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.
"""

overwrite_existing: bool = False
"""Silently overwrite existing files."""

def _path_with_destdir(self, scheme: Scheme, path: str) -> str:
file = os.path.join(self.scheme_dict[scheme], path)
Expand Down
64 changes: 25 additions & 39 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,46 +17,34 @@
]


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

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

def __repr__(self) -> str:
return f"InvalidRecordEntry(elements={self.elements!r}, issues={self.issues!r})"
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.
name: str
"""Name of the hash function."""

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

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 @@ -83,27 +72,24 @@ def parse(cls, h: str) -> "Hash":
return cls(name, value)


@dataclass
class RecordEntry:
"""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.
path: str
"""File's path."""

:param path: file's path
:param hash\_: hash of the file's contents
:param size: file's size in bytes
"""
super().__init__()
hash_: Optional[Hash]
"""Hash of the file's contents."""

self.path = path
self.hash_ = hash_
self.size = size
size: Optional[int]
"""File's size in bytes."""

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
32 changes: 13 additions & 19 deletions src/installer/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import shlex
import sys
import zipfile
from dataclasses import dataclass, field
from types import ModuleType
from typing import TYPE_CHECKING, Mapping, Optional, Tuple, Union

Expand Down Expand Up @@ -90,31 +91,24 @@ 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")
name: str
"""Name of the script."""

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

: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
attr: str
"""Final attribute access, for the entry point."""

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

def __repr__(self) -> str:
return f"Script(name={self.name!r}, module={self.module!r}, attr={self.attr!r}"
section: "ScriptSection" = field(repr=False)
"""
Denotes the "entry point section" where this was specified. Valid values
are ``"gui"`` and ``"console"``.
"""

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

0 comments on commit da12e58

Please sign in to comment.