diff --git a/pyproject.toml b/pyproject.toml index c67818d..d5d39b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,17 +31,28 @@ dynamic = [ fix = true extend-exclude = [ "noxfile.py", - "docs/*" + "docs/*", ] [tool.ruff.lint] -select = [ - "E", - "F", - "W", - "I", - "ISC", - "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 + "ISC", # implicit-str-concat + "W", # pycodestyle warnings ] ignore = [ "D105", @@ -55,11 +66,12 @@ ignore = [ "ISC001", "ISC002", "E501", + "N818", ] [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 8b61a16..203b6a4 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 b587232..52e81d0 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 9a02728..156164f 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 c1fe192..2d09beb 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/records.py b/src/installer/records.py index 4be5191..dde4b3e 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 @@ -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 e45e27f..124767f 100644 --- a/src/installer/scripts.py +++ b/src/installer/scripts.py @@ -84,7 +84,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 9892794..5b87fb1 100644 --- a/src/installer/sources.py +++ b/src/installer/sources.py @@ -1,17 +1,30 @@ """Source of information about a wheel file.""" -import os import posixpath import stat import zipfile from contextlib import contextmanager from functools import cached_property -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] @@ -156,7 +169,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 0a43c22..77db362 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_destinations.py b/tests/test_destinations.py index db6eb57..480d238 100644 --- a/tests/test_destinations.py +++ b/tests/test_destinations.py @@ -1,5 +1,5 @@ import io -import os.path +from pathlib import Path import pytest @@ -86,10 +86,7 @@ def destination_overwrite_existing(self, tmp_path): ) def test_write_file(self, destination, scheme, path, data, expected): record = destination.write_file(scheme, path, io.BytesIO(data), False) - file_path = os.path.join(destination.scheme_dict[scheme], path) - with open(file_path, "rb") as f: - file_data = f.read() - + file_data = (Path(destination.scheme_dict[scheme]) / path).read_bytes() assert file_data == expected assert record.path == path @@ -111,12 +108,11 @@ def test_write_record_duplicate_with_overwrite_existing( def test_write_script(self, destination): script_args = ("my_entrypoint", "my_module", "my_function", "console") record = destination.write_script(*script_args) - file_path = os.path.join(destination.scheme_dict["scripts"], "my_entrypoint") + file_path = Path(destination.scheme_dict["scripts"]) / "my_entrypoint" - assert os.path.isfile(file_path) + assert file_path.is_file() - with open(file_path, "rb") as f: - file_data = f.read() + file_data = file_path.read_bytes() name, expected_data = Script(*script_args).generate("/my/python", "posix") assert file_data == expected_data @@ -179,11 +175,9 @@ def test_finalize_write_record(self, destination): ] destination.finalize_installation("purelib", "RECORD", records) - file_path = os.path.join(destination.scheme_dict["purelib"], "RECORD") - - with open(file_path, "rb") as f: - data = f.read() + file_path = Path(destination.scheme_dict["purelib"]) / "RECORD" + data = file_path.read_bytes() assert data == ( b"RECORD,,\n" b"../data/my_data1.bin,sha256=NV0A-M4OPuqTsHjeD6Wth_-UqrpAAAdyplcustFZ8s4,9\n" diff --git a/tests/test_records.py b/tests/test_records.py index 92d0f8c..a681608 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,4 +1,5 @@ from io import BytesIO +from pathlib import Path import pytest @@ -25,7 +26,7 @@ def record_simple_iter(record_simple_list): def record_simple_file(tmpdir, record_simple_list): p = tmpdir.join("RECORD") p.write("\n".join(record_simple_list)) - with open(str(p)) as f: + with Path(p).open() as f: yield f diff --git a/tests/test_scripts.py b/tests/test_scripts.py index f50504d..dfb5317 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -1,6 +1,6 @@ import io -import os import zipfile +from pathlib import Path import pytest @@ -32,12 +32,8 @@ def test_script_generate_space_in_executable(): def _read_launcher_data(section, kind): prefix = {"console": "t", "gui": "w"}[section] suffix = {"win-ia32": "32", "win-amd64": "64", "win-arm": "_arm"}[kind] - filename = os.path.join( - os.path.dirname(os.path.abspath(_scripts.__file__)), - f"{prefix}{suffix}.exe", - ) - with open(filename, "rb") as f: - return f.read() + file = Path(_scripts.__file__).parent / f"{prefix}{suffix}.exe" + return file.read_bytes() @pytest.mark.parametrize("section", ["console", "gui"]) diff --git a/tests/test_sources.py b/tests/test_sources.py index 419668f..6db761c 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 4c67089..22c262d 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