diff --git a/poetry.lock b/poetry.lock
index 02dc2635f97..8f8122c985c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -28,6 +28,27 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+[[package]]
+name = "build"
+version = "0.4.0"
+description = "A simple, correct PEP517 package builder"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "os_name == \"nt\""}
+importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""}
+packaging = ">=19.0"
+pep517 = ">=0.9.1"
+toml = ">=0.10.0"
+
+[package.extras]
+docs = ["furo (>=2020.11.19b18)", "sphinx (>=3.0,<4.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"]
+test = ["filelock (>=3)", "pytest (>=4)", "pytest-cov (>=2)", "pytest-mock (>=2)", "pytest-xdist (>=1.34)"]
+typing = ["mypy (==0.800)", "typing-extensions (>=3.7.4.3)"]
+virtualenv = ["virtualenv (>=20.0.35)"]
+
[[package]]
name = "cachecontrol"
version = "0.12.6"
@@ -112,7 +133,7 @@ pylev = ">=1.3.0,<2.0.0"
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@@ -283,6 +304,17 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "installer"
+version = "0.2.3"
+description = "A library for installing Python wheels."
+category = "main"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
+[package.dependencies]
+importlib-resources = {version = "*", markers = "python_version < \"3.7\""}
+
[[package]]
name = "jeepney"
version = "0.7.1"
@@ -356,6 +388,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
+[[package]]
+name = "pep517"
+version = "0.10.0"
+description = "Wrappers to build Python packages using PEP 517 hooks"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+importlib_metadata = {version = "*", markers = "python_version < \"3.8\""}
+toml = "*"
+zipp = {version = "*", markers = "python_version < \"3.8\""}
+
[[package]]
name = "pexpect"
version = "4.8.0"
@@ -612,7 +657,7 @@ python-versions = "*"
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
@@ -703,7 +748,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6"
-content-hash = "458035eaa19225869083c98c699e753de1844dd82030447634836e2f79255df1"
+content-hash = "575f9d8b6c311a8238cf5fe5e665e0b7101addd1a3dd26813870165d41e871f8"
[metadata.files]
appdirs = [
@@ -718,6 +763,10 @@ attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
+build = [
+ {file = "build-0.4.0-py2.py3-none-any.whl", hash = "sha256:5950f98775a59f0c5ac68586691003d2db58a809fbea2ade3fe32109dfd12790"},
+ {file = "build-0.4.0.tar.gz", hash = "sha256:b798f3f490c779fa88c99816ebee97ab636acd6630b1d91c8cf8eb8a4d922a19"},
+]
cachecontrol = [
{file = "CacheControl-0.12.6-py2.py3-none-any.whl", hash = "sha256:10d056fa27f8563a271b345207402a6dcce8efab7e5b377e270329c62471b10d"},
{file = "CacheControl-0.12.6.tar.gz", hash = "sha256:be9aa45477a134aee56c8fac518627e1154df063e85f67d4f83ce0ccc23688e8"},
@@ -912,6 +961,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
+installer = [
+ {file = "installer-0.2.3-py2.py3-none-any.whl", hash = "sha256:e649b9c7a454708a33a39db69f8daee515c9345020ab2fb2299d6639aad65711"},
+ {file = "installer-0.2.3.tar.gz", hash = "sha256:82c899f5e3c78303242df9c9ca7ac58001c9806d8c23fa2772be769d1f560fe5"},
+]
jeepney = [
{file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
{file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
@@ -965,6 +1018,10 @@ packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
]
+pep517 = [
+ {file = "pep517-0.10.0-py2.py3-none-any.whl", hash = "sha256:eba39d201ef937584ad3343df3581069085bacc95454c80188291d5b3ac7a249"},
+ {file = "pep517-0.10.0.tar.gz", hash = "sha256:ac59f3f6b9726a49e15a649474539442cf76e0697e39df4869d25e68e880931b"},
+]
pexpect = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
diff --git a/poetry/console/commands/source/add.py b/poetry/console/commands/source/add.py
index 1b097076b84..be83cb6df15 100644
--- a/poetry/console/commands/source/add.py
+++ b/poetry/console/commands/source/add.py
@@ -11,7 +11,6 @@
from poetry.config.source import Source
from poetry.console.commands.command import Command
from poetry.factory import Factory
-from poetry.repositories import Pool
class SourceAddCommand(Command):
@@ -89,13 +88,14 @@ def handle(self) -> Optional[int]:
self.line(f"Adding source with name {name}.")
sources.append(self.source_to_table(new_source))
+ self.poetry.config.merge(
+ {"sources": {source["name"]: source for source in sources}}
+ )
+
# ensure new source is valid. eg: invalid name etc.
- self.poetry._pool = Pool()
try:
- Factory.configure_sources(
- self.poetry, sources, self.poetry.config, NullIO()
- )
- self.poetry.pool.repository(name)
+ pool = Factory.create_pool(self.poetry.config, NullIO())
+ pool.repository(name)
except ValueError as e:
self.line_error(
f"Failed to validate addition of {name}: {e}"
diff --git a/poetry/factory.py b/poetry/factory.py
index 144bfe35871..afefabb99f9 100644
--- a/poetry/factory.py
+++ b/poetry/factory.py
@@ -4,7 +4,6 @@
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Dict
-from typing import List
from typing import Optional
from cleo.io.io import IO
@@ -25,6 +24,7 @@
if TYPE_CHECKING:
from .repositories.legacy_repository import LegacyRepository
+ from .repositories.pool import Pool
class Factory(BaseFactory):
@@ -80,11 +80,18 @@ def create_poetry(
config,
)
- # Configuring sources
- self.configure_sources(
- poetry, poetry.local_config.get("source", []), config, io
+ config.merge(
+ {
+ "sources": {
+ source["name"]: source
+ for source in poetry.local_config.get("source", [])
+ }
+ }
)
+ # Configuring sources
+ poetry.set_pool(self.create_pool(config, io))
+
plugin_manager = PluginManager("plugin", disable_plugins=disable_plugins)
plugin_manager.load_plugins()
poetry.set_plugin_manager(plugin_manager)
@@ -133,10 +140,15 @@ def create_config(cls, io: Optional[IO] = None) -> Config:
return config
@classmethod
- def configure_sources(
- cls, poetry: "Poetry", sources: List[Dict[str, str]], config: "Config", io: "IO"
- ) -> None:
- for source in sources:
+ def create_pool(cls, config: "Config", io: Optional["IO"] = None) -> "Pool":
+ from poetry.repositories.pool import Pool
+
+ if io is None:
+ io = NullIO()
+
+ pool = Pool()
+
+ for source_name, source in config.get("sources").items():
repository = cls.create_legacy_repository(source, config)
is_default = source.get("default", False)
is_secondary = source.get("secondary", False)
@@ -151,17 +163,19 @@ def configure_sources(
io.write_line(message)
- poetry.pool.add_repository(repository, is_default, secondary=is_secondary)
+ pool.add_repository(repository, is_default, secondary=is_secondary)
# Put PyPI last to prefer private repositories
# unless we have no default source AND no primary sources
# (default = false, secondary = false)
- if poetry.pool.has_default():
+ if pool.has_default():
if io.is_debug():
io.write_line("Deactivating the PyPI repository")
else:
- default = not poetry.pool.has_primary_repositories()
- poetry.pool.add_repository(PyPiRepository(), default, not default)
+ default = not pool.has_primary_repositories()
+ pool.add_repository(PyPiRepository(), default, not default)
+
+ return pool
@classmethod
def create_legacy_repository(
diff --git a/poetry/installation/chef.py b/poetry/installation/chef.py
index ce095a03d5c..87794382ac5 100644
--- a/poetry/installation/chef.py
+++ b/poetry/installation/chef.py
@@ -1,23 +1,77 @@
import hashlib
import json
+import tarfile
+import tempfile
+import threading
+import zipfile
+from contextlib import redirect_stdout
+from io import StringIO
from pathlib import Path
from typing import TYPE_CHECKING
from typing import List
from typing import Optional
+from build import ProjectBuilder
+from build.env import IsolatedEnv as BaseIsolatedEnv
+from pep517.wrappers import quiet_subprocess_runner
+
from poetry.core.packages.utils.link import Link
+from ..utils.helpers import temporary_directory
from .chooser import InvalidWheelName
from .chooser import Wheel
if TYPE_CHECKING:
-
from poetry.config.config import Config
from poetry.utils.env import Env
+class IsolatedEnv(BaseIsolatedEnv):
+ def __init__(self, env: "Env", config: "Config") -> None:
+ self._env = env
+ self._config = config
+
+ @property
+ def executable(self) -> str:
+ return str(self._env.python)
+
+ @property
+ def scripts_dir(self) -> str:
+ return str(self._env._bin_dir)
+
+ def install(self, requirements) -> None:
+ from cleo.io.null_io import NullIO
+
+ from poetry.core.packages.dependency import Dependency
+ from poetry.core.packages.project_package import ProjectPackage
+ from poetry.factory import Factory
+ from poetry.installation.installer import Installer
+ from poetry.packages.locker import NullLocker
+ from poetry.repositories.installed_repository import InstalledRepository
+
+ # We build Poetry dependencies from the requirements
+ package = ProjectPackage("__root__", "0.0.0")
+ package.python_versions = ".".join(str(v) for v in self._env.version_info[:3])
+ for requirement in requirements:
+ dependency = Dependency.create_from_pep_508(requirement)
+ package.add_dependency(dependency)
+
+ pool = Factory.create_pool(self._config)
+ installer = Installer(
+ NullIO(),
+ self._env,
+ package,
+ NullLocker(self._env.path.joinpath("poetry.lock"), {}),
+ pool,
+ Factory.create_config(NullIO()),
+ InstalledRepository.load(self._env),
+ )
+ installer.update(True)
+ installer.run()
+
+
class Chef:
def __init__(self, config: "Config", env: "Env") -> None:
self._config = config
@@ -25,27 +79,91 @@ def __init__(self, config: "Config", env: "Env") -> None:
self._cache_dir = (
Path(config.get("cache-dir")).expanduser().joinpath("artifacts")
)
+ self._lock = threading.Lock()
+
+ def prepare(self, archive: Path, output_dir: Optional[Path] = None) -> Path:
+ if not self.should_prepare(archive):
+ return archive
+
+ if archive.is_dir():
+ tmp_dir = tempfile.mkdtemp(prefix="poetry-chef-")
+
+ return self._prepare(archive, Path(tmp_dir))
- def prepare(self, archive: Path) -> Path:
- return archive
+ return self._prepare_sdist(archive, destination=output_dir)
- def prepare_sdist(self, archive: Path) -> Path:
- return archive
+ def _prepare_sdist(self, archive: Path, destination: Optional[Path] = None) -> Path:
+ suffix = archive.suffix
- def prepare_wheel(self, archive: Path) -> Path:
- return archive
+ if suffix == ".zip":
+ context = zipfile.ZipFile
+ else:
+ context = tarfile.open
+
+ with temporary_directory() as archive_dir:
+ with context(archive.as_posix()) as archive_archive:
+ archive_archive.extractall(archive_dir)
+
+ archive_dir = Path(archive_dir)
+
+ elements = list(archive_dir.glob("*"))
+
+ if len(elements) == 1 and elements[0].is_dir():
+ sdist_dir = elements[0]
+ else:
+ sdist_dir = archive_dir / archive.name.rstrip(suffix)
+ if not sdist_dir.is_dir():
+ sdist_dir = archive_dir
+
+ if destination is None:
+ destination = self.get_cache_directory_for_link(Link(archive.as_uri()))
+
+ destination.mkdir(parents=True, exist_ok=True)
+
+ return self._prepare(
+ sdist_dir,
+ destination,
+ )
+
+ def _prepare(self, directory: Path, destination: Path) -> Path:
+ from poetry.utils.env import EnvManager
+ from poetry.utils.env import VirtualEnv
+
+ with self._lock:
+ with temporary_directory() as tmp_dir:
+ EnvManager.build_venv(
+ tmp_dir, executable=self._env.python, with_pip=True
+ )
+ venv = VirtualEnv(Path(tmp_dir))
+ env = IsolatedEnv(venv, self._config)
+ builder = ProjectBuilder(
+ directory,
+ python_executable=env.executable,
+ scripts_dir=env.scripts_dir,
+ runner=quiet_subprocess_runner,
+ )
+ env.install(builder.build_system_requires)
+ env.install(
+ builder.build_system_requires
+ | builder.get_requires_for_build("wheel")
+ )
+
+ stdout = StringIO()
+ with redirect_stdout(stdout):
+ return Path(
+ builder.build(
+ "wheel",
+ destination.as_posix(),
+ )
+ )
def should_prepare(self, archive: Path) -> bool:
- return not self.is_wheel(archive)
+ return archive.is_dir() or not self.is_wheel(archive)
def is_wheel(self, archive: Path) -> bool:
return archive.suffix == ".whl"
def get_cached_archive_for_link(self, link: Link) -> Optional[Link]:
- # If the archive is already a wheel, there is no need to cache it.
- if link.is_wheel:
- pass
-
archives = self.get_cached_archives_for_link(link)
if not archives:
diff --git a/poetry/installation/executor.py b/poetry/installation/executor.py
index 00b709da2aa..df48ded8fff 100644
--- a/poetry/installation/executor.py
+++ b/poetry/installation/executor.py
@@ -18,6 +18,7 @@
from poetry.core.packages.file_dependency import FileDependency
from poetry.core.packages.utils.link import Link
+from poetry.core.packages.utils.utils import url_to_path
from poetry.core.pyproject.toml import PyProjectTOML
from poetry.utils._compat import decode
from poetry.utils.env import EnvCommandError
@@ -32,6 +33,7 @@
from .operations.operation import Operation
from .operations.uninstall import Uninstall
from .operations.update import Update
+from .wheel_installer import WheelInstaller
if TYPE_CHECKING:
@@ -62,6 +64,7 @@ def __init__(
self._authenticator = Authenticator(config, self._io)
self._chef = Chef(config, self._env)
self._chooser = Chooser(pool, self._env)
+ self._wheel_installer = WheelInstaller(self._env)
if parallel is None:
parallel = config.get("installer.parallel", True)
@@ -100,6 +103,11 @@ def updates_count(self) -> int:
def removals_count(self) -> int:
return self._executed["uninstall"]
+ def set_chef(self, chef: "Chef") -> "Executor":
+ self._chef = chef
+
+ return self
+
def supports_fancy_output(self) -> bool:
return self._io.output.is_decorated() and not self._dry_run
@@ -480,7 +488,7 @@ def _execute_uninstall(self, operation: Uninstall) -> int:
)
self._write(operation, message)
- return self._remove(operation)
+ return self._remove(operation.package)
def _install(self, operation: Union[Install, Update]) -> int:
package = operation.package
@@ -491,7 +499,7 @@ def _install(self, operation: Union[Install, Update]) -> int:
return self._install_git(operation)
if package.source_type == "file":
- archive = self._prepare_file(operation)
+ archive = self._prepare_archive(operation)
elif package.source_type == "url":
archive = self._download_link(operation, Link(package.source_url))
else:
@@ -504,14 +512,15 @@ def _install(self, operation: Union[Install, Update]) -> int:
)
)
self._write(operation, message)
- return self.pip_install(str(archive), upgrade=operation.job_type == "update")
+
+ self._wheel_installer.install(archive)
+
+ return 0
def _update(self, operation: Union[Install, Update]) -> int:
return self._install(operation)
- def _remove(self, operation: Uninstall) -> int:
- package = operation.package
-
+ def _remove(self, package: "Package") -> int:
# If we have a VCS package, remove its source directory
if package.source_type == "git":
src_dir = self._env.path / "src" / package.name
@@ -526,7 +535,7 @@ def _remove(self, operation: Uninstall) -> int:
raise
- def _prepare_file(self, operation: Union[Install, Update]) -> Path:
+ def _prepare_archive(self, operation: Union[Install, Update]) -> Path:
package = operation.package
message = (
@@ -564,18 +573,10 @@ def _install_directory(self, operation: Union[Install, Update]) -> int:
pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml"))
- if pyproject.is_poetry_project():
- # Even if there is a build system specified
- # some versions of pip (< 19.0.0) don't understand it
- # so we need to check the version of pip to know
- # if we can rely on the build system
- legacy_pip = (
- self._env.pip_version
- < self._env.pip_version.__class__.from_parts(19, 0, 0)
- )
+ if pyproject.is_poetry_project() and package.develop:
package_poetry = Factory().create_poetry(pyproject.file.path.parent)
- if package.develop and not package_poetry.package.build_script:
+ if package.develop:
from poetry.masonry.builders.editable import EditableBuilder
# This is a Poetry package in editable mode
@@ -585,24 +586,23 @@ def _install_directory(self, operation: Union[Install, Update]) -> int:
builder.build()
return 0
- elif legacy_pip or package_poetry.package.build_script:
- from poetry.core.masonry.builders.sdist import SdistBuilder
-
- # We need to rely on creating a temporary setup.py
- # file since the version of pip does not support
- # build-systems
- # We also need it for non-PEP-517 packages
- builder = SdistBuilder(package_poetry)
+ elif package.develop:
+ # Editable installations are currently not supported
+ # for PEP-517 build systems so we defer to pip.
+ # TODO: Remove this workaround once either PEP-660 or PEP-662 is accepted
+ return self.pip_install(req, editable=True)
- with builder.setup_py():
- if package.develop:
- return self.pip_install(req, editable=True)
- return self.pip_install(req, upgrade=True)
+ archive = self._prepare_archive(operation)
- if package.develop:
- return self.pip_install(req, editable=True)
+ try:
+ if operation.job_type == "update":
+ # Uninstall first
+ # TODO: Make an uninstaller and find a way to rollback in case the new package can't be installed
+ self._remove(operation.initial_package)
- return self.pip_install(req, upgrade=True)
+ self._wheel_installer.install(archive)
+ finally:
+ archive.unlink()
def _install_git(self, operation: Union[Install, Update]) -> int:
from poetry.core.vcs import Git
@@ -650,27 +650,33 @@ def _download(self, operation: Union[Install, Update]) -> Link:
def _download_link(self, operation: Union[Install, Update], link: Link) -> Link:
package = operation.package
- archive = self._chef.get_cached_archive_for_link(link)
- if archive is link:
+ output_dir = self._chef.get_cache_directory_for_link(link)
+ archive_link = self._chef.get_cached_archive_for_link(link)
+ if archive_link is link:
# No cached distributions was found, so we download and prepare it
try:
archive = self._download_archive(operation, link)
except BaseException:
cache_directory = self._chef.get_cache_directory_for_link(link)
cached_file = cache_directory.joinpath(link.filename)
- # We can't use unlink(missing_ok=True) because it's not available
- # in pathlib2 for Python 2.7
if cached_file.exists():
cached_file.unlink()
raise
# TODO: Check readability of the created archive
+ else:
+ archive = Path(url_to_path(archive_link.url))
+
+ if not archive.suffix == ".whl":
+ message = " •> {message}: Preparing...".format(
+ message=self.get_operation_message(operation),
+ )
+ self._write(operation, message)
- if not link.is_wheel:
- archive = self._chef.prepare(archive)
+ archive = self._chef.prepare(archive, output_dir=output_dir)
- if package.files:
+ if package.files and archive.name in {f["file"] for f in package.files}:
archive_hash = (
"sha256:"
+ FileDependency(
diff --git a/poetry/installation/wheel_installer.py b/poetry/installation/wheel_installer.py
new file mode 100644
index 00000000000..115716d262f
--- /dev/null
+++ b/poetry/installation/wheel_installer.py
@@ -0,0 +1,117 @@
+import os
+import platform
+import sys
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+from typing import Iterator
+from typing import Tuple
+from typing import Union
+from typing import cast
+
+from installer.destinations import SchemeDictionaryDestination as BaseDestination
+from installer.records import parse_record_file
+from installer.sources import WheelFile as BaseWheelFile
+
+from poetry import __version__
+from poetry.utils._compat import WINDOWS
+
+
+if TYPE_CHECKING:
+ from typing import BinaryIO
+
+ from installer.records import RecordEntry
+ from installer.utils import Scheme
+
+ from poetry.utils.env import Env
+
+
+class WheelFile(BaseWheelFile):
+ def get_contents(
+ self,
+ ) -> Iterator[Tuple[Tuple[Union[Path, str], str, str], "BinaryIO"]]:
+ record_lines = self.read_dist_info("RECORD").splitlines()
+ records = parse_record_file(record_lines)
+ record_mapping = {record[0]: record for record in records}
+
+ for item in self._zipfile.infolist():
+ if item.is_dir():
+ continue
+
+ record = record_mapping.pop(item.filename)
+ assert record is not None, "In {}, {} is not mentioned in RECORD".format(
+ self._zipfile.filename,
+ item.filename,
+ ) # should not happen for valid wheels
+
+ with self._zipfile.open(item) as stream:
+ stream_casted = cast("BinaryIO", stream)
+ yield record, stream_casted
+
+
+class WheelDestination(BaseDestination):
+ def write_to_fs(
+ self, scheme: "Scheme", path: Union[Path, str], stream: "BinaryIO"
+ ) -> "RecordEntry":
+ from installer.records import Hash
+ from installer.records import RecordEntry
+ from installer.utils import copyfileobj_with_hashing
+
+ 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)
+
+ 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
+
+ with WheelFile.open(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(),
+ },
+ )
diff --git a/poetry/repositories/installed_repository.py b/poetry/repositories/installed_repository.py
index e91e0bd3c9e..2c9f2101097 100644
--- a/poetry/repositories/installed_repository.py
+++ b/poetry/repositories/installed_repository.py
@@ -2,6 +2,8 @@
import json
from pathlib import Path
+from typing import Dict
+from typing import Optional
from typing import Set
from typing import Tuple
from typing import Union
@@ -26,6 +28,9 @@
class InstalledRepository(Repository):
+
+ _distributions: Dict[str, metadata.Distribution] = {}
+
@classmethod
def get_package_paths(cls, env: Env, name: str) -> Set[Path]:
"""
@@ -258,5 +263,9 @@ def load(cls, env: Env, with_dependencies: bool = False) -> "InstalledRepository
seen.add(package.name)
repo.add_package(package)
+ repo._distributions[package.name] = distribution
return repo
+
+ def distribution(self, name: str) -> Optional[metadata.Distribution]:
+ return self._distributions.get(name)
diff --git a/poetry/utils/env.py b/poetry/utils/env.py
index 35493ec6f58..d1f55f1b856 100644
--- a/poetry/utils/env.py
+++ b/poetry/utils/env.py
@@ -1197,6 +1197,18 @@ 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",
+ "python{}.{}".format(
+ self.version_info[0], self.version_info[1]
+ ),
+ )
+ )
+
return self._paths
@property
diff --git a/pyproject.toml b/pyproject.toml
index a049e1f8369..a2040461b33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,8 @@ keyring = ">=21.2.0"
entrypoints = "^0.3"
importlib-metadata = {version = "^1.6.0", python = "<3.8"}
dataclasses = {version = "^0.8", python = "~3.6"}
+build = "^0.4.0"
+installer = "^0.2.3"
[tool.poetry.dev-dependencies]
pytest = "^6.2"
diff --git a/tests/console/commands/plugin/conftest.py b/tests/console/commands/plugin/conftest.py
index 84509c81172..b916c73b159 100644
--- a/tests/console/commands/plugin/conftest.py
+++ b/tests/console/commands/plugin/conftest.py
@@ -17,19 +17,18 @@ def installed():
return repository
-def configure_sources_factory(repo):
- def _configure_sources(poetry, sources, config, io): # noqa
+def create_pool_factory(repo):
+ def _create_pool(config, io): # noqa
pool = Pool()
pool.add_repository(repo)
- poetry.set_pool(pool)
- return _configure_sources
+ return pool
+
+ return _create_pool
@pytest.fixture(autouse=True)
def setup_mocks(mocker, env, repo, installed):
mocker.patch.object(EnvManager, "get_system_env", return_value=env)
mocker.patch.object(InstalledRepository, "load", return_value=installed)
- mocker.patch.object(
- Factory, "configure_sources", side_effect=configure_sources_factory(repo)
- )
+ mocker.patch.object(Factory, "create_pool", side_effect=create_pool_factory(repo))
diff --git a/tests/console/commands/plugin/test_add.py b/tests/console/commands/plugin/test_add.py
index 88c02812afd..602a8ba4545 100644
--- a/tests/console/commands/plugin/test_add.py
+++ b/tests/console/commands/plugin/test_add.py
@@ -2,6 +2,22 @@
from poetry.core.packages.package import Package
from poetry.factory import Factory
+from poetry.installation.chef import Chef
+from poetry.installation.executor import Executor
+from poetry.installation.wheel_installer import WheelInstaller
+
+
+@pytest.fixture()
+def setup(mocker, fixture_dir):
+ mocker.patch.object(
+ Executor,
+ "_download",
+ return_value=fixture_dir("distributions").joinpath(
+ "demo-0.1.2-py2.py3-none-any.whl"
+ ),
+ )
+
+ mocker.patch.object(WheelInstaller, "install")
@pytest.fixture()
@@ -61,7 +77,9 @@ def test_add_with_constraint(app, repo, tester, env, installed):
assert_plugin_add_result(tester, app, env, expected, "^0.2.0")
-def test_add_with_git_constraint(app, repo, tester, env, installed):
+def test_add_with_git_constraint(app, repo, tester, env, installed, mocker):
+ mocker.patch.object(Chef, "_prepare")
+
repo.add_package(Package("pendulum", "2.0.5"))
tester.execute("git+https://github.com/demo/poetry-plugin.git")
@@ -83,7 +101,9 @@ def test_add_with_git_constraint(app, repo, tester, env, installed):
)
-def test_add_with_git_constraint_with_extras(app, repo, tester, env, installed):
+def test_add_with_git_constraint_with_extras(app, repo, tester, env, installed, mocker):
+ mocker.patch.object(Chef, "_prepare")
+
repo.add_package(Package("pendulum", "2.0.5"))
repo.add_package(Package("tomlkit", "0.7.0"))
diff --git a/tests/fixtures/distributions/demo-0.1.0.tar.gz b/tests/fixtures/distributions/demo-0.1.0.tar.gz
index 133b64421f8..42e4933130e 100644
Binary files a/tests/fixtures/distributions/demo-0.1.0.tar.gz and b/tests/fixtures/distributions/demo-0.1.0.tar.gz differ
diff --git a/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl
new file mode 100644
index 00000000000..a01175c144e
Binary files /dev/null and b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl differ
diff --git a/tests/fixtures/extended_with_no_setup/README.rst b/tests/fixtures/extended_with_no_setup/README.rst
new file mode 100644
index 00000000000..a7508bd515e
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/README.rst
@@ -0,0 +1,2 @@
+Module 1
+========
diff --git a/tests/fixtures/extended_with_no_setup/build.py b/tests/fixtures/extended_with_no_setup/build.py
new file mode 100644
index 00000000000..4f1fee59574
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/build.py
@@ -0,0 +1,30 @@
+import os
+import shutil
+
+from distutils.command.build_ext import build_ext
+from distutils.core import Distribution
+from distutils.core import Extension
+
+
+extensions = [Extension("extended.extended", ["extended/extended.c"])]
+
+
+def build():
+ distribution = Distribution({"name": "extended", "ext_modules": extensions})
+ distribution.package_dir = "extended"
+
+ cmd = build_ext(distribution)
+ cmd.ensure_finalized()
+ cmd.run()
+
+ # Copy built extensions back to the project
+ for output in cmd.get_outputs():
+ relative_extension = os.path.relpath(output, cmd.build_lib)
+ shutil.copyfile(output, relative_extension)
+ mode = os.stat(relative_extension).st_mode
+ mode |= (mode & 0o444) >> 2
+ os.chmod(relative_extension, mode)
+
+
+if __name__ == "__main__":
+ build()
diff --git a/tests/fixtures/extended_with_no_setup/extended/__init__.py b/tests/fixtures/extended_with_no_setup/extended/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/fixtures/extended_with_no_setup/extended/extended.c b/tests/fixtures/extended_with_no_setup/extended/extended.c
new file mode 100644
index 00000000000..25a028eb11e
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/extended/extended.c
@@ -0,0 +1,58 @@
+#include
+
+
+static PyObject *hello(PyObject *self) {
+ return PyUnicode_FromString("Hello");
+}
+
+
+static PyMethodDef module_methods[] = {
+ {
+ "hello",
+ (PyCFunction) hello,
+ NULL,
+ PyDoc_STR("Say hello.")
+ },
+ {NULL}
+};
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "extended",
+ NULL,
+ -1,
+ module_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+};
+#endif
+
+PyMODINIT_FUNC
+#if PY_MAJOR_VERSION >= 3
+PyInit_extended(void)
+#else
+init_extended(void)
+#endif
+{
+ PyObject *module;
+
+#if PY_MAJOR_VERSION >= 3
+ module = PyModule_Create(&moduledef);
+#else
+ module = Py_InitModule3("extended", module_methods, NULL);
+#endif
+
+ if (module == NULL)
+#if PY_MAJOR_VERSION >= 3
+ return NULL;
+#else
+ return;
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ return module;
+#endif
+}
diff --git a/tests/fixtures/extended_with_no_setup/pyproject.toml b/tests/fixtures/extended_with_no_setup/pyproject.toml
new file mode 100644
index 00000000000..cebd29eabbb
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/pyproject.toml
@@ -0,0 +1,26 @@
+[tool.poetry]
+name = "extended"
+version = "0.1"
+description = "Some description."
+authors = [
+ "Sébastien Eustace "
+]
+license = "MIT"
+
+readme = "README.rst"
+
+homepage = "https://python-poetry.org/"
+
+include = [
+ # C extensions must be included in the wheel distributions
+ {path = "extended/*.so", format = "wheel"},
+ {path = "extended/*.pyd", format = "wheel"},
+]
+
+[tool.poetry.build]
+script = "build.py"
+generate-setup-file = false
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py
index 332d9c4794f..136081b131b 100644
--- a/tests/installation/test_chef.py
+++ b/tests/installation/test_chef.py
@@ -1,10 +1,30 @@
from pathlib import Path
+import pytest
+
from packaging.tags import Tag
from poetry.core.packages.utils.link import Link
+from poetry.factory import Factory
from poetry.installation.chef import Chef
+from poetry.repositories import Pool
+from poetry.utils.env import EnvManager
from poetry.utils.env import MockEnv
+from tests.repositories.test_pypi_repository import MockRepository
+
+
+@pytest.fixture()
+def pool():
+ pool = Pool()
+
+ pool.add_repository(MockRepository())
+
+ return pool
+
+
+@pytest.fixture(autouse=True)
+def setup(mocker, pool):
+ mocker.patch.object(Factory, "create_pool", return_value=pool)
def test_get_cached_archive_for_link(config, mocker):
@@ -59,7 +79,7 @@ def test_get_cached_archives_for_link(config, mocker):
assert archives
assert set(archives) == {
- Link(path.as_uri()) for path in distributions.glob("demo-0.1.0*")
+ Link(path.as_uri()) for path in distributions.glob("demo-0.1.*")
}
@@ -82,3 +102,45 @@ def test_get_cache_directory_for_link(config, config_cache_dir):
)
assert expected == directory
+
+
+def test_prepare_sdist(config, config_cache_dir):
+ chef = Chef(config, EnvManager.get_system_env())
+
+ archive = (
+ Path(__file__)
+ .parent.parent.joinpath("fixtures/distributions/demo-0.1.0.tar.gz")
+ .resolve()
+ )
+
+ destination = chef.get_cache_directory_for_link(Link(archive.as_uri()))
+
+ wheel = chef.prepare(archive)
+
+ assert wheel.parent == destination
+ assert wheel.name == "demo-0.1.0-py2.py3-none-any.whl"
+
+
+def test_prepare_directory(config, config_cache_dir):
+ chef = Chef(config, EnvManager.get_system_env())
+
+ archive = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve()
+
+ wheel = chef.prepare(archive)
+
+ assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl"
+
+
+def test_prepare_directory_with_extensions(config, config_cache_dir):
+ env = EnvManager.get_system_env()
+ chef = Chef(config, env)
+
+ archive = (
+ Path(__file__)
+ .parent.parent.joinpath("fixtures/extended_with_no_setup")
+ .resolve()
+ )
+
+ wheel = chef.prepare(archive)
+
+ assert wheel.name == "extended-0.1-{}.whl".format(env.supported_tags[0])
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 9b7b413b92b..2ab117c9a87 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -6,6 +6,7 @@
import shutil
from pathlib import Path
+from urllib.parse import urlparse
import pytest
@@ -15,15 +16,41 @@
from poetry.config.config import Config
from poetry.core.packages.package import Package
from poetry.core.utils._compat import PY36
+from poetry.installation.chef import Chef as BaseChef
from poetry.installation.executor import Executor
from poetry.installation.operations import Install
from poetry.installation.operations import Uninstall
from poetry.installation.operations import Update
+from poetry.installation.wheel_installer import WheelInstaller
from poetry.repositories.pool import Pool
from poetry.utils.env import MockEnv
from tests.repositories.test_pypi_repository import MockRepository
+class Chef(BaseChef):
+
+ _directory_wheel = None
+ _sdist_wheel = None
+
+ def set_directory_wheel(self, wheel: Path) -> None:
+ self._directory_wheel = wheel
+
+ def set_sdist_wheel(self, wheel: Path) -> None:
+ self._sdist_wheel = wheel
+
+ def _prepare_sdist(self, archive: Path) -> Path:
+ if self._sdist_wheel is not None:
+ return self._sdist_wheel
+
+ return super()._prepare_sdist()
+
+ def _prepare(self, directory: Path, destination: Path) -> Path:
+ if self._directory_wheel is not None:
+ return self._directory_wheel
+
+ return super()._prepare(directory, destination)
+
+
@pytest.fixture
def env(tmp_dir):
path = Path(tmp_dir) / ".venv"
@@ -70,9 +97,15 @@ def pool():
@pytest.fixture()
def mock_file_downloads(http):
def callback(request, uri, headers):
+ name = Path(urlparse(uri).path).name
+
fixture = Path(__file__).parent.parent.joinpath(
- "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
+ "repositories/fixtures/pypi.org/dists/" + name
)
+ if not fixture.exists():
+ fixture = Path(__file__).parent.parent.joinpath(
+ "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
+ )
with fixture.open("rb") as f:
return [200, headers, f.read()]
@@ -84,17 +117,37 @@ def callback(request, uri, headers):
)
+@pytest.fixture()
+def wheel(tmp_dir):
+ shutil.copyfile(
+ Path(__file__)
+ .parent.parent.joinpath(
+ "fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl"
+ )
+ .as_posix(),
+ Path(tmp_dir).joinpath("demo-0.1.2-py2.py3-none-any.whl").as_posix(),
+ )
+
+ return Path(tmp_dir).joinpath("demo-0.1.2-py2.py3-none-any.whl")
+
+
def test_execute_executes_a_batch_of_operations(
- mocker, config, pool, io, tmp_dir, mock_file_downloads, env
+ mocker, config, pool, io, tmp_dir, mock_file_downloads, env, wheel
):
pip_editable_install = mocker.patch(
"poetry.installation.executor.pip_editable_install", unsafe=not PY36
)
+ wheel_install = mocker.patch.object(WheelInstaller, "install")
config = Config()
config.merge({"cache-dir": tmp_dir})
+ chef = Chef(config, env)
+ chef.set_directory_wheel(wheel)
+ chef.set_sdist_wheel(wheel)
+
executor = Executor(env, pool, config, io)
+ executor.set_chef(chef)
file_package = Package(
"demo",
@@ -155,9 +208,11 @@ def test_execute_executes_a_batch_of_operations(
expected = set(expected.splitlines())
output = set(io.fetch_output().splitlines())
assert expected == output
- assert 5 == len(env.executed)
+ # One pip uninstall command
+ assert 1 == len(env.executed)
assert 0 == return_code
pip_editable_install.assert_called_once()
+ assert wheel_install.call_count == 4
def test_execute_shows_skipped_operations_if_verbose(
@@ -204,30 +259,25 @@ def test_execute_should_show_errors(config, mocker, io, env):
def test_execute_works_with_ansi_output(
- mocker, config, pool, io_decorated, tmp_dir, mock_file_downloads, env
+ config, pool, io_decorated, tmp_dir, mock_file_downloads, env, wheel
):
config = Config()
config.merge({"cache-dir": tmp_dir})
executor = Executor(env, pool, config, io_decorated)
- install_output = (
- "some string that does not contain a keyb0ard !nterrupt or cance11ed by u$er"
- )
- mocker.patch.object(env, "_run", return_value=install_output)
return_code = executor.execute(
[
- Install(Package("pytest", "3.5.2")),
+ Install(Package("clikit", "0.2.4")),
]
)
- env._run.assert_called_once()
expected = [
"\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals",
- "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m",
- "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m",
- "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m",
- "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.2\x1b[39m\x1b[39m)\x1b[39m", # finished
+ "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m0.2.4\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m",
+ "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m0.2.4\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m",
+ "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m0.2.4\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m",
+ "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[32m0.2.4\x1b[39m\x1b[39m)\x1b[39m", # finished
]
output = io_decorated.fetch_output()
# hint: use print(repr(output)) if you need to debug this
@@ -238,28 +288,23 @@ def test_execute_works_with_ansi_output(
def test_execute_works_with_no_ansi_output(
- mocker, config, pool, io_not_decorated, tmp_dir, mock_file_downloads, env
+ config, pool, io_not_decorated, tmp_dir, mock_file_downloads, env
):
config = Config()
config.merge({"cache-dir": tmp_dir})
executor = Executor(env, pool, config, io_not_decorated)
- install_output = (
- "some string that does not contain a keyb0ard !nterrupt or cance11ed by u$er"
- )
- mocker.patch.object(env, "_run", return_value=install_output)
return_code = executor.execute(
[
- Install(Package("pytest", "3.5.2")),
+ Install(Package("clikit", "0.2.4")),
]
)
- env._run.assert_called_once()
expected = """
Package operations: 1 install, 0 updates, 0 removals
- • Installing pytest (3.5.2)
+ • Installing clikit (0.2.4)
"""
expected = set(expected.splitlines())
output = set(io_not_decorated.fetch_output().splitlines())
@@ -389,14 +434,21 @@ def test_executor_should_write_pep610_url_references_for_files(
def test_executor_should_write_pep610_url_references_for_directories(
- tmp_venv, pool, config, io
+ tmp_venv, pool, config, io, wheel
):
- url = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve()
+ url = (
+ Path(__file__)
+ .parent.parent.joinpath("fixtures/git/github.com/demo/demo")
+ .resolve()
+ )
package = Package(
- "simple-project", "1.2.3", source_type="directory", source_url=url.as_posix()
+ "demo", "0.1.2", source_type="directory", source_url=url.as_posix()
)
+ chef = Chef(config, tmp_venv)
+ chef.set_directory_wheel(wheel)
executor = Executor(tmp_venv, pool, config, io)
+ executor.set_chef(chef)
executor.execute([Install(package)])
verify_installed_distribution(
tmp_venv, package, {"dir_info": {}, "url": url.as_uri()}
@@ -440,7 +492,7 @@ def test_executor_should_write_pep610_url_references_for_urls(
def test_executor_should_write_pep610_url_references_for_git(
- tmp_venv, pool, config, io, mock_file_downloads
+ tmp_venv, pool, config, io, mock_file_downloads, wheel
):
package = Package(
"demo",
@@ -451,7 +503,10 @@ def test_executor_should_write_pep610_url_references_for_git(
source_url="https://github.com/demo/demo.git",
)
+ chef = Chef(config, tmp_venv)
+ chef.set_directory_wheel(wheel)
executor = Executor(tmp_venv, pool, config, io)
+ executor.set_chef(chef)
executor.execute([Install(package)])
verify_installed_distribution(
tmp_venv,
diff --git a/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl
index 87db51230e3..6afdd907294 100644
Binary files a/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl and b/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.0.3-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.0.3-py2.py3-none-any.whl
new file mode 100644
index 00000000000..dc468143f43
Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.0.3-py2.py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json
new file mode 100644
index 00000000000..595674750f8
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json
@@ -0,0 +1,140 @@
+{
+ "info": {
+ "author": "Barry Warsaw",
+ "author_email": "barry@python.org",
+ "bugtrack_url": null,
+ "classifiers": [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 3",
+ "Topic :: Software Development :: Libraries"
+ ],
+ "description": "",
+ "description_content_type": "",
+ "docs_url": null,
+ "download_url": "",
+ "downloads": {
+ "last_day": -1,
+ "last_month": -1,
+ "last_week": -1
+ },
+ "home_page": "http://importlib-metadata.readthedocs.io/",
+ "keywords": "",
+ "license": "Apache Software License",
+ "maintainer": "",
+ "maintainer_email": "",
+ "name": "importlib-metadata",
+ "package_url": "https://pypi.org/project/importlib-metadata/",
+ "platform": "",
+ "project_url": "https://pypi.org/project/importlib-metadata/",
+ "project_urls": {
+ "Homepage": "http://importlib-metadata.readthedocs.io/"
+ },
+ "release_url": "https://pypi.org/project/importlib-metadata/1.7.0/",
+ "requires_dist": [
+ "zipp (>=0.5)",
+ "pathlib2 ; python_version < \"3\"",
+ "contextlib2 ; python_version < \"3\"",
+ "configparser (>=3.5) ; python_version < \"3\"",
+ "sphinx ; extra == 'docs'",
+ "rst.linker ; extra == 'docs'",
+ "packaging ; extra == 'testing'",
+ "pep517 ; extra == 'testing'",
+ "importlib-resources (>=1.3) ; (python_version < \"3.9\") and extra == 'testing'"
+ ],
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "summary": "Read metadata from Python packages",
+ "version": "1.7.0",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ "last_serial": 10821863,
+ "releases": {
+ "1.7.0": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "8ae1f31228e29443c08e07501a99d1b8",
+ "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
+ },
+ "downloads": -1,
+ "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "8ae1f31228e29443c08e07501a99d1b8",
+ "packagetype": "bdist_wheel",
+ "python_version": "py2.py3",
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "size": 31809,
+ "upload_time": "2020-06-26T21:38:16",
+ "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z",
+ "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "4505ea85600cca1e693a4f8f5dd27ba8",
+ "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"
+ },
+ "downloads": -1,
+ "filename": "importlib_metadata-1.7.0.tar.gz",
+ "has_sig": false,
+ "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "size": 29233,
+ "upload_time": "2020-06-26T21:38:17",
+ "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z",
+ "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ]
+ },
+ "urls": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "8ae1f31228e29443c08e07501a99d1b8",
+ "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
+ },
+ "downloads": -1,
+ "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "8ae1f31228e29443c08e07501a99d1b8",
+ "packagetype": "bdist_wheel",
+ "python_version": "py2.py3",
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "size": 31809,
+ "upload_time": "2020-06-26T21:38:16",
+ "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z",
+ "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "4505ea85600cca1e693a4f8f5dd27ba8",
+ "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"
+ },
+ "downloads": -1,
+ "filename": "importlib_metadata-1.7.0.tar.gz",
+ "has_sig": false,
+ "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "size": 29233,
+ "upload_time": "2020-06-26T21:38:17",
+ "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z",
+ "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ]
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core.json b/tests/repositories/fixtures/pypi.org/json/poetry-core.json
new file mode 100644
index 00000000000..51eb4dbd135
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/poetry-core.json
@@ -0,0 +1,143 @@
+{
+ "info": {
+ "author": "Sébastien Eustace",
+ "author_email": "sebastien@eustace.io",
+ "bugtrack_url": null,
+ "classifiers": [
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Topic :: Software Development :: Build Tools",
+ "Topic :: Software Development :: Libraries :: Python Modules"
+ ],
+ "description": "",
+ "description_content_type": "text/markdown",
+ "docs_url": null,
+ "download_url": "",
+ "downloads": {
+ "last_day": -1,
+ "last_month": -1,
+ "last_week": -1
+ },
+ "home_page": "https://github.com/python-poetry/poetry-core",
+ "keywords": "packaging,dependency,poetry",
+ "license": "MIT",
+ "maintainer": "",
+ "maintainer_email": "",
+ "name": "poetry-core",
+ "package_url": "https://pypi.org/project/poetry-core/",
+ "platform": "",
+ "project_url": "https://pypi.org/project/poetry-core/",
+ "project_urls": {
+ "Bug Tracker": "https://github.com/python-poetry/poetry/issues",
+ "Homepage": "https://github.com/python-poetry/poetry-core",
+ "Repository": "https://github.com/python-poetry/poetry-core"
+ },
+ "release_url": "https://pypi.org/project/poetry-core/1.0.3/",
+ "requires_dist": [
+ "importlib-metadata (>=1.7.0,<2.0.0); python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.5\" and python_version < \"3.8\"",
+ "pathlib2 (>=2.3.5,<3.0.0); python_version >= \"2.7\" and python_version < \"2.8\"",
+ "typing (>=3.7.4.1,<4.0.0.0); python_version >= \"2.7\" and python_version < \"2.8\"",
+ "enum34 (>=1.1.10,<2.0.0); python_version >= \"2.7\" and python_version < \"2.8\"",
+ "functools32 (>=3.2.3-2,<4.0.0); python_version >= \"2.7\" and python_version < \"2.8\""
+ ],
+ "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ "summary": "Poetry PEP 517 Build Backend",
+ "version": "1.0.3",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ "last_serial": 10427564,
+ "releases": {
+ "1.0.3": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "16ce2526c342c1305d541425d406e35e",
+ "sha256": "c6bde46251112de8384013e1ab8d66e7323d2c75172f80220aba2bc07e208e9a"
+ },
+ "downloads": -1,
+ "filename": "poetry_core-1.0.3-py2.py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "16ce2526c342c1305d541425d406e35e",
+ "packagetype": "bdist_wheel",
+ "python_version": "py2.py3",
+ "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ "size": 424184,
+ "upload_time": "2021-04-09T15:44:41",
+ "upload_time_iso_8601": "2021-04-09T15:44:41.970294Z",
+ "url": "https://files.pythonhosted.org/packages/bf/e1/08c7478df1e93dea47b06c9d9a80dbb54af7421462e1b22c280d063df807/poetry_core-1.0.3-py2.py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "dc82606f54a984a99daf8864cca1f028",
+ "sha256": "2315c928249fc3207801a81868b64c66273077b26c8d8da465dccf8f488c90c5"
+ },
+ "downloads": -1,
+ "filename": "poetry-core-1.0.3.tar.gz",
+ "has_sig": false,
+ "md5_digest": "dc82606f54a984a99daf8864cca1f028",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ "size": 345626,
+ "upload_time": "2021-04-09T15:44:40",
+ "upload_time_iso_8601": "2021-04-09T15:44:40.639806Z",
+ "url": "https://files.pythonhosted.org/packages/d0/b3/1017f2f6d801f1e3e4ffee3f058a10d20df1a9560aba9c5b49e92cdd9912/poetry-core-1.0.3.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ]
+ },
+ "urls": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "16ce2526c342c1305d541425d406e35e",
+ "sha256": "c6bde46251112de8384013e1ab8d66e7323d2c75172f80220aba2bc07e208e9a"
+ },
+ "downloads": -1,
+ "filename": "poetry_core-1.0.3-py2.py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "16ce2526c342c1305d541425d406e35e",
+ "packagetype": "bdist_wheel",
+ "python_version": "py2.py3",
+ "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ "size": 424184,
+ "upload_time": "2021-04-09T15:44:41",
+ "upload_time_iso_8601": "2021-04-09T15:44:41.970294Z",
+ "url": "https://files.pythonhosted.org/packages/bf/e1/08c7478df1e93dea47b06c9d9a80dbb54af7421462e1b22c280d063df807/poetry_core-1.0.3-py2.py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "dc82606f54a984a99daf8864cca1f028",
+ "sha256": "2315c928249fc3207801a81868b64c66273077b26c8d8da465dccf8f488c90c5"
+ },
+ "downloads": -1,
+ "filename": "poetry-core-1.0.3.tar.gz",
+ "has_sig": false,
+ "md5_digest": "dc82606f54a984a99daf8864cca1f028",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ "size": 345626,
+ "upload_time": "2021-04-09T15:44:40",
+ "upload_time_iso_8601": "2021-04-09T15:44:40.639806Z",
+ "url": "https://files.pythonhosted.org/packages/d0/b3/1017f2f6d801f1e3e4ffee3f058a10d20df1a9560aba9c5b49e92cdd9912/poetry-core-1.0.3.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ]
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/zipp.json b/tests/repositories/fixtures/pypi.org/json/zipp.json
new file mode 100644
index 00000000000..356bd2b3483
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/zipp.json
@@ -0,0 +1,142 @@
+{
+ "info": {
+ "author": "Jason R. Coombs",
+ "author_email": "jaraco@jaraco.com",
+ "bugtrack_url": null,
+ "classifiers": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only"
+ ],
+ "description": "",
+ "description_content_type": "",
+ "docs_url": null,
+ "download_url": "",
+ "downloads": {
+ "last_day": -1,
+ "last_month": -1,
+ "last_week": -1
+ },
+ "home_page": "https://github.com/jaraco/zipp",
+ "keywords": "",
+ "license": "",
+ "maintainer": "",
+ "maintainer_email": "",
+ "name": "zipp",
+ "package_url": "https://pypi.org/project/zipp/",
+ "platform": "",
+ "project_url": "https://pypi.org/project/zipp/",
+ "project_urls": {
+ "Homepage": "https://github.com/jaraco/zipp"
+ },
+ "release_url": "https://pypi.org/project/zipp/3.5.0/",
+ "requires_dist": [
+ "sphinx ; extra == 'docs'",
+ "jaraco.packaging (>=8.2) ; extra == 'docs'",
+ "rst.linker (>=1.9) ; extra == 'docs'",
+ "pytest (>=4.6) ; extra == 'testing'",
+ "pytest-checkdocs (>=2.4) ; extra == 'testing'",
+ "pytest-flake8 ; extra == 'testing'",
+ "pytest-cov ; extra == 'testing'",
+ "pytest-enabler (>=1.0.1) ; extra == 'testing'",
+ "jaraco.itertools ; extra == 'testing'",
+ "func-timeout ; extra == 'testing'",
+ "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'",
+ "pytest-mypy ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'"
+ ],
+ "requires_python": ">=3.6",
+ "summary": "Backport of pathlib-compatible object wrapper for zip files",
+ "version": "3.5.0",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ "last_serial": 10811847,
+ "releases": {
+ "3.5.0": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "0ec47fbf522751f6c5fa904cb33f1f59",
+ "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"
+ },
+ "downloads": -1,
+ "filename": "zipp-3.5.0-py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "0ec47fbf522751f6c5fa904cb33f1f59",
+ "packagetype": "bdist_wheel",
+ "python_version": "py3",
+ "requires_python": ">=3.6",
+ "size": 5700,
+ "upload_time": "2021-07-02T23:51:45",
+ "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z",
+ "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "617efbf3edb707c57008ec00f408972f",
+ "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
+ },
+ "downloads": -1,
+ "filename": "zipp-3.5.0.tar.gz",
+ "has_sig": false,
+ "md5_digest": "617efbf3edb707c57008ec00f408972f",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": ">=3.6",
+ "size": 13270,
+ "upload_time": "2021-07-02T23:51:47",
+ "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z",
+ "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ]
+ },
+ "urls": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "0ec47fbf522751f6c5fa904cb33f1f59",
+ "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"
+ },
+ "downloads": -1,
+ "filename": "zipp-3.5.0-py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "0ec47fbf522751f6c5fa904cb33f1f59",
+ "packagetype": "bdist_wheel",
+ "python_version": "py3",
+ "requires_python": ">=3.6",
+ "size": 5700,
+ "upload_time": "2021-07-02T23:51:45",
+ "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z",
+ "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "617efbf3edb707c57008ec00f408972f",
+ "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
+ },
+ "downloads": -1,
+ "filename": "zipp-3.5.0.tar.gz",
+ "has_sig": false,
+ "md5_digest": "617efbf3edb707c57008ec00f408972f",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": ">=3.6",
+ "size": 13270,
+ "upload_time": "2021-07-02T23:51:47",
+ "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z",
+ "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ]
+}
diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py
index d09012c821f..2ddeb16b1b8 100644
--- a/tests/utils/test_env.py
+++ b/tests/utils/test_env.py
@@ -950,6 +950,13 @@ def test_venv_has_correct_paths(tmp_venv):
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(
+ "include/site/python{}.{}".format(
+ tmp_venv.version_info[0], tmp_venv.version_info[1]
+ )
+ )
+ )
def test_env_system_packages(tmp_path, config):