From 852000f8c1bab7917fb65912e5c914cf7d2d9930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Fri, 31 Mar 2023 21:44:13 +0200 Subject: [PATCH 01/20] docs: promote semantic versioning less aggressively --- docs/faq.md | 17 ++++++++++++++--- docs/libraries.md | 7 ++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 2a2051d294a..52e7a2be690 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -33,21 +33,32 @@ Once Poetry has cached the releases' information on your machine, the dependency will be much faster. {{% /note %}} -### Why are unbound version constraints a bad idea? +### Are unbound version constraints a bad idea? A version constraint without an upper bound such as `*` or `>=3.4` will allow updates to any future version of the dependency. This includes major versions breaking backward compatibility. Once a release of your package is published, you cannot tweak its dependencies anymore in case a dependency breaks BC – you have to do a new release but the previous one stays broken. +(Users can still work around the broken dependency by restricting it by themselves.) -The only good alternative is to define an upper bound on your constraints, +To avoid such issues you can define an upper bound on your constraints, which you can increase in a new release after testing that your package is compatible with the new major version of your dependency. -For example instead of using `>=3.4` you should use `^3.4` which allows all versions `<4.0`. +For example instead of using `>=3.4` you can use `^3.4` which allows all versions `<4.0`. The `^` operator works very well with libraries following [semantic versioning](https://semver.org). +However, when defining an upper bound, users of your package are not able to update +a dependency beyond the upper bound even if it does not break anything +and is fully compatible with your package. +You have to release a new version of your package with an increased upper bound first. + +If your package will be used as a library in other packages, it might be better to avoid +upper bounds and thus unnecessary dependency conflicts (unless you already know for sure +that the next release of the dependency will break your package). +If your package will be used as an application, it might be worth to define an upper bound. + ### Is tox supported? **Yes**. By using the [isolated builds](https://tox.readthedocs.io/en/latest/config.html#conf-isolated_build) `tox` provides, diff --git a/docs/libraries.md b/docs/libraries.md index ab56979540b..5ebe373cc94 100644 --- a/docs/libraries.md +++ b/docs/libraries.md @@ -19,10 +19,11 @@ This chapter will tell you how to make your library installable through Poetry. Poetry requires [PEP 440](https://peps.python.org/pep-0440)-compliant versions for all projects. -While Poetry does not enforce any release convention, it does encourage the use of +While Poetry does not enforce any release convention, it used to encourage the use of [semantic versioning](https://semver.org/) within the scope of -[PEP 440](https://peps.python.org/pep-0440/#semantic-versioning). This has many advantages for the end users -and allows them to set appropriate [version constraints]({{< relref "dependency-specification#version-constraints" >}}). +[PEP 440](https://peps.python.org/pep-0440/#semantic-versioning) and supports +[version constraints]({{< relref "dependency-specification/#caret-requirements" >}}) +that are especially suitable for semver. {{% note %}} From 28f708fbf3756d6596aa354dbe630ed5433fb351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Fri, 31 Mar 2023 21:44:50 +0200 Subject: [PATCH 02/20] docs: document Poetry's own versioning scheme --- docs/faq.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 52e7a2be690..0a8b97b3075 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -33,6 +33,28 @@ Once Poetry has cached the releases' information on your machine, the dependency will be much faster. {{% /note %}} +### What kind of versioning scheme does Poetry use for itself? + +Poetry uses "major.minor.micro" version identifiers as mentioned in +[PEP 440](https://peps.python.org/pep-0440/#final-releases). + +Version bumps are done similar to Python's versioning: +* A major version bump (incrementing the first number) is only done for breaking changes + if a deprecation cycle is not possible and many users have to perform some manual steps + to migrate from one version to the next one. +* A minor version bump (incrementing the second number) may include new features as well + as new deprecations and drop features deprecated in an earlier minor release. +* A micro version bump (incrementing the third number) usually only includes bug fixes. + Deprecated features will not be dropped in a micro release. + +### Why does Poetry not adhere to semantic versioning? + +Because of its large user base, even small changes not considered relevant by most users +can turn out to be a breaking change for some users in hindsight. +Sticking to strict [semantic versioning](https://semver.org) and (almost) always bumping +the major version instead of the minor version does not seem desirable +since the minor version will not carry any meaning anymore. + ### Are unbound version constraints a bad idea? A version constraint without an upper bound such as `*` or `>=3.4` will allow updates to any future version of the dependency. From c8e963e668af2f71ddce2053e575ae0427b363e6 Mon Sep 17 00:00:00 2001 From: alm Date: Sat, 1 Apr 2023 16:24:39 +0300 Subject: [PATCH 03/20] Improve the locker test coverage by testing git rev and tag (#7749) --- tests/packages/test_locker.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index 6f832dc8206..80b689b1570 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -874,6 +874,16 @@ def test_locker_dumps_dependency_information_correctly( }, ) ) + package_a.add_dependency( + Factory.create_dependency( + "H", {"git": "https://github.com/python-poetry/poetry.git", "tag": "baz"} + ) + ) + package_a.add_dependency( + Factory.create_dependency( + "I", {"git": "https://github.com/python-poetry/poetry.git", "rev": "spam"} + ) + ) packages = [package_a] @@ -901,6 +911,8 @@ def test_locker_dumps_dependency_information_correctly( E = {{url = "https://python-poetry.org/poetry-1.2.0.tar.gz"}} F = {{git = "https://github.com/python-poetry/poetry.git", branch = "foo"}} G = {{git = "https://github.com/python-poetry/poetry.git", subdirectory = "bar"}} +H = {{git = "https://github.com/python-poetry/poetry.git", tag = "baz"}} +I = {{git = "https://github.com/python-poetry/poetry.git", rev = "spam"}} [metadata] lock-version = "2.0" From c89e994eead3fa56f573cc08276f3e8ad74f6e02 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 3 Apr 2023 12:05:42 +0100 Subject: [PATCH 04/20] update poetry to work with poetry-core after tomlkit is removed from there (#6616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com> --- src/poetry/config/config.py | 2 +- src/poetry/config/file_config_source.py | 3 +- src/poetry/console/commands/check.py | 3 +- src/poetry/console/commands/config.py | 2 +- src/poetry/console/commands/init.py | 2 +- .../console/commands/self/self_command.py | 2 +- src/poetry/factory.py | 14 +++-- src/poetry/inspection/info.py | 2 +- src/poetry/installation/executor.py | 3 +- src/poetry/installation/pip_installer.py | 2 +- src/poetry/layouts/layout.py | 3 +- src/poetry/masonry/builders/editable.py | 14 ++--- src/poetry/packages/locker.py | 2 +- src/poetry/poetry.py | 22 ++++++- src/poetry/pyproject/__init__.py | 0 src/poetry/pyproject/toml.py | 62 +++++++++++++++++++ src/poetry/toml/__init__.py | 7 +++ src/poetry/toml/exceptions.py | 8 +++ src/poetry/toml/file.py | 41 ++++++++++++ src/poetry/utils/env.py | 2 +- tests/console/commands/env/test_list.py | 2 +- tests/console/commands/env/test_use.py | 2 +- .../console/commands/self/test_add_plugins.py | 22 ++++--- .../commands/self/test_remove_plugins.py | 3 +- tests/console/commands/test_check.py | 2 +- tests/helpers.py | 5 +- tests/installation/test_installer.py | 10 +-- tests/installation/test_installer_old.py | 10 +-- tests/integration/test_utils_vcs_git.py | 2 +- tests/json/test_schema_sources.py | 3 +- tests/pyproject/__init__.py | 0 tests/pyproject/conftest.py | 43 +++++++++++++ tests/pyproject/test_pyproject_toml.py | 47 ++++++++++++++ tests/pyproject/test_pyproject_toml_file.py | 28 +++++++++ tests/test_factory.py | 2 +- tests/utils/test_env.py | 2 +- 36 files changed, 318 insertions(+), 61 deletions(-) create mode 100644 src/poetry/pyproject/__init__.py create mode 100644 src/poetry/pyproject/toml.py create mode 100644 src/poetry/toml/__init__.py create mode 100644 src/poetry/toml/exceptions.py create mode 100644 src/poetry/toml/file.py create mode 100644 tests/pyproject/__init__.py create mode 100644 tests/pyproject/conftest.py create mode 100644 tests/pyproject/test_pyproject_toml.py create mode 100644 tests/pyproject/test_pyproject_toml_file.py diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index 49b4631fbf4..9fb220be675 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -11,12 +11,12 @@ from typing import Any from packaging.utils import canonicalize_name -from poetry.core.toml import TOMLFile from poetry.config.dict_config_source import DictConfigSource from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR from poetry.locations import DEFAULT_CACHE_DIR +from poetry.toml import TOMLFile if TYPE_CHECKING: diff --git a/src/poetry/config/file_config_source.py b/src/poetry/config/file_config_source.py index 7119fa19911..b3ccbb2f925 100644 --- a/src/poetry/config/file_config_source.py +++ b/src/poetry/config/file_config_source.py @@ -13,9 +13,10 @@ if TYPE_CHECKING: from collections.abc import Iterator - from poetry.core.toml.file import TOMLFile from tomlkit.toml_document import TOMLDocument + from poetry.toml.file import TOMLFile + class FileConfigSource(ConfigSource): def __init__(self, file: TOMLFile, auth_config: bool = False) -> None: diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index 8a53cd5bc09..3ca831e7213 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -58,9 +58,8 @@ def validate_classifiers( return errors, warnings def handle(self) -> int: - from poetry.core.pyproject.toml import PyProjectTOML - from poetry.factory import Factory + from poetry.pyproject.toml import PyProjectTOML # Load poetry config and display errors, if any poetry_file = self.poetry.file.path diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 281735a6f41..a35a8b5d263 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -86,11 +86,11 @@ def handle(self) -> int: from pathlib import Path from poetry.core.pyproject.exceptions import PyProjectException - from poetry.core.toml.file import TOMLFile from poetry.config.config import Config from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR + from poetry.toml.file import TOMLFile config = Config.create() config_file = TOMLFile(CONFIG_DIR / "config.toml") diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index fdf48832ce7..c77bff07468 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -73,11 +73,11 @@ def __init__(self) -> None: def handle(self) -> int: from pathlib import Path - from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.vcs.git import GitConfig from poetry.config.config import Config from poetry.layouts import layout + from poetry.pyproject.toml import PyProjectTOML from poetry.utils.env import EnvManager project_path = Path.cwd() diff --git a/src/poetry/console/commands/self/self_command.py b/src/poetry/console/commands/self/self_command.py index 5be8f8b65e7..db626f1ed4e 100644 --- a/src/poetry/console/commands/self/self_command.py +++ b/src/poetry/console/commands/self/self_command.py @@ -5,11 +5,11 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage -from poetry.core.pyproject.toml import PyProjectTOML from poetry.__version__ import __version__ from poetry.console.commands.installer_command import InstallerCommand from poetry.factory import Factory +from poetry.pyproject.toml import PyProjectTOML from poetry.utils.env import EnvManager from poetry.utils.env import SystemEnv from poetry.utils.helpers import directory diff --git a/src/poetry/factory.py b/src/poetry/factory.py index c8ec61d4cdd..a2e7205f229 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -12,7 +12,6 @@ from poetry.core.factory import Factory as BaseFactory from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.project_package import ProjectPackage -from poetry.core.toml.file import TOMLFile from poetry.config.config import Config from poetry.json import validate_object @@ -20,6 +19,7 @@ from poetry.plugins.plugin import Plugin from poetry.plugins.plugin_manager import PluginManager from poetry.poetry import Poetry +from poetry.toml.file import TOMLFile if TYPE_CHECKING: @@ -55,15 +55,19 @@ def create_poetry( base_poetry = super().create_poetry(cwd=cwd, with_groups=with_groups) - locker = Locker( - base_poetry.file.parent / "poetry.lock", base_poetry.local_config + # TODO: backward compatibility, can be simplified if poetry-core with + # https://github.com/python-poetry/poetry-core/pull/483 is available + poetry_file: Path = ( + getattr(base_poetry, "pyproject_path", None) or base_poetry.file.path ) + locker = Locker(poetry_file.parent / "poetry.lock", base_poetry.local_config) + # Loading global configuration config = Config.create() # Loading local configuration - local_config_file = TOMLFile(base_poetry.file.parent / "poetry.toml") + local_config_file = TOMLFile(poetry_file.parent / "poetry.toml") if local_config_file.exists(): if io.is_debug(): io.write_line(f"Loading configuration file {local_config_file.path}") @@ -82,7 +86,7 @@ def create_poetry( config.merge({"repositories": repositories}) poetry = Poetry( - base_poetry.file.path, + poetry_file, base_poetry.local_config, base_poetry.package, locker, diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index 0e3fb4b4977..e538a9cc979 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -17,12 +17,12 @@ from poetry.core.factory import Factory from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.utils.helpers import parse_requires from poetry.core.utils.helpers import temporary_directory from poetry.core.version.markers import InvalidMarker from poetry.core.version.requirements import InvalidRequirement +from poetry.pyproject.toml import PyProjectTOML from poetry.utils.env import EnvCommandError from poetry.utils.env import ephemeral_environment from poetry.utils.setup_reader import SetupReader diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 7f8b5701cd9..fc3efe37059 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -636,9 +636,8 @@ def _prepare_git_archive(self, operation: Install | Update) -> Path: def _install_directory_without_wheel_installer( self, operation: Install | Update ) -> int: - from poetry.core.pyproject.toml import PyProjectTOML - from poetry.factory import Factory + from poetry.pyproject.toml import PyProjectTOML package = operation.package operation_message = self.get_operation_message(operation) diff --git a/src/poetry/installation/pip_installer.py b/src/poetry/installation/pip_installer.py index a528c24f215..65c91bd15e3 100644 --- a/src/poetry/installation/pip_installer.py +++ b/src/poetry/installation/pip_installer.py @@ -11,9 +11,9 @@ from typing import Any from poetry.core.constraints.version import Version -from poetry.core.pyproject.toml import PyProjectTOML from poetry.installation.base_installer import BaseInstaller +from poetry.pyproject.toml import PyProjectTOML from poetry.repositories.http_repository import HTTPRepository from poetry.utils._compat import encode from poetry.utils.helpers import remove_directory diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py index 74dcebfedaa..21d4c29234b 100644 --- a/src/poetry/layouts/layout.py +++ b/src/poetry/layouts/layout.py @@ -5,13 +5,14 @@ from typing import Any from packaging.utils import canonicalize_name -from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.utils.helpers import module_name from tomlkit import inline_table from tomlkit import loads from tomlkit import table from tomlkit.toml_document import TOMLDocument +from poetry.pyproject.toml import PyProjectTOML + if TYPE_CHECKING: from typing import Mapping diff --git a/src/poetry/masonry/builders/editable.py b/src/poetry/masonry/builders/editable.py index d9725145814..81d577b1445 100644 --- a/src/poetry/masonry/builders/editable.py +++ b/src/poetry/masonry/builders/editable.py @@ -4,7 +4,6 @@ import hashlib import json import os -import shutil from base64 import urlsafe_b64encode from pathlib import Path @@ -44,6 +43,7 @@ class EditableBuilder(Builder): def __init__(self, poetry: Poetry, env: Env, io: IO) -> None: + self._poetry: Poetry super().__init__(poetry) self._env = env @@ -105,19 +105,15 @@ def _setup_build(self) -> None: pip_install(self._path, self._env, upgrade=True, editable=True) else: # Temporarily rename pyproject.toml - shutil.move( - str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp")) - ) + renamed_pyproject = self._poetry.file.with_suffix(".tmp") + self._poetry.file.path.rename(renamed_pyproject) try: pip_install(self._path, self._env, upgrade=True, editable=True) finally: - shutil.move( - str(self._poetry.file.with_suffix(".tmp")), - str(self._poetry.file), - ) + renamed_pyproject.rename(self._poetry.file.path) finally: if not has_setup: - os.remove(str(setup)) + os.remove(setup) def _add_pth(self) -> list[Path]: paths = { diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 3d0206afb73..8784a391ebd 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -16,7 +16,6 @@ from poetry.core.constraints.version import parse_constraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -from poetry.core.toml.file import TOMLFile from poetry.core.version.markers import parse_marker from poetry.core.version.requirements import InvalidRequirement from tomlkit import array @@ -26,6 +25,7 @@ from tomlkit import table from poetry.__version__ import __version__ +from poetry.toml.file import TOMLFile from poetry.utils._compat import tomllib diff --git a/src/poetry/poetry.py b/src/poetry/poetry.py index e9cb6e5db04..3cb227eaaaa 100644 --- a/src/poetry/poetry.py +++ b/src/poetry/poetry.py @@ -2,11 +2,13 @@ from typing import TYPE_CHECKING from typing import Any +from typing import cast from poetry.core.poetry import Poetry as BasePoetry from poetry.__version__ import __version__ from poetry.config.source import Source +from poetry.pyproject.toml import PyProjectTOML if TYPE_CHECKING: @@ -18,6 +20,7 @@ from poetry.packages.locker import Locker from poetry.plugins.plugin_manager import PluginManager from poetry.repositories.repository_pool import RepositoryPool + from poetry.toml import TOMLFile class Poetry(BasePoetry): @@ -34,7 +37,15 @@ def __init__( ) -> None: from poetry.repositories.repository_pool import RepositoryPool - super().__init__(file, local_config, package) + try: + super().__init__( # type: ignore[call-arg] + file, local_config, package, pyproject_type=PyProjectTOML + ) + except TypeError: + # TODO: backward compatibility, can be simplified if poetry-core with + # https://github.com/python-poetry/poetry-core/pull/483 is available + super().__init__(file, local_config, package) + self._pyproject = PyProjectTOML(file) self._locker = locker self._config = config @@ -42,6 +53,15 @@ def __init__( self._plugin_manager: PluginManager | None = None self._disable_cache = disable_cache + @property + def pyproject(self) -> PyProjectTOML: + pyproject = super().pyproject + return cast("PyProjectTOML", pyproject) + + @property + def file(self) -> TOMLFile: # type: ignore[override] + return self.pyproject.file + @property def locker(self) -> Locker: return self._locker diff --git a/src/poetry/pyproject/__init__.py b/src/poetry/pyproject/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/poetry/pyproject/toml.py b/src/poetry/pyproject/toml.py new file mode 100644 index 00000000000..3e9010b4c85 --- /dev/null +++ b/src/poetry/pyproject/toml.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from poetry.core.pyproject.toml import PyProjectTOML as BasePyProjectTOML +from tomlkit.api import table +from tomlkit.items import Table +from tomlkit.toml_document import TOMLDocument + +from poetry.toml import TOMLFile + + +if TYPE_CHECKING: + from pathlib import Path + + +class PyProjectTOML(BasePyProjectTOML): + """ + Enhanced version of poetry-core's PyProjectTOML + which is capable of writing pyproject.toml + + The poetry-core class uses tomli to read the file, + here we use tomlkit to preserve comments and formatting when writing. + """ + + def __init__(self, path: Path) -> None: + super().__init__(path) + self._toml_file = TOMLFile(path=path) + self._toml_document: TOMLDocument | None = None + + @property + def file(self) -> TOMLFile: # type: ignore[override] + return self._toml_file + + @property + def data(self) -> TOMLDocument: + if self._toml_document is None: + if not self.file.exists(): + self._toml_document = TOMLDocument() + else: + self._toml_document = self.file.read() + + return self._toml_document + + def save(self) -> None: + data = self.data + + if self._build_system is not None: + if "build-system" not in data: + data["build-system"] = table() + + build_system = data["build-system"] + assert isinstance(build_system, Table) + + build_system["requires"] = self._build_system.requires + build_system["build-backend"] = self._build_system.build_backend + + self.file.write(data=data) + + def reload(self) -> None: + self._toml_document = None + self._build_system = None diff --git a/src/poetry/toml/__init__.py b/src/poetry/toml/__init__.py new file mode 100644 index 00000000000..32aee9a2f04 --- /dev/null +++ b/src/poetry/toml/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from poetry.toml.exceptions import TOMLError +from poetry.toml.file import TOMLFile + + +__all__ = ["TOMLError", "TOMLFile"] diff --git a/src/poetry/toml/exceptions.py b/src/poetry/toml/exceptions.py new file mode 100644 index 00000000000..66fcec0063b --- /dev/null +++ b/src/poetry/toml/exceptions.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from poetry.core.exceptions import PoetryCoreException +from tomlkit.exceptions import TOMLKitError + + +class TOMLError(TOMLKitError, PoetryCoreException): + pass diff --git a/src/poetry/toml/file.py b/src/poetry/toml/file.py new file mode 100644 index 00000000000..79c2e018419 --- /dev/null +++ b/src/poetry/toml/file.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any + +from tomlkit.toml_file import TOMLFile as BaseTOMLFile + + +if TYPE_CHECKING: + from pathlib import Path + + from tomlkit.toml_document import TOMLDocument + + +class TOMLFile(BaseTOMLFile): + def __init__(self, path: Path) -> None: + super().__init__(path) + self.__path = path + + @property + def path(self) -> Path: + return self.__path + + def exists(self) -> bool: + return self.__path.exists() + + def read(self) -> TOMLDocument: + from tomlkit.exceptions import TOMLKitError + + from poetry.toml import TOMLError + + try: + return super().read() + except (ValueError, TOMLKitError) as e: + raise TOMLError(f"Invalid TOML file {self.path.as_posix()}: {e}") + + def __getattr__(self, item: str) -> Any: + return getattr(self.__path, item) + + def __str__(self) -> str: + return self.__path.as_posix() diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 734a7d64836..d5a96fac4c2 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -33,10 +33,10 @@ from packaging.tags import sys_tags from poetry.core.constraints.version import Version from poetry.core.constraints.version import parse_constraint -from poetry.core.toml.file import TOMLFile from poetry.core.utils.helpers import temporary_directory from virtualenv.seed.wheels.embed import get_embed_wheel +from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS from poetry.utils._compat import decode from poetry.utils._compat import encode diff --git a/tests/console/commands/env/test_list.py b/tests/console/commands/env/test_list.py index 4a69dac0c3f..6c949b66d50 100644 --- a/tests/console/commands/env/test_list.py +++ b/tests/console/commands/env/test_list.py @@ -5,7 +5,7 @@ import pytest import tomlkit -from poetry.core.toml.file import TOMLFile +from poetry.toml.file import TOMLFile if TYPE_CHECKING: diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py index 9cc5a9fc92a..72df646d97c 100644 --- a/tests/console/commands/env/test_use.py +++ b/tests/console/commands/env/test_use.py @@ -9,8 +9,8 @@ import tomlkit from poetry.core.constraints.version import Version -from poetry.core.toml.file import TOMLFile +from poetry.toml.file import TOMLFile from poetry.utils.env import MockEnv from tests.console.commands.env.helpers import build_venv from tests.console.commands.env.helpers import check_output_wrapper diff --git a/tests/console/commands/self/test_add_plugins.py b/tests/console/commands/self/test_add_plugins.py index e8447a32b13..37de59731f0 100644 --- a/tests/console/commands/self/test_add_plugins.py +++ b/tests/console/commands/self/test_add_plugins.py @@ -190,8 +190,10 @@ def test_add_existing_plugin_warns_about_no_operation( repo: TestRepository, installed: TestRepository, ): - SelfCommand.get_default_system_pyproject_file().write_text( - f"""\ + pyproject = SelfCommand.get_default_system_pyproject_file() + with open(pyproject, "w", encoding="utf-8", newline="") as f: + f.write( + f"""\ [tool.poetry] name = "poetry-instance" version = "1.2.0" @@ -203,9 +205,8 @@ def test_add_existing_plugin_warns_about_no_operation( [tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" -""", - encoding="utf-8", - ) +""" + ) installed.add_package(Package("poetry-plugin", "1.2.3")) @@ -230,8 +231,10 @@ def test_add_existing_plugin_updates_if_requested( repo: TestRepository, installed: TestRepository, ): - SelfCommand.get_default_system_pyproject_file().write_text( - f"""\ + pyproject = SelfCommand.get_default_system_pyproject_file() + with open(pyproject, "w", encoding="utf-8", newline="") as f: + f.write( + f"""\ [tool.poetry] name = "poetry-instance" version = "1.2.0" @@ -243,9 +246,8 @@ def test_add_existing_plugin_updates_if_requested( [tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" -""", - encoding="utf-8", - ) +""" + ) installed.add_package(Package("poetry-plugin", "1.2.3")) diff --git a/tests/console/commands/self/test_remove_plugins.py b/tests/console/commands/self/test_remove_plugins.py index 2b988443469..660b3584012 100644 --- a/tests/console/commands/self/test_remove_plugins.py +++ b/tests/console/commands/self/test_remove_plugins.py @@ -37,7 +37,8 @@ def install_plugin(installed: Repository) -> None: ) content = Factory.create_pyproject_from_package(package) system_pyproject_file = SelfCommand.get_default_system_pyproject_file() - system_pyproject_file.write_text(content.as_string(), encoding="utf-8") + with open(system_pyproject_file, "w", encoding="utf-8", newline="") as f: + f.write(content.as_string()) lock_content = { "package": [ diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index dbf665b6227..f23276c5c6d 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -29,7 +29,7 @@ def test_check_valid(tester: CommandTester): def test_check_invalid(mocker: MockerFixture, tester: CommandTester): - from poetry.core.toml import TOMLFile + from poetry.toml import TOMLFile mocker.patch( "poetry.poetry.Poetry.file", diff --git a/tests/helpers.py b/tests/helpers.py index feef37728c0..0bbaf414dde 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -12,7 +12,6 @@ from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link -from poetry.core.toml.file import TOMLFile from poetry.core.vcs.git import ParsedUrl from poetry.config.config import Config @@ -187,8 +186,8 @@ def reset_poetry(self) -> None: class TestLocker(Locker): - def __init__(self, lock: str | Path, local_config: dict) -> None: - self._lock = TOMLFile(lock) + def __init__(self, lock: Path, local_config: dict) -> None: + self._lock = lock self._local_config = local_config self._lock_data = None self._content_hash = self._get_content_hash() diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 76e11ecd611..98aac9c8430 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -18,7 +18,6 @@ from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage -from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.installation import Installer as BaseInstaller @@ -28,6 +27,7 @@ from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml.file import TOMLFile from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import MOCK_DEFAULT_GIT_REVISION @@ -101,8 +101,8 @@ def load( class Locker(BaseLocker): - def __init__(self, lock_path: str | Path) -> None: - self._lock = TOMLFile(Path(lock_path).joinpath("poetry.lock")) + def __init__(self, lock_path: Path) -> None: + self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False self._content_hash = self._get_content_hash() @@ -111,8 +111,8 @@ def __init__(self, lock_path: str | Path) -> None: def written_data(self) -> dict | None: return self._written_data - def set_lock_path(self, lock: str | Path) -> Locker: - self._lock = TOMLFile(Path(lock).joinpath("poetry.lock")) + def set_lock_path(self, lock: Path) -> Locker: + self._lock = lock / "poetry.lock" return self diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py index e76fabb21f3..02ea33bc89b 100644 --- a/tests/installation/test_installer_old.py +++ b/tests/installation/test_installer_old.py @@ -10,7 +10,6 @@ from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage -from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.installation import Installer as BaseInstaller @@ -19,6 +18,7 @@ from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml.file import TOMLFile from poetry.utils.env import MockEnv from poetry.utils.env import NullEnv from tests.helpers import get_dependency @@ -58,8 +58,8 @@ def load( class Locker(BaseLocker): - def __init__(self, lock_path: str | Path) -> None: - self._lock = TOMLFile(Path(lock_path).joinpath("poetry.lock")) + def __init__(self, lock_path: Path) -> None: + self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False self._content_hash = self._get_content_hash() @@ -68,8 +68,8 @@ def __init__(self, lock_path: str | Path) -> None: def written_data(self) -> dict | None: return self._written_data - def set_lock_path(self, lock: str | Path) -> Locker: - self._lock = TOMLFile(Path(lock).joinpath("poetry.lock")) + def set_lock_path(self, lock: Path) -> Locker: + self._lock = lock / "poetry.lock" return self diff --git a/tests/integration/test_utils_vcs_git.py b/tests/integration/test_utils_vcs_git.py index ec0f558f3fe..aaa864283ce 100644 --- a/tests/integration/test_utils_vcs_git.py +++ b/tests/integration/test_utils_vcs_git.py @@ -14,9 +14,9 @@ from dulwich.client import get_transport_and_path from dulwich.config import ConfigFile from dulwich.repo import Repo -from poetry.core.pyproject.toml import PyProjectTOML from poetry.console.exceptions import PoetryConsoleError +from poetry.pyproject.toml import PyProjectTOML from poetry.utils.authenticator import Authenticator from poetry.vcs.git import Git from poetry.vcs.git.backend import GitRefSpec diff --git a/tests/json/test_schema_sources.py b/tests/json/test_schema_sources.py index 4f20a0b3884..22769922e1c 100644 --- a/tests/json/test_schema_sources.py +++ b/tests/json/test_schema_sources.py @@ -2,9 +2,8 @@ from pathlib import Path -from poetry.core.toml import TOMLFile - from poetry.factory import Factory +from poetry.toml import TOMLFile FIXTURE_DIR = Path(__file__).parent / "fixtures" / "source" diff --git a/tests/pyproject/__init__.py b/tests/pyproject/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/pyproject/conftest.py b/tests/pyproject/conftest.py new file mode 100644 index 00000000000..82ff2198389 --- /dev/null +++ b/tests/pyproject/conftest.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + + +if TYPE_CHECKING: + from pathlib import Path + + +@pytest.fixture +def pyproject_toml(tmp_path: Path) -> Path: + path = tmp_path / "pyproject.toml" + with path.open(mode="w"): + pass + return path + + +@pytest.fixture +def build_system_section(pyproject_toml: Path) -> str: + content = """ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +""" + with pyproject_toml.open(mode="a") as f: + f.write(content) + return content + + +@pytest.fixture +def poetry_section(pyproject_toml: Path) -> str: + content = """ +[tool.poetry] +name = "poetry" + +[tool.poetry.dependencies] +python = "^3.5" +""" + with pyproject_toml.open(mode="a") as f: + f.write(content) + return content diff --git a/tests/pyproject/test_pyproject_toml.py b/tests/pyproject/test_pyproject_toml.py new file mode 100644 index 00000000000..4f85d91c18f --- /dev/null +++ b/tests/pyproject/test_pyproject_toml.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import uuid + +from typing import TYPE_CHECKING + +from poetry.pyproject.toml import PyProjectTOML + + +if TYPE_CHECKING: + from pathlib import Path + + +def test_pyproject_toml_reload(pyproject_toml: Path, poetry_section: str) -> None: + pyproject = PyProjectTOML(pyproject_toml) + name_original = pyproject.poetry_config["name"] + name_new = str(uuid.uuid4()) + + pyproject.poetry_config["name"] = name_new + assert isinstance(pyproject.poetry_config["name"], str) + assert pyproject.poetry_config["name"] == name_new + + pyproject.reload() + assert pyproject.poetry_config["name"] == name_original + + +def test_pyproject_toml_save( + pyproject_toml: Path, poetry_section: str, build_system_section: str +) -> None: + pyproject = PyProjectTOML(pyproject_toml) + + name = str(uuid.uuid4()) + build_backend = str(uuid.uuid4()) + build_requires = str(uuid.uuid4()) + + pyproject.poetry_config["name"] = name + pyproject.build_system.build_backend = build_backend + pyproject.build_system.requires.append(build_requires) + + pyproject.save() + + pyproject = PyProjectTOML(pyproject_toml) + + assert isinstance(pyproject.poetry_config["name"], str) + assert pyproject.poetry_config["name"] == name + assert pyproject.build_system.build_backend == build_backend + assert build_requires in pyproject.build_system.requires diff --git a/tests/pyproject/test_pyproject_toml_file.py b/tests/pyproject/test_pyproject_toml_file.py new file mode 100644 index 00000000000..1c7c02a1439 --- /dev/null +++ b/tests/pyproject/test_pyproject_toml_file.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from poetry.core.exceptions import PoetryCoreException + +from poetry.toml import TOMLFile + + +if TYPE_CHECKING: + from pathlib import Path + + +def test_pyproject_toml_file_invalid(pyproject_toml: Path) -> None: + with pyproject_toml.open(mode="a") as f: + f.write("<<<<<<<<<<<") + + with pytest.raises(PoetryCoreException) as excval: + _ = TOMLFile(pyproject_toml).read() + + assert f"Invalid TOML file {pyproject_toml.as_posix()}" in str(excval.value) + + +def test_pyproject_toml_file_getattr(tmp_path: Path, pyproject_toml: Path) -> None: + file = TOMLFile(pyproject_toml) + assert file.parent == tmp_path diff --git a/tests/test_factory.py b/tests/test_factory.py index 7eafc85d210..5b56ddc6200 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -8,12 +8,12 @@ from deepdiff import DeepDiff from packaging.utils import canonicalize_name from poetry.core.constraints.version import parse_constraint -from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.plugins.plugin import Plugin from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.pypi_repository import PyPiRepository +from poetry.toml.file import TOMLFile from tests.helpers import mock_metadata_entry_points diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index b720974212a..b41d22e6b53 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -13,10 +13,10 @@ import tomlkit from poetry.core.constraints.version import Version -from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS from poetry.utils.env import GET_BASE_PREFIX from poetry.utils.env import GET_PYTHON_VERSION_ONELINER From d07dddbc0e9c4bb4576023e1cfb71152c3672bbb Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 3 Apr 2023 13:12:31 +0100 Subject: [PATCH 05/20] installer tests should verify result of install (#7759) --- .../fixtures/with-pypi-repository.test | 134 ++++-------- tests/installation/test_installer.py | 205 +++++++++++------- tests/installation/test_installer_old.py | 175 +++++++++------ .../fixtures/pypi.org/json/setuptools.json | 28 ++- .../pypi.org/json/setuptools/67.6.1.json | 140 ++++++++++++ .../fixtures/pypi.org/json/wheel.json | 34 +++ .../fixtures/pypi.org/json/wheel/0.40.0.json | 98 +++++++++ 7 files changed, 585 insertions(+), 229 deletions(-) create mode 100644 tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json create mode 100644 tests/repositories/fixtures/pypi.org/json/wheel.json create mode 100644 tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json diff --git a/tests/installation/fixtures/with-pypi-repository.test b/tests/installation/fixtures/with-pypi-repository.test index 03444764ee9..de60c4ffce2 100644 --- a/tests/installation/fixtures/with-pypi-repository.test +++ b/tests/installation/fixtures/with-pypi-repository.test @@ -1,3 +1,5 @@ +# This file is automatically @generated by Poetry 1.5.0.dev0 and should not be changed by hand. + [[package]] name = "attrs" version = "17.4.0" @@ -5,14 +7,10 @@ description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "attrs-17.4.0-py2.py3-none-any.whl" -hash = "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" - -[[package.files]] -file = "attrs-17.4.0.tar.gz" -hash = "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" +files = [ + {file = "attrs-17.4.0-py2.py3-none-any.whl", hash = "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"}, + {file = "attrs-17.4.0.tar.gz", hash = "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9"}, +] [package.extras] dev = ["coverage", "hypothesis", "pympler", "pytest", "six", "sphinx", "zope.interface", "zope.interface"] @@ -26,30 +24,10 @@ description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "colorama-0.3.9-py2.py3-none-any.whl" -hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda" - -[[package.files]] -file = "colorama-0.3.9.tar.gz" -hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" - -[[package]] -name = "funcsigs" -version = "1.0.2" -description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" -category = "dev" -optional = false -python-versions = "*" - -[[package.files]] -file = "funcsigs-1.0.2-py2.py3-none-any.whl" -hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca" - -[[package.files]] -file = "funcsigs-1.0.2.tar.gz" -hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" +files = [ + {file = "colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"}, + {file = "colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"}, +] [[package]] name = "more-itertools" @@ -58,18 +36,11 @@ description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "more-itertools-4.1.0.tar.gz" -hash = "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44" - -[[package.files]] -file = "more_itertools-4.1.0-py2-none-any.whl" -hash = "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e" - -[[package.files]] -file = "more_itertools-4.1.0-py3-none-any.whl" -hash = "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea" +files = [ + {file = "more-itertools-4.1.0.tar.gz", hash = "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"}, + {file = "more_itertools-4.1.0-py2-none-any.whl", hash = "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e"}, + {file = "more_itertools-4.1.0-py3-none-any.whl", hash = "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea"}, +] [package.dependencies] six = ">=1.0.0,<2.0.0" @@ -81,10 +52,9 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package.files]] -file = "pluggy-0.6.0.tar.gz" -hash = "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" +files = [ + {file = "pluggy-0.6.0.tar.gz", hash = "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"}, +] [[package]] name = "py" @@ -93,14 +63,10 @@ description = "library with cross-python path, ini-parsing, io, code, log facili category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package.files]] -file = "py-1.5.3-py2.py3-none-any.whl" -hash = "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a" - -[[package.files]] -file = "py-1.5.3.tar.gz" -hash = "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881" +files = [ + {file = "py-1.5.3-py2.py3-none-any.whl", hash = "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"}, + {file = "py-1.5.3.tar.gz", hash = "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881"}, +] [[package]] name = "pytest" @@ -109,44 +75,36 @@ description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package.files]] -file = "pytest-3.5.0-py2.py3-none-any.whl" -hash = "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c" - -[[package.files]] -file = "pytest-3.5.0.tar.gz" -hash = "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1" +files = [ + {file = "pytest-3.5.0-py2.py3-none-any.whl", hash = "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c"}, + {file = "pytest-3.5.0.tar.gz", hash = "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"}, +] [package.dependencies] -py = ">=1.5.0" -six = ">=1.10.0" attrs = ">=17.4.0" -setuptools = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} more-itertools = ">=4.0.0" pluggy = ">=0.5,<0.7" -funcsigs = {"version" = "*", "markers" = "python_version < \"3.0\""} -colorama = {"version" = "*", "markers" = "sys_platform == \"win32\""} +py = ">=1.5.0" +setuptools = "*" +six = ">=1.10.0" [[package]] name = "setuptools" -version = "39.2.0" +version = "67.6.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" - -[[package.files]] -file = "setuptools-39.2.0-py2.py3-none-any.whl" -hash = "sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926" - -[[package.files]] -file = "setuptools-39.2.0.zip" -hash = "sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2" +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, +] [package.extras] -certs = ["certifi (==2016.9.26)"] -ssl = ["wincertstore (==0.2)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -155,16 +113,12 @@ description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = "*" - -[[package.files]] -file = "six-1.11.0-py2.py3-none-any.whl" -hash = "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" - -[[package.files]] -file = "six-1.11.0.tar.gz" -hash = "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" +files = [ + {file = "six-1.11.0-py2.py3-none-any.whl", hash = "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"}, + {file = "six-1.11.0.tar.gz", hash = "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"}, +] [metadata] -python-versions = "*" lock-version = "2.0" +python-versions = ">=3.7" content-hash = "123456789" diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 98aac9c8430..841a2ccde64 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -207,9 +207,10 @@ def fixture(name: str) -> dict: def test_run_no_dependencies(installer: Installer, locker: Locker): - installer.run() - expected = fixture("no-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("no-dependencies") assert locker.written_data == expected @@ -224,9 +225,10 @@ def test_run_with_dependencies( package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -292,9 +294,10 @@ def test_run_update_after_removing_dependencies( package.add_dependency(Factory.create_dependency("B", "~1.1")) installer.update(True) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected assert installer.executor.installations_count == 0 @@ -413,7 +416,8 @@ def test_run_install_with_dependency_groups( installer.only_groups(groups) installer.requires_synchronization(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == installs assert installer.executor.updates_count == updates @@ -483,7 +487,8 @@ def test_run_install_does_not_remove_locked_packages_if_installed_but_not_requir } ) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -625,7 +630,8 @@ def test_run_install_removes_no_longer_locked_packages_if_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -703,7 +709,8 @@ def test_run_install_with_synchronization( ) installer.requires_synchronization(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -756,9 +763,10 @@ def test_run_whitelist_add( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -811,9 +819,10 @@ def test_run_whitelist_remove( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("remove") + result = installer.run() + assert result == 0 + expected = fixture("remove") assert locker.written_data == expected assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 @@ -838,9 +847,10 @@ def test_add_with_sub_dependencies( package_a.add_dependency(Factory.create_dependency("D", "^1.0")) package_b.add_dependency(Factory.create_dependency("C", "~1.2")) - installer.run() - expected = fixture("with-sub-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-sub-dependencies") assert locker.written_data == expected @@ -865,9 +875,10 @@ def test_run_with_python_versions( package.add_dependency(Factory.create_dependency("B", "^1.0")) package.add_dependency(Factory.create_dependency("C", "^1.0")) - installer.run() - expected = fixture("with-python-versions") + result = installer.run() + assert result == 0 + expected = fixture("with-python-versions") assert locker.written_data == expected @@ -900,9 +911,10 @@ def test_run_with_optional_and_python_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "python": "~2.7 || ^3.4"}) ) - installer.run() - expected = fixture("with-optional-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-optional-dependencies") assert locker.written_data == expected # We should only have 2 installs: @@ -946,9 +958,10 @@ def test_run_with_optional_and_platform_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "platform": "darwin"}) ) - installer.run() - expected = fixture("with-platform-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-platform-dependencies") assert locker.written_data == expected # We should only have 2 installs: @@ -980,9 +993,10 @@ def test_run_with_dependencies_extras( Factory.create_dependency("B", {"version": "^1.0", "extras": ["foo"]}) ) - installer.run() - expected = fixture("with-dependencies-extras") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies-extras") assert locker.written_data == expected @@ -1011,9 +1025,10 @@ def test_run_with_dependencies_nested_extras( package.add_dependency(dependency_a) - installer.run() - expected = fixture("with-dependencies-nested-extras") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies-nested-extras") assert locker.written_data == expected @@ -1038,9 +1053,10 @@ def test_run_does_not_install_extras_if_not_requested( Factory.create_dependency("D", {"version": "^1.0", "optional": True}) ) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 + expected = fixture("extras") # Extras are pinned in lock assert locker.written_data == expected @@ -1070,10 +1086,11 @@ def test_run_installs_extras_if_requested( ) installer.extras(["foo"]) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras") assert locker.written_data == expected # But should not be installed @@ -1103,7 +1120,9 @@ def test_run_installs_extras_with_deps_if_requested( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() + result = installer.run() + assert result == 0 + expected = fixture("extras-with-dependencies") # Extras are pinned in lock @@ -1138,9 +1157,9 @@ def test_run_installs_extras_with_deps_if_requested_locked( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() + result = installer.run() + assert result == 0 - # But should not be installed assert installer.executor.installations_count == 4 # A, B, C, D @@ -1158,8 +1177,10 @@ def test_installer_with_pypi_repository( NullIO(), env, package, locker, pool, config, installed=installed ) + package.python_versions = ">=3.7" package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-pypi-repository") @@ -1185,7 +1206,8 @@ def test_run_installs_with_local_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency") @@ -1212,7 +1234,8 @@ def test_run_installs_wheel_with_no_requires_dist( ) ) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-wheel-dependency-no-requires-dist") @@ -1243,7 +1266,8 @@ def test_run_installs_with_local_poetry_directory_and_extras( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry") assert locker.written_data == expected @@ -1274,7 +1298,8 @@ def test_run_installs_with_local_poetry_directory_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry-transitive") @@ -1308,7 +1333,8 @@ def test_run_installs_with_local_poetry_file_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency-transitive") @@ -1340,7 +1366,8 @@ def test_run_installs_with_local_setuptools_directory( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-setuptools") @@ -1386,9 +1413,10 @@ def test_run_with_prereleases( installer.update(True) installer.whitelist({"B": "^1.1"}) - installer.run() - expected = fixture("with-prereleases") + result = installer.run() + assert result == 0 + expected = fixture("with-prereleases") assert locker.written_data == expected @@ -1472,9 +1500,10 @@ def test_run_update_all_with_lock( installer.update(True) - installer.run() - expected = fixture("update-with-lock") + result = installer.run() + assert result == 0 + expected = fixture("update-with-lock") assert locker.written_data == expected @@ -1545,9 +1574,10 @@ def test_run_update_with_locked_extras( installer.update(True) installer.whitelist("D") - installer.run() - expected = fixture("update-with-locked-extras") + result = installer.run() + assert result == 0 + expected = fixture("update-with-locked-extras") assert locker.written_data == expected @@ -1578,7 +1608,8 @@ def test_run_install_duplicate_dependencies_different_constraints( repo.add_package(package_c12) repo.add_package(package_c15) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1690,7 +1721,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock( repo.add_package(package_c15) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1756,7 +1788,8 @@ def test_run_update_uninstalls_after_removal_transient_dependency( installed.add_package(get_package("B", "1.0")) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 0 assert installer.executor.updates_count == 0 @@ -1861,7 +1894,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda installer.update(True) installer.whitelist(["A"]) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies-update") @@ -1896,7 +1930,8 @@ def test_installer_test_solver_finds_compatible_package_for_dependency_python_no repo.add_package(package_a100) repo.add_package(package_a101) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-conditional-dependency") assert locker.written_data == expected @@ -1935,7 +1970,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de repo.add_package(package_d) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 @@ -1961,7 +1997,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de ) installer.update(True) installer.whitelist(["D"]) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 @@ -1996,7 +2033,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de package.add_dependency(Factory.create_dependency("poetry", {"version": "^0.12.0"})) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 3 assert installer.executor.updates_count == 0 @@ -2022,7 +2060,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de ) installer.update(True) installer.whitelist(["pytest"]) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 7 assert installer.executor.updates_count == 0 @@ -2057,7 +2096,8 @@ def test_installer_required_extras_should_be_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 0 @@ -2077,7 +2117,8 @@ def test_installer_required_extras_should_be_installed( executor=Executor(env, pool, config, NullIO()), ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 2 assert installer.executor.updates_count == 0 @@ -2144,21 +2185,24 @@ def test_update_multiple_times_with_split_dependencies_is_idempotent( expected = fixture("with-multiple-updates") installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected @@ -2190,7 +2234,8 @@ def test_installer_can_install_dependencies_from_forced_source( executor=Executor(env, pool, config, NullIO()), ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 1 assert installer.executor.updates_count == 0 @@ -2205,7 +2250,8 @@ def test_run_installs_with_url_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-url-dependency") @@ -2254,7 +2300,8 @@ def test_run_installs_with_same_version_url_files( NullIO(), ), ) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-same-version-url-dependencies") assert locker.written_data == expected @@ -2278,7 +2325,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( repo.add_package(package_b) - installer.run() + result = installer.run() + assert result == 0 del installer.installer.installs[:] locker.locked(True) @@ -2288,7 +2336,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( installer.whitelist(["b"]) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 2 @@ -2318,7 +2367,8 @@ def test_installer_can_handle_old_lock_files( installed=installed, executor=Executor(MockEnv(), pool, config, NullIO()), ) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations_count == 6 @@ -2337,7 +2387,8 @@ def test_installer_can_handle_old_lock_files( NullIO(), ), ) - installer.run() + result = installer.run() + assert result == 0 # funcsigs will be added assert installer.executor.installations_count == 7 @@ -2357,7 +2408,8 @@ def test_installer_can_handle_old_lock_files( NullIO(), ), ) - installer.run() + result = installer.run() + assert result == 0 # colorama will be added assert installer.executor.installations_count == 8 @@ -2382,9 +2434,10 @@ def test_run_with_dependencies_quiet( package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected installer._io.output._buffer.seek(0) @@ -2445,7 +2498,8 @@ def test_installer_should_use_the_locked_version_of_git_dependencies( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 assert installer.executor.installations[-1] == Package( "demo", @@ -2486,7 +2540,8 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_with_extras repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cleo", "1.0.0")) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.executor.installations) == 3 assert installer.executor.installations[-1] == Package( @@ -2524,7 +2579,8 @@ def test_installer_should_use_the_locked_version_of_git_dependencies_without_ref repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.executor.installations) == 2 assert installer.executor.installations[-1] == Package( @@ -2620,7 +2676,8 @@ def test_installer_distinguishes_locked_packages_by_source( NullIO(), ), ) - installer.run() + result = installer.run() + assert result == 0 # Results of installation are consistent with the platform requirements. version = "1.11.0" if env_platform == "darwin" else "1.11.0+cpu" diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py index 02ea33bc89b..45d3a5c767d 100644 --- a/tests/installation/test_installer_old.py +++ b/tests/installation/test_installer_old.py @@ -154,9 +154,10 @@ def fixture(name: str) -> str: def test_run_no_dependencies(installer: Installer, locker: Locker): - installer.run() - expected = fixture("no-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("no-dependencies") assert locker.written_data == expected @@ -171,9 +172,10 @@ def test_run_with_dependencies( package.add_dependency(Factory.create_dependency("A", "~1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -239,9 +241,10 @@ def test_run_update_after_removing_dependencies( package.add_dependency(Factory.create_dependency("B", "~1.1")) installer.update(True) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected installs = installer.installer.installs @@ -317,7 +320,8 @@ def test_run_install_no_group( package.add_dependency(Factory.create_dependency("C", "~1.2", groups=["dev"])) installer.only_groups([]) - installer.run() + result = installer.run() + assert result == 0 installs = installer.installer.installs assert len(installs) == 0 @@ -400,7 +404,8 @@ def test_run_install_with_synchronization( ) installer.requires_synchronization(True) - installer.run() + result = installer.run() + assert result == 0 installs = installer.installer.installs assert len(installs) == 0 @@ -456,9 +461,10 @@ def test_run_whitelist_add( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("with-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies") assert locker.written_data == expected @@ -511,9 +517,10 @@ def test_run_whitelist_remove( installer.update(True) installer.whitelist(["B"]) - installer.run() - expected = fixture("remove") + result = installer.run() + assert result == 0 + expected = fixture("remove") assert locker.written_data == expected assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 @@ -538,9 +545,10 @@ def test_add_with_sub_dependencies( package_a.add_dependency(Factory.create_dependency("D", "^1.0")) package_b.add_dependency(Factory.create_dependency("C", "~1.2")) - installer.run() - expected = fixture("with-sub-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-sub-dependencies") assert locker.written_data == expected @@ -565,9 +573,10 @@ def test_run_with_python_versions( package.add_dependency(Factory.create_dependency("B", "^1.0")) package.add_dependency(Factory.create_dependency("C", "^1.0")) - installer.run() - expected = fixture("with-python-versions") + result = installer.run() + assert result == 0 + expected = fixture("with-python-versions") assert locker.written_data == expected @@ -600,9 +609,10 @@ def test_run_with_optional_and_python_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "python": "~2.7 || ^3.4"}) ) - installer.run() - expected = fixture("with-optional-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-optional-dependencies") assert locker.written_data == expected installer = installer.installer @@ -647,9 +657,10 @@ def test_run_with_optional_and_platform_restricted_dependencies( Factory.create_dependency("C", {"version": "^1.0", "platform": "darwin"}) ) - installer.run() - expected = fixture("with-platform-dependencies") + result = installer.run() + assert result == 0 + expected = fixture("with-platform-dependencies") assert locker.written_data == expected installer = installer.installer @@ -682,9 +693,10 @@ def test_run_with_dependencies_extras( Factory.create_dependency("B", {"version": "^1.0", "extras": ["foo"]}) ) - installer.run() - expected = fixture("with-dependencies-extras") + result = installer.run() + assert result == 0 + expected = fixture("with-dependencies-extras") assert locker.written_data == expected @@ -709,10 +721,11 @@ def test_run_does_not_install_extras_if_not_requested( Factory.create_dependency("D", {"version": "^1.0", "optional": True}) ) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras") assert locker.written_data == expected # But should not be installed @@ -742,10 +755,11 @@ def test_run_installs_extras_if_requested( ) installer.extras(["foo"]) - installer.run() - expected = fixture("extras") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras") assert locker.written_data == expected # But should not be installed @@ -776,10 +790,11 @@ def test_run_installs_extras_with_deps_if_requested( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() - expected = fixture("extras-with-dependencies") + result = installer.run() + assert result == 0 # Extras are pinned in lock + expected = fixture("extras-with-dependencies") assert locker.written_data == expected # But should not be installed @@ -812,7 +827,8 @@ def test_run_installs_extras_with_deps_if_requested_locked( package_c.add_dependency(Factory.create_dependency("D", "^1.0")) installer.extras(["foo"]) - installer.run() + result = installer.run() + assert result == 0 # But should not be installed installer = installer.installer @@ -833,11 +849,12 @@ def test_installer_with_pypi_repository( NullIO(), env, package, locker, pool, config, installed=installed ) + package.python_versions = ">=3.7" package.add_dependency(Factory.create_dependency("pytest", "^3.5", groups=["dev"])) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-pypi-repository") - assert expected == locker.written_data @@ -853,7 +870,8 @@ def test_run_installs_with_local_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency") @@ -874,7 +892,8 @@ def test_run_installs_wheel_with_no_requires_dist( ) package.add_dependency(Factory.create_dependency("demo", {"file": str(file_path)})) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-wheel-dependency-no-requires-dist") @@ -900,7 +919,8 @@ def test_run_installs_with_local_poetry_directory_and_extras( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry") @@ -932,7 +952,8 @@ def test_run_installs_with_local_poetry_directory_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-poetry-transitive") @@ -964,7 +985,8 @@ def test_run_installs_with_local_poetry_file_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-file-dependency-transitive") @@ -989,7 +1011,8 @@ def test_run_installs_with_local_setuptools_directory( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-directory-dependency-setuptools") @@ -1036,9 +1059,10 @@ def test_run_with_prereleases( installer.update(True) installer.whitelist({"B": "^1.1"}) - installer.run() - expected = fixture("with-prereleases") + result = installer.run() + assert result == 0 + expected = fixture("with-prereleases") assert locker.written_data == expected @@ -1122,9 +1146,10 @@ def test_run_update_all_with_lock( installer.update(True) - installer.run() - expected = fixture("update-with-lock") + result = installer.run() + assert result == 0 + expected = fixture("update-with-lock") assert locker.written_data == expected @@ -1195,9 +1220,10 @@ def test_run_update_with_locked_extras( installer.update(True) installer.whitelist("D") - installer.run() - expected = fixture("update-with-locked-extras") + result = installer.run() + assert result == 0 + expected = fixture("update-with-locked-extras") assert locker.written_data == expected @@ -1228,7 +1254,8 @@ def test_run_install_duplicate_dependencies_different_constraints( repo.add_package(package_c12) repo.add_package(package_c15) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1342,7 +1369,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock( repo.add_package(package_c15) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies") @@ -1411,7 +1439,8 @@ def test_run_update_uninstalls_after_removal_transient_dependency( installed.add_package(get_package("B", "1.0")) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 installs = installer.installer.installs assert len(installs) == 0 @@ -1519,7 +1548,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda installer.update(True) installer.whitelist(["A"]) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-duplicate-dependencies-update") @@ -1557,7 +1587,8 @@ def test_installer_test_solver_finds_compatible_package_for_dependency_python_no repo.add_package(package_a100) repo.add_package(package_a101) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-conditional-dependency") assert locker.written_data == expected @@ -1598,7 +1629,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de repo.add_package(package_d) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 3 assert len(installer.installer.updates) == 0 @@ -1618,7 +1650,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.update(True) installer.whitelist(["D"]) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 @@ -1646,7 +1679,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de package.add_dependency(Factory.create_dependency("poetry", {"version": "^0.12.0"})) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 3 assert len(installer.installer.updates) == 0 @@ -1666,7 +1700,8 @@ def test_installer_required_extras_should_not_be_removed_when_updating_single_de installer.update(True) installer.whitelist(["pytest"]) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 7 assert len(installer.installer.updates) == 0 @@ -1695,7 +1730,8 @@ def test_installer_required_extras_should_be_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 2 assert len(installer.installer.updates) == 0 @@ -1709,7 +1745,8 @@ def test_installer_required_extras_should_be_installed( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 2 assert len(installer.installer.updates) == 0 @@ -1776,21 +1813,24 @@ def test_update_multiple_times_with_split_dependencies_is_idempotent( expected = fixture("with-multiple-updates") installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected locker.mock_lock_data(locker.written_data) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert locker.written_data == expected @@ -1816,7 +1856,8 @@ def test_installer_can_install_dependencies_from_forced_source( ) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 1 assert len(installer.installer.updates) == 0 @@ -1831,7 +1872,8 @@ def test_run_installs_with_url_file( repo.add_package(get_package("pendulum", "1.4.4")) - installer.run() + result = installer.run() + assert result == 0 expected = fixture("with-url-dependency") @@ -1855,7 +1897,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( repo.add_package(package_b) - installer.run() + result = installer.run() + assert result == 0 del installer.installer.installs[:] locker.locked(True) @@ -1865,7 +1908,8 @@ def test_installer_uses_prereleases_if_they_are_compatible( installer.whitelist(["b"]) installer.update(True) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 2 @@ -1889,7 +1933,8 @@ def test_installer_can_handle_old_lock_files( NullIO(), MockEnv(), package, locker, pool, config, installed=installed ) - installer.run() + result = installer.run() + assert result == 0 assert len(installer.installer.installs) == 6 @@ -1903,7 +1948,8 @@ def test_installer_can_handle_old_lock_files( installed=installed, ) - installer.run() + result = installer.run() + assert result == 0 # funcsigs will be added assert len(installer.installer.installs) == 7 @@ -1918,7 +1964,8 @@ def test_installer_can_handle_old_lock_files( installed=installed, ) - installer.run() + result = installer.run() + assert result == 0 # colorama will be added assert len(installer.installer.installs) == 8 diff --git a/tests/repositories/fixtures/pypi.org/json/setuptools.json b/tests/repositories/fixtures/pypi.org/json/setuptools.json index 6703a5efc9b..24858bd11b8 100644 --- a/tests/repositories/fixtures/pypi.org/json/setuptools.json +++ b/tests/repositories/fixtures/pypi.org/json/setuptools.json @@ -16,10 +16,36 @@ "md5": "dd4e3fa83a21bf7bf9c51026dc8a4e59", "sha256": "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2" } + }, + { + "filename": "setuptools-67.6.1-py3-none-any.whl", + "hashes": { + "sha256": "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" + }, + "requires-python": ">=3.7", + "size": 1089263, + "upload-time": "2023-03-28T13:45:43.525946Z", + "url": "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", + "yanked": false + }, + { + "filename": "setuptools-67.6.1.tar.gz", + "hashes": { + "sha256": "257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a" + }, + "requires-python": ">=3.7", + "size": 2486256, + "upload-time": "2023-03-28T13:45:45.967259Z", + "url": "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", + "yanked": false } ], "meta": { "api-version": "1.0", "_last-serial": 3879671 - } + }, + "versions": [ + "39.2.0", + "67.6.1" + ] } diff --git a/tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json b/tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json new file mode 100644 index 00000000000..7a2c4192756 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/setuptools/67.6.1.json @@ -0,0 +1,140 @@ +{ + "info": { + "author": "Python Packaging Authority", + "author_email": "distutils-sig@python.org", + "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", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Archiving :: Packaging", + "Topic :: System :: Systems Administration", + "Topic :: Utilities" + ], + "description": ".. image:: https://img.shields.io/pypi/v/setuptools.svg\n :target: https://pypi.org/project/setuptools\n\n.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg\n\n.. image:: https://github.com/pypa/setuptools/workflows/tests/badge.svg\n :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22\n :alt: tests\n\n.. image:: https://img.shields.io/badge/code%20style-black-000000.svg\n :target: https://github.com/psf/black\n :alt: Code style: Black\n\n.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg\n :target: https://setuptools.pypa.io\n\n.. image:: https://img.shields.io/badge/skeleton-2023-informational\n :target: https://blog.jaraco.com/skeleton\n\n.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white\n :target: https://codecov.io/gh/pypa/setuptools\n\n.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat\n :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme\n\n.. image:: https://img.shields.io/discord/803025117553754132\n :target: https://discord.com/channels/803025117553754132/815945031150993468\n :alt: Discord\n\nSee the `Installation Instructions\n`_ in the Python Packaging\nUser's Guide for instructions on installing, upgrading, and uninstalling\nSetuptools.\n\nQuestions and comments should be directed to `GitHub Discussions\n`_.\nBug reports and especially tested patches may be\nsubmitted directly to the `bug tracker\n`_.\n\n\nCode of Conduct\n===============\n\nEveryone interacting in the setuptools project's codebases, issue trackers,\nchat rooms, and fora is expected to follow the\n`PSF Code of Conduct `_.\n\n\nFor Enterprise\n==============\n\nAvailable as part of the Tidelift Subscription.\n\nSetuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.\n\n`Learn more `_.\n\n\nSecurity Contact\n================\n\nTo report a security vulnerability, please use the\n`Tidelift security contact `_.\nTidelift will coordinate the fix and disclosure.\n", + "description_content_type": "", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "https://github.com/pypa/setuptools", + "keywords": "CPAN PyPI distutils eggs package management", + "license": "", + "maintainer": "", + "maintainer_email": "", + "name": "setuptools", + "package_url": "https://pypi.org/project/setuptools/", + "platform": null, + "project_url": "https://pypi.org/project/setuptools/", + "project_urls": { + "Changelog": "https://setuptools.pypa.io/en/stable/history.html", + "Documentation": "https://setuptools.pypa.io/", + "Homepage": "https://github.com/pypa/setuptools" + }, + "release_url": "https://pypi.org/project/setuptools/67.6.1/", + "requires_dist": [ + "sphinx (>=3.5) ; extra == 'docs'", + "jaraco.packaging (>=9) ; extra == 'docs'", + "rst.linker (>=1.9) ; extra == 'docs'", + "furo ; extra == 'docs'", + "sphinx-lint ; extra == 'docs'", + "jaraco.tidelift (>=1.4) ; extra == 'docs'", + "pygments-github-lexers (==0.0.5) ; extra == 'docs'", + "sphinx-favicon ; extra == 'docs'", + "sphinx-inline-tabs ; extra == 'docs'", + "sphinx-reredirects ; extra == 'docs'", + "sphinxcontrib-towncrier ; extra == 'docs'", + "sphinx-notfound-page (==0.8.3) ; extra == 'docs'", + "sphinx-hoverxref (<2) ; extra == 'docs'", + "pytest (>=6) ; extra == 'testing'", + "pytest-checkdocs (>=2.4) ; extra == 'testing'", + "flake8 (<5) ; extra == 'testing'", + "pytest-enabler (>=1.3) ; extra == 'testing'", + "pytest-perf ; extra == 'testing'", + "flake8-2020 ; extra == 'testing'", + "virtualenv (>=13.0.0) ; extra == 'testing'", + "wheel ; extra == 'testing'", + "pip (>=19.1) ; extra == 'testing'", + "jaraco.envs (>=2.2) ; extra == 'testing'", + "pytest-xdist ; extra == 'testing'", + "jaraco.path (>=3.2.0) ; extra == 'testing'", + "build[virtualenv] ; extra == 'testing'", + "filelock (>=3.4.0) ; extra == 'testing'", + "pip-run (>=8.8) ; extra == 'testing'", + "ini2toml[lite] (>=0.9) ; extra == 'testing'", + "tomli-w (>=1.0.0) ; extra == 'testing'", + "pytest-timeout ; extra == 'testing'", + "pytest ; extra == 'testing-integration'", + "pytest-xdist ; extra == 'testing-integration'", + "pytest-enabler ; extra == 'testing-integration'", + "virtualenv (>=13.0.0) ; extra == 'testing-integration'", + "tomli ; extra == 'testing-integration'", + "wheel ; extra == 'testing-integration'", + "jaraco.path (>=3.2.0) ; extra == 'testing-integration'", + "jaraco.envs (>=2.2) ; extra == 'testing-integration'", + "build[virtualenv] ; extra == 'testing-integration'", + "filelock (>=3.4.0) ; extra == 'testing-integration'", + "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", + "pytest-cov ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", + "pytest-mypy (>=0.9.1) ; (platform_python_implementation != \"PyPy\") and extra == 'testing'", + "pytest-flake8 ; (python_version < \"3.12\") and extra == 'testing'" + ], + "requires_python": ">=3.7", + "summary": "Easily download, build, install, upgrade, and uninstall Python packages", + "version": "67.6.1", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 17478645, + "urls": [ + { + "comment_text": "", + "digests": { + "blake2b_256": "0bfc8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7", + "md5": "3b5b846e000da033d54eeaaf7915126e", + "sha256": "e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" + }, + "downloads": -1, + "filename": "setuptools-67.6.1-py3-none-any.whl", + "has_sig": false, + "md5_digest": "3b5b846e000da033d54eeaaf7915126e", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.7", + "size": 1089263, + "upload_time": "2023-03-28T13:45:43", + "upload_time_iso_8601": "2023-03-28T13:45:43.525946Z", + "url": "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "blake2b_256": "cb4622ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146", + "md5": "a661b7cdf4cf1e914f866506c1022dee", + "sha256": "257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a" + }, + "downloads": -1, + "filename": "setuptools-67.6.1.tar.gz", + "has_sig": false, + "md5_digest": "a661b7cdf4cf1e914f866506c1022dee", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.7", + "size": 2486256, + "upload_time": "2023-03-28T13:45:45", + "upload_time_iso_8601": "2023-03-28T13:45:45.967259Z", + "url": "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} diff --git a/tests/repositories/fixtures/pypi.org/json/wheel.json b/tests/repositories/fixtures/pypi.org/json/wheel.json new file mode 100644 index 00000000000..6a6d6b8df7b --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/wheel.json @@ -0,0 +1,34 @@ +{ + "files": [ + { + "filename": "wheel-0.40.0-py3-none-any.whl", + "hashes": { + "sha256": "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" + }, + "requires-python": ">=3.7", + "size": 64545, + "upload-time": "2023-03-14T15:10:00.828550Z", + "url": "https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl", + "yanked": false + }, + { + "filename": "wheel-0.40.0.tar.gz", + "hashes": { + "sha256": "cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873" + }, + "requires-python": ">=3.7", + "size": 96226, + "upload-time": "2023-03-14T15:10:02.873691Z", + "url": "https://files.pythonhosted.org/packages/fc/ef/0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735/wheel-0.40.0.tar.gz", + "yanked": false + } + ], + "meta": { + "_last-serial": 17289142, + "api-version": "1.1" + }, + "name": "wheel", + "versions": [ + "0.40.0" + ] +} diff --git a/tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json b/tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json new file mode 100644 index 00000000000..6043ad6cd68 --- /dev/null +++ b/tests/repositories/fixtures/pypi.org/json/wheel/0.40.0.json @@ -0,0 +1,98 @@ +{ + "info": { + "author": "", + "author_email": "Daniel Holth ", + "bugtrack_url": null, + "classifiers": [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "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 :: System :: Archiving :: Packaging" + ], + "description": "wheel\n=====\n\nThis library is the reference implementation of the Python wheel packaging\nstandard, as defined in `PEP 427`_.\n\nIt has two different roles:\n\n#. A setuptools_ extension for building wheels that provides the\n ``bdist_wheel`` setuptools command\n#. A command line tool for working with wheel files\n\nIt should be noted that wheel is **not** intended to be used as a library, and\nas such there is no stable, public API.\n\n.. _PEP 427: https://www.python.org/dev/peps/pep-0427/\n.. _setuptools: https://pypi.org/project/setuptools/\n\nDocumentation\n-------------\n\nThe documentation_ can be found on Read The Docs.\n\n.. _documentation: https://wheel.readthedocs.io/\n\nCode of Conduct\n---------------\n\nEveryone interacting in the wheel project's codebases, issue trackers, chat\nrooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.\n\n.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md\n\n", + "description_content_type": "text/x-rst", + "docs_url": null, + "download_url": "", + "downloads": { + "last_day": -1, + "last_month": -1, + "last_week": -1 + }, + "home_page": "", + "keywords": "wheel,packaging", + "license": "", + "maintainer": "", + "maintainer_email": "Alex Grönholm ", + "name": "wheel", + "package_url": "https://pypi.org/project/wheel/", + "platform": null, + "project_url": "https://pypi.org/project/wheel/", + "project_urls": { + "Changelog": "https://wheel.readthedocs.io/en/stable/news.html", + "Documentation": "https://wheel.readthedocs.io/", + "Issue Tracker": "https://github.com/pypa/wheel/issues" + }, + "release_url": "https://pypi.org/project/wheel/0.40.0/", + "requires_dist": [ + "pytest >= 6.0.0 ; extra == \"test\"" + ], + "requires_python": ">=3.7", + "summary": "A built-package format for Python", + "version": "0.40.0", + "yanked": false, + "yanked_reason": null + }, + "last_serial": 17289142, + "urls": [ + { + "comment_text": "", + "digests": { + "blake2b_256": "6186cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5", + "md5": "517d39f133bd7b1ff17caf09784b7543", + "sha256": "d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247" + }, + "downloads": -1, + "filename": "wheel-0.40.0-py3-none-any.whl", + "has_sig": false, + "md5_digest": "517d39f133bd7b1ff17caf09784b7543", + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": ">=3.7", + "size": 64545, + "upload_time": "2023-03-14T15:10:00", + "upload_time_iso_8601": "2023-03-14T15:10:00.828550Z", + "url": "https://files.pythonhosted.org/packages/61/86/cc8d1ff2ca31a312a25a708c891cf9facbad4eae493b3872638db6785eb5/wheel-0.40.0-py3-none-any.whl", + "yanked": false, + "yanked_reason": null + }, + { + "comment_text": "", + "digests": { + "blake2b_256": "fcef0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735", + "md5": "ec5004c46d1905da98bb5bc1a10ddd21", + "sha256": "cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873" + }, + "downloads": -1, + "filename": "wheel-0.40.0.tar.gz", + "has_sig": false, + "md5_digest": "ec5004c46d1905da98bb5bc1a10ddd21", + "packagetype": "sdist", + "python_version": "source", + "requires_python": ">=3.7", + "size": 96226, + "upload_time": "2023-03-14T15:10:02", + "upload_time_iso_8601": "2023-03-14T15:10:02.873691Z", + "url": "https://files.pythonhosted.org/packages/fc/ef/0335f7217dd1e8096a9e8383e1d472aa14717878ffe07c4772e68b6e8735/wheel-0.40.0.tar.gz", + "yanked": false, + "yanked_reason": null + } + ], + "vulnerabilities": [] +} From fba14ba5edabd98fd57737e2d583896db54c3fee Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Tue, 22 Nov 2022 13:49:44 +0100 Subject: [PATCH 06/20] fix: consistently retry on error codes in publish and install --- src/poetry/publishing/uploader.py | 3 ++- src/poetry/utils/authenticator.py | 3 ++- src/poetry/utils/constants.py | 3 +++ tests/utils/test_authenticator.py | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py index 1f3a6bd81b9..7e6cd6d9b6a 100644 --- a/src/poetry/publishing/uploader.py +++ b/src/poetry/publishing/uploader.py @@ -21,6 +21,7 @@ from poetry.__version__ import __version__ from poetry.utils.constants import REQUESTS_TIMEOUT +from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.patterns import wheel_file_re @@ -68,7 +69,7 @@ def adapter(self) -> adapters.HTTPAdapter: connect=5, total=10, allowed_methods=["GET"], - status_forcelist=[500, 501, 502, 503], + status_forcelist=STATUS_FORCELIST, ) return adapters.HTTPAdapter(max_retries=retry) diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index 8db92913043..2b1011e4d56 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -24,6 +24,7 @@ from poetry.config.config import Config from poetry.exceptions import PoetryException from poetry.utils.constants import REQUESTS_TIMEOUT +from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager @@ -259,7 +260,7 @@ def request( if is_last_attempt: raise e else: - if resp.status_code not in [502, 503, 504] or is_last_attempt: + if resp.status_code not in STATUS_FORCELIST or is_last_attempt: if raise_for_status: resp.raise_for_status() return resp diff --git a/src/poetry/utils/constants.py b/src/poetry/utils/constants.py index 0f799b16d7d..b755fb68e5e 100644 --- a/src/poetry/utils/constants.py +++ b/src/poetry/utils/constants.py @@ -3,3 +3,6 @@ # Timeout for HTTP requests using the requests library. REQUESTS_TIMEOUT = 15 + +# Server response codes to retry requests on. +STATUS_FORCELIST = [500, 501, 502, 503, 504] diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 91e6a574bc8..545b73f53bf 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -249,7 +249,8 @@ def callback(*_: Any, **___: Any) -> None: (401, 0), (403, 0), (404, 0), - (500, 0), + (500, 5), + (501, 5), (502, 5), (503, 5), (504, 5), From 6b3a6161103ff35908608eb324cfa015bb4a785a Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Tue, 22 Nov 2022 16:32:25 +0100 Subject: [PATCH 07/20] fix: respect retry-after header with 429 responses --- src/poetry/publishing/uploader.py | 1 + src/poetry/utils/authenticator.py | 12 +++++++++++- src/poetry/utils/constants.py | 4 +++- tests/utils/test_authenticator.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py index 7e6cd6d9b6a..256fd235259 100644 --- a/src/poetry/publishing/uploader.py +++ b/src/poetry/publishing/uploader.py @@ -69,6 +69,7 @@ def adapter(self) -> adapters.HTTPAdapter: connect=5, total=10, allowed_methods=["GET"], + respect_retry_after_header=True, status_forcelist=STATUS_FORCELIST, ) diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index 2b1011e4d56..0fb238fb56c 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -24,6 +24,7 @@ from poetry.config.config import Config from poetry.exceptions import PoetryException from poetry.utils.constants import REQUESTS_TIMEOUT +from poetry.utils.constants import RETRY_AFTER_HEADER from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager @@ -251,6 +252,7 @@ def request( send_kwargs.update(settings) attempt = 0 + resp = None while True: is_last_attempt = attempt >= 5 @@ -267,7 +269,7 @@ def request( if not is_last_attempt: attempt += 1 - delay = 0.5 * attempt + delay = self._get_backoff(resp, attempt) logger.debug("Retrying HTTP request in %s seconds.", delay) time.sleep(delay) continue @@ -275,6 +277,14 @@ def request( # this should never really be hit under any sane circumstance raise PoetryException("Failed HTTP {} request", method.upper()) + def _get_backoff(self, response: requests.Response | None, attempt: int) -> float: + if response is not None: + retry_after = response.headers.get(RETRY_AFTER_HEADER, "") + if retry_after: + return float(retry_after) + + return 0.5 * attempt + def get(self, url: str, **kwargs: Any) -> requests.Response: return self.request("get", url, **kwargs) diff --git a/src/poetry/utils/constants.py b/src/poetry/utils/constants.py index b755fb68e5e..56bec540ae2 100644 --- a/src/poetry/utils/constants.py +++ b/src/poetry/utils/constants.py @@ -4,5 +4,7 @@ # Timeout for HTTP requests using the requests library. REQUESTS_TIMEOUT = 15 +RETRY_AFTER_HEADER = "retry-after" + # Server response codes to retry requests on. -STATUS_FORCELIST = [500, 501, 502, 503, 504] +STATUS_FORCELIST = [429, 500, 501, 502, 503, 504] diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 545b73f53bf..05c5490ac11 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -242,6 +242,33 @@ def callback(*_: Any, **___: Any) -> None: assert sleep.call_count == 5 +def test_authenticator_request_respects_retry_header( + mocker: MockerFixture, + config: Config, + http: type[httpretty.httpretty], +): + sleep = mocker.patch("time.sleep") + sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz" + content = str(uuid.uuid4()) + seen = [] + + def callback( + request: requests.Request, uri: str, response_headers: dict + ) -> list[int | dict | str]: + if not seen.count(uri): + seen.append(uri) + return [429, {"Retry-After": "42"}, "Retry later"] + + return [200, response_headers, content] + + http.register_uri(httpretty.GET, sdist_uri, body=callback) + authenticator = Authenticator(config, NullIO()) + + response = authenticator.request("get", sdist_uri) + assert sleep.call_args[0] == (42.0,) + assert response.text == content + + @pytest.mark.parametrize( ["status", "attempts"], [ @@ -249,6 +276,7 @@ def callback(*_: Any, **___: Any) -> None: (401, 0), (403, 0), (404, 0), + (429, 5), (500, 5), (501, 5), (502, 5), From 7585c375204880091ec01a9cf375f2a5cea70f9e Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:48:11 +0000 Subject: [PATCH 08/20] Error when invalid groups are referenced (#7529) --- docs/managing-dependencies.md | 2 +- src/poetry/console/commands/group_command.py | 22 ++++++++++ src/poetry/console/exceptions.py | 4 ++ tests/console/commands/test_install.py | 46 ++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/managing-dependencies.md b/docs/managing-dependencies.md index 16622624857..14932d43df9 100644 --- a/docs/managing-dependencies.md +++ b/docs/managing-dependencies.md @@ -163,7 +163,7 @@ poetry install --only docs ``` {{% note %}} -If you only want to install the project's runtime dependencies, you can do so with the +If you only want to install the project's runtime dependencies, you can do so with the `--only main` notation: ```bash diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index 27438bf0c07..88b9a1ce39a 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -1,11 +1,13 @@ from __future__ import annotations +from collections import defaultdict from typing import TYPE_CHECKING from cleo.helpers import option from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.command import Command +from poetry.console.exceptions import GroupNotFound if TYPE_CHECKING: @@ -78,6 +80,7 @@ def activated_groups(self) -> set[str]: for groups in self.option(key, "") for group in groups.split(",") } + self._validate_group_options(groups) for opt, new, group in [ ("no-dev", "only", MAIN_GROUP), @@ -107,3 +110,22 @@ def project_with_activated_groups_only(self) -> ProjectPackage: return self.poetry.package.with_dependency_groups( list(self.activated_groups), only=True ) + + def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: + """ + Raises en error if it detects that a group is not part of pyproject.toml + """ + invalid_options = defaultdict(set) + for opt, groups in group_options.items(): + for group in groups: + if not self.poetry.package.has_dependency_group(group): + invalid_options[group].add(opt) + if invalid_options: + message_parts = [] + for group in sorted(invalid_options): + opts = ", ".join( + f"--{opt}" + for opt in sorted(invalid_options[group]) + ) + message_parts.append(f"{group} (via {opts})") + raise GroupNotFound(f"Group(s) not found: {', '.join(message_parts)}") diff --git a/src/poetry/console/exceptions.py b/src/poetry/console/exceptions.py index aadc8c17e7f..2cc359ddb75 100644 --- a/src/poetry/console/exceptions.py +++ b/src/poetry/console/exceptions.py @@ -5,3 +5,7 @@ class PoetryConsoleError(CleoError): pass + + +class GroupNotFound(PoetryConsoleError): + pass diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 0200a452bfa..f39081872d0 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -1,5 +1,7 @@ from __future__ import annotations +import re + from typing import TYPE_CHECKING import pytest @@ -7,6 +9,8 @@ from poetry.core.masonry.utils.module import ModuleOrPackageNotFound from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.console.exceptions import GroupNotFound + if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester @@ -257,6 +261,48 @@ def test_only_root_conflicts_with_without_only( ) +@pytest.mark.parametrize( + ("options", "valid_groups", "should_raise"), + [ + ({"--with": MAIN_GROUP}, {MAIN_GROUP}, False), + ({"--with": "spam"}, set(), True), + ({"--with": "spam,foo"}, {"foo"}, True), + ({"--without": "spam"}, set(), True), + ({"--without": "spam,bar"}, {"bar"}, True), + ({"--with": "eggs,ham", "--without": "spam"}, set(), True), + ({"--with": "eggs,ham", "--without": "spam,baz"}, {"baz"}, True), + ({"--only": "spam"}, set(), True), + ({"--only": "bim"}, {"bim"}, False), + ({"--only": MAIN_GROUP}, {MAIN_GROUP}, False), + ], +) +def test_invalid_groups_with_without_only( + tester: CommandTester, + mocker: MockerFixture, + options: dict[str, str], + valid_groups: set[str], + should_raise: bool, +): + mocker.patch.object(tester.command.installer, "run", return_value=0) + + cmd_args = " ".join(f"{flag} {groups}" for (flag, groups) in options.items()) + + if not should_raise: + tester.execute(cmd_args) + assert tester.status_code == 0 + else: + with pytest.raises(GroupNotFound, match=r"^Group\(s\) not found:") as e: + tester.execute(cmd_args) + assert tester.status_code is None + for opt, groups in options.items(): + group_list = groups.split(",") + invalid_groups = sorted(set(group_list) - valid_groups) + for group in invalid_groups: + assert ( + re.search(rf"{group} \(via .*{opt}.*\)", str(e.value)) is not None + ) + + def test_remove_untracked_outputs_deprecation_warning( tester: CommandTester, mocker: MockerFixture, From 161b19cb4e4686fc5a0a7925001534a87f6c4052 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Wed, 5 Apr 2023 16:07:06 +0200 Subject: [PATCH 09/20] poetry run: deprecate uninstalled entry points (#7606) --- src/poetry/console/commands/run.py | 14 ++++++++++++++ tests/console/commands/test_run.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/poetry/console/commands/run.py b/src/poetry/console/commands/run.py index 63af286d3b1..5b008a2fc53 100644 --- a/src/poetry/console/commands/run.py +++ b/src/poetry/console/commands/run.py @@ -63,6 +63,9 @@ def run_script(self, script: str | dict[str, str], args: list[str]) -> int: if script_path.exists(): args = [str(script_path), *args[1:]] break + else: + # If we reach this point, the script is not installed + self._warning_not_installed_script(args[0]) if isinstance(script, dict): script = script["callable"] @@ -81,3 +84,14 @@ def run_script(self, script: str | dict[str, str], args: list[str]) -> int: ] return self.env.execute(*cmd) + + def _warning_not_installed_script(self, script: str) -> None: + message = f"""\ +Warning: '{script}' is an entry point defined in pyproject.toml, but it's not \ +installed as a script. You may get improper `sys.argv[0]`. + +The support to run uninstalled scripts will be removed in a future release. + +Run `poetry install` to resolve and get rid of this message. +""" + self.line_error(message, style="warning") diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 6b5bd95a728..7442ac10f37 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -180,3 +180,17 @@ def test_run_script_sys_argv0( ) argv1 = "absolute" if installed_script else "relative" assert tester.execute(f"check-argv0 {argv1}") == 0 + + if installed_script: + expected_message = "" + else: + expected_message = """\ +Warning: 'check-argv0' is an entry point defined in pyproject.toml, but it's not \ +installed as a script. You may get improper `sys.argv[0]`. + +The support to run uninstalled scripts will be removed in a future release. + +Run `poetry install` to resolve and get rid of this message. + +""" + assert tester.io.fetch_error() == expected_message From 623bfffbe79961b7fb999906ff60b5df578f9825 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Thu, 6 Apr 2023 16:57:10 +0200 Subject: [PATCH 10/20] Only write lock file when installation is success (#7498) (affects `poetry add` and `poetry update`) --- src/poetry/installation/installer.py | 16 +- src/poetry/packages/locker.py | 18 +- .../console/commands/self/test_add_plugins.py | 28 +- .../commands/self/test_remove_plugins.py | 4 +- tests/console/commands/self/test_update.py | 4 +- tests/console/commands/test_add.py | 249 ++++++++++-------- tests/installation/test_installer.py | 24 ++ tests/installation/test_installer_old.py | 1 + 8 files changed, 213 insertions(+), 131 deletions(-) diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index b5aa54f7072..ec5911ce8f7 100644 --- a/src/poetry/installation/installer.py +++ b/src/poetry/installation/installer.py @@ -291,12 +291,10 @@ def _do_install(self) -> int: lockfile_repo = LockfileRepository() self._populate_lockfile_repo(lockfile_repo, ops) - if self._update: + if self._lock and self._update: + # If we are only in lock mode, no need to go any further self._write_lock_file(lockfile_repo) - - if self._lock: - # If we are only in lock mode, no need to go any further - return 0 + return 0 if self._groups is not None: root = self._package.with_dependency_groups(list(self._groups), only=True) @@ -362,7 +360,13 @@ def _do_install(self) -> int: self._filter_operations(ops, lockfile_repo) # Execute operations - return self._execute(ops) + status = self._execute(ops) + + if status == 0 and self._update: + # Only write lock file when installation is success + self._write_lock_file(lockfile_repo) + + return status def _write_lock_file(self, repo: LockfileRepository, force: bool = False) -> None: if self._write_lock and (force or self._update): diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 8784a391ebd..987a1acd609 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -224,6 +224,18 @@ def locked_repository(self) -> LockfileRepository: return repository def set_lock_data(self, root: Package, packages: list[Package]) -> bool: + """Store lock data and eventually persist to the lock file""" + lock = self._compute_lock_data(root, packages) + + if self._should_write(lock): + self._write_lock_data(lock) + return True + + return False + + def _compute_lock_data( + self, root: Package, packages: list[Package] + ) -> TOMLDocument: package_specs = self._lock_packages(packages) # Retrieving hashes for package in package_specs: @@ -254,6 +266,10 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: "content-hash": self._content_hash, } + return lock + + def _should_write(self, lock: TOMLDocument) -> bool: + # if lock file exists: compare with existing lock data do_write = True if self.is_locked(): try: @@ -263,8 +279,6 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: pass else: do_write = lock != lock_data - if do_write: - self._write_lock_data(lock) return do_write def _write_lock_data(self, data: TOMLDocument) -> None: diff --git a/tests/console/commands/self/test_add_plugins.py b/tests/console/commands/self/test_add_plugins.py index 37de59731f0..e7bd3c1707f 100644 --- a/tests/console/commands/self/test_add_plugins.py +++ b/tests/console/commands/self/test_add_plugins.py @@ -49,11 +49,11 @@ def test_add_no_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing poetry-plugin (0.1.0) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^0.1.0") @@ -71,11 +71,11 @@ def test_add_with_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing poetry-plugin (0.2.0) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^0.2.0") @@ -93,12 +93,12 @@ def test_add_with_git_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (2.0.5) • Installing poetry-plugin (0.1.2 9cf87a2) + +Writing lock file """ assert_plugin_add_result( @@ -119,13 +119,13 @@ def test_add_with_git_constraint_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 3 installs, 0 updates, 0 removals • Installing pendulum (2.0.5) • Installing tomlkit (0.7.0) • Installing poetry-plugin (0.1.2 9cf87a2) + +Writing lock file """ assert_plugin_add_result( @@ -162,12 +162,12 @@ def test_add_with_git_constraint_with_subdirectory( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (2.0.5) • Installing poetry-plugin (0.1.2 9cf87a2) + +Writing lock file """ constraint = { @@ -262,11 +262,11 @@ def test_add_existing_plugin_updates_if_requested( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 0 installs, 1 update, 0 removals • Updating poetry-plugin (1.2.3 -> 2.3.4) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^2.3.4") @@ -298,12 +298,12 @@ def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 1 update, 0 removals • Updating tomlkit (0.7.1 -> 0.7.2) • Installing poetry-plugin (1.2.3) + +Writing lock file """ assert_plugin_add_result(tester, expected, "^1.2.3") diff --git a/tests/console/commands/self/test_remove_plugins.py b/tests/console/commands/self/test_remove_plugins.py index 660b3584012..17f24df5708 100644 --- a/tests/console/commands/self/test_remove_plugins.py +++ b/tests/console/commands/self/test_remove_plugins.py @@ -73,11 +73,11 @@ def test_remove_installed_package(tester: CommandTester): Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 0 installs, 0 updates, 1 removal • Removing poetry-plugin (1.2.3) + +Writing lock file """ assert tester.io.fetch_output() == expected diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py index 09c3a21b501..6720406de8b 100644 --- a/tests/console/commands/self/test_update.py +++ b/tests/console/commands/self/test_update.py @@ -71,12 +71,12 @@ def test_self_update_can_update_from_recommended_installation( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 0 installs, 2 updates, 0 removals • Updating cleo (0.8.2 -> 1.0.0) • Updating poetry ({__version__} -> {new_version}) + +Writing lock file """ assert tester.io.fetch_output() == expected_output diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index b7ffde8ae1b..5c2a1bafe1f 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -16,6 +16,8 @@ if TYPE_CHECKING: + from typing import Any + from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture @@ -70,11 +72,11 @@ def test_add_no_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -100,11 +102,11 @@ def test_add_replace_by_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected assert tester.command.installer.executor.installations_count == 1 @@ -119,11 +121,11 @@ def test_add_replace_by_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.1.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -167,11 +169,11 @@ def test_add_equal_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.1.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -191,11 +193,11 @@ def test_add_greater_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -225,12 +227,12 @@ def test_add_constraint_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing msgpack-python (0.5.3) • Installing cachy (0.1.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -255,12 +257,12 @@ def test_add_constraint_dependencies( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing msgpack-python (0.5.3) • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -285,12 +287,12 @@ def test_add_git_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -321,12 +323,12 @@ def test_add_git_constraint_with_poetry( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -354,14 +356,14 @@ def test_add_git_constraint_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals • Installing cleo (0.6.5) • Installing pendulum (1.4.4) • Installing tomlkit (0.5.5) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output().strip() == expected.strip() @@ -400,11 +402,11 @@ def test_add_git_constraint_with_subdirectory( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing two (2.0.0 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output().strip() == expected.strip() assert tester.command.installer.executor.installations_count == 1 @@ -444,12 +446,12 @@ def test_add_git_ssh_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -491,12 +493,12 @@ def test_add_directory_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -532,12 +534,12 @@ def test_add_directory_with_poetry( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -564,12 +566,12 @@ def test_add_file_constraint_wheel( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -600,12 +602,12 @@ def test_add_file_constraint_sdist( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -640,12 +642,12 @@ def test_add_constraint_with_extras_option( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing msgpack-python (0.5.3) • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -680,13 +682,13 @@ def test_add_url_constraint_wheel( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals • Installing pendulum (1.4.4) • Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -722,8 +724,6 @@ def test_add_url_constraint_wheel_with_extras( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals • Installing cleo (0.6.5) @@ -731,6 +731,8 @@ def test_add_url_constraint_wheel_with_extras( • Installing tomlkit (0.5.5) • Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ # Order might be different, split into lines and compare the overall output. expected = set(expected.splitlines()) @@ -764,11 +766,11 @@ def test_add_constraint_with_python( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -802,11 +804,11 @@ def test_add_constraint_with_platform( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -853,11 +855,11 @@ def test_add_constraint_with_source( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -915,11 +917,11 @@ def test_add_to_section_that_does_not_exist_yet( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -962,11 +964,11 @@ def test_add_to_dev_section_deprecated( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing cachy (0.2.0) + +Writing lock file """ assert tester.io.fetch_error() == warning @@ -993,11 +995,11 @@ def test_add_should_not_select_prereleases( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing pyyaml (3.13) + +Writing lock file """ assert tester.io.fetch_output() == expected @@ -1112,11 +1114,11 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing foo (1.1.2) + +Writing lock file """ assert expected in tester.io.fetch_output() @@ -1141,11 +1143,11 @@ def test_add_chooses_prerelease_if_only_prereleases_are_available( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing foo (1.2.3b1) + +Writing lock file """ assert expected in tester.io.fetch_output() @@ -1164,11 +1166,11 @@ def test_add_prefers_stable_releases( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals • Installing foo (1.2.3) + +Writing lock file """ assert expected in tester.io.fetch_output() @@ -1212,11 +1214,11 @@ def test_add_no_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1245,11 +1247,11 @@ def test_add_equal_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.1.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1273,11 +1275,11 @@ def test_add_greater_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1309,12 +1311,12 @@ def test_add_constraint_with_extras_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.1.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1343,12 +1345,12 @@ def test_add_constraint_dependencies_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1372,12 +1374,12 @@ def test_add_git_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1407,12 +1409,12 @@ def test_add_git_constraint_with_poetry_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1439,14 +1441,14 @@ def test_add_git_constraint_with_extras_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) - Installing pendulum (1.4.4) - Installing tomlkit (0.5.5) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1478,12 +1480,12 @@ def test_add_git_ssh_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 9cf87a2) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1520,12 +1522,12 @@ def test_add_directory_constraint_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1558,12 +1560,12 @@ def test_add_directory_with_poetry_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.2 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1591,12 +1593,12 @@ def test_add_file_constraint_wheel_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1629,12 +1631,12 @@ def test_add_file_constraint_sdist_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo (0.1.0 {app.poetry.file.parent.joinpath(path).resolve().as_posix()}) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1671,12 +1673,12 @@ def test_add_constraint_with_extras_option_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing msgpack-python (0.5.3) - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1713,13 +1715,13 @@ def test_add_url_constraint_wheel_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 2 installs, 0 updates, 0 removals - Installing pendulum (1.4.4) - Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1756,8 +1758,6 @@ def test_add_url_constraint_wheel_with_extras_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 4 installs, 0 updates, 0 removals - Installing cleo (0.6.5) @@ -1765,6 +1765,8 @@ def test_add_url_constraint_wheel_with_extras_old_installer( - Installing tomlkit (0.5.5) - Installing demo\ (0.1.0 https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1800,11 +1802,11 @@ def test_add_constraint_with_python_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1840,11 +1842,11 @@ def test_add_constraint_with_platform_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1893,11 +1895,11 @@ def test_add_constraint_with_source_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1959,11 +1961,11 @@ def test_add_to_section_that_does_no_exist_yet_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing cachy (0.2.0) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -1993,11 +1995,11 @@ def test_add_should_not_select_prereleases_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing pyyaml (3.13) + +Writing lock file """ assert old_tester.io.fetch_output() == expected @@ -2058,11 +2060,11 @@ def test_add_should_work_when_adding_existing_package_with_latest_constraint_old Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.1.2) + +Writing lock file """ assert expected in old_tester.io.fetch_output() @@ -2090,11 +2092,11 @@ def test_add_chooses_prerelease_if_only_prereleases_are_available_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3b1) + +Writing lock file """ assert expected in old_tester.io.fetch_output() @@ -2116,11 +2118,11 @@ def test_add_preferes_stable_releases_old_installer( Updating dependencies Resolving dependencies... -Writing lock file - Package operations: 1 install, 0 updates, 0 removals - Installing foo (1.2.3) + +Writing lock file """ assert expected in old_tester.io.fetch_output() @@ -2157,7 +2159,8 @@ def test_add_keyboard_interrupt_restore_content( tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) mocker.patch( - "poetry.installation.installer.Installer.run", side_effect=KeyboardInterrupt() + "poetry.installation.installer.Installer._execute", + side_effect=KeyboardInterrupt(), ) original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() original_lockfile_content = poetry_with_up_to_date_lockfile._locker.lock_data @@ -2200,3 +2203,39 @@ def test_add_with_dry_run_keep_files_intact( assert ( poetry_with_up_to_date_lockfile._locker.lock_data == original_lockfile_content ) + + +def test_add_should_not_change_lock_file_when_dependency_installation_fail( + poetry_with_up_to_date_lockfile: Poetry, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, + mocker: MockerFixture, +): + tester = command_tester_factory("add", poetry=poetry_with_up_to_date_lockfile) + + repo.add_package(get_package("docker", "4.3.1")) + repo.add_package(get_package("cachy", "0.2.0")) + + original_pyproject_content = poetry_with_up_to_date_lockfile.file.read() + original_lockfile_content = poetry_with_up_to_date_lockfile.locker.lock_data + + def error(_: Any) -> int: + tester.io.write("\n BuildError\n\n") + return 1 + + mocker.patch("poetry.installation.installer.Installer._execute", side_effect=error) + tester.execute("cachy") + + expected = """\ +Using version ^0.2.0 for cachy + +Updating dependencies +Resolving dependencies... + + BuildError + +""" + + assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content + assert poetry_with_up_to_date_lockfile.locker.lock_data == original_lockfile_content + assert tester.io.fetch_output() == expected diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 841a2ccde64..400278e7798 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -105,6 +105,7 @@ def __init__(self, lock_path: Path) -> None: self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False + self._lock_data = None self._content_hash = self._get_content_hash() @property @@ -2415,6 +2416,29 @@ def test_installer_can_handle_old_lock_files( assert installer.executor.installations_count == 8 +def test_installer_does_not_write_lock_file_when_installation_fails( + installer: Installer, + locker: Locker, + repo: Repository, + package: ProjectPackage, + mocker: MockerFixture, +): + repo.add_package(get_package("A", "1.0")) + package.add_dependency(Factory.create_dependency("A", "~1.0")) + + locker.locked(False) + + mocker.patch("poetry.installation.installer.Installer._execute", return_value=1) + result = installer.run() + assert result == 1 # error + + assert locker._lock_data is None + + assert installer.executor.installations_count == 0 + assert installer.executor.updates_count == 0 + assert installer.executor.removals_count == 0 + + @pytest.mark.parametrize("quiet", [True, False]) def test_run_with_dependencies_quiet( installer: Installer, diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py index 45d3a5c767d..ced7a6fc934 100644 --- a/tests/installation/test_installer_old.py +++ b/tests/installation/test_installer_old.py @@ -62,6 +62,7 @@ def __init__(self, lock_path: Path) -> None: self._lock = lock_path / "poetry.lock" self._written_data = None self._locked = False + self._lock_data = None self._content_hash = self._get_content_hash() @property From bd4c6a6973ab2bccae49514f131b26d4237a311e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 2 Apr 2023 10:20:48 +0200 Subject: [PATCH 11/20] chore: merge changelog from 1.4.2 --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0f729831a..8cc748df8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Change Log +## [1.4.2] - 2023-04-02 + +### Changed + +- When trying to install wheels with invalid `RECORD` files, Poetry does not fail anymore but only prints a warning. + This mitigates an unintended change introduced in Poetry 1.4.1 ([#7694](https://github.com/python-poetry/poetry/pull/7694)). + +### Fixed + +- Fix an issue where relative git submodule urls were not parsed correctly ([#7017](https://github.com/python-poetry/poetry/pull/7017)). +- Fix an issue where Poetry could freeze when building a project with a build script if it generated enough output to fill the OS pipe buffer ([#7699](https://github.com/python-poetry/poetry/pull/7699)). + + ## [1.4.1] - 2023-03-19 ### Fixed @@ -1786,7 +1799,8 @@ Initial release -[Unreleased]: https://github.com/python-poetry/poetry/compare/1.4.1...master +[Unreleased]: https://github.com/python-poetry/poetry/compare/1.4.2...master +[1.4.2]: https://github.com/python-poetry/poetry/releases/tag/1.4.2 [1.4.1]: https://github.com/python-poetry/poetry/releases/tag/1.4.1 [1.4.0]: https://github.com/python-poetry/poetry/releases/tag/1.4.0 [1.3.2]: https://github.com/python-poetry/poetry/releases/tag/1.3.2 From 860c83872b7d93a25acfb331ccf1d00ebf2e302a Mon Sep 17 00:00:00 2001 From: David Hotham Date: Thu, 6 Apr 2023 16:23:57 +0100 Subject: [PATCH 12/20] Avoid resource warning false positive in unit test (#7769) --- tests/console/commands/test_add.py | 3 +-- tests/console/commands/test_build.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 5c2a1bafe1f..516934c4b32 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -52,8 +52,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: @pytest.fixture() def old_tester(tester: CommandTester) -> CommandTester: - with pytest.warns(DeprecationWarning): - tester.command.installer.use_executor(False) + tester.command.installer._use_executor = False return tester diff --git a/tests/console/commands/test_build.py b/tests/console/commands/test_build.py index 6bb71aace71..2d7ed87007a 100644 --- a/tests/console/commands/test_build.py +++ b/tests/console/commands/test_build.py @@ -39,6 +39,8 @@ def test_build_with_multiple_readme_files( assert wheel_file.exists() assert wheel_file.stat().st_size > 0 - sdist_content = tarfile.open(sdist_file).getnames() + with tarfile.open(sdist_file) as tf: + sdist_content = tf.getnames() + assert "my_package-0.1/README-1.rst" in sdist_content assert "my_package-0.1/README-2.rst" in sdist_content From f6e1f93691eb4b0292ac0a54b659b5516084bd11 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sat, 8 Apr 2023 13:04:38 +0100 Subject: [PATCH 13/20] handle importlib-metadata deprecation (#7774) --- src/poetry/repositories/installed_repository.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/poetry/repositories/installed_repository.py b/src/poetry/repositories/installed_repository.py index 2a3496ac5ba..d8ff78c8455 100644 --- a/src/poetry/repositories/installed_repository.py +++ b/src/poetry/repositories/installed_repository.py @@ -257,9 +257,8 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository: if path in skipped: continue - try: - name = canonicalize_name(distribution.metadata["name"]) - except TypeError: + name = distribution.metadata.get("name") # type: ignore[attr-defined] + if name is None: logger.warning( ( "Project environment contains an invalid distribution" @@ -271,6 +270,8 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository: skipped.add(path) continue + name = canonicalize_name(name) + if name in seen: continue From dfb4904813220566de31fd061e117bf916044841 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sat, 8 Apr 2023 14:07:43 +0100 Subject: [PATCH 14/20] use shutil.which() to detect the active python (#7771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com> --- src/poetry/utils/env.py | 72 ++++++++++++--------- tests/console/commands/env/helpers.py | 8 ++- tests/console/commands/env/test_use.py | 5 ++ tests/utils/test_env.py | 90 ++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index d5a96fac4c2..91a35f212e7 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -9,6 +9,7 @@ import platform import plistlib import re +import shutil import subprocess import sys import sysconfig @@ -472,6 +473,11 @@ def __init__(self, e: CalledProcessError, input: str | None = None) -> None: super().__init__("\n\n".join(message_parts)) +class PythonVersionNotFound(EnvError): + def __init__(self, expected: str) -> None: + super().__init__(f"Could not find the python executable {expected}") + + class NoCompatiblePythonVersionFound(EnvError): def __init__(self, expected: str, given: str | None = None) -> None: if given: @@ -517,34 +523,39 @@ def __init__(self, poetry: Poetry, io: None | IO = None) -> None: self._io = io or NullIO() @staticmethod - def _full_python_path(python: str) -> Path: + def _full_python_path(python: str) -> Path | None: + # eg first find pythonXY.bat on windows. + path_python = shutil.which(python) + if path_python is None: + return None + try: executable = decode( subprocess.check_output( - [python, "-c", "import sys; print(sys.executable)"], + [path_python, "-c", "import sys; print(sys.executable)"], ).strip() ) - except CalledProcessError as e: - raise EnvCommandError(e) + return Path(executable) - return Path(executable) + except CalledProcessError: + return None @staticmethod def _detect_active_python(io: None | IO = None) -> Path | None: io = io or NullIO() - executable = None + io.write_error_line( + ( + "Trying to detect current active python executable as specified in" + " the config." + ), + verbosity=Verbosity.VERBOSE, + ) - try: - io.write_error_line( - ( - "Trying to detect current active python executable as specified in" - " the config." - ), - verbosity=Verbosity.VERBOSE, - ) - executable = EnvManager._full_python_path("python") + executable = EnvManager._full_python_path("python") + + if executable is not None: io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) - except EnvCommandError: + else: io.write_error_line( ( "Unable to detect the current active python executable. Falling" @@ -552,6 +563,7 @@ def _detect_active_python(io: None | IO = None) -> Path | None: ), verbosity=Verbosity.VERBOSE, ) + return executable @staticmethod @@ -592,6 +604,8 @@ def activate(self, python: str) -> Env: pass python_path = self._full_python_path(python) + if python_path is None: + raise PythonVersionNotFound(python) try: python_version_string = decode( @@ -949,25 +963,26 @@ def create_venv( "Trying to find and use a compatible version. " ) - for python_to_try in sorted( + for suffix in sorted( self._poetry.package.AVAILABLE_PYTHONS, key=lambda v: (v.startswith("3"), -len(v), v), reverse=True, ): - if len(python_to_try) == 1: - if not parse_constraint(f"^{python_to_try}.0").allows_any( + if len(suffix) == 1: + if not parse_constraint(f"^{suffix}.0").allows_any( supported_python ): continue - elif not supported_python.allows_any( - parse_constraint(python_to_try + ".*") - ): + elif not supported_python.allows_any(parse_constraint(suffix + ".*")): continue - python = "python" + python_to_try - + python_name = f"python{suffix}" if self._io.is_debug(): - self._io.write_error_line(f"Trying {python}") + self._io.write_error_line(f"Trying {python_name}") + + python = self._full_python_path(python_name) + if python is None: + continue try: python_patch = decode( @@ -979,14 +994,11 @@ def create_venv( except CalledProcessError: continue - if not python_patch: - continue - if supported_python.allows(Version.parse(python_patch)): self._io.write_error_line( - f"Using {python} ({python_patch})" + f"Using {python_name} ({python_patch})" ) - executable = self._full_python_path(python) + executable = python python_minor = ".".join(python_patch.split(".")[:2]) break diff --git a/tests/console/commands/env/helpers.py b/tests/console/commands/env/helpers.py index 0a067b3c430..942c27243d4 100644 --- a/tests/console/commands/env/helpers.py +++ b/tests/console/commands/env/helpers.py @@ -1,5 +1,7 @@ from __future__ import annotations +import os + from pathlib import Path from typing import TYPE_CHECKING from typing import Any @@ -28,9 +30,11 @@ def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str: elif "sys.version_info[:2]" in python_cmd: return f"{version.major}.{version.minor}" elif "import sys; print(sys.executable)" in python_cmd: - return f"/usr/bin/{cmd[0]}" + executable = cmd[0] + basename = os.path.basename(executable) + return f"/usr/bin/{basename}" else: assert "import sys; print(sys.prefix)" in python_cmd - return str(Path("/prefix")) + return "/prefix" return check_output diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py index 72df646d97c..d0abd38f8c4 100644 --- a/tests/console/commands/env/test_use.py +++ b/tests/console/commands/env/test_use.py @@ -56,6 +56,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( venv_name: str, venvs_in_cache_config: None, ) -> None: + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -94,6 +95,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( + mocker: MockerFixture, tester: CommandTester, current_python: tuple[int, int, int], venv_cache: Path, @@ -112,6 +114,8 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var( doc[venv_name] = {"minor": python_minor, "patch": python_patch} envs_file.write(doc) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") + tester.execute(python_minor) expected = f"""\ @@ -134,6 +138,7 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var( python_minor = ".".join(str(v) for v in current_python[:2]) venv_dir = venv_cache / f"{venv_name}-py{python_minor}" + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "poetry.utils.env.EnvManager._env", new_callable=mocker.PropertyMock, diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index b41d22e6b53..5e1fb3cdbb9 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -27,6 +27,7 @@ from poetry.utils.env import InvalidCurrentPythonVersionError from poetry.utils.env import MockEnv from poetry.utils.env import NoCompatiblePythonVersionFound +from poetry.utils.env import PythonVersionNotFound from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv from poetry.utils.env import build_environment @@ -197,10 +198,12 @@ def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str: elif "sys.version_info[:2]" in python_cmd: return f"{version.major}.{version.minor}" elif "import sys; print(sys.executable)" in python_cmd: - return f"/usr/bin/{cmd[0]}" + executable = cmd[0] + basename = os.path.basename(executable) + return f"/usr/bin/{basename}" else: assert "import sys; print(sys.prefix)" in python_cmd - return str(Path("/prefix")) + return "/prefix" return check_output @@ -218,6 +221,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -252,6 +256,30 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( assert env.base == Path("/prefix") +def test_activate_fails_when_python_cannot_be_found( + tmp_dir: str, + manager: EnvManager, + poetry: Poetry, + config: Config, + mocker: MockerFixture, + venv_name: str, +) -> None: + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + os.mkdir(os.path.join(tmp_dir, f"{venv_name}-py3.7")) + + config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + + mocker.patch("shutil.which", return_value=None) + + with pytest.raises(PythonVersionNotFound) as e: + manager.activate("python3.7") + + expected_message = "Could not find the python executable python3.7" + assert str(e.value) == expected_message + + def test_activate_activates_existing_virtualenv_no_envs_file( tmp_dir: str, manager: EnvManager, @@ -267,6 +295,7 @@ def test_activate_activates_existing_virtualenv_no_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -311,6 +340,7 @@ def test_activate_activates_same_virtualenv_with_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -354,6 +384,7 @@ def test_activate_activates_different_virtualenv_with_envs_file( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), @@ -407,6 +438,7 @@ def test_activate_activates_recreates_for_different_patch( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -474,6 +506,7 @@ def test_activate_does_not_recreate_when_switching_minor( config.merge({"virtualenvs": {"path": str(tmp_dir)}}) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.6.6")), @@ -1070,6 +1103,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ poetry.package.python_versions = "^3.6" mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.7.5")), @@ -1093,6 +1127,34 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ ) +def test_create_venv_finds_no_python_executable( + manager: EnvManager, + poetry: Poetry, + config: Config, + mocker: MockerFixture, + config_virtualenvs_path: Path, + venv_name: str, +) -> None: + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + poetry.package.python_versions = "^3.6" + + mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", return_value=None) + + with pytest.raises(NoCompatiblePythonVersionFound) as e: + manager.create_venv() + + expected_message = ( + "Poetry was unable to find a compatible version. " + "If you have one, you can explicitly use it " + 'via the "env use" command.' + ) + + assert str(e.value) == expected_message + + def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific_ones( manager: EnvManager, poetry: Poetry, @@ -1107,8 +1169,10 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific poetry.package.python_versions = "^3.6" mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( - "subprocess.check_output", side_effect=["3.5.3", "3.9.0", "/usr/bin/python3.9"] + "subprocess.check_output", + side_effect=["/usr/bin/python3", "3.5.3", "/usr/bin/python3.9", "3.9.0"], ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" @@ -1309,6 +1373,7 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( } ) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(), @@ -1546,13 +1611,15 @@ def test_create_venv_accepts_fallback_version_w_nonzero_patchlevel( def mock_check_output(cmd: str, *args: Any, **kwargs: Any) -> str: if GET_PYTHON_VERSION_ONELINER in cmd: - if "python3.5" in cmd: + executable = cmd[0] + if "python3.5" in str(executable): return "3.5.12" else: return "3.7.1" else: return "/usr/bin/python3.5" + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") check_output = mocker.patch( "subprocess.check_output", side_effect=mock_check_output, @@ -1662,6 +1729,7 @@ def test_create_venv_project_name_empty_sets_correct_prompt( venv_name = manager.generate_env_name("", str(poetry.file.parent)) mocker.patch("sys.version_info", (2, 7, 16)) + mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}") mocker.patch( "subprocess.check_output", side_effect=check_output_wrapper(Version.parse("3.7.5")), @@ -1697,3 +1765,17 @@ def test_fallback_on_detect_active_python( assert active_python is None assert m.call_count == 1 + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") +def test_detect_active_python_with_bat(poetry: Poetry, tmp_path: Path) -> None: + """On Windows pyenv uses batch files for python management.""" + python_wrapper = tmp_path / "python.bat" + wrapped_python = Path(r"C:\SpecialPython\python.exe") + with python_wrapper.open("w") as f: + f.write(f"@echo {wrapped_python}") + os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] + + active_python = EnvManager(poetry)._detect_active_python() + + assert active_python == wrapped_python From fd706c8c12a7e7392a92dce0b6767a1d5ba13cb1 Mon Sep 17 00:00:00 2001 From: Maxim Koltsov Date: Fri, 16 Dec 2022 15:00:24 +0300 Subject: [PATCH 15/20] Cache git dependencies as wheels (#7473) Currently, poetry install will clone, build and install every git dependency when it's not present in the environment. This is OK for developer's machines, but not OK for CI - there environment is always fresh, and installing git dependencies takes significant time on each CI run, especially if the dependency has C extensions that need to be built. This commit builds a wheel for every git dependency that has precise reference hash in lock file and is not required to be in editable mode, stores that wheel in a cache dir and will install from it instead of cloning the repository again. --- src/poetry/installation/chef.py | 4 +- src/poetry/installation/executor.py | 39 +++++++++-- src/poetry/utils/cache.py | 45 ++++++++++-- tests/installation/test_executor.py | 103 ++++++++++++++++++++++++++-- tests/utils/test_cache.py | 45 ++++++++---- 5 files changed, 205 insertions(+), 31 deletions(-) diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index 5f57ba3e27b..e62b0b6f422 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -94,8 +94,8 @@ def prepare( return archive if archive.is_dir(): - tmp_dir = tempfile.mkdtemp(prefix="poetry-chef-") - return self._prepare(archive, Path(tmp_dir), editable=editable) + destination = output_dir or Path(tempfile.mkdtemp(prefix="poetry-chef-")) + return self._prepare(archive, destination=destination, editable=editable) return self._prepare_sdist(archive, destination=output_dir) diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index fc3efe37059..a22d21e16b2 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -529,7 +529,7 @@ def _install(self, operation: Install | Update) -> int: cleanup_archive: bool = False if package.source_type == "git": archive = self._prepare_git_archive(operation) - cleanup_archive = True + cleanup_archive = operation.package.develop elif package.source_type == "file": archive = self._prepare_archive(operation) elif package.source_type == "directory": @@ -584,7 +584,9 @@ def _remove(self, package: Package) -> int: raise - def _prepare_archive(self, operation: Install | Update) -> Path: + def _prepare_archive( + self, operation: Install | Update, *, output_dir: Path | None = None + ) -> Path: package = operation.package operation_message = self.get_operation_message(operation) @@ -603,12 +605,28 @@ def _prepare_archive(self, operation: Install | Update) -> Path: self._populate_hashes_dict(archive, package) - return self._chef.prepare(archive, editable=package.develop) + return self._chef.prepare( + archive, editable=package.develop, output_dir=output_dir + ) def _prepare_git_archive(self, operation: Install | Update) -> Path: from poetry.vcs.git import Git package = operation.package + assert package.source_url is not None + + if package.source_resolved_reference and not package.develop: + # Only cache git archives when we know precise reference hash, + # otherwise we might get stale archives + cached_archive = self._artifact_cache.get_cached_archive_for_git( + package.source_url, + package.source_resolved_reference, + package.source_subdirectory, + env=self._env, + ) + if cached_archive is not None: + return cached_archive + operation_message = self.get_operation_message(operation) message = ( @@ -616,7 +634,6 @@ def _prepare_git_archive(self, operation: Install | Update) -> Path: ) self._write(operation, message) - assert package.source_url is not None source = Git.clone( url=package.source_url, source_root=self._env.path / "src", @@ -627,10 +644,22 @@ def _prepare_git_archive(self, operation: Install | Update) -> Path: original_url = package.source_url package._source_url = str(source.path) - archive = self._prepare_archive(operation) + output_dir = None + if package.source_resolved_reference and not package.develop: + output_dir = self._artifact_cache.get_cache_directory_for_git( + original_url, + package.source_resolved_reference, + package.source_subdirectory, + ) + archive = self._prepare_archive(operation, output_dir=output_dir) package._source_url = original_url + if output_dir is not None and output_dir.is_dir(): + # Mark directories with cached git packages, to distinguish from + # "normal" cache + (output_dir / ".created_from_git_dependency").touch() + return archive def _install_directory_without_wheel_installer( diff --git a/src/poetry/utils/cache.py b/src/poetry/utils/cache.py index d0d07fb113f..6f2220556ea 100644 --- a/src/poetry/utils/cache.py +++ b/src/poetry/utils/cache.py @@ -231,6 +231,9 @@ def get_cache_directory_for_link(self, link: Link) -> Path: if link.subdirectory_fragment: key_parts["subdirectory"] = link.subdirectory_fragment + return self._get_directory_from_hash(key_parts) + + def _get_directory_from_hash(self, key_parts: object) -> Path: key = hashlib.sha256( json.dumps( key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True @@ -238,28 +241,60 @@ def get_cache_directory_for_link(self, link: Link) -> Path: ).hexdigest() split_key = [key[:2], key[2:4], key[4:6], key[6:]] - return self._cache_dir.joinpath(*split_key) + def get_cache_directory_for_git( + self, url: str, ref: str, subdirectory: str | None + ) -> Path: + key_parts = {"url": url, "ref": ref} + if subdirectory: + key_parts["subdirectory"] = subdirectory + + return self._get_directory_from_hash(key_parts) + def get_cached_archive_for_link( self, link: Link, *, strict: bool, env: Env | None = None, + ) -> Path | None: + cache_dir = self.get_cache_directory_for_link(link) + + return self._get_cached_archive( + cache_dir, strict=strict, filename=link.filename, env=env + ) + + def get_cached_archive_for_git( + self, url: str, reference: str, subdirectory: str | None, env: Env + ) -> Path | None: + cache_dir = self.get_cache_directory_for_git(url, reference, subdirectory) + + return self._get_cached_archive(cache_dir, strict=False, env=env) + + def _get_cached_archive( + self, + cache_dir: Path, + *, + strict: bool, + filename: str | None = None, + env: Env | None = None, ) -> Path | None: assert strict or env is not None + # implication "strict -> filename should not be None" + assert not strict or filename is not None - archives = self._get_cached_archives_for_link(link) + archives = self._get_cached_archives(cache_dir) if not archives: return None candidates: list[tuple[float | None, Path]] = [] + for archive in archives: if strict: # in strict mode return the original cached archive instead of the # prioritized archive type. - if link.filename == archive.name: + if filename == archive.name: return archive continue @@ -286,9 +321,7 @@ def get_cached_archive_for_link( return min(candidates)[1] - def _get_cached_archives_for_link(self, link: Link) -> list[Path]: - cache_dir = self.get_cache_directory_for_link(link) - + def _get_cached_archives(self, cache_dir: Path) -> list[Path]: archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] paths: list[Path] = [] for archive_type in archive_types: diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 98412529977..1401ffec981 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -34,6 +34,7 @@ from poetry.repositories.repository_pool import RepositoryPool from poetry.utils.cache import ArtifactCache from poetry.utils.env import MockEnv +from poetry.vcs.git.backend import Git from tests.repositories.test_pypi_repository import MockRepository @@ -81,7 +82,10 @@ def _prepare( wheel = self._directory_wheels.pop(0) self._directory_wheels.append(wheel) - return wheel + destination.mkdir(parents=True, exist_ok=True) + dst_wheel = destination / wheel.name + shutil.copyfile(wheel, dst_wheel) + return dst_wheel return super()._prepare(directory, destination, editable=editable) @@ -276,8 +280,8 @@ def test_execute_executes_a_batch_of_operations( 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), + mocker.call(chef, mocker.ANY, destination=mocker.ANY, editable=False), + mocker.call(chef, mocker.ANY, destination=mocker.ANY, editable=True), ] @@ -675,6 +679,7 @@ def test_executor_should_not_write_pep610_url_references_for_cached_package( executor = Executor(tmp_venv, pool, config, io) executor.execute([Install(package)]) verify_installed_distribution(tmp_venv, package) + assert link_cached.exists(), "cached file should not be deleted" def test_executor_should_write_pep610_url_references_for_wheel_files( @@ -707,6 +712,7 @@ def test_executor_should_write_pep610_url_references_for_wheel_files( "url": url.as_uri(), } verify_installed_distribution(tmp_venv, package, expected_url_reference) + assert url.exists(), "source file should not be deleted" def test_executor_should_write_pep610_url_references_for_non_wheel_files( @@ -739,6 +745,7 @@ def test_executor_should_write_pep610_url_references_for_non_wheel_files( "url": url.as_uri(), } verify_installed_distribution(tmp_venv, package, expected_url_reference) + assert url.exists(), "source file should not be deleted" def test_executor_should_write_pep610_url_references_for_directories( @@ -749,6 +756,7 @@ def test_executor_should_write_pep610_url_references_for_directories( io: BufferedIO, wheel: Path, fixture_dir: FixtureDirGetter, + mocker: MockerFixture, ): url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( @@ -757,6 +765,7 @@ def test_executor_should_write_pep610_url_references_for_directories( chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef @@ -764,6 +773,7 @@ def test_executor_should_write_pep610_url_references_for_directories( verify_installed_distribution( tmp_venv, package, {"dir_info": {}, "url": url.as_uri()} ) + assert not prepare_spy.spy_return.exists(), "archive not cleaned up" def test_executor_should_write_pep610_url_references_for_editable_directories( @@ -774,6 +784,7 @@ def test_executor_should_write_pep610_url_references_for_editable_directories( io: BufferedIO, wheel: Path, fixture_dir: FixtureDirGetter, + mocker: MockerFixture, ): url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( @@ -786,6 +797,7 @@ def test_executor_should_write_pep610_url_references_for_editable_directories( chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef @@ -793,6 +805,7 @@ def test_executor_should_write_pep610_url_references_for_editable_directories( verify_installed_distribution( tmp_venv, package, {"dir_info": {"editable": True}, "url": url.as_uri()} ) + assert not prepare_spy.spy_return.exists(), "archive not cleaned up" @pytest.mark.parametrize("is_artifact_cached", [False, True]) @@ -848,6 +861,7 @@ def test_executor_should_write_pep610_url_references_for_wheel_urls( download_spy.assert_called_once_with( mocker.ANY, operation, Link(package.source_url) ) + assert download_spy.spy_return.exists(), "cached file should not be deleted" @pytest.mark.parametrize( @@ -938,10 +952,12 @@ def mock_get_cached_archive_for_link_func(_: Link, *, strict: bool, **__: Any): download_spy.assert_called_once_with( mocker.ANY, operation, Link(package.source_url) ) + assert download_spy.spy_return.exists(), "cached file should not be deleted" else: download_spy.assert_not_called() +@pytest.mark.parametrize("is_artifact_cached", [False, True]) def test_executor_should_write_pep610_url_references_for_git( tmp_venv: VirtualEnv, pool: RepositoryPool, @@ -950,18 +966,33 @@ def test_executor_should_write_pep610_url_references_for_git( io: BufferedIO, mock_file_downloads: None, wheel: Path, + mocker: MockerFixture, + fixture_dir: FixtureDirGetter, + is_artifact_cached: bool, ): + if is_artifact_cached: + link_cached = fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" + mocker.patch( + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_git", + return_value=link_cached, + ) + clone_spy = mocker.spy(Git, "clone") + + source_resolved_reference = "123456" + source_url = "https://github.com/demo/demo.git" + package = Package( "demo", "0.1.2", source_type="git", source_reference="master", - source_resolved_reference="123456", - source_url="https://github.com/demo/demo.git", + source_resolved_reference=source_resolved_reference, + source_url=source_url, ) chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") executor = Executor(tmp_venv, pool, config, io) executor._chef = chef @@ -979,6 +1010,68 @@ def test_executor_should_write_pep610_url_references_for_git( }, ) + if is_artifact_cached: + clone_spy.assert_not_called() + prepare_spy.assert_not_called() + else: + clone_spy.assert_called_once_with( + url=source_url, source_root=mocker.ANY, revision=source_resolved_reference + ) + prepare_spy.assert_called_once() + assert prepare_spy.spy_return.exists(), "cached file should not be deleted" + assert (prepare_spy.spy_return.parent / ".created_from_git_dependency").exists() + + +def test_executor_should_write_pep610_url_references_for_editable_git( + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + mock_file_downloads: None, + wheel: Path, + mocker: MockerFixture, + fixture_dir: FixtureDirGetter, +): + source_resolved_reference = "123456" + source_url = "https://github.com/demo/demo.git" + + package = Package( + "demo", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference=source_resolved_reference, + source_url=source_url, + develop=True, + ) + + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + prepare_spy = mocker.spy(chef, "prepare") + cache_spy = mocker.spy(artifact_cache, "get_cached_archive_for_git") + + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef + executor.execute([Install(package)]) + verify_installed_distribution( + tmp_venv, + package, + { + "vcs_info": { + "vcs": "git", + "requested_revision": "master", + "commit_id": "123456", + }, + "url": package.source_url, + }, + ) + + cache_spy.assert_not_called() + prepare_spy.assert_called_once() + assert not prepare_spy.spy_return.exists(), "editable git should not be cached" + assert not (prepare_spy.spy_return.parent / ".created_from_git_dependency").exists() + def test_executor_should_append_subdirectory_for_git( mocker: MockerFixture, diff --git a/tests/utils/test_cache.py b/tests/utils/test_cache.py index c2cd47f6d68..3475ab25cb2 100644 --- a/tests/utils/test_cache.py +++ b/tests/utils/test_cache.py @@ -270,20 +270,32 @@ def test_get_cache_directory_for_link(tmp_path: Path) -> None: assert directory == expected -def test_get_cached_archives_for_link( - fixture_dir: FixtureDirGetter, mocker: MockerFixture -) -> None: +@pytest.mark.parametrize("subdirectory", [None, "subdir"]) +def test_get_cache_directory_for_git(tmp_path: Path, subdirectory: str | None) -> None: + cache = ArtifactCache(cache_dir=tmp_path) + directory = cache.get_cache_directory_for_git( + url="https://github.com/demo/demo.git", ref="123456", subdirectory=subdirectory + ) + + if subdirectory: + expected = Path( + f"{tmp_path.as_posix()}/53/08/33/" + "7851e5806669aa15ab0c555b13bd5523978057323c6a23a9cee18ec51c" + ) + else: + expected = Path( + f"{tmp_path.as_posix()}/61/14/30/" + "7c57f8fd71e4eee40b18893b9b586cba45177f15e300f4fb8b14ccc933" + ) + + assert directory == expected + + +def test_get_cached_archives(fixture_dir: FixtureDirGetter) -> None: distributions = fixture_dir("distributions") cache = ArtifactCache(cache_dir=Path()) - mocker.patch.object( - cache, - "get_cache_directory_for_link", - return_value=distributions, - ) - archives = cache._get_cached_archives_for_link( - Link("https://files.python-poetry.org/demo-0.1.0.tar.gz") - ) + archives = cache._get_cached_archives(distributions) assert archives assert set(archives) == set(distributions.glob("*.whl")) | set( @@ -328,7 +340,7 @@ def test_get_not_found_cached_archive_for_link( mocker.patch.object( cache, - "_get_cached_archives_for_link", + "_get_cached_archives", return_value=available_packages, ) @@ -380,7 +392,7 @@ def test_get_found_cached_archive_for_link( mocker.patch.object( cache, - "_get_cached_archives_for_link", + "_get_cached_archives", return_value=[ Path("/cache/demo-0.1.0-py2.py3-none-any"), Path("/cache/demo-0.1.0.tar.gz"), @@ -392,3 +404,10 @@ def test_get_found_cached_archive_for_link( archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) assert Path(cached) == archive + + +def test_get_cached_archive_for_git() -> None: + """Smoke test that checks that no assertion is raised.""" + cache = ArtifactCache(cache_dir=Path()) + archive = cache.get_cached_archive_for_git("url", "ref", "subdirectory", MockEnv()) + assert archive is None From d5f83fffc9c5813a589f7ef928fe31549171954e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 9 Apr 2023 18:17:00 +0200 Subject: [PATCH 16/20] installer: fix content of `direct_url.json` for editable installs from git (#7473) --- src/poetry/installation/executor.py | 9 +++++---- tests/installation/test_executor.py | 8 ++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index a22d21e16b2..d8b658461f0 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -653,7 +653,8 @@ def _prepare_git_archive(self, operation: Install | Update) -> Path: ) archive = self._prepare_archive(operation, output_dir=output_dir) - package._source_url = original_url + if not package.develop: + package._source_url = original_url if output_dir is not None and output_dir.is_dir(): # Mark directories with cached git packages, to distinguish from @@ -893,12 +894,12 @@ def _save_url_reference(self, operation: Operation) -> None: url_reference: dict[str, Any] | None = None - if package.source_type == "git": + if package.source_type == "git" and not package.develop: url_reference = self._create_git_url_reference(package) + elif package.source_type in ("directory", "git"): + url_reference = self._create_directory_url_reference(package) elif package.source_type == "url": url_reference = self._create_url_url_reference(package) - elif package.source_type == "directory": - url_reference = self._create_directory_url_reference(package) elif package.source_type == "file": url_reference = self._create_file_url_reference(package) diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 1401ffec981..0ac7f78e2cd 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1058,12 +1058,8 @@ def test_executor_should_write_pep610_url_references_for_editable_git( tmp_venv, package, { - "vcs_info": { - "vcs": "git", - "requested_revision": "master", - "commit_id": "123456", - }, - "url": package.source_url, + "dir_info": {"editable": True}, + "url": Path(package.source_url).as_uri(), }, ) From c5a711159fc00d3408884877d61a72ff0bd0a53e Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 10 Apr 2023 12:49:00 +0100 Subject: [PATCH 17/20] safe decoding of build backend errors (#7781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com> --- src/poetry/installation/chef.py | 5 +-- src/poetry/utils/cache.py | 39 ++--------------------- tests/installation/test_executor.py | 48 +++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index e62b0b6f422..5ea53476c25 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -17,6 +17,7 @@ from poetry.core.utils.helpers import temporary_directory from pyproject_hooks import quiet_subprocess_runner # type: ignore[import] +from poetry.utils._compat import decode from poetry.utils.env import ephemeral_environment @@ -135,9 +136,9 @@ def _prepare( e.exception.stdout is not None or e.exception.stderr is not None ): message_parts.append( - e.exception.stderr.decode() + decode(e.exception.stderr) if e.exception.stderr is not None - else e.exception.stdout.decode() + else decode(e.exception.stdout) ) error = ChefBuildError("\n\n".join(message_parts)) diff --git a/src/poetry/utils/cache.py b/src/poetry/utils/cache.py index 6f2220556ea..0b8c9e4d611 100644 --- a/src/poetry/utils/cache.py +++ b/src/poetry/utils/cache.py @@ -1,6 +1,5 @@ from __future__ import annotations -import contextlib import dataclasses import hashlib import json @@ -15,6 +14,8 @@ from typing import Generic from typing import TypeVar +from poetry.utils._compat import decode +from poetry.utils._compat import encode from poetry.utils.wheel import InvalidWheelName from poetry.utils.wheel import Wheel @@ -32,42 +33,6 @@ logger = logging.getLogger(__name__) -def decode(string: bytes, encodings: list[str] | None = None) -> str: - """ - Compatiblity decode function pulled from cachy. - - :param string: The byte string to decode. - :param encodings: List of encodings to apply - :return: Decoded string - """ - if encodings is None: - encodings = ["utf-8", "latin1", "ascii"] - - for encoding in encodings: - with contextlib.suppress(UnicodeDecodeError): - return string.decode(encoding) - - return string.decode(encodings[0], errors="ignore") - - -def encode(string: str, encodings: list[str] | None = None) -> bytes: - """ - Compatibility encode function from cachy. - - :param string: The string to encode. - :param encodings: List of encodings to apply - :return: Encoded byte string - """ - if encodings is None: - encodings = ["utf-8", "latin1", "ascii"] - - for encoding in encodings: - with contextlib.suppress(UnicodeDecodeError): - return string.encode(encoding) - - return string.encode(encodings[0], errors="ignore") - - def _expiration(minutes: int) -> int: """ Calculates the time in seconds since epoch that occurs 'minutes' from now. diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 0ac7f78e2cd..05e2cdc648f 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1240,7 +1240,6 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( config: Config, pool: RepositoryPool, io: BufferedIO, - tmp_dir: str, mock_file_downloads: None, env: MockEnv, fixture_dir: FixtureDirGetter, @@ -1265,11 +1264,7 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( # must not be included in the error message directory_package.python_versions = ">=3.7" - return_code = executor.execute( - [ - Install(directory_package), - ] - ) + return_code = executor.execute([Install(directory_package)]) assert return_code == 1 @@ -1306,6 +1301,47 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( assert output.endswith(expected_end) +@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"]) +@pytest.mark.parametrize("stderr", [None, "Errör on stderr"]) +def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess_encoding( + encoding: str, + stderr: str | None, + mocker: MockerFixture, + config: Config, + pool: RepositoryPool, + io: BufferedIO, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + """Test that the output of the subprocess is decoded correctly.""" + stdout = "Errör on stdout" + error = BuildBackendException( + CalledProcessError( + 1, + ["pip"], + output=stdout.encode(encoding), + stderr=stderr.encode(encoding) if stderr else None, + ) + ) + mocker.patch.object(ProjectBuilder, "get_requires_for_build", side_effect=error) + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + directory_package = Package( + "simple-project", + "1.2.3", + source_type="directory", + source_url=fixture_dir("simple_project").resolve().as_posix(), + ) + + return_code = executor.execute([Install(directory_package)]) + + assert return_code == 1 + assert (stderr or stdout) in io.fetch_output() + + def test_build_system_requires_not_available( config: Config, pool: RepositoryPool, From d2e6ad6e456c6688c1c4be691aaa5e230644ccd6 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 10 Apr 2023 13:06:25 +0100 Subject: [PATCH 18/20] avoid infinite loop when adding a dependency (#7405) --- src/poetry/mixology/version_solver.py | 4 +- tests/conftest.py | 13 ++- tests/console/commands/test_add.py | 33 +++++++ .../with_path_dependency/bazz/setup.py | 12 +++ .../fixtures/with_path_dependency/poetry.lock | 98 +++++++++++++++++++ .../with_path_dependency/pyproject.toml | 15 +++ tests/types.py | 1 + 7 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/with_path_dependency/bazz/setup.py create mode 100644 tests/fixtures/with_path_dependency/poetry.lock create mode 100644 tests/fixtures/with_path_dependency/pyproject.toml diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py index 6c66dc4c5e9..04ba762d963 100644 --- a/src/poetry/mixology/version_solver.py +++ b/src/poetry/mixology/version_solver.py @@ -331,7 +331,9 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility # .. _algorithm documentation: # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution # noqa: E501 if difference is not None: - new_terms.append(difference.inverse) + inverse = difference.inverse + if inverse.dependency != most_recent_satisfier.dependency: + new_terms.append(inverse) incompatibility = Incompatibility( new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause) diff --git a/tests/conftest.py b/tests/conftest.py index 47c11049479..5094c97b667 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -378,6 +378,7 @@ def _factory( install_deps: bool = True, source: Path | None = None, locker_config: dict[str, Any] | None = None, + use_test_locker: bool = True, ) -> Poetry: project_dir = workspace / f"poetry-fixture-{name}" dependencies = dependencies or {} @@ -412,12 +413,14 @@ def _factory( poetry = Factory().create_poetry(project_dir) - locker = TestLocker( - poetry.locker.lock, locker_config or poetry.locker._local_config - ) - locker.write() + if use_test_locker: + locker = TestLocker( + poetry.locker.lock, locker_config or poetry.locker._local_config + ) + locker.write() + + poetry.set_locker(locker) - poetry.set_locker(locker) poetry.set_config(config) pool = RepositoryPool() diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 516934c4b32..706aa637edc 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -10,6 +10,7 @@ from poetry.core.constraints.version import Version from poetry.core.packages.package import Package +from poetry.puzzle.exceptions import SolverProblemError from poetry.repositories.legacy_repository import LegacyRepository from tests.helpers import get_dependency from tests.helpers import get_package @@ -45,6 +46,20 @@ def poetry_with_up_to_date_lockfile( ) +@pytest.fixture +def poetry_with_path_dependency( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + source = fixture_dir("with_path_dependency") + + poetry = project_factory( + name="foobar", + source=source, + use_test_locker=False, + ) + return poetry + + @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("add") @@ -2238,3 +2253,21 @@ def error(_: Any) -> int: assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content assert poetry_with_up_to_date_lockfile.locker.lock_data == original_lockfile_content assert tester.io.fetch_output() == expected + + +def test_add_with_path_dependency_no_loopiness( + poetry_with_path_dependency: Poetry, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, +) -> None: + """https://github.com/python-poetry/poetry/issues/7398""" + tester = command_tester_factory("add", poetry=poetry_with_path_dependency) + + requests_old = get_package("requests", "2.25.1") + requests_new = get_package("requests", "2.28.2") + + repo.add_package(requests_old) + repo.add_package(requests_new) + + with pytest.raises(SolverProblemError): + tester.execute("requests") diff --git a/tests/fixtures/with_path_dependency/bazz/setup.py b/tests/fixtures/with_path_dependency/bazz/setup.py new file mode 100644 index 00000000000..97223196678 --- /dev/null +++ b/tests/fixtures/with_path_dependency/bazz/setup.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from distutils.core import setup + + +setup( + name="bazz", + version="1", + py_modules=["demo"], + package_dir={"src": "src"}, + install_requires=["requests~=2.25.1"], +) diff --git a/tests/fixtures/with_path_dependency/poetry.lock b/tests/fixtures/with_path_dependency/poetry.lock new file mode 100644 index 00000000000..3c2e1b9b7c6 --- /dev/null +++ b/tests/fixtures/with_path_dependency/poetry.lock @@ -0,0 +1,98 @@ +# This file is automatically @generated by Poetry 1.4.0.dev0 and should not be changed by hand. + +[[package]] +name = "bazz" +version = "1" +description = "" +category = "main" +optional = false +python-versions = "*" +files = [] +develop = true + +[package.dependencies] +requests = ">=2.25.1,<2.26.0" + +[package.source] +type = "directory" +url = "bazz" + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "81853eb3be9c7faf1950f23273d0fb3dae75833f6e4aa21b6c3038da25ba26f6" diff --git a/tests/fixtures/with_path_dependency/pyproject.toml b/tests/fixtures/with_path_dependency/pyproject.toml new file mode 100644 index 00000000000..705ba4fe229 --- /dev/null +++ b/tests/fixtures/with_path_dependency/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = [] +readme = "README.md" +packages = [{include = "foobar"}] + +[tool.poetry.dependencies] +python = "^3.9" +bazz = { path = "./bazz", develop = true } + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/types.py b/tests/types.py index 95ce4cbc940..c990414f52e 100644 --- a/tests/types.py +++ b/tests/types.py @@ -46,6 +46,7 @@ def __call__( poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, + use_test_locker: bool = True, ) -> Poetry: ... From 7c9a565992bc442bccb81e473b37bc6264f8dc9b Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 10 Apr 2023 07:21:08 -0500 Subject: [PATCH 19/20] Add option to skip installing directory dependencies (#6845) --- docs/cli.md | 10 ++++++++ docs/faq.md | 34 ++++++++++++++++++++++++++ src/poetry/console/commands/install.py | 11 +++++++++ src/poetry/installation/installer.py | 7 ++++++ src/poetry/puzzle/transaction.py | 11 +++++++-- tests/console/commands/test_install.py | 18 ++++++++++++++ tests/installation/test_installer.py | 27 ++++++++++++++++---- 7 files changed, 111 insertions(+), 7 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index f09ffb97273..9bef8f7a699 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -225,6 +225,14 @@ If you want to skip this installation, use the `--no-root` option. poetry install --no-root ``` +Similar to `--no-root` you can use `--no-directory` to skip directory path dependencies: + +```bash +poetry install --no-directory +``` + +This is mainly useful for caching in CI or when building Docker images. See the [FAQ entry]({{< relref "faq#poetry-busts-my-docker-cache-because-it-requires-me-to-copy-my-source-files-in-before-installing-3rd-party-dependencies" >}}) for more information on this option. + 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. @@ -240,6 +248,7 @@ 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. @@ -248,6 +257,7 @@ is set to `false` because the old installer always compiles source files to byte * `--only-root`: Install only the root project, exclude all dependencies. * `--sync`: Synchronize the environment with the locked packages and the specified groups. * `--no-root`: Do not install the root package (your project). +* `--no-directory`: Skip all directory path dependencies (including transitive ones). * `--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). diff --git a/docs/faq.md b/docs/faq.md index 0a8b97b3075..71ac22c1c2d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -189,3 +189,37 @@ This is done so to be compliant with the broader Python ecosystem. For example, if Poetry builds a distribution for a project that uses a version that is not valid according to [PEP 440](https://peps.python.org/pep-0440), third party tools will be unable to parse the version correctly. + + +### Poetry busts my Docker cache because it requires me to COPY my source files in before installing 3rd party dependencies + +By default running `poetry install ...` requires you to have your source files present (both the "root" package and any directory path dependencies you might have). +This interacts poorly with Docker's caching mechanisms because any change to a source file will make any layers (subsequent commands in your Dockerfile) re-run. +For example, you might have a Dockerfile that looks something like this: + +```text +FROM python +COPY pyproject.toml poetry.lock . +COPY src/ ./src +RUN pip install poetry && poetry install --no-dev +``` + +As soon as *any* source file changes, the cache for the `RUN` layer will be invalidated, which forces all 3rd party dependencies (likely the slowest step out of these) to be installed again if you changed any files in `src/`. + +To avoid this cache busting you can split this into two steps: + +1. Install 3rd party dependencies. +2. Copy over your source code and install just the source code. + +This might look something like this: + +```text +FROM python +COPY pyproject.toml poetry.lock . +RUN pip install poetry && poetry install --no-root --no-directory +COPY src/ ./src +RUN poetry install --no-dev +``` + +The two key options we are using here are `--no-root` (skips installing the project source) and `--no-directory` (skips installing any local directory path dependencies, you can omit this if you don't have any). +[More information on the options available for `poetry install`]({{< relref "cli#install" >}}). diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index 844ec37176f..90003d5793e 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -30,6 +30,16 @@ class InstallCommand(InstallerCommand): option( "no-root", None, "Do not install the root package (the current project)." ), + option( + "no-directory", + None, + ( + "Do not install any directory path dependencies; useful to install" + " dependencies without source code, e.g. for caching of Docker layers)" + ), + flag=True, + multiple=False, + ), option( "dry-run", None, @@ -148,6 +158,7 @@ def handle(self) -> int: with_synchronization = True self.installer.only_groups(self.activated_groups) + self.installer.skip_directory(self.option("no-directory")) self.installer.dry_run(self.option("dry-run")) self.installer.requires_synchronization(with_synchronization) self.installer.executor.enable_bytecode_compilation(self.option("compile")) diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index ec5911ce8f7..1df9a887853 100644 --- a/src/poetry/installation/installer.py +++ b/src/poetry/installation/installer.py @@ -59,6 +59,7 @@ def __init__( self._verbose = False self._write_lock = True self._groups: Iterable[str] | None = None + self._skip_directory = False self._execute_operations = True self._lock = False @@ -150,6 +151,11 @@ def update(self, update: bool = True) -> Installer: return self + def skip_directory(self, skip_directory: bool = False) -> Installer: + self._skip_directory = skip_directory + + return self + def lock(self, update: bool = True) -> Installer: """ Prepare the installer for locking only. @@ -334,6 +340,7 @@ def _do_install(self) -> int: ops = solver.solve(use_latest=self._whitelist).calculate_operations( with_uninstalls=self._requires_synchronization, synchronize=self._requires_synchronization, + skip_directory=self._skip_directory, ) if not self._requires_synchronization: diff --git a/src/poetry/puzzle/transaction.py b/src/poetry/puzzle/transaction.py index 74d0d6c5e61..665093416cb 100644 --- a/src/poetry/puzzle/transaction.py +++ b/src/poetry/puzzle/transaction.py @@ -27,7 +27,11 @@ def __init__( self._root_package = root_package def calculate_operations( - self, with_uninstalls: bool = True, synchronize: bool = False + self, + with_uninstalls: bool = True, + synchronize: bool = False, + *, + skip_directory: bool = False, ) -> list[Operation]: from poetry.installation.operations import Install from poetry.installation.operations import Uninstall @@ -70,7 +74,10 @@ def calculate_operations( break - if not installed: + if not ( + installed + or (skip_directory and result_package.source_type == "directory") + ): operations.append(Install(result_package, priority=priority)) if with_uninstalls: diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index f39081872d0..a1e9379de49 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -184,6 +184,24 @@ def test_compile_option_is_passed_to_the_installer( enable_bytecode_compilation_mock.assert_called_once_with(compile) +@pytest.mark.parametrize("skip_directory_cli_value", [True, False]) +def test_no_directory_is_passed_to_installer( + tester: CommandTester, mocker: MockerFixture, skip_directory_cli_value: bool +): + """ + The --no-directory option is passed to the installer. + """ + + mocker.patch.object(tester.command.installer, "run", return_value=1) + + if skip_directory_cli_value is True: + tester.execute("--no-directory") + else: + tester.execute() + + assert tester.command.installer._skip_directory is skip_directory_cli_value + + def test_no_all_extras_doesnt_populate_installer( tester: CommandTester, mocker: MockerFixture ): diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 400278e7798..4d6e975b45a 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -60,12 +60,12 @@ class Executor(BaseExecutor): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self._installs: list[DependencyPackage] = [] + self._installs: list[Package] = [] self._updates: list[DependencyPackage] = [] self._uninstalls: list[DependencyPackage] = [] @property - def installations(self) -> list[DependencyPackage]: + def installations(self) -> list[Package]: return self._installs @property @@ -1276,14 +1276,18 @@ def test_run_installs_with_local_poetry_directory_and_extras( assert installer.executor.installations_count == 2 -def test_run_installs_with_local_poetry_directory_transitive( +@pytest.mark.parametrize("skip_directory", [True, False]) +def test_run_installs_with_local_poetry_directory_and_skip_directory_flag( installer: Installer, locker: Locker, repo: Repository, package: ProjectPackage, - tmpdir: Path, fixture_dir: FixtureDirGetter, + skip_directory: bool, ): + """When we set Installer.skip_directory(True) no path dependencies should + be installed (including transitive dependencies). + """ root_dir = fixture_dir("directory") package.root_dir = root_dir locker.set_lock_path(root_dir) @@ -1299,14 +1303,27 @@ def test_run_installs_with_local_poetry_directory_transitive( repo.add_package(get_package("pendulum", "1.4.4")) repo.add_package(get_package("cachy", "0.2.0")) + installer.skip_directory(skip_directory) + result = installer.run() assert result == 0 + executor: Executor = installer.executor # type: ignore + expected = fixture("with-directory-dependency-poetry-transitive") assert locker.written_data == expected - assert installer.executor.installations_count == 6 + directory_installs = [ + p.name for p in executor.installations if p.source_type == "directory" + ] + + if skip_directory: + assert not directory_installs, directory_installs + assert installer.executor.installations_count == 2 + else: + assert directory_installs, directory_installs + assert installer.executor.installations_count == 6 def test_run_installs_with_local_poetry_file_transitive( From 71754bcd239f3665a2940fbb5ca523982d54960a Mon Sep 17 00:00:00 2001 From: David Hotham Date: Tue, 11 Apr 2023 16:05:33 +0100 Subject: [PATCH 20/20] more information in case of error (#7784) --- src/poetry/inspection/info.py | 2 +- tests/inspection/test_info.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index e538a9cc979..10bd7d04ac9 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -623,7 +623,7 @@ def get_pep517_metadata(path: Path) -> PackageInfo: info = PackageInfo.from_metadata(path) except EnvCommandError as fbe: raise PackageInfoError( - path, "Fallback egg_info generation failed.", fbe + path, e, "Fallback egg_info generation failed.", fbe ) finally: os.chdir(cwd) diff --git a/tests/inspection/test_info.py b/tests/inspection/test_info.py index c789fae8185..cf222059286 100644 --- a/tests/inspection/test_info.py +++ b/tests/inspection/test_info.py @@ -250,12 +250,8 @@ def test_info_setup_missing_mandatory_should_trigger_pep517( setup_py.write_text(decode(setup)) spy = mocker.spy(VirtualEnv, "run") - try: - PackageInfo.from_directory(source_dir) - except PackageInfoError: - assert spy.call_count == 3 - else: - assert spy.call_count == 1 + _ = PackageInfo.from_directory(source_dir) + assert spy.call_count == 1 def test_info_prefer_poetry_config_over_egg_info():