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