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):