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 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 2a2051d294a..71ac22c1c2d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -33,21 +33,54 @@ 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? +### 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. 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, @@ -156,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/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 %}} 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/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/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/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/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/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/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/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/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..10bd7d04ac9 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 @@ -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/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index 5f57ba3e27b..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 @@ -94,8 +95,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) @@ -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/installation/executor.py b/src/poetry/installation/executor.py index 7f8b5701cd9..d8b658461f0 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,18 +644,30 @@ 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) + if not package.develop: + package._source_url = original_url - 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( 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) @@ -865,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/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index b5aa54f7072..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. @@ -291,12 +297,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) @@ -336,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: @@ -362,7 +367,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/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/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/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 3d0206afb73..987a1acd609 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 @@ -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/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/publishing/uploader.py b/src/poetry/publishing/uploader.py index 1f3a6bd81b9..256fd235259 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,8 @@ def adapter(self) -> adapters.HTTPAdapter: connect=5, total=10, allowed_methods=["GET"], - status_forcelist=[500, 501, 502, 503], + respect_retry_after_header=True, + status_forcelist=STATUS_FORCELIST, ) return adapters.HTTPAdapter(max_retries=retry) 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/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/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 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/authenticator.py b/src/poetry/utils/authenticator.py index 8db92913043..0fb238fb56c 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -24,6 +24,8 @@ 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 @@ -250,6 +252,7 @@ def request( send_kwargs.update(settings) attempt = 0 + resp = None while True: is_last_attempt = attempt >= 5 @@ -259,14 +262,14 @@ 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 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 @@ -274,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/cache.py b/src/poetry/utils/cache.py index d0d07fb113f..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. @@ -231,6 +196,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 +206,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 +286,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/src/poetry/utils/constants.py b/src/poetry/utils/constants.py index 0f799b16d7d..56bec540ae2 100644 --- a/src/poetry/utils/constants.py +++ b/src/poetry/utils/constants.py @@ -3,3 +3,8 @@ # 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 = [429, 500, 501, 502, 503, 504] diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 734a7d64836..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 @@ -33,10 +34,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 @@ -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/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/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_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..d0abd38f8c4 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 @@ -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/console/commands/self/test_add_plugins.py b/tests/console/commands/self/test_add_plugins.py index e8447a32b13..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 = { @@ -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")) @@ -260,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") @@ -296,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 2b988443469..17f24df5708 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": [ @@ -72,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..706aa637edc 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -10,12 +10,15 @@ 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 if TYPE_CHECKING: + from typing import Any + from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture @@ -43,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") @@ -50,8 +67,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 @@ -70,11 +86,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 +116,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 +135,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 +183,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 +207,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 +241,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 +271,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 +301,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 +337,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 +370,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 +416,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 +460,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 +507,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 +548,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 +580,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 +616,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 +656,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 +696,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 +738,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 +745,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 +780,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 +818,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 +869,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 +931,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 +978,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 +1009,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 +1128,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 +1157,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 +1180,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 +1228,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 +1261,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 +1289,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 +1325,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 +1359,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 +1388,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 +1423,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 +1455,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 +1494,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 +1536,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 +1574,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 +1607,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 +1645,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 +1687,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 +1729,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 +1772,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 +1779,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 +1816,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 +1856,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 +1909,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 +1975,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 +2009,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 +2074,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 +2106,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 +2132,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 +2173,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 +2217,57 @@ 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 + + +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/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 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/console/commands/test_install.py b/tests/console/commands/test_install.py index 0200a452bfa..a1e9379de49 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 @@ -180,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 ): @@ -257,6 +279,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, 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 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/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/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(): 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_executor.py b/tests/installation/test_executor.py index 98412529977..05e2cdc648f 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,64 @@ 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, + { + "dir_info": {"editable": True}, + "url": Path(package.source_url).as_uri(), + }, + ) + + 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, @@ -1151,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, @@ -1176,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 @@ -1217,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, diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 76e11ecd611..4d6e975b45a 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 @@ -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 @@ -101,18 +101,19 @@ 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._lock_data = None self._content_hash = self._get_content_hash() @property 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 @@ -207,9 +208,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 +226,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 +295,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 +417,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 +488,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 +631,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 +710,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 +764,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 +820,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 +848,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 +876,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 +912,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 +959,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 +994,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 +1026,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 +1054,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 +1087,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 +1121,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 +1158,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 +1178,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 +1207,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 +1235,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 +1267,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 @@ -1251,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) @@ -1274,13 +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.run() + 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( @@ -1308,7 +1351,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 +1384,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 +1431,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 +1518,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 +1592,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 +1626,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 +1739,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 +1806,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 +1912,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 +1948,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 +1988,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 +2015,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 +2051,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 +2078,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 +2114,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 +2135,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 +2203,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 +2252,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 +2268,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 +2318,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 +2343,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 +2354,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 +2385,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 +2405,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,12 +2426,36 @@ 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 +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, @@ -2382,9 +2475,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 +2539,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 +2581,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 +2620,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 +2717,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 e76fabb21f3..ced7a6fc934 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,18 +58,19 @@ 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._lock_data = None self._content_hash = self._get_content_hash() @property 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 @@ -154,9 +155,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 +173,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 +242,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 +321,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 +405,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 +462,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 +518,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 +546,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 +574,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 +610,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 +658,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 +694,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 +722,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 +756,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 +791,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 +828,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 +850,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 +871,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 +893,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 +920,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 +953,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 +986,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 +1012,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 +1060,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 +1147,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 +1221,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 +1255,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 +1370,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 +1440,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 +1549,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 +1588,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 +1630,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 +1651,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 +1680,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 +1701,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 +1731,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 +1746,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 +1814,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 +1857,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 +1873,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 +1898,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 +1909,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 +1934,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 +1949,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 +1965,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/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/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" 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/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": [] +} 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/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: ... diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 91e6a574bc8..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,7 +276,9 @@ def callback(*_: Any, **___: Any) -> None: (401, 0), (403, 0), (404, 0), - (500, 0), + (429, 5), + (500, 5), + (501, 5), (502, 5), (503, 5), (504, 5), 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 diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index b720974212a..5e1fb3cdbb9 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 @@ -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