Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a wheel installer
Browse files Browse the repository at this point in the history
sdispater committed Aug 23, 2022
1 parent 9568644 commit 8e62d6e
Showing 5 changed files with 132 additions and 5 deletions.
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ xattr = { version = "^0.9.7", markers = "sys_platform == 'darwin'" }
urllib3 = "^1.26.0"
dulwich = "^0.20.44"
build = "^0.8.0"
installer = "^0.5.1"

[tool.poetry.dev-dependencies]
tox = "^3.18"
101 changes: 101 additions & 0 deletions src/poetry/installation/wheel_installer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from __future__ import annotations

import os
import platform
import sys

from pathlib import Path
from typing import TYPE_CHECKING

from installer.destinations import SchemeDictionaryDestination # type: ignore[import]
from installer.sources import WheelFile # type: ignore[import]

from poetry import __version__
from poetry.utils._compat import WINDOWS


if TYPE_CHECKING:
from typing import BinaryIO

from installer.records import RecordEntry # type: ignore[import]
from installer.utils import Scheme # type: ignore[import]

from poetry.utils.env import Env


class WheelDestination(SchemeDictionaryDestination): # type: ignore[misc]
""" """

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 = os.path.join(self.scheme_dict[scheme], path)
if os.path.exists(target_path):
# Contrary to the base library we don't raise an error
# here since it can break namespace packages (like Poetry's)
pass

parent_folder = os.path.dirname(target_path)
if not os.path.exists(parent_folder):
os.makedirs(parent_folder)

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(path, Hash(self.hash_algorithm, hash_), size)

def for_source(self, source: WheelFile) -> WheelDestination:
scheme_dict = self.scheme_dict.copy()

scheme_dict["headers"] = os.path.join(
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

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:
from installer import install # type: ignore[import]

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(),
},
)
13 changes: 10 additions & 3 deletions src/poetry/utils/env.py
Original file line number Diff line number Diff line change
@@ -58,7 +58,6 @@

from poetry.poetry import Poetry


GET_SYS_TAGS = f"""
import importlib.util
import json
@@ -83,7 +82,6 @@
)
"""


GET_ENVIRONMENT_INFO = """\
import json
import os
@@ -154,7 +152,6 @@ def _version_nodot(version):
print(json.dumps(env))
"""


GET_BASE_PREFIX = """\
import sys
@@ -1362,6 +1359,16 @@ def paths(self) -> dict[str, str]:
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
8 changes: 7 additions & 1 deletion tests/utils/test_env.py
Original file line number Diff line number Diff line change
@@ -1017,7 +1017,7 @@ 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(
@@ -1131,6 +1131,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(
@@ -1152,6 +1153,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):

0 comments on commit 8e62d6e

Please sign in to comment.