From 6c1167d2229f5e2313d6f500bba4092873846e34 Mon Sep 17 00:00:00 2001 From: Bartosz Sokorski Date: Sat, 12 Oct 2024 14:30:07 +0200 Subject: [PATCH] Drop support for Python 3.8 --- .github/workflows/ci.yml | 2 +- noxfile.py | 4 ++-- pyproject.toml | 6 ++++-- src/installer/__main__.py | 5 +++-- src/installer/_core.py | 6 +++--- src/installer/destinations.py | 11 ++++------- src/installer/records.py | 9 +++++---- src/installer/scripts.py | 21 ++++++--------------- src/installer/sources.py | 15 ++++++++------- src/installer/utils.py | 12 +++++------- 10 files changed, 41 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac9e7ea2..b1d53fc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: os: [Windows, Ubuntu, MacOS] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] include: # Only run PyPy jobs, on Ubuntu. - os: Ubuntu diff --git a/noxfile.py b/noxfile.py index 238c8333..22a8b2d5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,7 +23,7 @@ def lint(session): session.run("pre-commit", "run", "--all-files", *args) -@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3"]) +@nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13", "pypy3"]) def test(session): session.install(".") session.install("-r", "tests/requirements.txt") @@ -43,7 +43,7 @@ def test(session): ) -@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3"]) +@nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13", "pypy3"]) def doctest(session): session.install(".") session.install("-r", "docs/requirements.txt") diff --git a/pyproject.toml b/pyproject.toml index c67818d6..d1b58518 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,15 +10,15 @@ readme = "README.md" authors = [ { name = "Pradyun Gedam", email = "pradyunsg@gmail.com" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dynamic = [ "description", @@ -28,6 +28,7 @@ dynamic = [ "GitHub" = "https://github.com/pypa/installer" [tool.ruff] +target-version = "py39" fix = true extend-exclude = [ "noxfile.py", @@ -42,6 +43,7 @@ select = [ "I", "ISC", "D", + "UP" ] ignore = [ "D105", diff --git a/src/installer/__main__.py b/src/installer/__main__.py index b5872329..ecfad11b 100644 --- a/src/installer/__main__.py +++ b/src/installer/__main__.py @@ -4,7 +4,8 @@ import os.path import sys import sysconfig -from typing import Dict, Optional, Sequence +from collections.abc import Sequence +from typing import Optional import installer from installer.destinations import SchemeDictionaryDestination @@ -61,7 +62,7 @@ def _get_main_parser() -> argparse.ArgumentParser: def _get_scheme_dict( distribution_name: str, prefix: Optional[str] = None -) -> Dict[str, str]: +) -> dict[str, str]: """Calculate the scheme dictionary for the current Python environment.""" vars = {} if prefix is None: diff --git a/src/installer/_core.py b/src/installer/_core.py index 9a02728f..caf004b9 100644 --- a/src/installer/_core.py +++ b/src/installer/_core.py @@ -2,7 +2,7 @@ import posixpath from io import BytesIO -from typing import Dict, Tuple, cast +from typing import cast from installer.destinations import WheelDestination from installer.exceptions import InvalidWheelSource @@ -35,7 +35,7 @@ def _process_WHEEL_file(source: WheelSource) -> Scheme: def _determine_scheme( path: str, source: WheelSource, root_scheme: Scheme -) -> Tuple[Scheme, str]: +) -> tuple[Scheme, str]: """Determine which scheme to place given path in, from source.""" data_dir = source.data_dir @@ -64,7 +64,7 @@ def _determine_scheme( def install( source: WheelSource, destination: WheelDestination, - additional_metadata: Dict[str, bytes], + additional_metadata: dict[str, bytes], ) -> None: """Install wheel described by ``source`` into ``destination``. diff --git a/src/installer/destinations.py b/src/installer/destinations.py index c1fe1923..eb583e8b 100644 --- a/src/installer/destinations.py +++ b/src/installer/destinations.py @@ -2,16 +2,13 @@ import io import os +from collections.abc import Collection, Iterable from dataclasses import dataclass from pathlib import Path from typing import ( TYPE_CHECKING, BinaryIO, - Collection, - Dict, - Iterable, Optional, - Tuple, Union, ) @@ -83,7 +80,7 @@ def finalize_installation( self, scheme: Scheme, record_file_path: str, - records: Iterable[Tuple[Scheme, RecordEntry]], + records: Iterable[tuple[Scheme, RecordEntry]], ) -> None: """Finalize installation, after all the files are written. @@ -105,7 +102,7 @@ def finalize_installation( class SchemeDictionaryDestination(WheelDestination): """Destination, based on a mapping of {scheme: file-system-path}.""" - scheme_dict: Dict[str, str] + scheme_dict: dict[str, str] """A mapping of {scheme: file-system-path}""" interpreter: str @@ -260,7 +257,7 @@ def finalize_installation( self, scheme: Scheme, record_file_path: str, - records: Iterable[Tuple[Scheme, RecordEntry]], + records: Iterable[tuple[Scheme, RecordEntry]], ) -> None: """Finalize installation, by writing the ``RECORD`` file & compiling bytecode. diff --git a/src/installer/records.py b/src/installer/records.py index 4be51912..a5a78597 100644 --- a/src/installer/records.py +++ b/src/installer/records.py @@ -4,8 +4,9 @@ import csv import hashlib import os +from collections.abc import Iterable, Iterator from dataclasses import dataclass -from typing import BinaryIO, Iterable, Iterator, Optional, Tuple, cast +from typing import BinaryIO, Optional, cast from installer.utils import copyfileobj_with_hashing, get_stream_length @@ -91,7 +92,7 @@ class RecordEntry: size: Optional[int] """File's size in bytes.""" - def to_row(self, path_prefix: Optional[str] = None) -> Tuple[str, str, str]: + 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. :param path_prefix: A prefix to attach to the path -- must end in `/` @@ -216,7 +217,7 @@ def from_elements(cls, path: str, hash_: str, size: str) -> "RecordEntry": return cls(path=path, hash_=hash_value, size=size_value) -def parse_record_file(rows: Iterable[str]) -> Iterator[Tuple[str, str, str]]: +def parse_record_file(rows: Iterable[str]) -> Iterator[tuple[str, str, str]]: """Parse a :pep:`376` RECORD. Returns an iterable of 3-value tuples, that can be passed to @@ -233,5 +234,5 @@ def parse_record_file(rows: Iterable[str]) -> Iterator[Tuple[str, str, str]]: # Convert Windows paths to use / for consistency elements[0] = elements[0].replace("\\", "/") - value = cast(Tuple[str, str, str], tuple(elements)) + value = cast(tuple[str, str, str], tuple(elements)) yield value diff --git a/src/installer/scripts.py b/src/installer/scripts.py index 0c7b7ea0..7643c773 100644 --- a/src/installer/scripts.py +++ b/src/installer/scripts.py @@ -3,20 +3,11 @@ import io import os import shlex -import sys import zipfile +from collections.abc import Mapping from dataclasses import dataclass, field -from types import ModuleType -from typing import TYPE_CHECKING, Mapping, Optional, Tuple, Union - -if sys.version_info >= (3, 9): # pragma: no cover - from importlib.resources import files - - def read_binary(package: Union[str, ModuleType], file_path: str) -> bytes: - return (files(package) / file_path).read_bytes() - -else: # pragma: no cover - from importlib.resources import read_binary +from importlib.resources import files +from typing import TYPE_CHECKING, Optional from installer import _scripts @@ -30,7 +21,7 @@ def read_binary(package: Union[str, ModuleType], file_path: str) -> bytes: __all__ = ["InvalidScript", "Script"] -_ALLOWED_LAUNCHERS: Mapping[Tuple["ScriptSection", "LauncherKind"], str] = { +_ALLOWED_LAUNCHERS: Mapping[tuple["ScriptSection", "LauncherKind"], str] = { ("console", "win-ia32"): "t32.exe", ("console", "win-amd64"): "t64.exe", ("console", "win-arm"): "t_arm.exe", @@ -119,7 +110,7 @@ def _get_launcher_data(self, kind: "LauncherKind") -> Optional[bytes]: except KeyError: error = f"{key!r} not in {sorted(_ALLOWED_LAUNCHERS)!r}" raise InvalidScript(error) from None - return read_binary(_scripts, name) + return (files(_scripts) / name).read_bytes() def _get_alternate_executable(self, executable: str, kind: "LauncherKind") -> str: """Get an alternate executable for the launcher. @@ -132,7 +123,7 @@ def _get_alternate_executable(self, executable: str, kind: "LauncherKind") -> st executable = os.path.join(dn, fn) return executable - def generate(self, executable: str, kind: "LauncherKind") -> Tuple[str, bytes]: + def generate(self, executable: str, kind: "LauncherKind") -> tuple[str, bytes]: """Generate a launcher for this script. :param executable: Path to the executable to invoke. diff --git a/src/installer/sources.py b/src/installer/sources.py index d42f08d8..cba8adb4 100644 --- a/src/installer/sources.py +++ b/src/installer/sources.py @@ -4,14 +4,15 @@ import posixpath import stat import zipfile +from collections.abc import Iterator from contextlib import contextmanager -from typing import BinaryIO, ClassVar, Iterator, List, Optional, Tuple, Type, cast +from typing import BinaryIO, ClassVar, Optional, cast from installer.exceptions import InstallerError from installer.records import RecordEntry, parse_record_file from installer.utils import canonicalize_name, parse_wheel_filename -WheelContentElement = Tuple[Tuple[str, str, str], BinaryIO, bool] +WheelContentElement = tuple[tuple[str, str, str], BinaryIO, bool] __all__ = ["WheelSource", "WheelFile"] @@ -23,7 +24,7 @@ class WheelSource: This is an abstract class, whose methods have to be implemented by subclasses. """ - validation_error: ClassVar[Type[Exception]] = ValueError #: :meta hide-value: + validation_error: ClassVar[type[Exception]] = ValueError #: :meta hide-value: """ .. versionadded:: 0.7.0 @@ -52,7 +53,7 @@ def data_dir(self) -> str: return f"{self.distribution}-{self.version}.data" @property - def dist_info_filenames(self) -> List[str]: + def dist_info_filenames(self) -> list[str]: """Get names of all files in the dist-info directory. Sample usage/behaviour:: @@ -113,7 +114,7 @@ def get_contents(self) -> Iterator[WheelContentElement]: class _WheelFileValidationError(ValueError, InstallerError): """Raised when a wheel file fails validation.""" - def __init__(self, issues: List[str]) -> None: + def __init__(self, issues: list[str]) -> None: super().__init__(repr(issues)) self.issues = issues @@ -208,7 +209,7 @@ def dist_info_dir(self) -> str: return dist_info_dir @property - def dist_info_filenames(self) -> List[str]: + def dist_info_filenames(self) -> list[str]: """Get names of all files in the dist-info directory.""" base = self.dist_info_dir return [ @@ -246,7 +247,7 @@ def validate_record(self, *, validate_contents: bool = True) -> None: [f"Unable to retrieve `RECORD` from {self._zipfile.filename}: {exc!r}"] ) from exc - issues: List[str] = [] + issues: list[str] = [] for item in self._zipfile.infolist(): if item.filename[-1:] == "/": # looks like a directory diff --git a/src/installer/utils.py b/src/installer/utils.py index c374edc2..e2b1a987 100644 --- a/src/installer/utils.py +++ b/src/installer/utils.py @@ -9,6 +9,7 @@ import re import sys from collections import namedtuple +from collections.abc import Iterable, Iterator from configparser import ConfigParser from email.message import Message from email.parser import FeedParser @@ -17,11 +18,8 @@ TYPE_CHECKING, BinaryIO, Callable, - Iterable, - Iterator, NewType, Optional, - Tuple, Union, cast, ) @@ -31,7 +29,7 @@ from installer.scripts import LauncherKind, ScriptSection Scheme = NewType("Scheme", str) -AllSchemes = Tuple[Scheme, ...] +AllSchemes = tuple[Scheme, ...] __all__ = [ "parse_metadata_file", @@ -117,7 +115,7 @@ def copyfileobj_with_hashing( source: BinaryIO, dest: BinaryIO, hash_algorithm: str, -) -> Tuple[str, int]: +) -> tuple[str, int]: """Copy a buffer while computing the content's hash and size. Copies the source buffer into the destination buffer while computing the @@ -206,7 +204,7 @@ def fix_shebang(stream: BinaryIO, interpreter: str) -> Iterator[BinaryIO]: def construct_record_file( - records: Iterable[Tuple[Scheme, "RecordEntry"]], + records: Iterable[tuple[Scheme, "RecordEntry"]], prefix_for_scheme: Callable[[Scheme], Optional[str]] = lambda _: None, ) -> BinaryIO: """Construct a RECORD file. @@ -228,7 +226,7 @@ def construct_record_file( return stream.detach() -def parse_entrypoints(text: str) -> Iterable[Tuple[str, str, str, "ScriptSection"]]: +def parse_entrypoints(text: str) -> Iterable[tuple[str, str, str, "ScriptSection"]]: """Parse ``entry_points.txt``-style files. :param text: entire contents of the file