Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion src/installer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
__version__ = "1.0.0.dev0"
__all__ = ["install"]

from installer._core import install # noqa
from installer._core import install
2 changes: 1 addition & 1 deletion src/installer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion src/installer/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 14 additions & 16 deletions src/installer/destinations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
11 changes: 4 additions & 7 deletions src/installer/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/installer/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
19 changes: 16 additions & 3 deletions src/installer/sources.py
Original file line number Diff line number Diff line change
@@ -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]


Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions src/installer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +23,6 @@
NewType,
Optional,
Tuple,
Union,
cast,
)

Expand Down Expand Up @@ -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<module>[\w.]+)\s*
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
20 changes: 7 additions & 13 deletions tests/test_destinations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import io
import os.path
from pathlib import Path

import pytest

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tests/test_records.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from io import BytesIO
from pathlib import Path

import pytest

Expand All @@ -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


Expand Down
10 changes: 3 additions & 7 deletions tests/test_scripts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import io
import os
import zipfile
from pathlib import Path

import pytest

Expand Down Expand Up @@ -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"])
Expand Down
Loading
Loading