Commit 444fe078 by Sébastien Eustace Committed by Randy Döring

Add a wheel installer

Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
parent 55127c89
......@@ -703,6 +703,18 @@ files = [
]
[[package]]
name = "installer"
version = "0.6.0"
description = "A library for installing Python wheels."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "installer-0.6.0-py3-none-any.whl", hash = "sha256:ae7c62d1d6158b5c096419102ad0d01fdccebf857e784cee57f94165635fe038"},
{file = "installer-0.6.0.tar.gz", hash = "sha256:f3bd36cd261b440a88a1190b1becca0578fee90b4b62decc796932fdd5ae8839"},
]
[[package]]
name = "jaraco-classes"
version = "3.2.3"
description = "Utility functions for Python class constructs"
......@@ -1933,4 +1945,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.7"
content-hash = "89dc4e56ed4a5f8713e1b6c74e3b9f6a6c9c5143350908feb39d71101b937f84"
content-hash = "47c828b086d203975e5d1e6deecc4821d51d09eb8dea20141f08fdd1cb280e03"
......@@ -57,6 +57,7 @@ dulwich = "^0.21.2"
filelock = "^3.8.0"
html5lib = "^1.0"
importlib-metadata = { version = ">=4.4", python = "<3.10" }
installer = "^0.6.0"
jsonschema = "^4.10.0"
keyring = "^23.9.0"
lockfile = "^0.12.2"
......
from __future__ import annotations
import os
import platform
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from installer import install
from installer.destinations import SchemeDictionaryDestination
from installer.sources import WheelFile
from poetry.__version__ import __version__
from poetry.utils._compat import WINDOWS
if TYPE_CHECKING:
from typing import BinaryIO
from installer.records import RecordEntry
from installer.scripts import LauncherKind
from installer.utils import Scheme
from poetry.utils.env import Env
class WheelDestination(SchemeDictionaryDestination):
""" """
def write_to_fs(
self,
scheme: Scheme,
path: Path | str,
stream: BinaryIO,
is_executable: bool,
) -> RecordEntry:
from installer.records import Hash
from installer.records import RecordEntry
from installer.utils import copyfileobj_with_hashing
from installer.utils import make_file_executable
target_path = Path(self.scheme_dict[scheme]) / path
if target_path.exists():
# Contrary to the base library we don't raise an error
# here since it can break namespace packages (like Poetry's)
pass
parent_folder = target_path.parent
if not parent_folder.exists():
# Due to the parallel installation it can happen
# that two threads try to create the directory.
os.makedirs(parent_folder, exist_ok=True)
with open(target_path, "wb") as f:
hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm)
if is_executable:
make_file_executable(target_path)
return RecordEntry(str(path), Hash(self.hash_algorithm, hash_), size)
def for_source(self, source: WheelFile) -> WheelDestination:
scheme_dict = self.scheme_dict.copy()
scheme_dict["headers"] = str(Path(scheme_dict["headers"]) / source.distribution)
return self.__class__(
scheme_dict, interpreter=self.interpreter, script_kind=self.script_kind
)
class WheelInstaller:
def __init__(self, env: Env) -> None:
self._env = env
script_kind: LauncherKind
if not WINDOWS:
script_kind = "posix"
else:
if platform.uname()[4].startswith("arm"):
script_kind = "win-arm64" if sys.maxsize > 2**32 else "win-arm"
else:
script_kind = "win-amd64" if sys.maxsize > 2**32 else "win-ia32"
schemes = self._env.paths
schemes["headers"] = schemes["include"]
self._destination = WheelDestination(
schemes, interpreter=self._env.python, script_kind=script_kind
)
def install(self, wheel: Path) -> None:
with WheelFile.open(Path(wheel.as_posix())) as source:
install(
source=source,
destination=self._destination.for_source(source),
# Additional metadata that is generated by the installation tool.
additional_metadata={
"INSTALLER": f"Poetry {__version__}".encode(),
},
)
......@@ -59,7 +59,6 @@ if TYPE_CHECKING:
from poetry.poetry import Poetry
GET_SYS_TAGS = f"""
import importlib.util
import json
......@@ -84,7 +83,6 @@ print(
)
"""
GET_ENVIRONMENT_INFO = """\
import json
import os
......@@ -155,7 +153,6 @@ env = {
print(json.dumps(env))
"""
GET_BASE_PREFIX = """\
import sys
......@@ -1438,6 +1435,16 @@ class Env:
if self._paths is None:
self._paths = self.get_paths()
if self.is_venv():
# We copy pip's logic here for the `include` path
self._paths["include"] = str(
self.path.joinpath(
"include",
"site",
f"python{self.version_info[0]}.{self.version_info[1]}",
)
)
return self._paths
@property
......
......@@ -279,12 +279,12 @@ def http() -> Iterator[type[httpretty.httpretty]]:
yield httpretty
@pytest.fixture
@pytest.fixture(scope="session")
def fixture_base() -> Path:
return Path(__file__).parent / "fixtures"
@pytest.fixture
@pytest.fixture(scope="session")
def fixture_dir(fixture_base: Path) -> FixtureDirGetter:
def _fixture_dir(name: str) -> Path:
return fixture_base / name
......
......@@ -222,8 +222,9 @@ def test_execute_prints_warning_for_yanked_package(
"(black-21.11b0-py3-none-any.whl) is yanked. Reason for being yanked: "
"Broken regex dependency. Use 21.11b1 instead."
)
output = io.fetch_output()
error = io.fetch_error()
assert return_code == 0
assert return_code == 0, f"\noutput: {output}\nerror: {error}\n"
assert "pytest" not in error
if has_warning:
assert expected in error
......
from __future__ import annotations
import re
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from poetry.core.constraints.version import parse_constraint
from poetry.installation.wheel_installer import WheelInstaller
from poetry.utils.env import MockEnv
if TYPE_CHECKING:
from _pytest.tmpdir import TempPathFactory
from tests.types import FixtureDirGetter
@pytest.fixture
def env(tmp_path: Path) -> MockEnv:
return MockEnv(path=tmp_path)
@pytest.fixture(scope="module")
def demo_wheel(fixture_dir: FixtureDirGetter) -> Path:
return fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl")
@pytest.fixture(scope="module")
def default_installation(tmp_path_factory: TempPathFactory, demo_wheel: Path) -> Path:
env = MockEnv(path=tmp_path_factory.mktemp("default_install"))
installer = WheelInstaller(env)
installer.install(demo_wheel)
return Path(env.paths["purelib"])
def test_default_installation_source_dir_content(default_installation: Path) -> None:
source_dir = default_installation / "demo"
assert source_dir.exists()
assert (source_dir / "__init__.py").exists()
def test_default_installation_dist_info_dir_content(default_installation: Path) -> None:
dist_info_dir = default_installation / "demo-0.1.0.dist-info"
assert dist_info_dir.exists()
assert (dist_info_dir / "INSTALLER").exists()
assert (dist_info_dir / "METADATA").exists()
assert (dist_info_dir / "RECORD").exists()
assert (dist_info_dir / "WHEEL").exists()
def test_installer_file_contains_valid_version(default_installation: Path) -> None:
installer_file = default_installation / "demo-0.1.0.dist-info" / "INSTALLER"
with open(installer_file) as f:
installer_content = f.read()
match = re.match(r"Poetry (?P<version>.*)", installer_content)
assert match
parse_constraint(match.group("version")) # must not raise an error
......@@ -1155,7 +1155,8 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable(
del os.environ["VIRTUAL_ENV"]
version = Version.from_parts(*sys.version_info[:3])
poetry.package.python_versions = f"~{version.major}.{version.minor-1}.0"
poetry.package.python_versions = f"~{version.major}.{version.minor - 1}.0"
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
check_output = mocker.patch(
"subprocess.check_output",
......@@ -1266,6 +1267,7 @@ def test_system_env_has_correct_paths():
assert paths.get("platlib") is not None
assert paths.get("scripts") is not None
assert env.site_packages.path == Path(paths["purelib"])
assert paths["include"] is not None
@pytest.mark.parametrize(
......@@ -1287,6 +1289,11 @@ def test_venv_has_correct_paths(tmp_venv: VirtualEnv):
assert paths.get("platlib") is not None
assert paths.get("scripts") is not None
assert tmp_venv.site_packages.path == Path(paths["purelib"])
assert paths["include"] == str(
tmp_venv.path.joinpath(
f"include/site/python{tmp_venv.version_info[0]}.{tmp_venv.version_info[1]}"
)
)
def test_env_system_packages(tmp_path: Path, poetry: Poetry):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment