diff --git a/docs/cli.md b/docs/cli.md
index a4c682a757d..f09ffb97273 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -225,6 +225,21 @@ If you want to skip this installation, use the `--no-root` option.
poetry install --no-root
```
+By default `poetry` does not compile Python source files to bytecode during installation.
+This speeds up the installation process, but the first execution may take a little more
+time because Python then compiles source files to bytecode automatically.
+If you want to compile source files to bytecode during installation,
+you can use the `--compile` option:
+
+```bash
+poetry install --compile
+```
+
+{{% note %}}
+The `--compile` option has no effect if `installer.modern-installation`
+is set to `false` because the old installer always compiles source files to bytecode.
+{{% /note %}}
+
### Options
* `--without`: The dependency groups to ignore.
@@ -236,6 +251,7 @@ poetry install --no-root
* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose).
* `--extras (-E)`: Features to install (multiple values allowed).
* `--all-extras`: Install all extra features (conflicts with --extras).
+* `--compile`: Compile Python source files to bytecode.
* `--no-dev`: Do not install dev dependencies. (**Deprecated**, use `--without dev` or `--only main` instead)
* `--remove-untracked`: Remove dependencies not presented in the lock file. (**Deprecated**, use `--sync` instead)
diff --git a/docs/configuration.md b/docs/configuration.md
index af537772277..212341232bb 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -190,6 +190,19 @@ the number of maximum workers is still limited at `number_of_cores + 4`.
This configuration is ignored when `installer.parallel` is set to `false`.
{{% /note %}}
+### `installer.modern-installation`
+
+**Type**: `boolean`
+
+**Default**: `true`
+
+*Introduced in 1.4.0*
+
+Use a more modern and faster method for package installation.
+
+If this causes issues, you can disable it by setting it to `false` and report the problems
+you encounter on the [issue tracker](https://github.com/python-poetry/poetry/issues).
+
### `installer.no-binary`
**Type**: `string | boolean`
diff --git a/poetry.lock b/poetry.lock
index def850e4f87..2bae3a2b2a2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -31,6 +31,31 @@ files = [
{file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"},
]
+[[package]]
+name = "build"
+version = "0.10.0"
+description = "A simple, correct Python build frontend"
+category = "main"
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "build-0.10.0-py3-none-any.whl", hash = "sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171"},
+ {file = "build-0.10.0.tar.gz", hash = "sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "os_name == \"nt\""}
+importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""}
+packaging = ">=19.0"
+pyproject_hooks = "*"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+docs = ["furo (>=2021.08.31)", "sphinx (>=4.0,<5.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"]
+test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"]
+typing = ["importlib-metadata (>=5.1)", "mypy (==0.991)", "tomli", "typing-extensions (>=3.7.4.3)"]
+virtualenv = ["virtualenv (>=20.0.35)"]
+
[[package]]
name = "cachecontrol"
version = "0.12.11"
@@ -288,7 +313,7 @@ rapidfuzz = ">=2.2.0,<3.0.0"
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@@ -702,6 +727,18 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
+[[package]]
+name = "installer"
+version = "0.6.0"
+description = "A library for installing Python wheels."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "installer-0.6.0-py3-none-any.whl", hash = "sha256:ae7c62d1d6158b5c096419102ad0d01fdccebf857e784cee57f94165635fe038"},
+ {file = "installer-0.6.0.tar.gz", hash = "sha256:f3bd36cd261b440a88a1190b1becca0578fee90b4b62decc796932fdd5ae8839"},
+]
+
[[package]]
name = "jaraco-classes"
version = "3.2.3"
@@ -1214,6 +1251,21 @@ files = [
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
+[[package]]
+name = "pyproject-hooks"
+version = "1.0.0"
+description = "Wrappers to call pyproject.toml-based build backend hooks."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"},
+ {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"},
+]
+
+[package.dependencies]
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
[[package]]
name = "pyrsistent"
version = "0.19.3"
@@ -1933,4 +1985,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.7"
-content-hash = "89dc4e56ed4a5f8713e1b6c74e3b9f6a6c9c5143350908feb39d71101b937f84"
+content-hash = "077b0abda1e0e5ffc7286738a63098606e3bd4650f7bb2569efe16cc106e055e"
diff --git a/pyproject.toml b/pyproject.toml
index c2411adf9b5..c45c01a2e67 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ python = "^3.7"
poetry-core = "1.5.0"
poetry-plugin-export = "^1.3.0"
"backports.cached-property" = { version = "^1.0.2", python = "<3.8" }
+build = "^0.10.0"
cachecontrol = { version = "^0.12.9", extras = ["filecache"] }
cleo = "^2.0.0"
crashtest = "^0.4.1"
@@ -57,6 +58,7 @@ dulwich = "^0.21.2"
filelock = "^3.8.0"
html5lib = "^1.0"
importlib-metadata = { version = ">=4.4", python = "<3.10" }
+installer = "^0.6.0"
jsonschema = "^4.10.0"
keyring = "^23.9.0"
lockfile = "^0.12.2"
@@ -65,6 +67,7 @@ packaging = ">=20.4"
pexpect = "^4.7.0"
pkginfo = "^1.9.4"
platformdirs = "^2.5.2"
+pyproject-hooks = "^1.0.0"
requests = "^2.18"
requests-toolbelt = ">=0.9.1,<0.11.0"
shellingham = "^1.5"
@@ -166,22 +169,22 @@ enable_error_code = [
# warning.
[[tool.mypy.overrides]]
module = [
- 'poetry.console.commands.self.show.plugins',
- 'poetry.plugins.plugin_manager',
- 'poetry.repositories.installed_repository',
- 'poetry.utils.env',
+ 'poetry.console.commands.self.show.plugins',
+ 'poetry.plugins.plugin_manager',
+ 'poetry.repositories.installed_repository',
+ 'poetry.utils.env',
]
warn_unused_ignores = false
[[tool.mypy.overrides]]
module = [
- 'cachecontrol.*',
- 'lockfile.*',
- 'pexpect.*',
- 'requests_toolbelt.*',
- 'shellingham.*',
- 'virtualenv.*',
- 'xattr.*',
+ 'cachecontrol.*',
+ 'lockfile.*',
+ 'pexpect.*',
+ 'requests_toolbelt.*',
+ 'shellingham.*',
+ 'virtualenv.*',
+ 'xattr.*',
]
ignore_missing_imports = true
diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py
index 7cbcba3fc10..6eda9699870 100644
--- a/src/poetry/config/config.py
+++ b/src/poetry/config/config.py
@@ -100,7 +100,6 @@ def validator(cls, policy: str) -> bool:
logger = logging.getLogger(__name__)
-
_default_config: Config | None = None
@@ -124,8 +123,16 @@ class Config:
"prefer-active-python": False,
"prompt": "{project_name}-py{python_version}",
},
- "experimental": {"new-installer": True, "system-git-client": False},
- "installer": {"parallel": True, "max-workers": None, "no-binary": None},
+ "experimental": {
+ "new-installer": True,
+ "system-git-client": False,
+ },
+ "installer": {
+ "modern-installation": True,
+ "parallel": True,
+ "max-workers": None,
+ "no-binary": None,
+ },
}
def __init__(
@@ -267,6 +274,7 @@ def _get_normalizer(name: str) -> Callable[[str], Any]:
"virtualenvs.options.prefer-active-python",
"experimental.new-installer",
"experimental.system-git-client",
+ "installer.modern-installation",
"installer.parallel",
}:
return boolean_normalizer
diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py
index 898312adc77..998af125755 100644
--- a/src/poetry/console/commands/config.py
+++ b/src/poetry/console/commands/config.py
@@ -70,6 +70,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]:
"virtualenvs.prefer-active-python": (boolean_validator, boolean_normalizer),
"experimental.new-installer": (boolean_validator, boolean_normalizer),
"experimental.system-git-client": (boolean_validator, boolean_normalizer),
+ "installer.modern-installation": (boolean_validator, boolean_normalizer),
"installer.parallel": (boolean_validator, boolean_normalizer),
"installer.max-workers": (lambda val: int(val) > 0, int_normalizer),
"virtualenvs.prompt": (str, lambda val: str(val)),
diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py
index b499970aac9..453feaf55d0 100644
--- a/src/poetry/console/commands/install.py
+++ b/src/poetry/console/commands/install.py
@@ -54,12 +54,15 @@ class InstallCommand(InstallerCommand):
multiple=True,
),
option("all-extras", None, "Install all extra dependencies."),
+ option("only-root", None, "Exclude all dependencies."),
option(
- "only-root",
+ "compile",
None,
- "Exclude all dependencies.",
- flag=True,
- multiple=False,
+ (
+ "Compile Python source files to bytecode."
+ " (This option has no effect if modern-installation is disabled"
+ " because the old installer always compiles.)"
+ ),
),
]
@@ -146,6 +149,7 @@ def handle(self) -> int:
self.installer.only_groups(self.activated_groups)
self.installer.dry_run(self.option("dry-run"))
self.installer.requires_synchronization(with_synchronization)
+ self.installer.executor.enable_bytecode_compilation(self.option("compile"))
self.installer.verbose(self.io.is_verbose())
return_code = self.installer.run()
diff --git a/src/poetry/console/commands/source/add.py b/src/poetry/console/commands/source/add.py
index 2848ce8a3be..f258e404af0 100644
--- a/src/poetry/console/commands/source/add.py
+++ b/src/poetry/console/commands/source/add.py
@@ -36,7 +36,6 @@ class SourceAddCommand(Command):
def handle(self) -> int:
from poetry.factory import Factory
- from poetry.repositories import RepositoryPool
from poetry.utils.source import source_to_table
name = self.argument("name")
@@ -84,13 +83,14 @@ def handle(self) -> int:
self.line(f"Adding source with name {name}.")
sources.append(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 = RepositoryPool()
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/src/poetry/factory.py b/src/poetry/factory.py
index 002fdcf4779..ff03718b48d 100644
--- a/src/poetry/factory.py
+++ b/src/poetry/factory.py
@@ -29,10 +29,10 @@
from poetry.core.packages.package import Package
from tomlkit.toml_document import TOMLDocument
+ from poetry.repositories import RepositoryPool
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.utils.dependency_specification import DependencySpec
-
logger = logging.getLogger(__name__)
@@ -90,13 +90,15 @@ def create_poetry(
)
# Configuring sources
- self.configure_sources(
- poetry,
- poetry.local_config.get("source", []),
- config,
- io,
- disable_cache=disable_cache,
+ config.merge(
+ {
+ "sources": {
+ source["name"]: source
+ for source in poetry.local_config.get("source", [])
+ }
+ }
)
+ poetry.set_pool(self.create_pool(config, io, disable_cache=disable_cache))
plugin_manager = PluginManager(Plugin.group, disable_plugins=disable_plugins)
plugin_manager.load_plugins()
@@ -110,23 +112,28 @@ def get_package(cls, name: str, version: str) -> ProjectPackage:
return ProjectPackage(name, version)
@classmethod
- def configure_sources(
+ def create_pool(
cls,
- poetry: Poetry,
- sources: list[dict[str, str]],
config: Config,
- io: IO,
+ io: IO | None = None,
disable_cache: bool = False,
- ) -> None:
+ ) -> RepositoryPool:
+ from poetry.repositories import RepositoryPool
+
+ if io is None:
+ io = NullIO()
+
if disable_cache:
logger.debug("Disabling source caches")
- for source in sources:
+ pool = RepositoryPool()
+
+ for source in config.get("sources", {}).values():
repository = cls.create_package_source(
source, config, disable_cache=disable_cache
)
- is_default = bool(source.get("default", False))
- is_secondary = bool(source.get("secondary", False))
+ is_default = source.get("default", False)
+ is_secondary = source.get("secondary", False)
if io.is_debug():
message = f"Adding repository {repository.name} ({repository.url})"
if is_default:
@@ -136,22 +143,24 @@ 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:
from poetry.repositories.pypi_repository import PyPiRepository
- default = not poetry.pool.has_primary_repositories()
- poetry.pool.add_repository(
+ default = not pool.has_primary_repositories()
+ pool.add_repository(
PyPiRepository(disable_cache=disable_cache), default, not default
)
+ return pool
+
@classmethod
def create_package_source(
cls, source: dict[str, str], auth_config: Config, disable_cache: bool = False
diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py
index fa3eb267f00..2a4c02c7451 100644
--- a/src/poetry/installation/chef.py
+++ b/src/poetry/installation/chef.py
@@ -2,28 +2,202 @@
import hashlib
import json
+import tarfile
+import tempfile
+import zipfile
+from contextlib import redirect_stdout
+from io import StringIO
from pathlib import Path
from typing import TYPE_CHECKING
+from typing import Callable
+from typing import Collection
+
+from build import BuildBackendException
+from build import ProjectBuilder
+from build.env import IsolatedEnv as BaseIsolatedEnv
+from poetry.core.utils.helpers import temporary_directory
+from pyproject_hooks import quiet_subprocess_runner # type: ignore[import]
from poetry.installation.chooser import InvalidWheelName
from poetry.installation.chooser import Wheel
+from poetry.utils.env import ephemeral_environment
if TYPE_CHECKING:
+ from contextlib import AbstractContextManager
+
from poetry.core.packages.utils.link import Link
from poetry.config.config import Config
from poetry.utils.env import Env
+class ChefError(Exception):
+ ...
+
+
+class ChefBuildError(ChefError):
+ ...
+
+
+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: Collection[str]) -> 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.config.config import Config
+ from poetry.factory import Factory
+ from poetry.installation.installer import Installer
+ from poetry.packages.locker import Locker
+ 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,
+ Locker(self._env.path.joinpath("poetry.lock"), {}),
+ pool,
+ Config.create(),
+ InstalledRepository.load(self._env),
+ )
+ installer.update(True)
+ installer.run()
+
+
class Chef:
def __init__(self, config: Config, env: Env) -> None:
+ self._config = config
self._env = env
self._cache_dir = (
Path(config.get("cache-dir")).expanduser().joinpath("artifacts")
)
+ def prepare(
+ self, archive: Path, output_dir: Path | None = None, *, editable: bool = False
+ ) -> 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), editable=editable)
+
+ return self._prepare_sdist(archive, destination=output_dir)
+
+ def _prepare(
+ self, directory: Path, destination: Path, *, editable: bool = False
+ ) -> Path:
+ from subprocess import CalledProcessError
+
+ with ephemeral_environment(self._env.python) as venv:
+ 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)
+
+ stdout = StringIO()
+ error: Exception | None = None
+ try:
+ with redirect_stdout(stdout):
+ env.install(
+ builder.build_system_requires
+ | builder.get_requires_for_build("wheel")
+ )
+ path = Path(
+ builder.build(
+ "wheel" if not editable else "editable",
+ destination.as_posix(),
+ )
+ )
+ except BuildBackendException as e:
+ message_parts = [str(e)]
+ if isinstance(e.exception, CalledProcessError) and (
+ e.exception.stdout is not None or e.exception.stderr is not None
+ ):
+ message_parts.append(
+ e.exception.stderr.decode()
+ if e.exception.stderr is not None
+ else e.exception.stdout.decode()
+ )
+
+ error = ChefBuildError("\n\n".join(message_parts))
+
+ if error is not None:
+ raise error from None
+
+ return path
+
+ def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path:
+ from poetry.core.packages.utils.link import Link
+
+ suffix = archive.suffix
+ context: Callable[
+ [str], AbstractContextManager[zipfile.ZipFile | tarfile.TarFile]
+ ]
+ if suffix == ".zip":
+ context = zipfile.ZipFile
+ else:
+ context = tarfile.open
+
+ with temporary_directory() as tmp_dir:
+ with context(archive.as_posix()) as archive_archive:
+ archive_archive.extractall(tmp_dir)
+
+ archive_dir = Path(tmp_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 _should_prepare(self, archive: Path) -> bool:
+ return archive.is_dir() or not self._is_wheel(archive)
+
+ @classmethod
+ def _is_wheel(cls, archive: Path) -> bool:
+ return archive.suffix == ".whl"
+
def get_cached_archive_for_link(self, link: Link) -> Path | None:
archives = self.get_cached_archives_for_link(link)
if not archives:
diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py
index dd72729a66d..6ba544eb40d 100644
--- a/src/poetry/installation/executor.py
+++ b/src/poetry/installation/executor.py
@@ -16,13 +16,14 @@
from cleo.io.null_io import NullIO
from poetry.core.packages.utils.link import Link
-from poetry.core.pyproject.toml import PyProjectTOML
from poetry.installation.chef import Chef
+from poetry.installation.chef import ChefBuildError
from poetry.installation.chooser import Chooser
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.utils._compat import decode
from poetry.utils.authenticator import Authenticator
from poetry.utils.env import EnvCommandError
@@ -60,6 +61,10 @@ def __init__(
self._dry_run = False
self._enabled = True
self._verbose = False
+ self._wheel_installer = WheelInstaller(self._env)
+ self._use_modern_installation = config.get(
+ "installer.modern-installation", True
+ )
if parallel is None:
parallel = config.get("installer.parallel", True)
@@ -118,6 +123,9 @@ def verbose(self, verbose: bool = True) -> Executor:
return self
+ def enable_bytecode_compilation(self, enable: bool = True) -> None:
+ self._wheel_installer.enable_bytecode_compilation(enable)
+
def pip_install(
self, req: Path, upgrade: bool = False, editable: bool = False
) -> int:
@@ -291,6 +299,19 @@ def _execute_operation(self, operation: Operation) -> None:
with self._lock:
trace = ExceptionTrace(e)
trace.render(io)
+ if isinstance(e, ChefBuildError):
+ pkg = operation.package
+ requirement = pkg.to_dependency().to_pep_508()
+ io.write_line("")
+ io.write_line(
+ ""
+ "Note: This error originates from the build backend,"
+ " and is likely not a problem with poetry"
+ f" but with {pkg.pretty_name} ({pkg.full_pretty_version})"
+ " not supporting PEP 517 builds. You can verify this by"
+ f" running 'pip wheel --use-pep517 \"{requirement}\"'."
+ ""
+ )
io.write_line("")
finally:
with self._lock:
@@ -471,18 +492,22 @@ def _execute_uninstall(self, operation: Uninstall) -> int:
message = f" •> {op_msg}: Removing..."
self._write(operation, message)
- return self._remove(operation)
+ return self._remove(operation.package)
def _install(self, operation: Install | Update) -> int:
package = operation.package
- if package.source_type == "directory":
- return self._install_directory(operation)
+ if package.source_type == "directory" and not self._use_modern_installation:
+ return self._install_directory_without_wheel_installer(operation)
+ cleanup_archive: bool = False
if package.source_type == "git":
- return self._install_git(operation)
-
- if package.source_type == "file":
- archive = self._prepare_file(operation)
+ archive = self._prepare_git_archive(operation)
+ cleanup_archive = True
+ elif package.source_type == "file":
+ archive = self._prepare_archive(operation)
+ elif package.source_type == "directory":
+ archive = self._prepare_directory_archive(operation)
+ cleanup_archive = True
elif package.source_type == "url":
assert package.source_url is not None
archive = self._download_link(operation, Link(package.source_url))
@@ -495,14 +520,29 @@ def _install(self, operation: Install | Update) -> int:
" Installing..."
)
self._write(operation, message)
- return self.pip_install(archive, upgrade=operation.job_type == "update")
+
+ if not self._use_modern_installation:
+ return self.pip_install(archive, upgrade=operation.job_type == "update")
+
+ 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
+ assert isinstance(operation, Update)
+ self._remove(operation.initial_package)
+
+ self._wheel_installer.install(archive)
+ finally:
+ if cleanup_archive:
+ archive.unlink()
+
+ return 0
def _update(self, operation: 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
@@ -517,7 +557,7 @@ def _remove(self, operation: Uninstall) -> int:
raise
- def _prepare_file(self, operation: Install | Update) -> Path:
+ def _prepare_archive(self, operation: Install | Update) -> Path:
package = operation.package
operation_message = self.get_operation_message(operation)
@@ -532,9 +572,62 @@ def _prepare_file(self, operation: Install | Update) -> Path:
if not Path(package.source_url).is_absolute() and package.root_dir:
archive = package.root_dir / archive
+ return self._chef.prepare(archive, editable=package.develop)
+
+ def _prepare_directory_archive(self, operation: Install | Update) -> Path:
+ package = operation.package
+ operation_message = self.get_operation_message(operation)
+
+ message = (
+ f" •> {operation_message}:"
+ " Building..."
+ )
+ self._write(operation, message)
+
+ assert package.source_url is not None
+ if package.root_dir:
+ req = package.root_dir / package.source_url
+ else:
+ req = Path(package.source_url).resolve(strict=False)
+
+ if package.source_subdirectory:
+ req /= package.source_subdirectory
+
+ return self._prepare_archive(operation)
+
+ def _prepare_git_archive(self, operation: Install | Update) -> Path:
+ from poetry.vcs.git import Git
+
+ package = operation.package
+ operation_message = self.get_operation_message(operation)
+
+ message = (
+ f" •> {operation_message}: Cloning..."
+ )
+ self._write(operation, message)
+
+ assert package.source_url is not None
+ source = Git.clone(
+ url=package.source_url,
+ source_root=self._env.path / "src",
+ revision=package.source_resolved_reference or package.source_reference,
+ )
+
+ # Now we just need to install from the source directory
+ original_url = package.source_url
+ package._source_url = str(source.path)
+
+ archive = self._prepare_directory_archive(operation)
+
+ package._source_url = original_url
+
return archive
- def _install_directory(self, operation: Install | Update) -> int:
+ def _install_directory_without_wheel_installer(
+ self, operation: Install | Update
+ ) -> int:
+ from poetry.core.pyproject.toml import PyProjectTOML
+
from poetry.factory import Factory
package = operation.package
@@ -596,34 +689,6 @@ def _install_directory(self, operation: Install | Update) -> int:
return self.pip_install(req, upgrade=True, editable=package.develop)
- def _install_git(self, operation: Install | Update) -> int:
- from poetry.vcs.git import Git
-
- package = operation.package
- operation_message = self.get_operation_message(operation)
-
- message = (
- f" •> {operation_message}: Cloning..."
- )
- self._write(operation, message)
-
- assert package.source_url is not None
- source = Git.clone(
- url=package.source_url,
- source_root=self._env.path / "src",
- revision=package.source_resolved_reference or package.source_reference,
- )
-
- # Now we just need to install from the source directory
- original_url = package.source_url
- package._source_url = str(source.path)
-
- status_code = self._install_directory(operation)
-
- package._source_url = original_url
-
- return status_code
-
def _download(self, operation: Install | Update) -> Path:
link = self._chooser.choose_for(operation.package)
@@ -644,6 +709,7 @@ def _download(self, operation: Install | Update) -> Path:
def _download_link(self, operation: Install | Update, link: Link) -> Path:
package = operation.package
+ output_dir = self._chef.get_cache_directory_for_link(link)
archive = self._chef.get_cached_archive_for_link(link)
if archive is None:
# No cached distributions was found, so we download and prepare it
@@ -659,7 +725,16 @@ def _download_link(self, operation: Install | Update, link: Link) -> Path:
raise
- if package.files:
+ if archive.suffix != ".whl":
+ message = (
+ f" •> {self.get_operation_message(operation)}:"
+ " Preparing..."
+ )
+ self._write(operation, message)
+
+ archive = self._chef.prepare(archive, output_dir=output_dir)
+
+ if package.files and archive.name in {f["file"] for f in package.files}:
archive_hash = self._validate_archive_hash(archive, package)
self._hashes[package.name] = archive_hash
diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py
new file mode 100644
index 00000000000..c8df26960f8
--- /dev/null
+++ b/src/poetry/installation/wheel_installer.py
@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+import os
+import platform
+import sys
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from installer import install
+from installer.destinations import SchemeDictionaryDestination
+from installer.sources import WheelFile
+
+from poetry.__version__ import __version__
+from poetry.utils._compat import WINDOWS
+
+
+if TYPE_CHECKING:
+ from typing import BinaryIO
+
+ from installer.records import RecordEntry
+ from installer.scripts import LauncherKind
+ from installer.utils import Scheme
+
+ from poetry.utils.env import Env
+
+
+class WheelDestination(SchemeDictionaryDestination):
+ """ """
+
+ def write_to_fs(
+ self,
+ scheme: Scheme,
+ path: Path | str,
+ stream: BinaryIO,
+ is_executable: bool,
+ ) -> RecordEntry:
+ from installer.records import Hash
+ from installer.records import RecordEntry
+ from installer.utils import copyfileobj_with_hashing
+ from installer.utils import make_file_executable
+
+ target_path = Path(self.scheme_dict[scheme]) / path
+ if target_path.exists():
+ # Contrary to the base library we don't raise an error
+ # here since it can break namespace packages (like Poetry's)
+ pass
+
+ parent_folder = target_path.parent
+ if not parent_folder.exists():
+ # Due to the parallel installation it can happen
+ # that two threads try to create the directory.
+ os.makedirs(parent_folder, exist_ok=True)
+
+ with open(target_path, "wb") as f:
+ hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm)
+
+ if is_executable:
+ make_file_executable(target_path)
+
+ return RecordEntry(str(path), Hash(self.hash_algorithm, hash_), size)
+
+ def for_source(self, source: WheelFile) -> WheelDestination:
+ scheme_dict = self.scheme_dict.copy()
+
+ scheme_dict["headers"] = str(Path(scheme_dict["headers"]) / source.distribution)
+
+ return self.__class__(
+ scheme_dict,
+ interpreter=self.interpreter,
+ script_kind=self.script_kind,
+ bytecode_optimization_levels=self.bytecode_optimization_levels,
+ )
+
+
+class WheelInstaller:
+ def __init__(self, env: Env) -> None:
+ self._env = env
+
+ script_kind: LauncherKind
+ if not WINDOWS:
+ script_kind = "posix"
+ else:
+ if platform.uname()[4].startswith("arm"):
+ script_kind = "win-arm64" if sys.maxsize > 2**32 else "win-arm"
+ else:
+ script_kind = "win-amd64" if sys.maxsize > 2**32 else "win-ia32"
+
+ schemes = self._env.paths
+ schemes["headers"] = schemes["include"]
+
+ self._destination = WheelDestination(
+ schemes, interpreter=self._env.python, script_kind=script_kind
+ )
+
+ def enable_bytecode_compilation(self, enable: bool = True) -> None:
+ self._destination.bytecode_optimization_levels = (1,) if enable else ()
+
+ def install(self, wheel: Path) -> None:
+ with WheelFile.open(Path(wheel.as_posix())) as source:
+ install(
+ source=source,
+ destination=self._destination.for_source(source),
+ # Additional metadata that is generated by the installation tool.
+ additional_metadata={
+ "INSTALLER": f"Poetry {__version__}".encode(),
+ },
+ )
diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py
index 7a3584a9e98..70f4b9b4772 100644
--- a/src/poetry/utils/env.py
+++ b/src/poetry/utils/env.py
@@ -59,7 +59,6 @@
from poetry.poetry import Poetry
-
GET_SYS_TAGS = f"""
import importlib.util
import json
@@ -84,7 +83,6 @@
)
"""
-
GET_ENVIRONMENT_INFO = """\
import json
import os
@@ -155,7 +153,6 @@ def _version_nodot(version):
print(json.dumps(env))
"""
-
GET_BASE_PREFIX = """\
import sys
@@ -1438,6 +1435,16 @@ def paths(self) -> dict[str, str]:
if self._paths is None:
self._paths = self.get_paths()
+ if self.is_venv():
+ # We copy pip's logic here for the `include` path
+ self._paths["include"] = str(
+ self.path.joinpath(
+ "include",
+ "site",
+ f"python{self.version_info[0]}.{self.version_info[1]}",
+ )
+ )
+
return self._paths
@property
diff --git a/tests/conftest.py b/tests/conftest.py
index 86e5dfd2b36..12f89a9f9b2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -279,12 +279,12 @@ def http() -> Iterator[type[httpretty.httpretty]]:
yield httpretty
-@pytest.fixture
+@pytest.fixture(scope="session")
def fixture_base() -> Path:
return Path(__file__).parent / "fixtures"
-@pytest.fixture
+@pytest.fixture(scope="session")
def fixture_dir(fixture_base: Path) -> FixtureDirGetter:
def _fixture_dir(name: str) -> Path:
return fixture_base / name
diff --git a/tests/console/commands/self/conftest.py b/tests/console/commands/self/conftest.py
index e6fa5a052bf..68aedf1ac21 100644
--- a/tests/console/commands/self/conftest.py
+++ b/tests/console/commands/self/conftest.py
@@ -1,12 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING
+from typing import Callable
import pytest
from poetry.core.packages.package import Package
from poetry.__version__ import __version__
+from poetry.factory import Factory
from poetry.repositories import RepositoryPool
from poetry.utils.env import EnvManager
@@ -14,8 +16,10 @@
if TYPE_CHECKING:
import httpretty
+ from cleo.io.io import IO
from pytest_mock import MockerFixture
+ from poetry.config.config import Config
from poetry.repositories.repository import Repository
from poetry.utils.env import VirtualEnv
from tests.helpers import TestRepository
@@ -38,6 +42,20 @@ def pool(repo: TestRepository) -> RepositoryPool:
return RepositoryPool([repo])
+def create_pool_factory(
+ repo: Repository,
+) -> Callable[[Config, IO, bool], RepositoryPool]:
+ def _create_pool(
+ config: Config, io: IO, disable_cache: bool = False
+ ) -> RepositoryPool:
+ pool = RepositoryPool()
+ pool.add_repository(repo)
+
+ return pool
+
+ return _create_pool
+
+
@pytest.fixture(autouse=True)
def setup_mocks(
mocker: MockerFixture,
@@ -45,6 +63,7 @@ def setup_mocks(
installed: Repository,
pool: RepositoryPool,
http: type[httpretty.httpretty],
+ repo: Repository,
) -> None:
mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv)
mocker.patch(
@@ -59,3 +78,4 @@ def setup_mocks(
"poetry.installation.installer.Installer._get_installed",
return_value=installed,
)
+ mocker.patch.object(Factory, "create_pool", side_effect=create_pool_factory(repo))
diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py
index 9cc7e523d23..09c3a21b501 100644
--- a/tests/console/commands/self/test_update.py
+++ b/tests/console/commands/self/test_update.py
@@ -10,10 +10,13 @@
from poetry.__version__ import __version__
from poetry.factory import Factory
+from poetry.installation.executor import Executor
+from poetry.installation.wheel_installer import WheelInstaller
if TYPE_CHECKING:
from cleo.testers.command_tester import CommandTester
+ from pytest_mock import MockerFixture
from tests.helpers import TestRepository
from tests.types import CommandTesterFactory
@@ -21,6 +24,19 @@
FIXTURES = Path(__file__).parent.joinpath("fixtures")
+@pytest.fixture()
+def setup(mocker: MockerFixture, fixture_dir: Path):
+ 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()
def tester(command_tester_factory: CommandTesterFactory) -> CommandTester:
return command_tester_factory("self update")
diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py
index 9bc238aeb7a..a654e835579 100644
--- a/tests/console/commands/test_add.py
+++ b/tests/console/commands/test_add.py
@@ -787,6 +787,7 @@ def test_add_constraint_with_platform(
):
platform = sys.platform
env._platform = platform
+ env._marker_env = None
cachy2 = get_package("cachy", "0.2.0")
@@ -1824,6 +1825,7 @@ def test_add_constraint_with_platform_old_installer(
):
platform = sys.platform
env._platform = platform
+ env._marker_env = None
cachy2 = get_package("cachy", "0.2.0")
diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py
index 052b726c2fd..22fca7dcafe 100644
--- a/tests/console/commands/test_config.py
+++ b/tests/console/commands/test_config.py
@@ -54,6 +54,7 @@ def test_list_displays_default_value_if_not_set(
experimental.new-installer = true
experimental.system-git-client = false
installer.max-workers = null
+installer.modern-installation = true
installer.no-binary = null
installer.parallel = true
virtualenvs.create = true
@@ -83,6 +84,7 @@ def test_list_displays_set_get_setting(
experimental.new-installer = true
experimental.system-git-client = false
installer.max-workers = null
+installer.modern-installation = true
installer.no-binary = null
installer.parallel = true
virtualenvs.create = false
@@ -136,6 +138,7 @@ def test_list_displays_set_get_local_setting(
experimental.new-installer = true
experimental.system-git-client = false
installer.max-workers = null
+installer.modern-installation = true
installer.no-binary = null
installer.parallel = true
virtualenvs.create = false
diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py
index 9e4e59d1b5e..0200a452bfa 100644
--- a/tests/console/commands/test_install.py
+++ b/tests/console/commands/test_install.py
@@ -162,6 +162,24 @@ def test_sync_option_is_passed_to_the_installer(
assert tester.command.installer._requires_synchronization
+@pytest.mark.parametrize("compile", [False, True])
+def test_compile_option_is_passed_to_the_installer(
+ tester: CommandTester, mocker: MockerFixture, compile: bool
+):
+ """
+ The --compile option is passed properly to the installer.
+ """
+ mocker.patch.object(tester.command.installer, "run", return_value=1)
+ enable_bytecode_compilation_mock = mocker.patch.object(
+ tester.command.installer.executor._wheel_installer,
+ "enable_bytecode_compilation",
+ )
+
+ tester.execute("--compile" if compile else "")
+
+ enable_bytecode_compilation_mock.assert_called_once_with(compile)
+
+
def test_no_all_extras_doesnt_populate_installer(
tester: CommandTester, mocker: MockerFixture
):
diff --git a/tests/fixtures/distributions/demo-0.1.0.tar.gz b/tests/fixtures/distributions/demo-0.1.0.tar.gz
index 133b64421f8..37349b770e5 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.md b/tests/fixtures/extended_with_no_setup/README.md
new file mode 100644
index 00000000000..a7508bd515e
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/README.md
@@ -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..3ef8cfae13a
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/build.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+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..779fb1bd9dc
--- /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.md"
+
+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.5.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/installation/fixtures/with-same-version-url-dependencies.test b/tests/installation/fixtures/with-same-version-url-dependencies.test
index bc509936da6..52a3690e887 100644
--- a/tests/installation/fixtures/with-same-version-url-dependencies.test
+++ b/tests/installation/fixtures/with-same-version-url-dependencies.test
@@ -28,7 +28,7 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
- {file = "demo-0.1.0.tar.gz", hash = "sha256:72e8531e49038c5f9c4a837b088bfcb8011f4a9f76335c8f0654df6ac539b3d6"}
+ {file = "demo-0.1.0.tar.gz", hash = "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad"}
]
[package.source]
type = "url"
diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py
index 6af8f682eea..ac084455850 100644
--- a/tests/installation/test_chef.py
+++ b/tests/installation/test_chef.py
@@ -2,14 +2,19 @@
from pathlib import Path
from typing import TYPE_CHECKING
+from zipfile import ZipFile
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 RepositoryPool
+from poetry.utils.env import EnvManager
from poetry.utils.env import MockEnv
+from tests.repositories.test_pypi_repository import MockRepository
if TYPE_CHECKING:
@@ -18,6 +23,20 @@
from tests.conftest import Config
+@pytest.fixture()
+def pool() -> RepositoryPool:
+ pool = RepositoryPool()
+
+ pool.add_repository(MockRepository())
+
+ return pool
+
+
+@pytest.fixture(autouse=True)
+def setup(mocker: MockerFixture, pool: RepositoryPool) -> None:
+ mocker.patch.object(Factory, "create_pool", return_value=pool)
+
+
@pytest.mark.parametrize(
("link", "cached"),
[
@@ -82,7 +101,7 @@ def test_get_cached_archives_for_link(config: Config, mocker: MockerFixture):
)
assert archives
- assert set(archives) == {Path(path) for path in distributions.glob("demo-0.1.0*")}
+ assert set(archives) == set(distributions.glob("demo-0.1.*"))
def test_get_cache_directory_for_link(config: Config, config_cache_dir: Path):
@@ -103,3 +122,60 @@ def test_get_cache_directory_for_link(config: Config, config_cache_dir: Path):
)
assert directory == expected
+
+
+def test_prepare_sdist(config: Config, config_cache_dir: Path) -> None:
+ 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-py3-none-any.whl"
+
+
+def test_prepare_directory(config: Config, config_cache_dir: Path):
+ 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, config_cache_dir: Path
+) -> None:
+ 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 == f"extended-0.1-{env.supported_tags[0]}.whl"
+
+
+def test_prepare_directory_editable(config: Config, config_cache_dir: Path):
+ chef = Chef(config, EnvManager.get_system_env())
+
+ archive = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve()
+
+ wheel = chef.prepare(archive, editable=True)
+
+ assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl"
+
+ with ZipFile(wheel) as z:
+ assert "simple_project.pth" in z.namelist()
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 6b8de09546b..c6277182f32 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -4,23 +4,33 @@
import json
import re
import shutil
+import tempfile
from pathlib import Path
+from subprocess import CalledProcessError
from typing import TYPE_CHECKING
from typing import Any
+from typing import Callable
+from urllib.parse import urlparse
import pytest
+from build import BuildBackendException
+from build import ProjectBuilder
from cleo.formatters.style import Style
from cleo.io.buffered_io import BufferedIO
+from cleo.io.outputs.output import Verbosity
from poetry.core.packages.package import Package
from poetry.core.packages.utils.link import Link
+from poetry.factory import Factory
+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.repositories.repository_pool import RepositoryPool
+from poetry.installation.wheel_installer import WheelInstaller
+from poetry.repositories.pool import RepositoryPool
from poetry.utils.env import MockEnv
from tests.repositories.test_pypi_repository import MockRepository
@@ -37,6 +47,43 @@
from tests.types import FixtureDirGetter
+class Chef(BaseChef):
+ _directory_wheels: list[Path] | None = None
+ _sdist_wheels: list[Path] | None = None
+
+ def set_directory_wheel(self, wheels: Path | list[Path]) -> None:
+ if not isinstance(wheels, list):
+ wheels = [wheels]
+
+ self._directory_wheels = wheels
+
+ def set_sdist_wheel(self, wheels: Path | list[Path]) -> None:
+ if not isinstance(wheels, list):
+ wheels = [wheels]
+
+ self._sdist_wheels = wheels
+
+ def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path:
+ if self._sdist_wheels is not None:
+ wheel = self._sdist_wheels.pop(0)
+ self._sdist_wheels.append(wheel)
+
+ return wheel
+
+ return super()._prepare_sdist(archive)
+
+ def _prepare(
+ self, directory: Path, destination: Path, *, editable: bool = False
+ ) -> Path:
+ if self._directory_wheels is not None:
+ wheel = self._directory_wheels.pop(0)
+ self._directory_wheels.append(wheel)
+
+ return wheel
+
+ return super()._prepare(directory, destination, editable=editable)
+
+
@pytest.fixture
def env(tmp_dir: str) -> MockEnv:
path = Path(tmp_dir) / ".venv"
@@ -85,12 +132,18 @@ def mock_file_downloads(http: type[httpretty.httpretty]) -> None:
def callback(
request: HTTPrettyRequest, uri: str, headers: dict[str, Any]
) -> list[int | dict[str, Any] | str]:
+ 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
)
- with fixture.open("rb") as f:
- return [200, headers, f.read()]
+ if not fixture.exists():
+ fixture = Path(__file__).parent.parent.joinpath(
+ "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
+ )
+
+ return [200, headers, fixture.read_bytes()]
http.register_uri(
http.GET,
@@ -99,6 +152,41 @@ def callback(
)
+@pytest.fixture()
+def copy_wheel(tmp_dir: Path) -> Callable[[], Path]:
+ def _copy_wheel() -> Path:
+ tmp_name = tempfile.mktemp()
+ Path(tmp_dir).joinpath(tmp_name).mkdir()
+
+ shutil.copyfile(
+ Path(__file__)
+ .parent.parent.joinpath(
+ "fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl"
+ )
+ .as_posix(),
+ Path(tmp_dir)
+ .joinpath(tmp_name)
+ .joinpath("demo-0.1.2-py2.py3-none-any.whl")
+ .as_posix(),
+ )
+
+ return (
+ Path(tmp_dir).joinpath(tmp_name).joinpath("demo-0.1.2-py2.py3-none-any.whl")
+ )
+
+ return _copy_wheel
+
+
+@pytest.fixture()
+def wheel(copy_wheel: Callable[[], Path]) -> Path:
+ archive = copy_wheel()
+
+ yield archive
+
+ if archive.exists():
+ archive.unlink()
+
+
def test_execute_executes_a_batch_of_operations(
mocker: MockerFixture,
config: Config,
@@ -107,12 +195,21 @@ def test_execute_executes_a_batch_of_operations(
tmp_dir: str,
mock_file_downloads: None,
env: MockEnv,
+ copy_wheel: Callable[[], Path],
):
- pip_install = mocker.patch("poetry.installation.executor.pip_install")
+ wheel_install = mocker.patch.object(WheelInstaller, "install")
config.merge({"cache-dir": tmp_dir})
+ prepare_spy = mocker.spy(Chef, "_prepare")
+ chef = Chef(config, env)
+ chef.set_directory_wheel([copy_wheel(), copy_wheel()])
+ chef.set_sdist_wheel(copy_wheel())
+
+ io.set_verbosity(Verbosity.VERY_VERBOSE)
+
executor = Executor(env, pool, config, io)
+ executor._chef = chef
file_package = Package(
"demo",
@@ -171,11 +268,16 @@ def test_execute_executes_a_batch_of_operations(
expected = set(expected.splitlines())
output = set(io.fetch_output().splitlines())
assert output == expected
- assert len(env.executed) == 1
+ assert wheel_install.call_count == 5
+ # Two pip uninstalls: one for the remove operation one for the update operation
+ assert len(env.executed) == 2
assert return_code == 0
- assert pip_install.call_count == 5
- assert pip_install.call_args.kwargs.get("upgrade", False)
- assert pip_install.call_args.kwargs.get("editable", False)
+
+ assert prepare_spy.call_count == 2
+ assert prepare_spy.call_args_list == [
+ mocker.call(chef, mocker.ANY, mocker.ANY, editable=False),
+ mocker.call(chef, mocker.ANY, mocker.ANY, editable=True),
+ ]
@pytest.mark.parametrize(
@@ -222,8 +324,9 @@ def test_execute_prints_warning_for_yanked_package(
"(black-21.11b0-py3-none-any.whl) is yanked. Reason for being yanked: "
"Broken regex dependency. Use 21.11b1 instead."
)
+ output = io.fetch_output()
error = io.fetch_error()
- assert return_code == 0
+ assert return_code == 0, f"\noutput: {output}\nerror: {error}\n"
assert "pytest" not in error
if has_warning:
assert expected in error
@@ -289,7 +392,6 @@ def test_execute_should_show_errors(
def test_execute_works_with_ansi_output(
- mocker: MockerFixture,
config: Config,
pool: RepositoryPool,
io_decorated: BufferedIO,
@@ -301,24 +403,19 @@ def test_execute_works_with_ansi_output(
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.1")),
+ Install(Package("cleo", "1.0.0a5")),
]
)
- env._run.assert_called_once()
# fmt: off
expected = [
"\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals", # noqa: E501
- "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", # noqa: E501
- "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", # noqa: E501
- "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", # noqa: E501
- "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.1\x1b[39m\x1b[39m)\x1b[39m", # finished # noqa: E501
+ "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", # noqa: E501
+ "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", # noqa: E501
+ "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m1.0.0a5\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", # noqa: E501
+ "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mcleo\x1b[39m\x1b[39m (\x1b[39m\x1b[32m1.0.0a5\x1b[39m\x1b[39m)\x1b[39m", # finished # noqa: E501
]
# fmt: on
@@ -343,21 +440,16 @@ def test_execute_works_with_no_ansi_output(
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.1")),
+ Install(Package("cleo", "1.0.0a5")),
]
)
- env._run.assert_called_once()
expected = """
Package operations: 1 install, 0 updates, 0 removals
- • Installing pytest (3.5.1)
+ • Installing cleo (1.0.0a5)
"""
expected = set(expected.splitlines())
output = set(io_not_decorated.fetch_output().splitlines())
@@ -539,14 +631,26 @@ def test_executor_should_write_pep610_url_references_for_files(
def test_executor_should_write_pep610_url_references_for_directories(
- tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO
+ tmp_venv: VirtualEnv,
+ pool: RepositoryPool,
+ config: Config,
+ io: BufferedIO,
+ wheel: Path,
):
- 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._chef = chef
executor.execute([Install(package)])
verify_installed_distribution(
tmp_venv, package, {"dir_info": {}, "url": url.as_uri()}
@@ -554,18 +658,30 @@ def test_executor_should_write_pep610_url_references_for_directories(
def test_executor_should_write_pep610_url_references_for_editable_directories(
- tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO
+ tmp_venv: VirtualEnv,
+ pool: RepositoryPool,
+ config: Config,
+ io: BufferedIO,
+ wheel: Path,
):
- 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",
+ "demo",
+ "0.1.2",
source_type="directory",
source_url=url.as_posix(),
develop=True,
)
+ chef = Chef(config, tmp_venv)
+ chef.set_directory_wheel(wheel)
+
executor = Executor(tmp_venv, pool, config, io)
+ executor._chef = chef
executor.execute([Install(package)])
verify_installed_distribution(
tmp_venv, package, {"dir_info": {"editable": True}, "url": url.as_uri()}
@@ -599,6 +715,7 @@ def test_executor_should_write_pep610_url_references_for_git(
config: Config,
io: BufferedIO,
mock_file_downloads: None,
+ wheel: Path,
):
package = Package(
"demo",
@@ -609,7 +726,11 @@ 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._chef = chef
executor.execute([Install(package)])
verify_installed_distribution(
tmp_venv,
@@ -631,10 +752,11 @@ def test_executor_should_write_pep610_url_references_for_git_with_subdirectories
config: Config,
io: BufferedIO,
mock_file_downloads: None,
+ wheel: Path,
):
package = Package(
- "two",
- "2.0.0",
+ "demo",
+ "0.1.2",
source_type="git",
source_reference="master",
source_resolved_reference="123456",
@@ -642,7 +764,11 @@ def test_executor_should_write_pep610_url_references_for_git_with_subdirectories
source_subdirectory="two",
)
+ chef = Chef(config, tmp_venv)
+ chef.set_directory_wheel(wheel)
+
executor = Executor(tmp_venv, pool, config, io)
+ executor._chef = chef
executor.execute([Install(package)])
verify_installed_distribution(
tmp_venv,
@@ -722,7 +848,7 @@ def test_executor_should_be_initialized_with_correct_workers(
assert executor._max_workers == expected_workers
-def test_executer_fallback_on_poetry_create_error(
+def test_executor_fallback_on_poetry_create_error_without_wheel_installer(
mocker: MockerFixture,
config: Config,
pool: RepositoryPool,
@@ -740,7 +866,12 @@ def test_executer_fallback_on_poetry_create_error(
"poetry.factory.Factory.create_poetry", side_effect=RuntimeError
)
- config.merge({"cache-dir": tmp_dir})
+ config.merge(
+ {
+ "cache-dir": tmp_dir,
+ "installer": {"modern-installation": False},
+ }
+ )
executor = Executor(env, pool, config, io)
@@ -776,3 +907,71 @@ def test_executer_fallback_on_poetry_create_error(
assert mock_pip_install.call_count == 1
assert mock_pip_install.call_args[1].get("upgrade") is True
assert mock_pip_install.call_args[1].get("editable") is False
+
+
+@pytest.mark.parametrize("failing_method", ["build", "get_requires_for_build"])
+def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess(
+ failing_method: str,
+ mocker: MockerFixture,
+ config: Config,
+ pool: RepositoryPool,
+ io: BufferedIO,
+ tmp_dir: str,
+ mock_file_downloads: None,
+ env: MockEnv,
+):
+ mocker.patch.object(Factory, "create_pool", return_value=pool)
+
+ error = BuildBackendException(
+ CalledProcessError(1, ["pip"], output=b"Error on stdout")
+ )
+ mocker.patch.object(ProjectBuilder, failing_method, side_effect=error)
+ io.set_verbosity(Verbosity.NORMAL)
+
+ executor = Executor(env, pool, config, io)
+
+ package_name = "simple-project"
+ package_version = "1.2.3"
+ directory_package = Package(
+ package_name,
+ package_version,
+ source_type="directory",
+ source_url=Path(__file__)
+ .parent.parent.joinpath("fixtures/simple_project")
+ .resolve()
+ .as_posix(),
+ )
+
+ return_code = executor.execute(
+ [
+ Install(directory_package),
+ ]
+ )
+
+ assert return_code == 1
+
+ package_url = directory_package.source_url
+ expected_start = f"""
+Package operations: 1 install, 0 updates, 0 removals
+
+ • Installing {package_name} ({package_version} {package_url})
+
+ ChefBuildError
+
+ Backend operation failed: CalledProcessError(1, ['pip'])
+ \
+
+ Error on stdout
+"""
+
+ requirement = directory_package.to_dependency().to_pep_508()
+ expected_end = f"""
+Note: This error originates from the build backend, and is likely not a problem with \
+poetry but with {package_name} ({package_version} {package_url}) not supporting \
+PEP 517 builds. You can verify this by running 'pip wheel --use-pep517 "{requirement}"'.
+
+"""
+
+ output = io.fetch_output()
+ assert output.startswith(expected_start)
+ assert output.endswith(expected_end)
diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py
new file mode 100644
index 00000000000..7fc4826f940
--- /dev/null
+++ b/tests/installation/test_wheel_installer.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import re
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+import pytest
+
+from poetry.core.constraints.version import parse_constraint
+
+from poetry.installation.wheel_installer import WheelInstaller
+from poetry.utils.env import MockEnv
+
+
+if TYPE_CHECKING:
+ from _pytest.tmpdir import TempPathFactory
+
+ from tests.types import FixtureDirGetter
+
+
+@pytest.fixture
+def env(tmp_path: Path) -> MockEnv:
+ return MockEnv(path=tmp_path)
+
+
+@pytest.fixture(scope="module")
+def demo_wheel(fixture_dir: FixtureDirGetter) -> Path:
+ return fixture_dir("distributions/demo-0.1.0-py2.py3-none-any.whl")
+
+
+@pytest.fixture(scope="module")
+def default_installation(tmp_path_factory: TempPathFactory, demo_wheel: Path) -> Path:
+ env = MockEnv(path=tmp_path_factory.mktemp("default_install"))
+ installer = WheelInstaller(env)
+ installer.install(demo_wheel)
+ return Path(env.paths["purelib"])
+
+
+def test_default_installation_source_dir_content(default_installation: Path) -> None:
+ source_dir = default_installation / "demo"
+ assert source_dir.exists()
+ assert (source_dir / "__init__.py").exists()
+
+
+def test_default_installation_dist_info_dir_content(default_installation: Path) -> None:
+ dist_info_dir = default_installation / "demo-0.1.0.dist-info"
+ assert dist_info_dir.exists()
+ assert (dist_info_dir / "INSTALLER").exists()
+ assert (dist_info_dir / "METADATA").exists()
+ assert (dist_info_dir / "RECORD").exists()
+ assert (dist_info_dir / "WHEEL").exists()
+
+
+def test_installer_file_contains_valid_version(default_installation: Path) -> None:
+ installer_file = default_installation / "demo-0.1.0.dist-info" / "INSTALLER"
+ with open(installer_file) as f:
+ installer_content = f.read()
+ match = re.match(r"Poetry (?P.*)", installer_content)
+ assert match
+ parse_constraint(match.group("version")) # must not raise an error
+
+
+def test_default_installation_no_bytecode(default_installation: Path) -> None:
+ cache_dir = default_installation / "demo" / "__pycache__"
+ assert not cache_dir.exists()
+
+
+@pytest.mark.parametrize("compile", [True, False])
+def test_enable_bytecode_compilation(
+ env: MockEnv, demo_wheel: Path, compile: bool
+) -> None:
+ installer = WheelInstaller(env)
+ installer.enable_bytecode_compilation(compile)
+ installer.install(demo_wheel)
+ cache_dir = Path(env.paths["purelib"]) / "demo" / "__pycache__"
+ if compile:
+ assert cache_dir.exists()
+ assert list(cache_dir.glob("*.pyc"))
+ else:
+ assert not cache_dir.exists()
diff --git a/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl
index f0e3956e20f..50daa412ff7 100644
Binary files a/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl and b/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/dists/cleo-1.0.0a5-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/cleo-1.0.0a5-py3-none-any.whl
new file mode 100644
index 00000000000..42ec0d1f0d1
Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/cleo-1.0.0a5-py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.5.0-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.5.0-py3-none-any.whl
new file mode 100644
index 00000000000..abb480016b6
Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.5.0-py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.0-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.0-py2.py3-none-any.whl
new file mode 100644
index 00000000000..911f902125d
Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.0-py2.py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl
new file mode 100644
index 00000000000..c3a868e5caa
Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json b/tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json
new file mode 100644
index 00000000000..570ed2f108b
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/cleo/1.0.0a5.json
@@ -0,0 +1,90 @@
+{
+ "info": {
+ "author": "Sébastien Eustace",
+ "author_email": "sebastien@eustace.io",
+ "bugtrack_url": null,
+ "classifiers": [
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9"
+ ],
+ "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/cleo",
+ "keywords": "cli,commands",
+ "license": "MIT",
+ "maintainer": "",
+ "maintainer_email": "",
+ "name": "cleo",
+ "package_url": "https://pypi.org/project/cleo/",
+ "platform": null,
+ "project_url": "https://pypi.org/project/cleo/",
+ "project_urls": {
+ "Homepage": "https://github.com/python-poetry/cleo"
+ },
+ "release_url": "https://pypi.org/project/cleo/1.0.0a5/",
+ "requires_dist": [
+ "crashtest (>=0.3.1,<0.4.0)",
+ "pylev (>=1.3.0,<2.0.0)"
+ ],
+ "requires_python": ">=3.7,<4.0",
+ "summary": "Cleo allows you to create beautiful and testable command-line interfaces.",
+ "version": "1.0.0a5",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ "last_serial": 14027784,
+ "urls": [
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "4c78006d13e68c0c0796b954b1df0a3f",
+ "sha256": "ff53056589300976e960f75afb792dfbfc9c78dcbb5a448e207a17b643826360"
+ },
+ "downloads": -1,
+ "filename": "cleo-1.0.0a5-py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "4c78006d13e68c0c0796b954b1df0a3f",
+ "packagetype": "bdist_wheel",
+ "python_version": "py3",
+ "requires_python": ">=3.7,<4.0",
+ "size": 78701,
+ "upload_time": "2022-06-03T20:16:19",
+ "upload_time_iso_8601": "2022-06-03T20:16:19.386916Z",
+ "url": "https://files.pythonhosted.org/packages/45/0c/3825603bf62f360829b1eea29a43dadce30829067e288170b3bf738aafd0/cleo-1.0.0a5-py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "md5": "90e60b2ad117d3534f92a4ce37f9f462",
+ "sha256": "097c9d0e0332fd53cc89fc11eb0a6ba0309e6a3933c08f7b38558555486925d3"
+ },
+ "downloads": -1,
+ "filename": "cleo-1.0.0a5.tar.gz",
+ "has_sig": false,
+ "md5_digest": "90e60b2ad117d3534f92a4ce37f9f462",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": ">=3.7,<4.0",
+ "size": 61431,
+ "upload_time": "2022-06-03T20:16:21",
+ "upload_time_iso_8601": "2022-06-03T20:16:21.133890Z",
+ "url": "https://files.pythonhosted.org/packages/2f/16/1c1902b225756745f9860451a44a2e2a3c26ee91c72295e83c63df605ed1/cleo-1.0.0a5.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ],
+ "vulnerabilities": []
+}
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..2796a50aadb
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json
@@ -0,0 +1,27 @@
+{
+ "name": "importlib-metadata",
+ "files": [
+ {
+ "hashes": {
+ "md5": "8ae1f31228e29443c08e07501a99d1b8",
+ "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
+ },
+ "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl",
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl"
+ },
+ {
+ "hashes": {
+ "md5": "4505ea85600cca1e693a4f8f5dd27ba8",
+ "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"
+ },
+ "filename": "importlib_metadata-1.7.0.tar.gz",
+ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+ "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz"
+ }
+ ],
+ "meta": {
+ "api-version": "1.0",
+ "_last-serial": 3879671
+ }
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.json
new file mode 100644
index 00000000000..595674750f8
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata/1.7.0.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..38b248a3c1d
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/poetry-core.json
@@ -0,0 +1,27 @@
+{
+ "files": [
+ {
+ "filename": "poetry_core-1.5.0-py3-none-any.whl",
+ "hashes": {
+ "sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84"
+ },
+ "requires-python": ">=3.7,<4.0",
+ "url": "https://files.pythonhosted.org/packages/2d/99/6b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73/poetry_core-1.5.0-py3-none-any.whl",
+ "yanked": false
+ },
+ {
+ "filename": "poetry_core-1.5.0.tar.gz",
+ "hashes": {
+ "sha256": "253521bb7104e1df81f64d7b49ea1825057c91fa156d7d0bd752fefdad6f8c7a"
+ },
+ "requires-python": ">=3.7,<4.0",
+ "url": "https://files.pythonhosted.org/packages/57/bb/2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69/poetry_core-1.5.0.tar.gz",
+ "yanked": false
+ }
+ ],
+ "meta": {
+ "_last-serial": 16600250,
+ "api-version": "1.0"
+ },
+ "name": "poetry-core"
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json b/tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json
new file mode 100644
index 00000000000..9cfc450c625
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/poetry-core/1.5.0.json
@@ -0,0 +1,96 @@
+{
+ "info": {
+ "author": "Sébastien Eustace",
+ "author_email": "sebastien@eustace.io",
+ "bugtrack_url": null,
+ "classifiers": [
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "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": null,
+ "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.5.0/",
+ "requires_dist": [
+ "importlib-metadata (>=1.7.0) ; python_version < \"3.8\""
+ ],
+ "requires_python": ">=3.7,<4.0",
+ "summary": "Poetry PEP 517 Build Backend",
+ "version": "1.5.0",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ "last_serial": 16600250,
+ "urls": [
+ {
+ "comment_text": "",
+ "digests": {
+ "blake2b_256": "2d996b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73",
+ "md5": "be7589b4902793e66d7d979bd8581591",
+ "sha256": "e216b70f013c47b82a72540d34347632c5bfe59fd54f5fe5d51f6a68b19aaf84"
+ },
+ "downloads": -1,
+ "filename": "poetry_core-1.5.0-py3-none-any.whl",
+ "has_sig": false,
+ "md5_digest": "be7589b4902793e66d7d979bd8581591",
+ "packagetype": "bdist_wheel",
+ "python_version": "py3",
+ "requires_python": ">=3.7,<4.0",
+ "size": 464992,
+ "upload_time": "2023-01-28T10:52:52",
+ "upload_time_iso_8601": "2023-01-28T10:52:52.445537Z",
+ "url": "https://files.pythonhosted.org/packages/2d/99/6b0c5fe90e247b2b7b96a27cdf39ee59a02aab3c01d7243fc0c63cd7fb73/poetry_core-1.5.0-py3-none-any.whl",
+ "yanked": false,
+ "yanked_reason": null
+ },
+ {
+ "comment_text": "",
+ "digests": {
+ "blake2b_256": "57bb2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69",
+ "md5": "481671a4895af7cdda4944eab67f3843",
+ "sha256": "253521bb7104e1df81f64d7b49ea1825057c91fa156d7d0bd752fefdad6f8c7a"
+ },
+ "downloads": -1,
+ "filename": "poetry_core-1.5.0.tar.gz",
+ "has_sig": false,
+ "md5_digest": "481671a4895af7cdda4944eab67f3843",
+ "packagetype": "sdist",
+ "python_version": "source",
+ "requires_python": ">=3.7,<4.0",
+ "size": 448812,
+ "upload_time": "2023-01-28T10:52:53",
+ "upload_time_iso_8601": "2023-01-28T10:52:53.916268Z",
+ "url": "https://files.pythonhosted.org/packages/57/bb/2435fef60bb01f6c0891d9482c7053b50e90639f0f74d7658e99bdd4da69/poetry_core-1.5.0.tar.gz",
+ "yanked": false,
+ "yanked_reason": null
+ }
+ ],
+ "vulnerabilities": []
+}
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..085679a0c6e
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/zipp.json
@@ -0,0 +1,27 @@
+{
+ "name": "zipp",
+ "files": [
+ {
+ "hashes": {
+ "md5": "0ec47fbf522751f6c5fa904cb33f1f59",
+ "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"
+ },
+ "filename": "zipp-3.5.0-py3-none-any.whl",
+ "requires_python": ">=3.6",
+ "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl"
+ },
+ {
+ "hashes": {
+ "md5": "617efbf3edb707c57008ec00f408972f",
+ "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
+ },
+ "filename": "zipp-3.5.0.tar.gz",
+ "requires_python": ">=3.6",
+ "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz"
+ }
+ ],
+ "meta": {
+ "api-version": "1.0",
+ "_last-serial": 3879671
+ }
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.json b/tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.json
new file mode 100644
index 00000000000..356bd2b3483
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/zipp/3.5.0.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/repositories/test_pypi_repository.py b/tests/repositories/test_pypi_repository.py
index bff1a4fb5d5..35468e057b5 100644
--- a/tests/repositories/test_pypi_repository.py
+++ b/tests/repositories/test_pypi_repository.py
@@ -16,11 +16,14 @@
from requests.models import Response
from poetry.factory import Factory
+from poetry.repositories.exceptions import PackageNotFound
+from poetry.repositories.link_sources.json import SimpleJsonPage
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.utils._compat import encode
if TYPE_CHECKING:
+ from packaging.utils import NormalizedName
from pytest_mock import MockerFixture
@@ -36,6 +39,14 @@ class MockRepository(PyPiRepository):
def __init__(self, fallback: bool = False) -> None:
super().__init__(url="http://foo.bar", disable_cache=True, fallback=fallback)
+ def get_json_page(self, name: NormalizedName) -> SimpleJsonPage:
+ fixture = self.JSON_FIXTURES / (name + ".json")
+
+ if not fixture.exists():
+ raise PackageNotFound(f"Package [{name}] not found.")
+
+ return SimpleJsonPage("", json.loads(fixture.read_text()))
+
def _get(
self, url: str, headers: dict[str, str] | None = None
) -> dict[str, Any] | None:
diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py
index 23cbbda3e7f..1d9f6c3ba63 100644
--- a/tests/utils/test_env.py
+++ b/tests/utils/test_env.py
@@ -1155,7 +1155,8 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable(
del os.environ["VIRTUAL_ENV"]
version = Version.from_parts(*sys.version_info[:3])
- poetry.package.python_versions = f"~{version.major}.{version.minor-1}.0"
+ poetry.package.python_versions = f"~{version.major}.{version.minor - 1}.0"
+ venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
check_output = mocker.patch(
"subprocess.check_output",
@@ -1266,6 +1267,7 @@ def test_system_env_has_correct_paths():
assert paths.get("platlib") is not None
assert paths.get("scripts") is not None
assert env.site_packages.path == Path(paths["purelib"])
+ assert paths["include"] is not None
@pytest.mark.parametrize(
@@ -1287,6 +1289,11 @@ def test_venv_has_correct_paths(tmp_venv: VirtualEnv):
assert paths.get("platlib") is not None
assert paths.get("scripts") is not None
assert tmp_venv.site_packages.path == Path(paths["purelib"])
+ assert paths["include"] == str(
+ tmp_venv.path.joinpath(
+ f"include/site/python{tmp_venv.version_info[0]}.{tmp_venv.version_info[1]}"
+ )
+ )
def test_env_system_packages(tmp_path: Path, poetry: Poetry):
diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py
index 835298fa405..dad06297609 100644
--- a/tests/utils/test_helpers.py
+++ b/tests/utils/test_helpers.py
@@ -73,7 +73,7 @@ def test_parse_requires():
def test_default_hash(fixture_dir: FixtureDirGetter) -> None:
root_dir = Path(__file__).parent.parent.parent
file_path = root_dir / fixture_dir("distributions/demo-0.1.0.tar.gz")
- sha_256 = "72e8531e49038c5f9c4a837b088bfcb8011f4a9f76335c8f0654df6ac539b3d6"
+ sha_256 = "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad"
assert get_file_hash(file_path) == sha_256
@@ -88,40 +88,40 @@ def test_default_hash(fixture_dir: FixtureDirGetter) -> None:
[
(hash_name, value)
for hash_name, value in [
- ("sha224", "972d02f36539a98599aed0566bc8aaf3e6701f4e895dd797d8f5248e"),
+ ("sha224", "d26bd24163fe91c16b4b0162e773514beab77b76114d9faf6a31e350"),
(
"sha3_512",
- "c04ee109ae52d6440445e24dbd6d244a1d0f0289ef79cb7ba9bc3c139c0237169af9a8f61cd1cf4fc17f853ddf84f97c475ac5bb6c91a4aff0b825b884d4896c", # noqa: E501
+ "196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728", # noqa: E501
),
(
"blake2s",
- "c336ecbc9d867c9d860accfba4c3723c51c4b5c47a1e0a955e1c8df499e36741",
+ "6dd9007d36c106defcf362cc637abeca41e8e93999928c8fcfaba515ed33bc93",
),
(
"sha3_384",
- "d4abb2459941369aabf8880c5287b7eeb80678e14f13c71b9ecf64c772029dc3f93939590bea9ecdb51a1d1a74fefc5a", # noqa: E501
+ "787264d7885a0c305d2ee4daecfff435d11818399ef96cacef7e7c6bb638ce475f630d39fdd2800ca187dcd0071dc410", # noqa: E501
),
(
"blake2b",
- "48e70abac547ab38e2330e6e6743a0c0f6274dcaa6df2c98135a78a9dd5b04a072d551fc3851b34da03eb0bf50dd71c7f32a8c36956e99fd6c66491bc7844800", # noqa: E501
+ "077a34e8252c8f6776bddd0d34f321cc52762cb4c11a1c7aa9b6168023f1722caf53c9f029074a6eb990a8de341d415dd986293bc2a2fccddad428be5605696e", # noqa: E501
),
(
"sha256",
- "72e8531e49038c5f9c4a837b088bfcb8011f4a9f76335c8f0654df6ac539b3d6",
+ "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad",
),
(
"sha512",
- "e08a00a4b86358e49a318e7e3ba7a3d2fabdd17a2fef95559a0af681ea07ab1296b0b8e11e645297da296290661dc07ae3c8f74eab66bd18a80dce0c0ccb355b", # noqa: E501
+ "766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", # noqa: E501
),
(
"sha384",
- "aa3144e28c6700a83247e8ec8711af5d3f5f75997990d48ec41e66bd275b3d0e19ee6f2fe525a358f874aa717afd06a9", # noqa: E501
+ "c638f32460f318035e4600284ba64fb531630740aebd33885946e527002d742787ff09eb65fd81bc34ce5ff5ef11cfe8", # noqa: E501
),
- ("sha3_224", "64bfc6e4125b4c6d67fd88ad1c7d1b5c4dc11a1970e433cd576c91d4"),
- ("sha1", "4c057579005ac3e68e951a11ffdc4b27c6ae16af"),
+ ("sha3_224", "72980fc7bdf8c4d34268dc469442b09e1ccd2a8ff390954fc4d55a5a"),
+ ("sha1", "91b585bd38f72d7ceedb07d03f94911b772fdc4c"),
(
"sha3_256",
- "ba3d2a964b0680b6dc9565a03952e29c294c785d5a2307d3e2d785d73b75ed7e",
+ "7da5c08b416e6bcb339d6bedc0fe077c6e69af00607251ef4424c356ea061fcb",
),
]
if hash_name in algorithms_guaranteed