diff --git a/pyproject.toml b/pyproject.toml index b97ad293..5f3923fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,16 +32,27 @@ fix = true src = ["src"] extend-exclude = [ "noxfile.py", - "docs/*" + "docs/*", + "tests/*" ] [tool.ruff.lint] -select = [ - "E", - "F", - "W", - "I", - "D", +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "ERA", # flake8-eradicate/eradicate + "I", # isort + "N", # pep8-naming + "PIE", # flake8-pie + "PGH", # pygrep + "RUF", # ruff checks + "SIM", # flake8-simplify + "T20", # flake8-print + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "D", # pydocstyle + "PTH", # flake8-use-pathlib ] ignore = [ "D105", "D203", "D213", "E501" @@ -49,7 +60,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "tests/*" = ["D"] -"tools/*" = ["D"] +"tools/*" = ["D", "T20"] [tool.ruff.lint.isort] known-first-party = ["src"] diff --git a/src/installer/__init__.py b/src/installer/__init__.py index 8b61a162..203b6a4a 100644 --- a/src/installer/__init__.py +++ b/src/installer/__init__.py @@ -3,4 +3,4 @@ __version__ = "1.0.0.dev0" __all__ = ["install"] -from installer._core import install # noqa +from installer._core import install diff --git a/src/installer/__main__.py b/src/installer/__main__.py index b5872329..52e81d06 100644 --- a/src/installer/__main__.py +++ b/src/installer/__main__.py @@ -75,7 +75,7 @@ def _get_scheme_dict( # calculate 'headers' path, not currently in sysconfig - see # https://bugs.python.org/issue44445. This is based on what distutils does. # TODO: figure out original vs normalised distribution names - scheme_dict["headers"] = os.path.join( + scheme_dict["headers"] = os.path.join( # noqa: PTH118 sysconfig.get_path("include", vars={"installed_base": installed_base}), distribution_name, ) diff --git a/src/installer/_core.py b/src/installer/_core.py index 9a02728f..156164f4 100644 --- a/src/installer/_core.py +++ b/src/installer/_core.py @@ -13,7 +13,7 @@ __all__ = ["install"] -def _process_WHEEL_file(source: WheelSource) -> Scheme: +def _process_WHEEL_file(source: WheelSource) -> Scheme: # noqa: N802 """Process the WHEEL file, from ``source``. Returns the scheme that the archive root should go in. diff --git a/src/installer/destinations.py b/src/installer/destinations.py index c1fe1923..2d09beb0 100644 --- a/src/installer/destinations.py +++ b/src/installer/destinations.py @@ -138,12 +138,11 @@ class SchemeDictionaryDestination(WheelDestination): 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) + def _path_with_destdir(self, scheme: Scheme, path: str) -> Path: + file = Path(self.scheme_dict[scheme]) / path if self.destdir is not None: - file_path = Path(file) - rel_path = file_path.relative_to(file_path.anchor) - return os.path.join(self.destdir, rel_path) + rel_path = file.relative_to(file.anchor) + return Path(self.destdir) / rel_path return file def write_to_fs( @@ -164,15 +163,15 @@ def write_to_fs( - Hashes the written content, to determine the entry in the ``RECORD`` file. """ target_path = self._path_with_destdir(scheme, path) - if not self.overwrite_existing and os.path.exists(target_path): - message = f"File already exists: {target_path}" + if not self.overwrite_existing and target_path.exists(): + message = f"File already exists: {target_path!s}" raise FileExistsError(message) - parent_folder = os.path.dirname(target_path) - if not os.path.exists(parent_folder): - os.makedirs(parent_folder) + parent_folder = target_path.parent + if not parent_folder.exists(): + parent_folder.mkdir(parents=True) - with open(target_path, "wb") as f: + with target_path.open("wb") as f: hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm) if is_executable: @@ -234,9 +233,9 @@ def write_script( ) path = self._path_with_destdir(Scheme("scripts"), script_name) - mode = os.stat(path).st_mode + mode = path.stat().st_mode mode |= (mode & 0o444) >> 2 - os.chmod(path, mode) + path.chmod(mode) return entry @@ -248,9 +247,8 @@ def _compile_bytecode(self, scheme: Scheme, record: RecordEntry) -> None: import compileall target_path = self._path_with_destdir(scheme, record.path) - dir_path_to_embed = os.path.dirname( # Without destdir - os.path.join(self.scheme_dict[scheme], record.path) - ) + dir_path_to_embed = (Path(self.scheme_dict[scheme]) / record.path).parent + for level in self.bytecode_optimization_levels: compileall.compile_file( target_path, optimize=level, quiet=1, ddir=dir_path_to_embed diff --git a/src/installer/exceptions.py b/src/installer/exceptions.py index 01f044ad..2d8757c0 100644 --- a/src/installer/exceptions.py +++ b/src/installer/exceptions.py @@ -5,5 +5,5 @@ class InstallerError(Exception): """All exceptions raised from this package's code.""" -class InvalidWheelSource(InstallerError): +class InvalidWheelSource(InstallerError): # noqa: N818 """When a wheel source violates a contract, or is not supported.""" diff --git a/src/installer/records.py b/src/installer/records.py index 4be51912..9d1428e8 100644 --- a/src/installer/records.py +++ b/src/installer/records.py @@ -5,6 +5,7 @@ import hashlib import os from dataclasses import dataclass +from pathlib import Path from typing import BinaryIO, Iterable, Iterator, Optional, Tuple, cast from installer.utils import copyfileobj_with_hashing, get_stream_length @@ -18,7 +19,7 @@ @dataclass -class InvalidRecordEntry(Exception): +class InvalidRecordEntry(Exception): # noqa: N818 """Raised when a RecordEntry is not valid, due to improper element values or count.""" elements: Iterable[str] @@ -152,23 +153,19 @@ def validate_stream(self, stream: BinaryIO) -> bool: :return: Whether data read from stream matches hash and size. """ if self.hash_ is not None: - with open(os.devnull, "wb") as new_target: + with Path(os.devnull).open("wb") as new_target: hash_, size = copyfileobj_with_hashing( stream, cast("BinaryIO", new_target), self.hash_.name ) if self.size is not None and size != self.size: return False - if self.hash_.value != hash_: - return False - return True + return self.hash_.value == hash_ elif self.size is not None: assert self.hash_ is None size = get_stream_length(stream) - if size != self.size: - return False - return True + return size == self.size return True diff --git a/src/installer/scripts.py b/src/installer/scripts.py index 0c7b7ea0..75928d94 100644 --- a/src/installer/scripts.py +++ b/src/installer/scripts.py @@ -87,7 +87,7 @@ def _build_shebang(executable: str, forlauncher: bool) -> bytes: return b"#!/bin/sh\n'''exec' " + quoted + b' "$0" "$@"\n' + b"' '''" -class InvalidScript(ValueError): +class InvalidScript(ValueError): # noqa: N818 """Raised if the user provides incorrect script section or kind.""" @@ -129,7 +129,7 @@ def _get_alternate_executable(self, executable: str, kind: "LauncherKind") -> st if self.section == "gui" and kind != "posix": dn, fn = os.path.split(executable) fn = fn.replace("python", "pythonw") - executable = os.path.join(dn, fn) + executable = os.path.join(dn, fn) # noqa: PTH118 return executable def generate(self, executable: str, kind: "LauncherKind") -> Tuple[str, bytes]: diff --git a/src/installer/sources.py b/src/installer/sources.py index d42f08d8..0d6aca86 100644 --- a/src/installer/sources.py +++ b/src/installer/sources.py @@ -1,16 +1,29 @@ """Source of information about a wheel file.""" -import os import posixpath import stat import zipfile from contextlib import contextmanager -from typing import BinaryIO, ClassVar, Iterator, List, Optional, Tuple, Type, cast +from pathlib import Path +from typing import ( + TYPE_CHECKING, + BinaryIO, + ClassVar, + Iterator, + List, + Optional, + Tuple, + Type, + cast, +) from installer.exceptions import InstallerError from installer.records import RecordEntry, parse_record_file from installer.utils import canonicalize_name, parse_wheel_filename +if TYPE_CHECKING: + import os + WheelContentElement = Tuple[Tuple[str, str, str], BinaryIO, bool] @@ -121,7 +134,7 @@ def __repr__(self) -> str: return f"WheelFileValidationError(issues={self.issues!r})" -class _WheelFileBadDistInfo(ValueError, InstallerError): +class _WheelFileBadDistInfo(ValueError, InstallerError): # noqa: N818 """Raised when a wheel file has issues around `.dist-info`.""" def __init__(self, *, reason: str, filename: Optional[str], dist_info: str) -> None: @@ -155,7 +168,7 @@ def __init__(self, f: zipfile.ZipFile) -> None: self._zipfile = f assert f.filename - basename = os.path.basename(f.filename) + basename = Path(f.filename).name parsed_name = parse_wheel_filename(basename) super().__init__( version=parsed_name.version, diff --git a/src/installer/utils.py b/src/installer/utils.py index c374edc2..97ec93a4 100644 --- a/src/installer/utils.py +++ b/src/installer/utils.py @@ -13,6 +13,7 @@ from email.message import Message from email.parser import FeedParser from email.policy import compat32 +from pathlib import Path from typing import ( TYPE_CHECKING, BinaryIO, @@ -22,7 +23,6 @@ NewType, Optional, Tuple, - Union, cast, ) @@ -67,7 +67,7 @@ "WheelFilename", ["distribution", "version", "build_tag", "tag"] ) -# Adapted from https://github.com/python/importlib_metadata/blob/v3.4.0/importlib_metadata/__init__.py#L90 # noqa +# Adapted from https://github.com/python/importlib_metadata/blob/v3.4.0/importlib_metadata/__init__.py#L90 _ENTRYPOINT_REGEX = re.compile( r""" (?P[\w.]+)\s* @@ -235,7 +235,7 @@ def parse_entrypoints(text: str) -> Iterable[Tuple[str, str, str, "ScriptSection :return: name of the script, module to use, attribute to call, kind of script (cli / gui) """ - # Borrowed from https://github.com/python/importlib_metadata/blob/v3.4.0/importlib_metadata/__init__.py#L115 # noqa + # Borrowed from https://github.com/python/importlib_metadata/blob/v3.4.0/importlib_metadata/__init__.py#L115 config = ConfigParser(delimiters="=") config.optionxform = str # type: ignore[assignment, method-assign] config.read_string(text) @@ -271,6 +271,6 @@ def _current_umask() -> int: # Borrowed from: # https://github.com/pypa/pip/blob/0f21fb92/src/pip/_internal/utils/unpacking.py#L93 -def make_file_executable(path: Union[str, "os.PathLike[str]"]) -> None: +def make_file_executable(path: Path) -> None: """Make the file at the provided path executable.""" - os.chmod(path, (0o777 & ~_current_umask() | 0o111)) + path.chmod(0o777 & ~_current_umask() | 0o111) diff --git a/tests/test_sources.py b/tests/test_sources.py index 419668f9..6db761c8 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -26,7 +26,7 @@ def test_raises_not_implemented_error(self): source = WheelSource(distribution="distribution", version="version") with pytest.raises(NotImplementedError): - source.dist_info_filenames + _ = source.dist_info_filenames with pytest.raises(NotImplementedError): source.read_dist_info("METADATA") @@ -66,9 +66,10 @@ def test_rejects_not_okay_name(self, tmp_path): with zipfile.ZipFile(str(path), "w"): pass - with pytest.raises(ValueError, match="Not a valid wheel filename: .+"): - with WheelFile.open(str(path)): - pass + with pytest.raises( + ValueError, match="Not a valid wheel filename: .+" + ), WheelFile.open(str(path)): + pass def test_provides_correct_dist_info_filenames(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -134,12 +135,10 @@ def test_requires_dist_info_name_match(self, fancy_wheel): ) # Python 3.7: rename doesn't return the new name: misnamed = fancy_wheel.parent / "misnamed-1.0.0-py3-none-any.whl" - with pytest.raises(InstallerError) as ctx: - with WheelFile.open(misnamed) as source: - source.dist_info_filenames + with pytest.raises(InstallerError) as ctx, WheelFile.open(misnamed) as source: + _ = source.dist_info_filenames error = ctx.value - print(error) assert error.filename == str(misnamed) assert error.dist_info == "fancy-1.0.0.dist-info" assert "" in error.reason @@ -152,12 +151,12 @@ def test_enforces_single_dist_info(self, fancy_wheel): b"This is a random file.", ) - with pytest.raises(InstallerError) as ctx: - with WheelFile.open(fancy_wheel) as source: - source.dist_info_filenames + with pytest.raises(InstallerError) as ctx, WheelFile.open( + fancy_wheel + ) as source: + _ = source.dist_info_filenames error = ctx.value - print(error) assert error.filename == str(fancy_wheel) assert error.dist_info == str(["fancy-1.0.0.dist-info", "name-1.0.0.dist-info"]) assert "exactly one .dist-info" in error.reason @@ -170,11 +169,10 @@ def test_rejects_no_record_on_validate(self, fancy_wheel): filename="fancy-1.0.0.dist-info/RECORD", content=None, ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, match="Unable to retrieve `RECORD`" - ): - source.validate_record(validate_contents=False) + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, match="Unable to retrieve `RECORD`" + ): + source.validate_record(validate_contents=False) def test_rejects_invalid_record_entry(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -187,12 +185,11 @@ def test_rejects_invalid_record_entry(self, fancy_wheel): line.replace("sha256=", "") for line in record_file_contents ), ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, - match="Unable to retrieve `RECORD`", - ): - source.validate_record() + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, + match="Unable to retrieve `RECORD`", + ): + source.validate_record() def test_rejects_record_missing_file_on_validate(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -205,11 +202,10 @@ def test_rejects_record_missing_file_on_validate(self, fancy_wheel): filename="fancy-1.0.0.dist-info/RECORD", content=new_record_file_contents, ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, match="not mentioned in RECORD" - ): - source.validate_record(validate_contents=False) + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, match="not mentioned in RECORD" + ): + source.validate_record(validate_contents=False) def test_rejects_record_missing_hash(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -224,12 +220,11 @@ def test_rejects_record_missing_hash(self, fancy_wheel): filename="fancy-1.0.0.dist-info/RECORD", content=new_record_file_contents, ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, - match="hash / size of (.+) is not included in RECORD", - ): - source.validate_record(validate_contents=False) + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, + match="hash / size of (.+) is not included in RECORD", + ): + source.validate_record(validate_contents=False) def test_accept_wheel_with_signature_file(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -268,12 +263,11 @@ def test_reject_signature_file_in_record(self, fancy_wheel): content=record_file_contents.rstrip("\n") + f"\nfancy-1.0.0.dist-info/RECORD.jws,sha256={jws_hash_nopad},{len(jws_content)}\n", ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, - match="digital signature file (.+) is incorrectly contained in RECORD.", - ): - source.validate_record(validate_contents=False) + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, + match="digital signature file (.+) is incorrectly contained in RECORD.", + ): + source.validate_record(validate_contents=False) def test_rejects_record_contain_self_hash(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -294,12 +288,11 @@ def test_rejects_record_contain_self_hash(self, fancy_wheel): filename="fancy-1.0.0.dist-info/RECORD", content="\n".join(new_record_file_lines), ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, - match="RECORD file incorrectly contains hash / size.", - ): - source.validate_record(validate_contents=False) + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, + match="RECORD file incorrectly contains hash / size.", + ): + source.validate_record(validate_contents=False) def test_rejects_record_validation_failed(self, fancy_wheel): with WheelFile.open(fancy_wheel) as source: @@ -319,9 +312,8 @@ def test_rejects_record_validation_failed(self, fancy_wheel): filename="fancy-1.0.0.dist-info/RECORD", content="\n".join(new_record_file_lines), ) - with WheelFile.open(fancy_wheel) as source: - with pytest.raises( - WheelFile.validation_error, - match="hash / size of (.+) didn't match RECORD", - ): - source.validate_record() + with WheelFile.open(fancy_wheel) as source, pytest.raises( + WheelFile.validation_error, + match="hash / size of (.+) didn't match RECORD", + ): + source.validate_record() diff --git a/tests/test_utils.py b/tests/test_utils.py index 4c67089c..22c262db 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -125,10 +125,9 @@ def test_basic_functionality(self): ) size = len(data) - with BytesIO(data) as source: - with BytesIO() as dest: - result = copyfileobj_with_hashing(source, dest, hash_algorithm="sha256") - written_data = dest.getvalue() + with BytesIO(data) as source, BytesIO() as dest: + result = copyfileobj_with_hashing(source, dest, hash_algorithm="sha256") + written_data = dest.getvalue() assert result == (hash_, size) assert written_data == data @@ -172,9 +171,8 @@ class TestScript: ], ) def test_replace_shebang(self, data, expected): - with BytesIO(data) as source: - with fix_shebang(source, "/my/python") as stream: - result = stream.read() + with BytesIO(data) as source, fix_shebang(source, "/my/python") as stream: + result = stream.read() assert result == expected @pytest.mark.parametrize( @@ -188,9 +186,8 @@ def test_replace_shebang(self, data, expected): ], ) def test_keep_data(self, data): - with BytesIO(data) as source: - with fix_shebang(source, "/my/python") as stream: - result = stream.read() + with BytesIO(data) as source, fix_shebang(source, "/my/python") as stream: + result = stream.read() assert result == data