diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index d50675a3330..b3aaa8b2c2f 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -31,10 +31,20 @@ jobs: with: repository: python-poetry/website + # use .github from pull request target instead of pull_request.head + # for pull_request_target trigger to avoid arbitrary code execution - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: - path: poetry + path: poetry-github + sparse-checkout: .github + + # only checkout docs from pull_request.head to not use something else by accident + # for pull_request_target trigger (security) + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + with: + path: poetry-docs ref: ${{ github.event.pull_request.head.sha }} + sparse-checkout: docs - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: @@ -44,16 +54,16 @@ jobs: with: hugo-version: '0.83.1' - - uses: ./poetry/.github/actions/bootstrap-poetry + - uses: ./poetry-github/.github/actions/bootstrap-poetry - - uses: ./poetry/.github/actions/poetry-install + - uses: ./poetry-github/.github/actions/poetry-install with: args: --no-root --only main - name: website-build run: | # Rebuild the docs files from the PR checkout. - poetry run python bin/website build --local ./poetry + poetry run python bin/website build --local ./poetry-docs # Build website assets (CSS/JS). npm ci && npm run prod # Build the static website. diff --git a/docs/cli.md b/docs/cli.md index 085f1fe86a9..c5ba541b138 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -321,8 +321,9 @@ poetry add requests pendulum ``` {{% note %}} -A package is looked up, by default, only from the [Default Package Source]({{< relref "repositories/#default-package-source" >}}). -You can modify the default source (PyPI); or add and use [Supplemental Package Sources]({{< relref "repositories/#supplemental-package-sources" >}}) +A package is looked up, by default, only from [PyPI](https://pypi.org). +You can modify the default source (PyPI); +or add and use [Supplemental Package Sources]({{< relref "repositories/#supplemental-package-sources" >}}) or [Explicit Package Sources]({{< relref "repositories/#explicit-package-sources" >}}). For more information, refer to the [Package Sources]({{< relref "repositories/#package-sources" >}}) documentation. @@ -708,7 +709,9 @@ poetry search requests pendulum This command locks (without installing) the dependencies specified in `pyproject.toml`. {{% note %}} -By default, this will lock all dependencies to the latest available compatible versions. To only refresh the lock file, use the `--no-update` option. +By default, packages that have already been added to the lock file before will not be updated. +To update all dependencies to the latest available compatible versions, use `poetry update --lock` +or `poetry lock --regenerate`, which normally produce the same result. This command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-lock">}}) for more information. {{% /note %}} @@ -719,7 +722,7 @@ poetry lock ### Options * `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml`. (**Deprecated**) Use `poetry check --lock` instead. -* `--no-update`: Do not update locked versions, only refresh lock file. +* `--regenerate`: Ignore existing lock file and overwrite it with a new lock file created from scratch. ## version @@ -865,13 +868,7 @@ poetry source add --priority=explicit pypi #### Options -* `--default`: Set this source as the [default]({{< relref "repositories#default-package-source" >}}) (disable PyPI). Deprecated in favor of `--priority`. -* `--secondary`: Set this source as a [secondary]({{< relref "repositories#secondary-package-sources" >}}) source. Deprecated in favor of `--priority`. -* `--priority`: Set the priority of this source. Accepted values are: [`default`]({{< relref "repositories#default-package-source" >}}), [`secondary`]({{< relref "repositories#secondary-package-sources" >}}), [`supplemental`]({{< relref "repositories#supplemental-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information. - -{{% note %}} -At most one of the options above can be provided. See [package sources]({{< relref "repositories#package-sources" >}}) for more information. -{{% /note %}} +* `--priority`: Set the priority of this source. Accepted values are: [`supplemental`]({{< relref "repositories#supplemental-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information. ### source show diff --git a/docs/configuration.md b/docs/configuration.md index ebbd085d149..7c64129d1ab 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -56,7 +56,6 @@ virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = true virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs virtualenvs.prefer-active-python = false @@ -389,35 +388,10 @@ Poetry, for its internal operations, uses the `pip` wheel embedded in the `virtu in Poetry's runtime environment. If a user runs `poetry run pip` when this option is set to `true`, the `pip` the embedded instance of `pip` is used. -You can safely set this, along with `no-setuptools`, to `true`, if you desire a virtual environment with no additional -packages. This is desirable for production environments. +You can safely set this to `true`, if you desire a virtual environment with no additional packages. +This is desirable for production environments. {{% /note %}} -### `virtualenvs.options.no-setuptools` - -**Type**: `boolean` - -**Default**: `false` - -**Environment Variable**: `POETRY_VIRTUALENVS_OPTIONS_NO_SETUPTOOLS` - -*Introduced in 1.2.0* - -If set to `true` the `--no-setuptools` parameter is passed to `virtualenv` on creation of the virtual environment. This -means when a new virtual environment is created, `setuptools` will not be installed in the environment. Poetry, for its -internal operations, does not require `setuptools` and this can safely be set to `true`. - -For environments using python 3.12 or later, `virtualenv` defaults to not -installing `setuptools` when creating a virtual environment. -In such environments this poetry configuration option therefore has no effect: -`setuptools` is not installed either way. -If your project relies on `setuptools`, you should declare it as a dependency. - -{{% warning %}} -Some development tools like IDEs, make an assumption that `setuptools` (and other) packages are always present and -available within a virtual environment. This can cause some features in these tools to not work as expected. -{{% /warning %}} - ### `virtualenvs.options.system-site-packages` **Type**: `boolean` diff --git a/docs/contributing.md b/docs/contributing.md index 3d6d430b1fa..995e899a4a7 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -152,6 +152,14 @@ poetry install poetry run pytest ``` +{{% note %}} +If you want to see the coverage stats after the tests are complete, use: + +```bash +poetry run pytest --cov=src/poetry --cov-report term +``` +{{% /note %}} + When you contribute to Poetry, automated tools will be run to make sure your code is suitable to be merged. Besides pytest, you will need to make sure your code typechecks properly using [mypy](http://mypy-lang.org/): diff --git a/docs/plugins.md b/docs/plugins.md index adcda0ba076..68a028acfe2 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -255,6 +255,28 @@ You can also list all currently installed plugins by running: poetry self show plugins ``` +### Project plugins + +You can also specify that a plugin is required for your project +in the `tool.poetry.requires-plugins` section of the pyproject.toml file: + +```toml +[tool.poetry.requires-plugins] +my-application-plugin = ">1.0" +``` + +If the plugin is not installed in Poetry's own environment when running `poetry install`, +it will be installed only for the current project under `.poetry/plugins` +in the project's directory. + +The syntax to specify `plugins` is the same as for [dependencies]({{< relref "managing-dependencies" >}}). + +{{% warning %}} +You can even overwrite a plugin in Poetry's own environment with another version. +However, if a plugin's dependencies are not compatible with packages in Poetry's own +environment, installation will fail. +{{% /warning %}} + ## Maintaining a plugin diff --git a/docs/pyproject.md b/docs/pyproject.md index 1d0f3a7c271..39b7b192253 100644 --- a/docs/pyproject.md +++ b/docs/pyproject.md @@ -843,6 +843,29 @@ any custom url in the `urls` section. If you publish your package on PyPI, they will appear in the `Project Links` section. +## `requires-poetry` + +A constraint for the Poetry version that is required for this project. +If you are using a Poetry version that is not allowed by this constraint, +an error will be raised. + +```toml +[tool.poetry] +requires-poetry = ">=2.0" +``` + +## `requires-plugins` + +In this section, you can specify that certain plugins are required for your project: + +```toml +[tool.poetry.requires-plugins] +my-application-plugin = ">=1.0" +my-plugin = ">=1.0,<2.0" +``` + +See [Project plugins]({{< relref "plugins#project-plugins" >}}) for more information. + ## Poetry and PEP-517 [PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way diff --git a/docs/repositories.md b/docs/repositories.md index 983628dd670..31122036dfe 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -104,12 +104,15 @@ poetry publish --build --repository foo-pub ## Package Sources -By default, Poetry is configured to use the Python ecosystem's canonical package index +By default, if you have not configured any primary source, +Poetry is configured to use the Python ecosystem's canonical package index [PyPI](https://pypi.org). +You can alter this behaviour and exclusively look up packages only from the configured +package sources by adding at least one primary source. {{% note %}} -With the exception of the implicitly configured source for [PyPI](https://pypi.org) named `pypi`, +Except for the implicitly configured source for [PyPI](https://pypi.org) named `PyPI`, package sources are local to a project and must be configured within the project's [`pyproject.toml`]({{< relref "pyproject" >}}) file. This is **not** the same configuration used when publishing a package. @@ -142,56 +145,17 @@ url = "https://foo.bar/simple/" priority = "primary" ``` -If `priority` is undefined, the source is considered a primary source that takes precedence over PyPI, secondary, supplemental and explicit sources. +If `priority` is undefined, the source is considered a primary source, +which disables the implicit PyPI source and takes precedence over supplemental sources. Package sources are considered in the following order: -1. [default source](#default-package-source-deprecated) (DEPRECATED), -2. [primary sources](#primary-package-sources), -3. implicit PyPI (unless disabled by another [primary source](#primary-package-sources), [default source](#default-package-source-deprecated) or configured explicitly), -4. [secondary sources](#secondary-package-sources-deprecated) (DEPRECATED), -5. [supplemental sources](#supplemental-package-sources). +1. [primary sources](#primary-package-sources) or implicit PyPI (if there are no primary sources), +2. [supplemental sources](#supplemental-package-sources). [Explicit sources](#explicit-package-sources) are considered only for packages that explicitly [indicate their source](#package-source-constraint). Within each priority class, package sources are considered in order of appearance in `pyproject.toml`. -{{% note %}} - -If you want to change the priority of [PyPI](https://pypi.org), you can set it explicitly, e.g. - -```bash -poetry source add --priority=primary PyPI -``` - -If you prefer to disable PyPI completely, -just add a [primary source](#primary-package-sources) -or configure PyPI as [explicit source](#explicit-package-sources). - -{{% /note %}} - - -#### Default Package Source (DEPRECATED) - -*Deprecated in 1.8.0* - -{{% warning %}} - -Configuring a default package source is deprecated because it is the same -as the topmost [primary source](#primary-package-sources). -Just configure a primary package source and put it first in the list of package sources. - -{{% /warning %}} - -By default, if you have not configured any primary source, -Poetry will configure [PyPI](https://pypi.org) as the package source for your project. -You can alter this behaviour and exclusively look up packages only from the configured -package sources by adding at least one primary source (recommended) -or a **single** source with `priority = "default"` (deprecated). - -```bash -poetry source add --priority=default foo https://foo.bar/simple/ -``` - #### Primary Package Sources @@ -234,27 +198,6 @@ with Poetry, the PyPI repository cannot be configured with a given URL. Remember {{% /warning %}} -#### Secondary Package Sources (DEPRECATED) - -*Deprecated in 1.5.0* - -If package sources are configured as secondary, all it means is that these will be given a lower -priority when selecting compatible package distribution that also exists in your default and primary package sources. If the package source should instead be searched only if higher-priority repositories did not return results, please consider a [supplemental source](#supplemental-package-sources) instead. - -You can configure a package source as a secondary source with `priority = "secondary"` in your package -source configuration. - -```bash -poetry source add --priority=secondary foo https://foo.bar/simple/ -``` - -There can be more than one secondary package source. - -{{% warning %}} - -Secondary package sources are deprecated in favor of supplemental package sources. - -{{% /warning %}} #### Supplemental Package Sources @@ -305,9 +248,10 @@ poetry add --source pytorch-gpu-src torch torchvision torchaudio #### Package Source Constraint -All package sources (including secondary and possibly supplemental sources) will be searched during the package lookup -process. These network requests will occur for all sources, regardless of if the package is -found at one or more sources. +All package sources (including possibly supplemental sources) will be searched +during the package lookup process. +These network requests will occur for all primary sources, regardless of if the package is +found at one or more sources, and all supplemental sources until the package is found. In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly. @@ -398,8 +342,8 @@ httpx = {version = "^0.22.0", source = "pypi"} {{% warning %}} -If any source within a project is configured with `priority = "default"`, The implicit `pypi` source will -be disabled and not used for any packages. +The implicit `PyPI` source will be disabled and not used for any packages +if at least one [primary source](#primary-package-sources) is configured. {{% /warning %}} diff --git a/poetry.lock b/poetry.lock index 91fc217135f..4e99899317c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,13 +17,13 @@ testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-ch [[package]] name = "build" -version = "1.2.1" +version = "1.2.2" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.8" files = [ - {file = "build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4"}, - {file = "build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d"}, + {file = "build-1.2.2-py3-none-any.whl", hash = "sha256:277ccc71619d98afdd841a0e96ac9fe1593b823af481d3b0cea748e8894e0613"}, + {file = "build-1.2.2.tar.gz", hash = "sha256:119b2fb462adef986483438377a13b2f42064a2a3a4161f24a0cca698a07ac8c"}, ] [package.dependencies] @@ -717,13 +717,13 @@ trio = ["async_generator", "trio"] [[package]] name = "keyring" -version = "25.3.0" +version = "25.4.1" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae"}, - {file = "keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef"}, + {file = "keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf"}, + {file = "keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b"}, ] [package.dependencies] @@ -737,9 +737,13 @@ pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "more-itertools" @@ -814,7 +818,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -984,7 +987,7 @@ develop = false type = "git" url = "https://github.com/python-poetry/poetry-core.git" reference = "main" -resolved_reference = "b57e32c1bc558031dbae371ec85894e941bf039e" +resolved_reference = "beb93b1aba6ad47667c05721a74c8f3961402046" [[package]] name = "poetry-plugin-export" diff --git a/pyproject.toml b/pyproject.toml index f9fe2d313e3..30cbc165f93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,6 @@ extend-select = [ ignore = [ "B904", # use 'raise ... from err' "B905", # use explicit 'strict=' parameter with 'zip()' - "N818", # Exception name should be named with an Error suffix ] extend-safe-fixes = [ "TCH", # move import from and to TYPE_CHECKING blocks diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index f2a8129f231..bf2f0948d03 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -114,12 +114,7 @@ class Config: "options": { "always-copy": False, "system-site-packages": False, - # we default to False here in order to prevent development environment - # breakages for IDEs etc. as when working in these environments - # assumptions are often made about virtual environments having pip and - # setuptools. "no-pip": False, - "no-setuptools": False, }, "prefer-active-python": False, "prompt": "{project_name}-py{python_version}", @@ -305,7 +300,6 @@ def _get_normalizer(name: str) -> Callable[[str], Any]: "virtualenvs.in-project", "virtualenvs.options.always-copy", "virtualenvs.options.no-pip", - "virtualenvs.options.no-setuptools", "virtualenvs.options.system-site-packages", "virtualenvs.options.prefer-active-python", "experimental.system-git-client", diff --git a/src/poetry/config/config_source.py b/src/poetry/config/config_source.py index ed97fa9176a..ae9da9da050 100644 --- a/src/poetry/config/config_source.py +++ b/src/poetry/config/config_source.py @@ -1,11 +1,13 @@ from __future__ import annotations +from abc import ABC +from abc import abstractmethod from typing import Any -class ConfigSource: - def add_property(self, key: str, value: Any) -> None: - raise NotImplementedError() +class ConfigSource(ABC): + @abstractmethod + def add_property(self, key: str, value: Any) -> None: ... - def remove_property(self, key: str) -> None: - raise NotImplementedError() + @abstractmethod + def remove_property(self, key: str) -> None: ... diff --git a/src/poetry/config/source.py b/src/poetry/config/source.py index f2ff82c2ece..aa7d9a25743 100644 --- a/src/poetry/config/source.py +++ b/src/poetry/config/source.py @@ -1,7 +1,6 @@ from __future__ import annotations import dataclasses -import warnings from typing import TYPE_CHECKING @@ -16,27 +15,13 @@ class Source: name: str url: str = "" - default: dataclasses.InitVar[bool] = False - secondary: dataclasses.InitVar[bool] = False priority: Priority = ( Priority.PRIMARY ) # cheating in annotation: str will be converted to Priority in __post_init__ - def __post_init__(self, default: bool, secondary: bool) -> None: + def __post_init__(self) -> None: if isinstance(self.priority, str): self.priority = Priority[self.priority.upper()] - if default or secondary: - warnings.warn( - "Parameters 'default' and 'secondary' to" - " 'Source' are deprecated. Please provide" - " 'priority' instead.", - DeprecationWarning, - stacklevel=2, - ) - if default: - self.priority = Priority.DEFAULT - elif secondary: - self.priority = Priority.SECONDARY def to_dict(self) -> dict[str, str | bool]: return dataclasses.asdict( diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index d144c427d4f..c81103f10a9 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -4,7 +4,9 @@ import re from contextlib import suppress +from functools import cached_property from importlib import import_module +from pathlib import Path from typing import TYPE_CHECKING from typing import cast @@ -111,20 +113,13 @@ def __init__(self) -> None: @property def poetry(self) -> Poetry: - from pathlib import Path - from poetry.factory import Factory if self._poetry is not None: return self._poetry - project_path = Path.cwd() - - if self._io and self._io.input.option("directory"): - project_path = Path(self._io.input.option("directory")).absolute() - self._poetry = Factory().create_poetry( - cwd=project_path, + cwd=self._directory, io=self._io, disable_plugins=self._disable_plugins, disable_cache=self._disable_cache, @@ -340,6 +335,7 @@ def _load_plugins(self, io: IO | None = None) -> None: from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin_manager import PluginManager + PluginManager.add_project_plugin_path(self._directory) manager = PluginManager(ApplicationPlugin.group) manager.load_plugins() manager.activate(self) @@ -382,6 +378,12 @@ def _default_definition(self) -> Definition: return definition + @cached_property + def _directory(self) -> Path: + if self._io and self._io.input.option("directory"): + return Path(self._io.input.option("directory")).absolute() + return Path.cwd() + def main() -> int: exit_code: int = Application().run() diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 25fec8ce72f..1a8a8e1307c 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -66,10 +66,6 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: boolean_normalizer, ), "virtualenvs.options.no-pip": (boolean_validator, boolean_normalizer), - "virtualenvs.options.no-setuptools": ( - boolean_validator, - boolean_normalizer, - ), "virtualenvs.path": (str, lambda val: str(Path(val))), "virtualenvs.prefer-active-python": (boolean_validator, boolean_normalizer), "virtualenvs.prompt": (str, str), @@ -95,7 +91,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: def handle(self) -> int: from pathlib import Path - from poetry.core.pyproject.exceptions import PyProjectException + from poetry.core.pyproject.exceptions import PyProjectError from poetry.config.config import Config from poetry.config.file_config_source import FileConfigSource @@ -109,7 +105,7 @@ def handle(self) -> int: local_config_file = TOMLFile(self.poetry.file.path.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) - except (RuntimeError, PyProjectException): + except (RuntimeError, PyProjectError): local_config_file = TOMLFile(Path.cwd() / "poetry.toml") if self.option("local"): diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index c080dc1e5e1..750666f69ae 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -7,7 +7,7 @@ from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.command import Command -from poetry.console.exceptions import GroupNotFound +from poetry.console.exceptions import GroupNotFoundError if TYPE_CHECKING: @@ -128,4 +128,4 @@ def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: for opt in sorted(invalid_options[group]) ) message_parts.append(f"{group} (via {opts})") - raise GroupNotFound(f"Group(s) not found: {', '.join(message_parts)}") + raise GroupNotFoundError(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 595b53fc232..e7fbf13109f 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -16,6 +16,7 @@ from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand from poetry.utils.dependency_specification import RequirementsParser +from poetry.utils.env.python_manager import Python if TYPE_CHECKING: @@ -96,7 +97,6 @@ def _init_pyproject( from poetry.config.config import Config from poetry.layouts import layout from poetry.pyproject.toml import PyProjectTOML - from poetry.utils.env import EnvManager is_interactive = self.io.is_interactive() and allow_interactive @@ -174,11 +174,7 @@ def _init_pyproject( config = Config.create() python = ( ">=" - + EnvManager.get_python_version( - precision=2, - prefer_active_python=config.get("virtualenvs.prefer-active-python"), - io=self.io, - ).to_string() + + Python.get_preferred_python(config, self.io).minor_version.to_string() ) if is_interactive: @@ -463,12 +459,12 @@ def _find_best_version_for_package( return package.pretty_name, f"^{version.to_string()}" def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: - from poetry.core.pyproject.exceptions import PyProjectException + from poetry.core.pyproject.exceptions import PyProjectError try: cwd = self.poetry.file.path.parent artifact_cache = self.poetry.pool.artifact_cache - except (PyProjectException, RuntimeError): + except (PyProjectError, RuntimeError): cwd = Path.cwd() artifact_cache = self._get_pool().artifact_cache diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index 63f4f93cb48..d40e6f079b7 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -6,6 +6,7 @@ from cleo.helpers import option from poetry.console.commands.installer_command import InstallerCommand +from poetry.plugins.plugin_manager import PluginManager if TYPE_CHECKING: @@ -100,10 +101,12 @@ def activated_groups(self) -> set[str]: return super().activated_groups def handle(self) -> int: - from poetry.core.masonry.utils.module import ModuleOrPackageNotFound + from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError from poetry.masonry.builders.editable import EditableBuilder + PluginManager.ensure_project_plugins(self.poetry, self.io) + if self.option("extras") and self.option("all-extras"): self.line_error( "You cannot specify explicit" @@ -189,7 +192,7 @@ def handle(self) -> int: try: builder = EditableBuilder(self.poetry, self.env, self.io) builder.build() - except (ModuleOrPackageNotFound, FileNotFoundError) as e: + except (ModuleOrPackageNotFoundError, FileNotFoundError) as e: # This is likely due to the fact that the project is an application # not following the structure expected by Poetry. # No need for an editable install in this case. @@ -201,10 +204,11 @@ def handle(self) -> int: "If you want to use Poetry only for dependency management" " but not for packaging, you can disable package mode by setting" " package-mode = false in your pyproject.toml file.\n" - "In a future version of Poetry this warning will become an error!", - style="warning", + "If you did intend to install the current project, you may need" + " to set `packages` in your pyproject.toml file.\n", + style="error", ) - return 0 + return 1 if overwrite: self.overwrite(log_install.format(tag="success")) diff --git a/src/poetry/console/commands/lock.py b/src/poetry/console/commands/lock.py index c64f1dfd2ad..9c7733d667d 100644 --- a/src/poetry/console/commands/lock.py +++ b/src/poetry/console/commands/lock.py @@ -18,7 +18,10 @@ class LockCommand(InstallerCommand): options: ClassVar[list[Option]] = [ option( - "no-update", None, "Do not update locked versions, only refresh lock file." + "regenerate", + None, + "Ignore existing lock file" + " and overwrite it with a new lock file created from scratch.", ), option( "check", @@ -34,6 +37,8 @@ class LockCommand(InstallerCommand): current directory, processes it, and locks the dependencies in the\ poetry.lock file. +By default, packages that have already been added to the lock file before +will not be updated. poetry lock """ @@ -57,6 +62,6 @@ def handle(self) -> int: ) return 1 - self.installer.lock(update=not self.option("no-update")) + self.installer.lock(update=self.option("regenerate")) return self.installer.run() diff --git a/src/poetry/console/commands/self/show/plugins.py b/src/poetry/console/commands/self/show/plugins.py index 9f8299a35fa..3c59a28a265 100644 --- a/src/poetry/console/commands/self/show/plugins.py +++ b/src/poetry/console/commands/self/show/plugins.py @@ -70,9 +70,7 @@ def _system_project_handle(self) -> int: } for group in [ApplicationPlugin.group, Plugin.group]: - for entry_point in PluginManager(group).get_plugin_entry_points( - env=system_env - ): + for entry_point in PluginManager(group).get_plugin_entry_points(): assert entry_point.dist is not None package = packages_by_name[canonicalize_name(entry_point.dist.name)] diff --git a/src/poetry/console/commands/source/add.py b/src/poetry/console/commands/source/add.py index 669ac2f3c01..541593cac1a 100644 --- a/src/poetry/console/commands/source/add.py +++ b/src/poetry/console/commands/source/add.py @@ -36,19 +36,6 @@ class SourceAddCommand(Command): ] options: ClassVar[list[Option]] = [ - option( - "default", - "d", - "Set this source as the default (disable PyPI). A " - "default source will also be the fallback source if " - "you add other sources. (Deprecated, use --priority)", - ), - option( - "secondary", - "s", - "Set this source as secondary. (Deprecated, use" - " --priority)", - ), option( "priority", "p", @@ -65,8 +52,6 @@ def handle(self) -> int: name: str = self.argument("name") lower_name = name.lower() url: str = self.argument("url") - is_default: bool = self.option("default", False) - is_secondary: bool = self.option("secondary", False) priority_str: str | None = self.option("priority", None) if lower_name == "pypi": @@ -82,66 +67,16 @@ def handle(self) -> int: ) return 1 - if is_default and is_secondary: - self.line_error( - "Cannot configure a source as both default and" - " secondary." - ) - return 1 - - if is_default or is_secondary: - if priority_str is not None: - self.line_error( - "Priority was passed through both --priority and a" - " deprecated flag (--default or --secondary). Please only provide" - " one of these." - ) - return 1 - else: - self.line_error( - "Warning: Priority was set through a deprecated flag" - " (--default or --secondary). Consider using --priority next" - " time." - ) - - if is_default: - priority = Priority.DEFAULT - elif is_secondary: - priority = Priority.SECONDARY - elif priority_str is None: + if priority_str is None: priority = Priority.PRIMARY else: priority = Priority[priority_str.upper()] - if priority is Priority.SECONDARY: - allowed_prios = ( - p for p in Priority if p not in {Priority.DEFAULT, Priority.SECONDARY} - ) - self.line_error( - "Warning: Priority 'secondary' is deprecated. Consider" - " changing the priority to one of the non-deprecated values:" - f" {', '.join(repr(p.name.lower()) for p in allowed_prios)}." - ) - if priority is Priority.DEFAULT: - self.line_error( - "Warning: Priority 'default' is deprecated. You can achieve" - " the same effect by changing the priority to 'primary' and putting" - " the source first." - ) - sources = AoT([]) new_source = Source(name=name, url=url, priority=priority) is_new_source = True for source in self.poetry.get_sources(): - if source.priority is Priority.DEFAULT and priority is Priority.DEFAULT: - self.line_error( - f"Source with name {source.name} is already set to" - " default. Only one default source can be configured at a" - " time." - ) - return 1 - if source.name.lower() == lower_name: source = new_source is_new_source = False diff --git a/src/poetry/console/commands/version.py b/src/poetry/console/commands/version.py index 85c97d4c28c..8bf7d61b428 100644 --- a/src/poetry/console/commands/version.py +++ b/src/poetry/console/commands/version.py @@ -6,7 +6,7 @@ from cleo.helpers import argument from cleo.helpers import option -from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.exceptions import InvalidVersionError from tomlkit.toml_document import TOMLDocument from poetry.console.commands.command import Command @@ -96,7 +96,7 @@ def increment_version( try: parsed = Version.parse(version) - except InvalidVersion: + except InvalidVersionError: raise ValueError("The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: diff --git a/src/poetry/console/exceptions.py b/src/poetry/console/exceptions.py index 2cc359ddb75..d45c38dd3df 100644 --- a/src/poetry/console/exceptions.py +++ b/src/poetry/console/exceptions.py @@ -7,5 +7,5 @@ class PoetryConsoleError(CleoError): pass -class GroupNotFound(PoetryConsoleError): +class GroupNotFoundError(PoetryConsoleError): pass diff --git a/src/poetry/console/logging/formatters/formatter.py b/src/poetry/console/logging/formatters/formatter.py index d18002be4b7..4d0f1302bab 100644 --- a/src/poetry/console/logging/formatters/formatter.py +++ b/src/poetry/console/logging/formatters/formatter.py @@ -1,6 +1,9 @@ from __future__ import annotations +from abc import ABC +from abc import abstractmethod -class Formatter: - def format(self, msg: str) -> str: - raise NotImplementedError() + +class Formatter(ABC): + @abstractmethod + def format(self, msg: str) -> str: ... diff --git a/src/poetry/exceptions.py b/src/poetry/exceptions.py index 68154f8d87d..170d1028edf 100644 --- a/src/poetry/exceptions.py +++ b/src/poetry/exceptions.py @@ -1,5 +1,5 @@ from __future__ import annotations -class PoetryException(Exception): +class PoetryError(Exception): pass diff --git a/src/poetry/factory.py b/src/poetry/factory.py index f016dcf88e6..18e7cdc1df5 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -10,11 +10,14 @@ from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import parse_constraint from poetry.core.factory import Factory as BaseFactory from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.__version__ import __version__ from poetry.config.config import Config -from poetry.exceptions import PoetryException +from poetry.exceptions import PoetryError from poetry.json import validate_object from poetry.packages.locker import Locker from poetry.plugins.plugin import Plugin @@ -56,6 +59,15 @@ def create_poetry( base_poetry = super().create_poetry(cwd=cwd, with_groups=with_groups) + if version_str := base_poetry.local_config.get("requires-poetry"): + version_constraint = parse_constraint(version_str) + version = Version.parse(__version__) + if not version_constraint.allows(version): + raise PoetryError( + f"This project requires Poetry {version_constraint}," + f" but you are using Poetry {version}" + ) + poetry_file = base_poetry.pyproject_path locker = Locker(poetry_file.parent / "poetry.lock", base_poetry.pyproject.data) @@ -99,9 +111,10 @@ def create_poetry( ) ) - plugin_manager = PluginManager(Plugin.group, disable_plugins=disable_plugins) - plugin_manager.load_plugins() - plugin_manager.activate(poetry, io) + if not disable_plugins: + plugin_manager = PluginManager(Plugin.group) + plugin_manager.load_plugins() + plugin_manager.activate(poetry, io) return poetry @@ -130,46 +143,12 @@ def create_pool( source, config, disable_cache=disable_cache ) priority = Priority[source.get("priority", Priority.PRIMARY.name).upper()] - if "default" in source or "secondary" in source: - warning = ( - "Found deprecated key 'default' or 'secondary' in" - " pyproject.toml configuration for source" - f" {source.get('name')}. Please provide the key 'priority'" - " instead. Accepted values are:" - f" {', '.join(repr(p.name.lower()) for p in Priority)}." - ) - io.write_error_line(f"Warning: {warning}") - if source.get("default"): - priority = Priority.DEFAULT - elif source.get("secondary"): - priority = Priority.SECONDARY - - if priority is Priority.SECONDARY: - allowed_prios = (p for p in Priority if p is not Priority.SECONDARY) - warning = ( - "Found deprecated priority 'secondary' for source" - f" '{source.get('name')}' in pyproject.toml. Consider changing the" - " priority to one of the non-deprecated values:" - f" {', '.join(repr(p.name.lower()) for p in allowed_prios)}." - ) - io.write_error_line(f"Warning: {warning}") - elif priority is Priority.DEFAULT: - warning = ( - "Found deprecated priority 'default' for source" - f" '{source.get('name')}' in pyproject.toml. You can achieve" - " the same effect by changing the priority to 'primary' and putting" - " the source first." - ) - io.write_error_line(f"Warning: {warning}") if io.is_debug(): - message = f"Adding repository {repository.name} ({repository.url})" - if priority is Priority.DEFAULT: - message += " and setting it as the default one" - else: - message += f" and setting it as {priority.name.lower()}" - - io.write_line(message) + io.write_line( + f"Adding repository {repository.name} ({repository.url})" + f" and setting it as {priority.name.lower()}" + ) pool.add_repository(repository, priority=priority) if repository.name.lower() == "pypi": @@ -177,7 +156,7 @@ def create_pool( # Only add PyPI if no default repository is configured if not explicit_pypi: - if pool.has_default() or pool.has_primary_repositories(): + if pool.has_primary_repositories(): if io.is_debug(): io.write_line("Deactivating the PyPI repository") else: @@ -189,7 +168,7 @@ def create_pool( ) if not pool.repositories: - raise PoetryException( + raise PoetryError( "At least one source must not be configured as 'explicit'." ) diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index 457fc97555f..e260818164d 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -22,8 +22,8 @@ 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.core.version.markers import InvalidMarkerError +from poetry.core.version.requirements import InvalidRequirementError from poetry.utils.helpers import extractall from poetry.utils.isolated_build import isolated_builder @@ -178,7 +178,7 @@ def to_package( try: # Attempt to parse the PEP-508 requirement string dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) - except InvalidMarker: + except InvalidMarkerError: # Invalid marker, We strip the markers hoping for the best logger.warning( "Stripping invalid marker (%s) found in %s-%s dependencies", @@ -188,7 +188,7 @@ def to_package( ) req = req.split(";")[0] dependency = Dependency.create_from_pep_508(req, relative_to=root_dir) - except InvalidRequirement: + except InvalidRequirementError: # Unable to parse requirement so we skip it logger.warning( "Invalid requirement (%s) found in %s-%s dependencies, skipping", diff --git a/src/poetry/inspection/lazy_wheel.py b/src/poetry/inspection/lazy_wheel.py index 66c6dfccd88..8722a04ef5c 100644 --- a/src/poetry/inspection/lazy_wheel.py +++ b/src/poetry/inspection/lazy_wheel.py @@ -14,7 +14,6 @@ from typing import Any from typing import BinaryIO from typing import ClassVar -from typing import TypeVar from typing import cast from urllib.parse import urlparse from zipfile import BadZipFile @@ -34,6 +33,7 @@ from packaging.metadata import RawMetadata from requests import Session + from typing_extensions import Self from poetry.utils.authenticator import Authenticator @@ -45,20 +45,20 @@ class LazyWheelUnsupportedError(Exception): """Raised when a lazy wheel is unsupported.""" -class HTTPRangeRequestUnsupported(LazyWheelUnsupportedError): +class HTTPRangeRequestUnsupportedError(LazyWheelUnsupportedError): """Raised when the remote server appears unable to support byte ranges.""" -class HTTPRangeRequestNotRespected(LazyWheelUnsupportedError): +class HTTPRangeRequestNotRespectedError(LazyWheelUnsupportedError): """Raised when the remote server tells us that it supports byte ranges but does not respect a respective request.""" -class UnsupportedWheel(LazyWheelUnsupportedError): +class UnsupportedWheelError(LazyWheelUnsupportedError): """Unsupported wheel.""" -class InvalidWheel(LazyWheelUnsupportedError): +class InvalidWheelError(LazyWheelUnsupportedError): """Invalid (e.g. corrupt) wheel.""" def __init__(self, location: str, name: str) -> None: @@ -77,8 +77,8 @@ def metadata_from_wheel_url( This uses HTTP range requests to only fetch the portion of the wheel containing metadata, just enough for the object to be constructed. - :raises HTTPRangeRequestUnsupported: if range requests are unsupported for ``url``. - :raises InvalidWheel: if the zip file contents could not be parsed. + :raises HTTPRangeRequestUnsupportedError: if range requests are unsupported for ``url``. + :raises InvalidWheelError: if the zip file contents could not be parsed. """ try: # After context manager exit, wheel.name will point to a deleted file path. @@ -89,11 +89,11 @@ def metadata_from_wheel_url( metadata, _ = parse_email(metadata_bytes) return metadata - except (BadZipFile, UnsupportedWheel): + except (BadZipFile, UnsupportedWheelError): # We assume that these errors have occurred because the wheel contents # themselves are invalid, not because we've messed up our bookkeeping # and produced an invalid file. - raise InvalidWheel(url, name) + raise InvalidWheelError(url, name) except Exception as e: if isinstance(e, LazyWheelUnsupportedError): # this is expected when the code handles issues with lazy wheel metadata retrieval correctly @@ -168,9 +168,6 @@ def minimal_intervals_covering( yield from self._merge(start, end, left, right) -T = TypeVar("T", bound="ReadOnlyIOWrapper") - - class ReadOnlyIOWrapper(BinaryIO): """Implement read-side ``BinaryIO`` methods wrapping an inner ``BinaryIO``. @@ -181,7 +178,7 @@ class ReadOnlyIOWrapper(BinaryIO): def __init__(self, inner: BinaryIO) -> None: self._file = inner - def __enter__(self: T) -> T: + def __enter__(self) -> Self: self._file.__enter__() return self @@ -286,15 +283,12 @@ def writelines(self, lines: Iterable[Any]) -> None: raise NotImplementedError -U = TypeVar("U", bound="LazyFileOverHTTP") - - class LazyFileOverHTTP(ReadOnlyIOWrapper): """File-like object representing a fixed-length file over HTTP. This uses HTTP range requests to lazily fetch the file's content into a temporary file. If such requests are not supported by the server, raises - ``HTTPRangeRequestUnsupported`` in the ``__enter__`` method.""" + ``HTTPRangeRequestUnsupportedError`` in the ``__enter__`` method.""" def __init__( self, @@ -311,7 +305,7 @@ def __init__( self._session = session self._url = url - def __enter__(self: U) -> U: + def __enter__(self) -> Self: super().__enter__() self._setup_content() return self @@ -407,7 +401,7 @@ def _reset_content(self) -> None: def _content_length_from_head(self) -> int: """Performs a HEAD request to extract the Content-Length. - :raises HTTPRangeRequestUnsupported: if the response fails to indicate support + :raises HTTPRangeRequestUnsupportedError: if the response fails to indicate support for "bytes" ranges.""" self._request_count += 1 head = self._session.head( @@ -417,7 +411,7 @@ def _content_length_from_head(self) -> int: assert head.status_code == codes.ok accepted_range = head.headers.get("Accept-Ranges", None) if accepted_range != "bytes": - raise HTTPRangeRequestUnsupported( + raise HTTPRangeRequestUnsupportedError( f"server does not support byte ranges: header was '{accepted_range}'" ) return int(head.headers["Content-Length"]) @@ -437,7 +431,7 @@ def _stream_response(self, start: int, end: int) -> Response: response = self._session.get(self._url, headers=headers, stream=True) response.raise_for_status() if int(response.headers["Content-Length"]) != (end - start + 1): - raise HTTPRangeRequestNotRespected( + raise HTTPRangeRequestNotRespectedError( f"server did not respect byte range request: " f"requested {end - start + 1} bytes, got " f"{response.headers['Content-Length']} bytes" @@ -590,7 +584,9 @@ def _parse_full_length_from_content_range(arg: str) -> int: """ m = re.match(r"bytes [^/]+/([0-9]+)", arg) if m is None: - raise HTTPRangeRequestUnsupported(f"could not parse Content-Range: '{arg}'") + raise HTTPRangeRequestUnsupportedError( + f"could not parse Content-Range: '{arg}'" + ) return int(m.group(1)) def _try_initial_chunk_request( @@ -620,7 +616,7 @@ def _try_initial_chunk_request( if accept_ranges == "bytes" and content_length <= initial_chunk_size: return content_length, tail - raise HTTPRangeRequestUnsupported( + raise HTTPRangeRequestUnsupportedError( f"did not receive partial content: got code {code}" ) @@ -722,7 +718,7 @@ def _prefetch_metadata(self, name: str) -> str: end = info.header_offset break if start is None: - raise UnsupportedWheel( + raise UnsupportedWheelError( f"no {self._metadata_regex!r} found for {name} in {self.name}" ) # If it is the last entry of the zip, then give us everything diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index 35586e42036..7341678a9df 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -50,7 +50,7 @@ def _prepare( ) -> Path: from subprocess import CalledProcessError - distribution: DistributionType = "editable" if editable else "wheel" # type: ignore[assignment] + distribution: DistributionType = "editable" if editable else "wheel" error: Exception | None = None try: diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index d680d6934da..474850011ab 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -6,6 +6,7 @@ import json import threading +from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from concurrent.futures import wait from pathlib import Path @@ -144,6 +145,7 @@ def execute(self, operations: list[Operation]) -> int: for _, group in groups: tasks = [] serial_operations = [] + serial_git_operations = defaultdict(list) for operation in group: if self._shutdown: break @@ -158,11 +160,36 @@ def execute(self, operations: list[Operation]) -> int: operation.package.develop and operation.package.source_type in {"directory", "git"} ) - if not operation.skipped and is_parallel_unsafe: + # Skipped operations are safe to execute in parallel + if operation.skipped: + is_parallel_unsafe = False + + if is_parallel_unsafe: serial_operations.append(operation) - continue + elif operation.package.source_type == "git": + # Git operations on the same repository should be executed serially + serial_git_operations[operation.package.source_url].append( + operation + ) + else: + tasks.append( + self._executor.submit(self._execute_operation, operation) + ) - tasks.append(self._executor.submit(self._execute_operation, operation)) + def _serialize( + repository_serial_operations: list[Operation], + ) -> None: + for operation in repository_serial_operations: + self._execute_operation(operation) + + # For each git repository, execute all operations serially + for repository_git_operations in serial_git_operations.values(): + tasks.append( + self._executor.submit( + _serialize, + repository_serial_operations=repository_git_operations, + ) + ) try: wait(tasks) diff --git a/src/poetry/installation/operations/operation.py b/src/poetry/installation/operations/operation.py index e6d7ce582f1..824da8b852e 100644 --- a/src/poetry/installation/operations/operation.py +++ b/src/poetry/installation/operations/operation.py @@ -1,16 +1,16 @@ from __future__ import annotations +from abc import ABC +from abc import abstractmethod from typing import TYPE_CHECKING -from typing import TypeVar if TYPE_CHECKING: from poetry.core.packages.package import Package + from typing_extensions import Self -T = TypeVar("T", bound="Operation") - -class Operation: +class Operation(ABC): def __init__(self, reason: str | None = None, priority: float = 0) -> None: self._reason = reason @@ -19,8 +19,8 @@ def __init__(self, reason: str | None = None, priority: float = 0) -> None: self._priority = priority @property - def job_type(self) -> str: - raise NotImplementedError + @abstractmethod + def job_type(self) -> str: ... @property def reason(self) -> str | None: @@ -39,14 +39,14 @@ def priority(self) -> float: return self._priority @property - def package(self) -> Package: - raise NotImplementedError() + @abstractmethod + def package(self) -> Package: ... def format_version(self, package: Package) -> str: version: str = package.full_pretty_version return version - def skip(self: T, reason: str) -> T: + def skip(self, reason: str) -> Self: self._skipped = True self._skip_reason = reason diff --git a/src/poetry/json/__init__.py b/src/poetry/json/__init__.py index c9872731a37..0ff29680144 100644 --- a/src/poetry/json/__init__.py +++ b/src/poetry/json/__init__.py @@ -30,8 +30,8 @@ def validate_object(obj: dict[str, Any]) -> list[str]: (CORE_SCHEMA_DIR / "poetry-schema.json").read_text(encoding="utf-8") ) - properties = {*schema["properties"].keys(), *core_schema["properties"].keys()} - additional_properties = set(obj.keys()) - properties + properties = schema["properties"].keys() | core_schema["properties"].keys() + additional_properties = obj.keys() - properties for key in additional_properties: errors.append(f"Additional properties are not allowed ('{key}' was unexpected)") diff --git a/src/poetry/json/schemas/poetry.json b/src/poetry/json/schemas/poetry.json index 93a822d28b9..ca48f5a9e3c 100644 --- a/src/poetry/json/schemas/poetry.json +++ b/src/poetry/json/schemas/poetry.json @@ -4,6 +4,17 @@ "type": "object", "required": [], "properties": { + "requires-poetry": { + "type": "string", + "description": "The version constraint for Poetry itself.", + "$ref": "#/definitions/dependency" + }, + "requires-plugins": { + "type": "object", + "description": "Poetry plugins that are required for this project.", + "$ref": "#/definitions/dependencies", + "additionalProperties": false + }, "source": { "type": "array", "description": "A set of additional repositories where packages can be found.", @@ -32,19 +43,9 @@ "description": "The url of the repository.", "format": "uri" }, - "default": { - "type": "boolean", - "description": "Make this repository the default (disable PyPI). (deprecated, see priority)" - }, - "secondary": { - "type": "boolean", - "description": "Declare this repository as secondary, i.e. default repositories take precedence. (deprecated, see priority)" - }, "priority": { "enum": [ "primary", - "default", - "secondary", "supplemental", "explicit" ], @@ -58,20 +59,336 @@ "type": "boolean", "description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)" } - }, - "not": { - "anyOf": [ + } + }, + "dependencies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z-_.0-9]+$": { + "oneOf": [ + { + "$ref": "#/definitions/dependency" + }, + { + "$ref": "#/definitions/long-dependency" + }, + { + "$ref": "#/definitions/git-dependency" + }, + { + "$ref": "#/definitions/file-dependency" + }, + { + "$ref": "#/definitions/path-dependency" + }, + { + "$ref": "#/definitions/url-dependency" + }, + { + "$ref": "#/definitions/multiple-constraints-dependency" + }, + { + "$ref": "#/definitions/dependency-options" + } + ] + } + } + }, + "dependency": { + "type": "string", + "description": "The constraint of the dependency." + }, + "long-dependency": { + "type": "object", + "required": [ + "version" + ], + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "description": "The constraint of the dependency." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "allows-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The exclusive source used to search for this dependency." + } + } + }, + "git-dependency": { + "type": "object", + "required": [ + "git" + ], + "additionalProperties": false, + "properties": { + "git": { + "type": "string", + "description": "The url of the git repository." + }, + "branch": { + "type": "string", + "description": "The branch to checkout." + }, + "tag": { + "type": "string", + "description": "The tag to checkout." + }, + "rev": { + "type": "string", + "description": "The revision to checkout." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "allows-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, + "file-dependency": { + "type": "object", + "required": [ + "file" + ], + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "The path to the file." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + } + } + }, + "path-dependency": { + "type": "object", + "required": [ + "path" + ], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "The path to the dependency." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, + "url-dependency": { + "type": "object", + "required": [ + "url" + ], + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The url to the file." + }, + "subdirectory": { + "type": "string", + "description": "The relative path to the directory where the package is located." + }, + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "optional": { + "type": "boolean", + "description": "Whether the dependency is optional or not." + }, + "extras": { + "type": "array", + "description": "The required extras for this dependency.", + "items": { + "type": "string" + } + } + } + }, + "dependency-options": { + "type": "object", + "additionalProperties": false, + "properties": { + "python": { + "type": "string", + "description": "The python versions for which the dependency should be installed." + }, + "platform": { + "type": "string", + "description": "The platform(s) for which the dependency should be installed." + }, + "markers": { + "type": "string", + "description": "The PEP 508 compliant environment markers for which the dependency should be installed." + }, + "allow-prereleases": { + "type": "boolean", + "description": "Whether the dependency allows prereleases or not." + }, + "source": { + "type": "string", + "description": "The exclusive source used to search for this dependency." + }, + "develop": { + "type": "boolean", + "description": "Whether to install the dependency in development mode." + } + } + }, + "multiple-constraints-dependency": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/dependency" + }, + { + "$ref": "#/definitions/long-dependency" + }, + { + "$ref": "#/definitions/git-dependency" + }, + { + "$ref": "#/definitions/file-dependency" + }, + { + "$ref": "#/definitions/path-dependency" + }, { - "required": [ - "priority", - "default" - ] + "$ref": "#/definitions/url-dependency" }, { - "required": [ - "priority", - "secondary" - ] + "$ref": "#/definitions/dependency-options" } ] } diff --git a/src/poetry/mixology/failure.py b/src/poetry/mixology/failure.py index 84a7fb0642d..230cbdbd85d 100644 --- a/src/poetry/mixology/failure.py +++ b/src/poetry/mixology/failure.py @@ -4,15 +4,15 @@ from poetry.core.constraints.version import parse_constraint -from poetry.mixology.incompatibility_cause import ConflictCause -from poetry.mixology.incompatibility_cause import PythonCause +from poetry.mixology.incompatibility_cause import ConflictCauseError +from poetry.mixology.incompatibility_cause import PythonCauseError if TYPE_CHECKING: from poetry.mixology.incompatibility import Incompatibility -class SolveFailure(Exception): +class SolveFailureError(Exception): def __init__(self, incompatibility: Incompatibility) -> None: self._incompatibility = incompatibility @@ -38,7 +38,7 @@ def write(self) -> str: version_solutions = [] required_python_version_notification = False for incompatibility in self._root.external_incompatibilities: - if isinstance(incompatibility.cause, PythonCause): + if isinstance(incompatibility.cause, PythonCauseError): root_constraint = parse_constraint( incompatibility.cause.root_python_version ) @@ -73,7 +73,7 @@ def write(self) -> str: if required_python_version_notification: buffer.append("") - if isinstance(self._root.cause, ConflictCause): + if isinstance(self._root.cause, ConflictCauseError): self._visit(self._root) else: self._write(self._root, f"Because {self._root}, version solving failed.") @@ -150,10 +150,10 @@ def _visit( incompatibility_string = str(incompatibility) cause = incompatibility.cause - assert isinstance(cause, ConflictCause) + assert isinstance(cause, ConflictCauseError) - if isinstance(cause.conflict.cause, ConflictCause) and isinstance( - cause.other.cause, ConflictCause + if isinstance(cause.conflict.cause, ConflictCauseError) and isinstance( + cause.other.cause, ConflictCauseError ): conflict_line = self._line_numbers.get(cause.conflict) other_line = self._line_numbers.get(cause.other) @@ -211,17 +211,17 @@ def _visit( f" {incompatibility_string}", numbered=numbered, ) - elif isinstance(cause.conflict.cause, ConflictCause) or isinstance( - cause.other.cause, ConflictCause + elif isinstance(cause.conflict.cause, ConflictCauseError) or isinstance( + cause.other.cause, ConflictCauseError ): derived = ( cause.conflict - if isinstance(cause.conflict.cause, ConflictCause) + if isinstance(cause.conflict.cause, ConflictCauseError) else cause.other ) ext = ( cause.other - if isinstance(cause.conflict.cause, ConflictCause) + if isinstance(cause.conflict.cause, ConflictCauseError) else cause.conflict ) @@ -235,8 +235,8 @@ def _visit( ) elif self._is_collapsible(derived): derived_cause = derived.cause - assert isinstance(derived_cause, ConflictCause) - if isinstance(derived_cause.conflict.cause, ConflictCause): + assert isinstance(derived_cause, ConflictCauseError) + if isinstance(derived_cause.conflict.cause, ConflictCauseError): collapsed_derived = derived_cause.conflict collapsed_ext = derived_cause.other else: @@ -271,29 +271,29 @@ def _is_collapsible(self, incompatibility: Incompatibility) -> bool: return False cause = incompatibility.cause - assert isinstance(cause, ConflictCause) - if isinstance(cause.conflict.cause, ConflictCause) and isinstance( - cause.other.cause, ConflictCause + assert isinstance(cause, ConflictCauseError) + if isinstance(cause.conflict.cause, ConflictCauseError) and isinstance( + cause.other.cause, ConflictCauseError ): return False - if not isinstance(cause.conflict.cause, ConflictCause) and not isinstance( - cause.other.cause, ConflictCause + if not isinstance(cause.conflict.cause, ConflictCauseError) and not isinstance( + cause.other.cause, ConflictCauseError ): return False complex = ( cause.conflict - if isinstance(cause.conflict.cause, ConflictCause) + if isinstance(cause.conflict.cause, ConflictCauseError) else cause.other ) return complex not in self._line_numbers - def _is_single_line(self, cause: ConflictCause) -> bool: - return not isinstance(cause.conflict.cause, ConflictCause) and not isinstance( - cause.other.cause, ConflictCause - ) + def _is_single_line(self, cause: ConflictCauseError) -> bool: + return not isinstance( + cause.conflict.cause, ConflictCauseError + ) and not isinstance(cause.other.cause, ConflictCauseError) def _count_derivations(self, incompatibility: Incompatibility) -> None: if incompatibility in self._derivations: @@ -301,6 +301,6 @@ def _count_derivations(self, incompatibility: Incompatibility) -> None: else: self._derivations[incompatibility] = 1 cause = incompatibility.cause - if isinstance(cause, ConflictCause): + if isinstance(cause, ConflictCauseError): self._count_derivations(cause.conflict) self._count_derivations(cause.other) diff --git a/src/poetry/mixology/incompatibility.py b/src/poetry/mixology/incompatibility.py index 9a2cc96294c..32ceec37863 100644 --- a/src/poetry/mixology/incompatibility.py +++ b/src/poetry/mixology/incompatibility.py @@ -2,30 +2,30 @@ from typing import TYPE_CHECKING -from poetry.mixology.incompatibility_cause import ConflictCause -from poetry.mixology.incompatibility_cause import DependencyCause -from poetry.mixology.incompatibility_cause import NoVersionsCause -from poetry.mixology.incompatibility_cause import PlatformCause -from poetry.mixology.incompatibility_cause import PythonCause -from poetry.mixology.incompatibility_cause import RootCause +from poetry.mixology.incompatibility_cause import ConflictCauseError +from poetry.mixology.incompatibility_cause import DependencyCauseError +from poetry.mixology.incompatibility_cause import NoVersionsCauseError +from poetry.mixology.incompatibility_cause import PlatformCauseError +from poetry.mixology.incompatibility_cause import PythonCauseError +from poetry.mixology.incompatibility_cause import RootCauseError if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Iterator - from poetry.mixology.incompatibility_cause import IncompatibilityCause + from poetry.mixology.incompatibility_cause import IncompatibilityCauseError from poetry.mixology.term import Term class Incompatibility: - def __init__(self, terms: list[Term], cause: IncompatibilityCause) -> None: + def __init__(self, terms: list[Term], cause: IncompatibilityCauseError) -> None: # Remove the root package from generated incompatibilities, since it will # always be satisfied. This makes error reporting clearer, and may also # make solving more efficient. if ( len(terms) != 1 - and isinstance(cause, ConflictCause) + and isinstance(cause, ConflictCauseError) and any(term.is_positive() and term.dependency.is_root for term in terms) ): terms = [ @@ -81,7 +81,7 @@ def terms(self) -> list[Term]: return self._terms @property - def cause(self) -> IncompatibilityCause: + def cause(self) -> IncompatibilityCauseError: return self._cause @property @@ -92,8 +92,8 @@ def external_incompatibilities( Returns all external incompatibilities in this incompatibility's derivation graph. """ - if isinstance(self._cause, ConflictCause): - cause: ConflictCause = self._cause + if isinstance(self._cause, ConflictCauseError): + cause: ConflictCauseError = self._cause yield from cause.conflict.external_incompatibilities yield from cause.other.external_incompatibilities @@ -106,7 +106,7 @@ def is_failure(self) -> bool: ) def __str__(self) -> str: - if isinstance(self._cause, DependencyCause): + if isinstance(self._cause, DependencyCauseError): assert len(self._terms) == 2 depender = self._terms[0] @@ -118,7 +118,7 @@ def __str__(self) -> str: f"{self._terse(depender, allow_every=True)} depends on" f" {self._terse(dependee)}" ) - elif isinstance(self._cause, PythonCause): + elif isinstance(self._cause, PythonCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() @@ -126,7 +126,7 @@ def __str__(self) -> str: text += f"Python {self._cause.python_version}" return text - elif isinstance(self._cause, PlatformCause): + elif isinstance(self._cause, PlatformCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() @@ -134,7 +134,7 @@ def __str__(self) -> str: text += f"platform {self._cause.platform}" return text - elif isinstance(self._cause, NoVersionsCause): + elif isinstance(self._cause, NoVersionsCauseError): assert len(self._terms) == 1 assert self._terms[0].is_positive() @@ -142,7 +142,7 @@ def __str__(self) -> str: f"no versions of {self._terms[0].dependency.name} match" f" {self._terms[0].constraint}" ) - elif isinstance(self._cause, RootCause): + elif isinstance(self._cause, RootCauseError): assert len(self._terms) == 1 assert not self._terms[0].is_positive() assert self._terms[0].dependency.is_root @@ -261,8 +261,8 @@ def _try_requires_both( ) buffer = [self._terse(this_positive, allow_every=True) + " "] - is_dependency = isinstance(self.cause, DependencyCause) and isinstance( - other.cause, DependencyCause + is_dependency = isinstance(self.cause, DependencyCauseError) and isinstance( + other.cause, DependencyCauseError ) if is_dependency: @@ -331,7 +331,7 @@ def _try_requires_through( prior_string = " or ".join([self._terse(term) for term in prior_positives]) buffer.append(f"if {prior_string} then ") else: - if isinstance(prior.cause, DependencyCause): + if isinstance(prior.cause, DependencyCauseError): verb = "depends on" else: verb = "requires" @@ -346,7 +346,7 @@ def _try_requires_through( buffer.append(" which ") - if isinstance(latter.cause, DependencyCause): + if isinstance(latter.cause, DependencyCauseError): buffer.append("depends on ") else: buffer.append("requires ") @@ -397,7 +397,7 @@ def _try_requires_forbidden( buffer.append(f"if {prior_string} then ") else: buffer.append(self._terse(positives[0], allow_every=True)) - if isinstance(prior.cause, DependencyCause): + if isinstance(prior.cause, DependencyCauseError): buffer.append(" depends on ") else: buffer.append(" requires ") @@ -406,10 +406,10 @@ def _try_requires_forbidden( if prior_line is not None: buffer.append(f"({prior_line}) ") - if isinstance(latter.cause, PythonCause): - cause: PythonCause = latter.cause + if isinstance(latter.cause, PythonCauseError): + cause: PythonCauseError = latter.cause buffer.append(f"which requires Python {cause.python_version}") - elif isinstance(latter.cause, NoVersionsCause): + elif isinstance(latter.cause, NoVersionsCauseError): buffer.append("which doesn't match any versions") else: buffer.append("which is forbidden") diff --git a/src/poetry/mixology/incompatibility_cause.py b/src/poetry/mixology/incompatibility_cause.py index 1536d1b22b2..aaabd570eda 100644 --- a/src/poetry/mixology/incompatibility_cause.py +++ b/src/poetry/mixology/incompatibility_cause.py @@ -7,25 +7,25 @@ from poetry.mixology.incompatibility import Incompatibility -class IncompatibilityCause(Exception): +class IncompatibilityCauseError(Exception): """ The reason and Incompatibility's terms are incompatible. """ -class RootCause(IncompatibilityCause): +class RootCauseError(IncompatibilityCauseError): pass -class NoVersionsCause(IncompatibilityCause): +class NoVersionsCauseError(IncompatibilityCauseError): pass -class DependencyCause(IncompatibilityCause): +class DependencyCauseError(IncompatibilityCauseError): pass -class ConflictCause(IncompatibilityCause): +class ConflictCauseError(IncompatibilityCauseError): """ The incompatibility was derived from two existing incompatibilities during conflict resolution. @@ -47,7 +47,7 @@ def __str__(self) -> str: return str(self._conflict) -class PythonCause(IncompatibilityCause): +class PythonCauseError(IncompatibilityCauseError): """ The incompatibility represents a package's python constraint (Python versions) being incompatible @@ -67,7 +67,7 @@ def root_python_version(self) -> str: return self._root_python_version -class PlatformCause(IncompatibilityCause): +class PlatformCauseError(IncompatibilityCauseError): """ The incompatibility represents a package's platform constraint (OS most likely) being incompatible with the current platform. diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py index 5bb02d9418a..30b87087f60 100644 --- a/src/poetry/mixology/version_solver.py +++ b/src/poetry/mixology/version_solver.py @@ -10,11 +10,11 @@ from poetry.core.packages.dependency import Dependency -from poetry.mixology.failure import SolveFailure +from poetry.mixology.failure import SolveFailureError from poetry.mixology.incompatibility import Incompatibility -from poetry.mixology.incompatibility_cause import ConflictCause -from poetry.mixology.incompatibility_cause import NoVersionsCause -from poetry.mixology.incompatibility_cause import RootCause +from poetry.mixology.incompatibility_cause import ConflictCauseError +from poetry.mixology.incompatibility_cause import NoVersionsCauseError +from poetry.mixology.incompatibility_cause import RootCauseError from poetry.mixology.partial_solution import PartialSolution from poetry.mixology.result import SolverResult from poetry.mixology.set_relation import SetRelation @@ -165,7 +165,7 @@ def solve(self) -> SolverResult: root_dependency.is_root = True self._add_incompatibility( - Incompatibility([Term(root_dependency, False)], RootCause()) + Incompatibility([Term(root_dependency, False)], RootCauseError()) ) try: @@ -412,7 +412,8 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility new_terms.append(inverse) incompatibility = Incompatibility( - new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause) + new_terms, + ConflictCauseError(incompatibility, most_recent_satisfier.cause), ) new_incompatibility = True @@ -424,7 +425,7 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility self._log(f'! which is caused by "{most_recent_satisfier.cause}"') self._log(f"! thus: {incompatibility}") - raise SolveFailure(incompatibility) + raise SolveFailureError(incompatibility) def _choose_package_version(self) -> str | None: """ @@ -503,7 +504,7 @@ def _get_min(dependency: Dependency) -> tuple[bool, int, int]: # If there are no versions that satisfy the constraint, # add an incompatibility that indicates that. self._add_incompatibility( - Incompatibility([Term(dependency, True)], NoVersionsCause()) + Incompatibility([Term(dependency, True)], NoVersionsCauseError()) ) complete_name = dependency.complete_name diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 5d40c782921..245a89e97cd 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -18,7 +18,7 @@ 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.version.requirements import InvalidRequirement +from poetry.core.version.requirements import InvalidRequirementError from tomlkit import array from tomlkit import comment from tomlkit import document @@ -194,7 +194,7 @@ def locked_repository(self) -> LockfileRepository: for dep in deps: try: dependency = Dependency.create_from_pep_508(dep) - except InvalidRequirement: + except InvalidRequirementError: # handle lock files with invalid PEP 508 m = re.match(r"^(.+?)(?:\[(.+?)])?(?:\s+\((.+)\))?$", dep) if not m: diff --git a/src/poetry/plugins/base_plugin.py b/src/poetry/plugins/base_plugin.py index 07146060746..4df1c984621 100644 --- a/src/poetry/plugins/base_plugin.py +++ b/src/poetry/plugins/base_plugin.py @@ -1,9 +1,10 @@ from __future__ import annotations +from abc import ABC from abc import abstractmethod -class BasePlugin: +class BasePlugin(ABC): """ Base class for all plugin types @@ -18,4 +19,3 @@ def group(self) -> str: """ Name of entrypoint group the plugin belongs to. """ - raise NotImplementedError() diff --git a/src/poetry/plugins/plugin.py b/src/poetry/plugins/plugin.py index ea72662c3c2..0e00bbba3c5 100644 --- a/src/poetry/plugins/plugin.py +++ b/src/poetry/plugins/plugin.py @@ -20,5 +20,4 @@ class Plugin(BasePlugin): group = "poetry.plugin" @abstractmethod - def activate(self, poetry: Poetry, io: IO) -> None: - raise NotImplementedError() + def activate(self, poetry: Poetry, io: IO) -> None: ... diff --git a/src/poetry/plugins/plugin_manager.py b/src/poetry/plugins/plugin_manager.py index 4ab286c36f7..95ac8b0341d 100644 --- a/src/poetry/plugins/plugin_manager.py +++ b/src/poetry/plugins/plugin_manager.py @@ -1,18 +1,42 @@ from __future__ import annotations +import hashlib +import json import logging +import shutil +import sys +from functools import cached_property +from pathlib import Path from typing import TYPE_CHECKING +from typing import Sequence +import tomlkit + +from poetry.core.packages.project_package import ProjectPackage + +from poetry.__version__ import __version__ +from poetry.installation import Installer +from poetry.packages import Locker from poetry.plugins.application_plugin import ApplicationPlugin from poetry.plugins.plugin import Plugin +from poetry.repositories import Repository +from poetry.repositories.installed_repository import InstalledRepository +from poetry.toml import TOMLFile from poetry.utils._compat import metadata +from poetry.utils._compat import tomllib +from poetry.utils.env import Env +from poetry.utils.env import EnvManager if TYPE_CHECKING: from typing import Any - from poetry.utils.env import Env + from cleo.io.io import IO + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.package import Package + + from poetry.poetry import Poetry logger = logging.getLogger(__name__) @@ -23,42 +47,42 @@ class PluginManager: This class registers and activates plugins. """ - def __init__(self, group: str, disable_plugins: bool = False) -> None: + def __init__(self, group: str) -> None: self._group = group - self._disable_plugins = disable_plugins self._plugins: list[Plugin] = [] - def load_plugins(self, env: Env | None = None) -> None: - if self._disable_plugins: + @staticmethod + def add_project_plugin_path(directory: Path) -> None: + from poetry.factory import Factory + + try: + pyproject_toml = Factory.locate(directory) + except RuntimeError: + # no pyproject.toml -> no project plugins return - plugin_entrypoints = self.get_plugin_entry_points(env=env) + plugin_path = pyproject_toml.parent / ProjectPluginCache.PATH + if plugin_path.exists(): + EnvManager.get_system_env(naive=True).sys_path.insert(0, str(plugin_path)) + + @classmethod + def ensure_project_plugins(cls, poetry: Poetry, io: IO) -> None: + ProjectPluginCache(poetry, io).ensure_plugins() + + def load_plugins(self) -> None: + plugin_entrypoints = self.get_plugin_entry_points() for ep in plugin_entrypoints: self._load_plugin_entry_point(ep) - @staticmethod - def _is_plugin_candidate(ep: metadata.EntryPoint, env: Env | None = None) -> bool: - """ - Helper method to check if given entry point is a valid as a plugin candidate. - When an environment is specified, the entry point's associated distribution - should be installed, and discoverable in the given environment. - """ - return env is None or ( - ep.dist is not None - and env.site_packages.find_distribution(ep.dist.name) is not None - ) + def get_plugin_entry_points(self) -> list[metadata.EntryPoint]: + return list(metadata.entry_points(group=self._group)) - def get_plugin_entry_points( - self, env: Env | None = None - ) -> list[metadata.EntryPoint]: - return [ - ep - for ep in metadata.entry_points(group=self._group) - if self._is_plugin_candidate(ep, env) - ] + def activate(self, *args: Any, **kwargs: Any) -> None: + for plugin in self._plugins: + plugin.activate(*args, **kwargs) - def add_plugin(self, plugin: Plugin) -> None: + def _add_plugin(self, plugin: Plugin) -> None: if not isinstance(plugin, (Plugin, ApplicationPlugin)): raise ValueError( "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" @@ -66,10 +90,6 @@ def add_plugin(self, plugin: Plugin) -> None: self._plugins.append(plugin) - def activate(self, *args: Any, **kwargs: Any) -> None: - for plugin in self._plugins: - plugin.activate(*args, **kwargs) - def _load_plugin_entry_point(self, ep: metadata.EntryPoint) -> None: logger.debug("Loading the %s plugin", ep.name) @@ -80,4 +100,215 @@ def _load_plugin_entry_point(self, ep: metadata.EntryPoint) -> None: "The Poetry plugin must be an instance of Plugin or ApplicationPlugin" ) - self.add_plugin(plugin()) + self._add_plugin(plugin()) + + +class ProjectPluginCache: + PATH = Path(".poetry") / "plugins" + + def __init__(self, poetry: Poetry, io: IO) -> None: + self._poetry = poetry + self._io = io + self._path = poetry.pyproject_path.parent / self.PATH + self._config_file = self._path / "config.toml" + self._gitignore_file = self._path.parent / ".gitignore" + + @property + def _plugin_section(self) -> dict[str, Any]: + plugins = self._poetry.local_config.get("requires-plugins", {}) + assert isinstance(plugins, dict) + return plugins + + @cached_property + def _config(self) -> dict[str, Any]: + return { + "python": sys.version, + "poetry": __version__, + "plugins-hash": hashlib.sha256( + json.dumps(self._plugin_section, sort_keys=True).encode() + ).hexdigest(), + } + + def ensure_plugins(self) -> None: + from poetry.factory import Factory + + # parse project plugins + plugins = [] + for name, constraints in self._plugin_section.items(): + _constraints = ( + constraints if isinstance(constraints, list) else [constraints] + ) + for _constraint in _constraints: + plugins.append(Factory.create_dependency(name, _constraint)) + + if not plugins: + if self._path.exists(): + self._io.write_line( + "No project plugins defined." + " Removing the project's plugin cache" + ) + self._io.write_line("") + shutil.rmtree(self._path) + return + + if self._is_fresh(): + if self._io.is_debug(): + self._io.write_line("The project's plugin cache is up to date.") + self._io.write_line("") + return + elif self._path.exists(): + self._io.write_line( + "Removing the project's plugin cache because it is outdated" + ) + # Just remove the cache for two reasons: + # 1. Since the path of the cache has already been added to sys.path + # at this point, we had to distinguish between packages installed + # directly into Poetry's env and packages installed in the project cache. + # 2. Updating packages in the cache does not work out of the box, + # probably, because we use pip to uninstall and pip does not know + # about the cache so that we end up with just overwriting installed + # packages and multiple dist-info folders per package. + # In sum, we keep it simple by always starting from an empty cache + # if something has changed. + shutil.rmtree(self._path) + + # determine plugins relevant for Poetry's environment + poetry_env = EnvManager.get_system_env(naive=True) + relevant_plugins = { + plugin.name: plugin + for plugin in plugins + if plugin.marker.validate(poetry_env.marker_env) + } + if not relevant_plugins: + if self._io.is_debug(): + self._io.write_line( + "No relevant project plugins for Poetry's environment defined." + ) + self._io.write_line("") + self._write_config() + return + + self._io.write_line( + "Ensuring that the Poetry plugins required" + " by the project are available..." + ) + + # check if required plugins are already available + missing_plugin_count = len(relevant_plugins) + satisfied_plugins = set() + insufficient_plugins = [] + installed_packages = [] + installed_repo = InstalledRepository.load(poetry_env) + for package in installed_repo.packages: + if required_plugin := relevant_plugins.get(package.name): + if package.satisfies(required_plugin): + satisfied_plugins.add(package.name) + installed_packages.append(package) + else: + insufficient_plugins.append((package, required_plugin)) + # Do not add the package to installed_packages so that + # the solver does not consider it. + missing_plugin_count -= 1 + if missing_plugin_count == 0: + break + else: + installed_packages.append(package) + + if missing_plugin_count == 0 and not insufficient_plugins: + # all required plugins are installed and satisfy the requirements + self._write_config() + self._io.write_line( + "All required plugins have already been installed" + " in Poetry's environment." + ) + self._io.write_line("") + return + + if insufficient_plugins and self._io.is_debug(): + plugins_str = "\n".join( + f" - {req}\n installed: {p}" for p, req in insufficient_plugins + ) + self._io.write_line( + "The following Poetry plugins are required by the project" + f" but are not satisfied by the installed versions:\n{plugins_str}" + ) + + # install missing plugins + missing_plugins = [ + plugin + for name, plugin in relevant_plugins.items() + if name not in satisfied_plugins + ] + plugins_str = "\n".join(f" - {p}" for p in missing_plugins) + self._io.write_line( + "The following Poetry plugins are required by the project" + f" but are not installed in Poetry's environment:\n{plugins_str}\n" + f"Installing Poetry plugins only for the current project..." + ) + self._install(missing_plugins, poetry_env, installed_packages) + self._io.write_line("") + self._write_config() + + def _is_fresh(self) -> bool: + if not self._config_file.exists(): + return False + + with self._config_file.open("rb") as f: + stored_config = tomllib.load(f) + + return stored_config == self._config + + def _install( + self, + plugins: Sequence[Dependency], + poetry_env: Env, + locked_packages: Sequence[Package], + ) -> None: + project = ProjectPackage(name="poetry-project-instance", version="0") + project.python_versions = ".".join(str(v) for v in poetry_env.version_info[:3]) + # consider all packages in Poetry's environment pinned + for package in locked_packages: + project.add_dependency(package.to_dependency()) + # add missing plugin dependencies + for dependency in plugins: + project.add_dependency(dependency) + + # force new package to be installed in the project cache instead of Poetry's env + poetry_env.paths["platlib"] = str(self._path) + poetry_env.paths["purelib"] = str(self._path) + + self._ensure_cache_directory() + + installer = Installer( + self._io, + poetry_env, + project, + Locker(self._path / "poetry.lock", {}), + self._poetry.pool, + self._poetry.config, + # Build installed repository from locked packages so that plugins + # that may be overwritten are not included. + Repository("poetry-repo", locked_packages), + ) + installer.update(True) + + if installer.run() != 0: + raise RuntimeError("Failed to install required Poetry plugins") + + def _write_config(self) -> None: + self._ensure_cache_directory() + + document = tomlkit.document() + + for key, value in self._config.items(): + document[key] = value + + TOMLFile(self._config_file).write(data=document) + + def _ensure_cache_directory(self) -> None: + if self._path.exists(): + return + + self._path.mkdir(parents=True, exist_ok=True) + # only write .gitignore if path did not exist before + self._gitignore_file.write_text("*", encoding="utf-8") diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py index c76ed052520..928edc855e1 100644 --- a/src/poetry/publishing/uploader.py +++ b/src/poetry/publishing/uploader.py @@ -8,8 +8,6 @@ from poetry.core.masonry.metadata import Metadata from poetry.core.masonry.utils.helpers import distribution_name -from requests.exceptions import ConnectionError -from requests.exceptions import HTTPError from requests_toolbelt import user_agent from requests_toolbelt.multipart import MultipartEncoder from requests_toolbelt.multipart import MultipartEncoderMonitor @@ -27,23 +25,7 @@ class UploadError(Exception): - def __init__(self, error: ConnectionError | HTTPError | str) -> None: - if isinstance(error, HTTPError): - if error.response is None: - message = "HTTP Error connecting to the repository" - else: - message = ( - f"HTTP Error {error.response.status_code}: " - f"{error.response.reason} | {error.response.content!r}" - ) - elif isinstance(error, ConnectionError): - message = ( - "Connection Error: We were unable to connect to the repository, " - "ensure the url is correct and can be reached." - ) - else: - message = error - super().__init__(message) + pass class Uploader: @@ -268,12 +250,22 @@ def _upload_file( bar.display() else: resp.raise_for_status() - except (requests.ConnectionError, requests.HTTPError) as e: + + except requests.RequestException as e: if self._io.output.is_decorated(): self._io.overwrite( f" - Uploading {file.name} FAILED" ) - raise UploadError(e) + + if e.response is not None: + message = ( + f"HTTP Error {e.response.status_code}: " + f"{e.response.reason} | {e.response.content!r}" + ) + raise UploadError(message) from e + + raise UploadError("Error connecting to repository") from e + finally: self._io.write_line("") diff --git a/src/poetry/puzzle/exceptions.py b/src/poetry/puzzle/exceptions.py index 6bb7c0027bc..c33ab4f2f93 100644 --- a/src/poetry/puzzle/exceptions.py +++ b/src/poetry/puzzle/exceptions.py @@ -7,21 +7,21 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package - from poetry.mixology.failure import SolveFailure + from poetry.mixology.failure import SolveFailureError class SolverProblemError(Exception): - def __init__(self, error: SolveFailure) -> None: + def __init__(self, error: SolveFailureError) -> None: self._error = error super().__init__(str(error)) @property - def error(self) -> SolveFailure: + def error(self) -> SolveFailureError: return self._error -class OverrideNeeded(Exception): +class OverrideNeededError(Exception): def __init__(self, *overrides: dict[Package, dict[str, Dependency]]) -> None: self._overrides = overrides diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 754795b845c..590afc8c7b1 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -20,14 +20,14 @@ from poetry.core.version.markers import union as marker_union from poetry.mixology.incompatibility import Incompatibility -from poetry.mixology.incompatibility_cause import DependencyCause -from poetry.mixology.incompatibility_cause import PythonCause +from poetry.mixology.incompatibility_cause import DependencyCauseError +from poetry.mixology.incompatibility_cause import PythonCauseError from poetry.mixology.term import Term from poetry.packages import DependencyPackage from poetry.packages.direct_origin import DirectOrigin from poetry.packages.package_collection import PackageCollection -from poetry.puzzle.exceptions import OverrideNeeded -from poetry.repositories.exceptions import PackageNotFound +from poetry.puzzle.exceptions import OverrideNeededError +from poetry.repositories.exceptions import PackageNotFoundError from poetry.utils.helpers import get_file_hash @@ -446,7 +446,7 @@ def incompatibilities_for( return [ Incompatibility( [Term(package.to_dependency(), True)], - PythonCause( + PythonCauseError( package.python_versions, str(self._python_constraint) ), ) @@ -464,7 +464,7 @@ def incompatibilities_for( return [ Incompatibility( [Term(package.to_dependency(), True), Term(dep, False)], - DependencyCause(), + DependencyCauseError(), ) for dep in dependencies ] @@ -493,7 +493,7 @@ def complete_package( repository_name=dependency.source_name, ), ) - except PackageNotFound as e: + except PackageNotFoundError as e: try: dependency_package = next( DependencyPackage(dependency, pkg) @@ -650,7 +650,7 @@ def fmt_warning(d: Dependency) -> str: overrides.append(current_overrides) if overrides: - raise OverrideNeeded(*overrides) + raise OverrideNeededError(*overrides) # Modifying dependencies as needed clean_dependencies = [] diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py index 9675bc1f6f0..0b6742217d8 100644 --- a/src/poetry/puzzle/solver.py +++ b/src/poetry/puzzle/solver.py @@ -7,11 +7,10 @@ from typing import TYPE_CHECKING from typing import FrozenSet from typing import Tuple -from typing import TypeVar from poetry.mixology import resolve_version -from poetry.mixology.failure import SolveFailure -from poetry.puzzle.exceptions import OverrideNeeded +from poetry.mixology.failure import SolveFailureError +from poetry.puzzle.exceptions import OverrideNeededError from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.provider import Indicator from poetry.puzzle.provider import Provider @@ -27,6 +26,7 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage + from typing_extensions import Self from poetry.puzzle.transaction import Transaction from poetry.repositories import RepositoryPool @@ -155,9 +155,9 @@ def _solve(self) -> tuple[list[Package], list[int]]: result = resolve_version(self._package, self._provider) packages = result.packages - except OverrideNeeded as e: + except OverrideNeededError as e: return self._solve_in_compatibility_mode(e.overrides) - except SolveFailure as e: + except SolveFailureError as e: raise SolverProblemError(e) combined_nodes = depth_first_search(PackageNode(self._package, packages)) @@ -179,16 +179,14 @@ def _solve(self) -> tuple[list[Package], list[int]]: if _package.name == dep.name: continue - try: - index = _package.requires.index(dep) - except ValueError: - _package.add_dependency(dep) - else: - _dep = _package.requires[index] - if _dep.marker != dep.marker: - # marker of feature package is more accurate - # because it includes relevant extras - _dep.marker = dep.marker + # Avoid duplication. + if any( + _dep == dep and _dep.marker == dep.marker + for _dep in _package.requires + ): + continue + + _package.add_dependency(dep) else: final_packages.append(package) depths.append(results[package]) @@ -199,8 +197,6 @@ def _solve(self) -> tuple[list[Package], list[int]]: DFSNodeID = Tuple[str, FrozenSet[str], bool] -T = TypeVar("T", bound="DFSNode") - class DFSNode: def __init__(self, id: DFSNodeID, name: str, base_name: str) -> None: @@ -208,7 +204,7 @@ def __init__(self, id: DFSNodeID, name: str, base_name: str) -> None: self.name = name self.base_name = base_name - def reachable(self: T) -> Sequence[T]: + def reachable(self) -> Sequence[Self]: return [] def visit(self, parents: list[PackageNode]) -> None: diff --git a/src/poetry/repositories/exceptions.py b/src/poetry/repositories/exceptions.py index c742f268a42..1e5be368749 100644 --- a/src/poetry/repositories/exceptions.py +++ b/src/poetry/repositories/exceptions.py @@ -5,7 +5,7 @@ class RepositoryError(Exception): pass -class PackageNotFound(Exception): +class PackageNotFoundError(Exception): pass diff --git a/src/poetry/repositories/http_repository.py b/src/poetry/repositories/http_repository.py index 7d0249b897d..581bd7eebb7 100644 --- a/src/poetry/repositories/http_repository.py +++ b/src/poetry/repositories/http_repository.py @@ -24,12 +24,12 @@ from poetry.inspection.lazy_wheel import LazyWheelUnsupportedError from poetry.inspection.lazy_wheel import metadata_from_wheel_url from poetry.repositories.cached_repository import CachedRepository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.exceptions import RepositoryError from poetry.repositories.link_sources.html import HTMLPage from poetry.utils.authenticator import Authenticator from poetry.utils.constants import REQUESTS_TIMEOUT -from poetry.utils.helpers import HTTPRangeRequestSupported +from poetry.utils.helpers import HTTPRangeRequestSupportedError from poetry.utils.helpers import download_file from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.patterns import wheel_file_re @@ -146,7 +146,7 @@ def _get_info_from_wheel(self, link: Link) -> PackageInfo: link, raise_accepts_ranges=raise_accepts_ranges ) as filepath: return PackageInfo.from_wheel(filepath) - except HTTPRangeRequestSupported: + except HTTPRangeRequestSupportedError: # The domain did not support range requests for the first URL(s) we tried, # but supports it for some URLs (especially the current URL), # so we abort the download, update _supports_range_requests to try @@ -336,7 +336,7 @@ def _get_info_from_links( def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]: if not links: - raise PackageNotFound( + raise PackageNotFoundError( f'No valid distribution links found for package: "{data.name}" version:' f' "{data.version}"' ) @@ -429,5 +429,5 @@ def _get_response(self, endpoint: str) -> requests.Response | None: def _get_page(self, name: NormalizedName) -> LinkSource: response = self._get_response(f"/{name}/") if not response: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) diff --git a/src/poetry/repositories/installed_repository.py b/src/poetry/repositories/installed_repository.py index 07a92156a19..891e8f6b79e 100644 --- a/src/poetry/repositories/installed_repository.py +++ b/src/poetry/repositories/installed_repository.py @@ -9,6 +9,7 @@ from packaging.utils import canonicalize_name from poetry.core.packages.package import Package +from poetry.core.packages.utils.utils import is_python_project from poetry.core.packages.utils.utils import url_to_path from poetry.core.utils.helpers import module_name @@ -141,7 +142,10 @@ def create_package_from_distribution( # TODO: handle multiple source directories? if is_editable_package: source_type = "directory" - source_url = paths.pop().as_posix() + path = paths.pop() + if path.name == "src": + path = path.parent + source_url = path.as_posix() elif cls.is_vcs_package(path, env): ( source_type, @@ -150,8 +154,7 @@ def create_package_from_distribution( ) = cls.get_package_vcs_properties_from_path( env.path / "src" / canonicalize_name(distribution.metadata["name"]) ) - else: - # If not, it's a path dependency + elif is_python_project(path.parent): source_type = "directory" source_url = str(path.parent) @@ -239,7 +242,7 @@ def load(cls, env: Env, with_dependencies: bool = False) -> InstalledRepository: seen = set() skipped = set() - for entry in reversed(env.sys_path): + for entry in env.sys_path: if not entry.strip(): logger.debug( "Project environment contains an empty path in sys_path," diff --git a/src/poetry/repositories/legacy_repository.py b/src/poetry/repositories/legacy_repository.py index 8b1bcea8236..5cdcd7cc35f 100644 --- a/src/poetry/repositories/legacy_repository.py +++ b/src/poetry/repositories/legacy_repository.py @@ -10,7 +10,7 @@ from poetry.core.packages.package import Package from poetry.inspection.info import PackageInfo -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.link_sources.html import SimpleRepositoryRootPage @@ -68,7 +68,7 @@ def package( def find_links_for_package(self, package: Package) -> list[Link]: try: page = self.get_page(package.name) - except PackageNotFound: + except PackageNotFoundError: return [] return list(page.links_for_version(package.name, package.version)) @@ -81,7 +81,7 @@ def _find_packages( """ try: page = self.get_page(name) - except PackageNotFound: + except PackageNotFoundError: self._log(f"No packages found for {name}", level="debug") return [] @@ -127,7 +127,7 @@ def _get_release_info( def _get_page(self, name: NormalizedName) -> HTMLPage: if not (response := self._get_response(f"/{name}/")): - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) @cached_property @@ -145,7 +145,7 @@ def search(self, query: str) -> list[Package]: results: list[Package] = [] for candidate in self.root_page.search(query): - with suppress(PackageNotFound): + with suppress(PackageNotFoundError): page = self.get_page(candidate) for package in page.packages: diff --git a/src/poetry/repositories/link_sources/base.py b/src/poetry/repositories/link_sources/base.py index 9948ec8cd35..53bf073c523 100644 --- a/src/poetry/repositories/link_sources/base.py +++ b/src/poetry/repositories/link_sources/base.py @@ -11,7 +11,7 @@ from poetry.core.constraints.version import Version from poetry.core.packages.package import Package -from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.exceptions import InvalidVersionError from poetry.utils.patterns import sdist_file_re from poetry.utils.patterns import wheel_file_re @@ -86,7 +86,7 @@ def link_package_data(cls, link: Link) -> Package | None: if version_string: try: version = Version.parse(version_string) - except InvalidVersion: + except InvalidVersionError: logger.debug( "Skipping url (%s) due to invalid version (%s)", link.url, version ) diff --git a/src/poetry/repositories/pypi_repository.py b/src/poetry/repositories/pypi_repository.py index 43d6c512324..5e47047b1c3 100644 --- a/src/poetry/repositories/pypi_repository.py +++ b/src/poetry/repositories/pypi_repository.py @@ -11,9 +11,9 @@ from cachecontrol.controller import logger as cache_control_logger from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link -from poetry.core.version.exceptions import InvalidVersion +from poetry.core.version.exceptions import InvalidVersionError -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.http_repository import HTTPRepository from poetry.repositories.link_sources.json import SimpleJsonPage from poetry.repositories.parsers.pypi_search_parser import SearchResultParser @@ -64,7 +64,7 @@ def search(self, query: str) -> list[Package]: package = Package(result.name, result.version) package.description = result.description.strip() results.append(package) - except InvalidVersion: + except InvalidVersionError: self._log( f'Unable to parse version "{result.version}" for the' f" {result.name} package, skipping", @@ -90,7 +90,7 @@ def _find_packages( """ try: json_page = self.get_page(name) - except PackageNotFound: + except PackageNotFoundError: self._log(f"No packages found for {name}", level="debug") return [] @@ -106,7 +106,7 @@ def _get_package_info(self, name: NormalizedName) -> dict[str, Any]: headers = {"Accept": "application/vnd.pypi.simple.v1+json"} info = self._get(f"simple/{name}/", headers=headers) if info is None: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return info @@ -132,7 +132,7 @@ def _get_release_info( json_data = self._get(f"pypi/{name}/{version}/json") if json_data is None: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") info = json_data["info"] diff --git a/src/poetry/repositories/repository.py b/src/poetry/repositories/repository.py index 1f6aef2ff7c..f1586090b14 100644 --- a/src/poetry/repositories/repository.py +++ b/src/poetry/repositories/repository.py @@ -8,10 +8,12 @@ from poetry.core.constraints.version import Version from poetry.repositories.abstract_repository import AbstractRepository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError if TYPE_CHECKING: + from collections.abc import Sequence + from packaging.utils import NormalizedName from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency @@ -20,7 +22,7 @@ class Repository(AbstractRepository): - def __init__(self, name: str, packages: list[Package] | None = None) -> None: + def __init__(self, name: str, packages: Sequence[Package] | None = None) -> None: super().__init__(name) self._packages: list[Package] = [] @@ -105,4 +107,4 @@ def package( if canonicalized_name == package.name and package.version == version: return package - raise PackageNotFound(f"Package {name} ({version}) not found.") + raise PackageNotFoundError(f"Package {name} ({version}) not found.") diff --git a/src/poetry/repositories/repository_pool.py b/src/poetry/repositories/repository_pool.py index 9d6338f4ff2..2adfdd872b2 100644 --- a/src/poetry/repositories/repository_pool.py +++ b/src/poetry/repositories/repository_pool.py @@ -10,7 +10,7 @@ from poetry.config.config import Config from poetry.repositories.abstract_repository import AbstractRepository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.repository import Repository from poetry.utils.cache import ArtifactCache @@ -26,9 +26,7 @@ class Priority(IntEnum): # The order of the members below dictates the actual priority. The first member has # top priority. - DEFAULT = enum.auto() PRIMARY = enum.auto() - SECONDARY = enum.auto() SUPPLEMENTAL = enum.auto() EXPLICIT = enum.auto() @@ -118,9 +116,6 @@ def _sorted_repositories(self) -> list[PrioritizedRepository]: def artifact_cache(self) -> ArtifactCache: return self._artifact_cache - def has_default(self) -> bool: - return self._contains_priority(Priority.DEFAULT) - def has_primary_repositories(self) -> bool: return self._contains_priority(Priority.PRIMARY) @@ -145,12 +140,7 @@ def _get_prioritized_repository(self, name: str) -> PrioritizedRepository: raise IndexError(f'Repository "{name}" does not exist.') def add_repository( - self, - repository: Repository, - default: bool = False, - secondary: bool = False, - *, - priority: Priority = Priority.PRIMARY, + self, repository: Repository, *, priority: Priority = Priority.PRIMARY ) -> RepositoryPool: """ Adds a repository to the pool. @@ -161,19 +151,6 @@ def add_repository( f"A repository with name {repository_name} was already added." ) - if default or secondary: - warnings.warn( - "Parameters 'default' and 'secondary' to" - " 'RepositoryPool.add_repository' are deprecated. Please provide" - " the keyword-argument 'priority' instead.", - DeprecationWarning, - stacklevel=2, - ) - priority = Priority.DEFAULT if default else Priority.SECONDARY - - if priority is Priority.DEFAULT and self.has_default(): - raise ValueError("Only one repository can be the default.") - self._repositories[repository_name] = PrioritizedRepository( repository, priority ) @@ -202,9 +179,9 @@ def package( for repo in self.repositories: try: return repo.package(name, version, extras=extras) - except PackageNotFound: + except PackageNotFoundError: continue - raise PackageNotFound(f"Package {name} ({version}) not found.") + raise PackageNotFoundError(f"Package {name} ({version}) not found.") def find_packages(self, dependency: Dependency) -> list[Package]: repository_name = dependency.source_name diff --git a/src/poetry/repositories/single_page_repository.py b/src/poetry/repositories/single_page_repository.py index 446957f12db..dc318fd91f6 100644 --- a/src/poetry/repositories/single_page_repository.py +++ b/src/poetry/repositories/single_page_repository.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.link_sources.html import HTMLPage @@ -18,5 +18,5 @@ def _get_page(self, name: NormalizedName) -> HTMLPage: """ response = self._get_response("") if not response: - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") return HTMLPage(response.url, response.text) diff --git a/src/poetry/toml/exceptions.py b/src/poetry/toml/exceptions.py index 66fcec0063b..176dbb95263 100644 --- a/src/poetry/toml/exceptions.py +++ b/src/poetry/toml/exceptions.py @@ -1,8 +1,8 @@ from __future__ import annotations -from poetry.core.exceptions import PoetryCoreException +from poetry.core.exceptions import PoetryCoreError from tomlkit.exceptions import TOMLKitError -class TOMLError(TOMLKitError, PoetryCoreException): +class TOMLError(TOMLKitError, PoetryCoreError): pass diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index 08185a362d2..f19ee99b18e 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -22,7 +22,7 @@ from poetry.__version__ import __version__ from poetry.config.config import Config -from poetry.exceptions import PoetryException +from poetry.exceptions import PoetryError from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.constants import RETRY_AFTER_HEADER from poetry.utils.constants import STATUS_FORCELIST @@ -78,7 +78,7 @@ def certs(self, config: Config) -> RepositoryCertificateConfig: return RepositoryCertificateConfig.create(self.name, config) def get_http_credentials( - self, password_manager: PasswordManager, username: str | None = None + self, password_manager: PasswordManager ) -> HTTPAuthCredential: # try with the repository name via the password manager credential = HTTPAuthCredential( @@ -247,7 +247,7 @@ def request( continue # this should never really be hit under any sane circumstance - raise PoetryException("Failed HTTP {} request", method.upper()) + raise PoetryError("Failed HTTP {} request", method.upper()) def _get_backoff(self, response: requests.Response | None, attempt: int) -> float: if response is not None: @@ -268,15 +268,15 @@ def post(self, url: str, **kwargs: Any) -> requests.Response: return self.request("post", url, **kwargs) def _get_credentials_for_repository( - self, repository: AuthenticatorRepositoryConfig, username: str | None = None + self, repository: AuthenticatorRepositoryConfig ) -> HTTPAuthCredential: # cache repository credentials by repository url to avoid multiple keyring # backend queries when packages are being downloaded from the same source - key = f"{repository.url}#username={username or ''}" + key = repository.url if key not in self._credentials: self._credentials[key] = repository.get_http_credentials( - password_manager=self._password_manager, username=username + password_manager=self._password_manager ) return self._credentials[key] @@ -346,9 +346,7 @@ def get_credentials_for_url(self, url: str) -> HTTPAuthCredential: def get_pypi_token(self, name: str) -> str | None: return self._password_manager.get_pypi_token(name) - def get_http_auth( - self, name: str, username: str | None = None - ) -> HTTPAuthCredential | None: + def get_http_auth(self, name: str) -> HTTPAuthCredential | None: if name == "pypi": repository = AuthenticatorRepositoryConfig( name, "https://upload.pypi.org/legacy/" @@ -358,9 +356,7 @@ def get_http_auth( return None repository = self.configured_repositories[name] - return self._get_credentials_for_repository( - repository=repository, username=username - ) + return self._get_credentials_for_repository(repository=repository) def get_certs_for_repository(self, name: str) -> RepositoryCertificateConfig: if name.lower() == "pypi" or name not in self.configured_repositories: diff --git a/src/poetry/utils/cache.py b/src/poetry/utils/cache.py index 1f0f18ed5e5..90dd1a46f52 100644 --- a/src/poetry/utils/cache.py +++ b/src/poetry/utils/cache.py @@ -19,7 +19,7 @@ from poetry.utils._compat import decode from poetry.utils._compat import encode from poetry.utils.helpers import get_highest_priority_hash_type -from poetry.utils.wheel import InvalidWheelName +from poetry.utils.wheel import InvalidWheelNameError from poetry.utils.wheel import Wheel @@ -316,7 +316,7 @@ def _get_cached_archive( try: wheel = Wheel(archive.name) - except InvalidWheelName: + except InvalidWheelNameError: continue if not wheel.is_supported_by_environment(env): diff --git a/src/poetry/utils/env/__init__.py b/src/poetry/utils/env/__init__.py index 89882ddfd05..c3834f4fbaf 100644 --- a/src/poetry/utils/env/__init__.py +++ b/src/poetry/utils/env/__init__.py @@ -12,8 +12,8 @@ from poetry.utils.env.exceptions import EnvError from poetry.utils.env.exceptions import IncorrectEnvError from poetry.utils.env.exceptions import InvalidCurrentPythonVersionError -from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound -from poetry.utils.env.exceptions import PythonVersionNotFound +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFoundError +from poetry.utils.env.exceptions import PythonVersionNotFoundError from poetry.utils.env.generic_env import GenericEnv from poetry.utils.env.mock_env import MockEnv from poetry.utils.env.null_env import NullEnv @@ -67,7 +67,7 @@ def build_environment( if not env or poetry.package.build_script: with ephemeral_environment( executable=env.python if env else None, - flags={"no-pip": True, "no-setuptools": True, "no-wheel": True}, + flags={"no-pip": True}, ) as venv: if io: requires = [ @@ -102,8 +102,8 @@ def build_environment( "EnvCommandError", "IncorrectEnvError", "InvalidCurrentPythonVersionError", - "NoCompatiblePythonVersionFound", - "PythonVersionNotFound", + "NoCompatiblePythonVersionFoundError", + "PythonVersionNotFoundError", "Env", "EnvManager", "GenericEnv", diff --git a/src/poetry/utils/env/base_env.py b/src/poetry/utils/env/base_env.py index 5a8f2fc6146..7c5574c19e3 100644 --- a/src/poetry/utils/env/base_env.py +++ b/src/poetry/utils/env/base_env.py @@ -7,6 +7,8 @@ import sys import sysconfig +from abc import ABC +from abc import abstractmethod from functools import cached_property from pathlib import Path from subprocess import CalledProcessError @@ -32,7 +34,7 @@ PythonVersion = Tuple[int, int, int, str, int] -class Env: +class Env(ABC): """ An abstract Python environment. """ @@ -58,7 +60,6 @@ def __init__(self, path: Path, base: Path | None = None) -> None: self._base = base or path self._site_packages: SitePackages | None = None - self._paths: dict[str, str] | None = None self._supported_tags: list[Tag] | None = None self._purelib: Path | None = None self._platlib: Path | None = None @@ -223,25 +224,23 @@ def is_path_relative_to_lib(self, path: Path) -> bool: return False @property - def sys_path(self) -> list[str]: - raise NotImplementedError() + @abstractmethod + def sys_path(self) -> list[str]: ... - @property + @cached_property def paths(self) -> dict[str, str]: - if self._paths is None: - self._paths = self.get_paths() - - if self.is_venv(): - # We copy pip's logic here for the `include` path - self._paths["include"] = str( - self.path.joinpath( - "include", - "site", - f"python{self.version_info[0]}.{self.version_info[1]}", - ) + paths = self.get_paths() + + if self.is_venv(): + # We copy pip's logic here for the `include` path + paths["include"] = str( + self.path.joinpath( + "include", + "site", + f"python{self.version_info[0]}.{self.version_info[1]}", ) - - return self._paths + ) + return paths @property def supported_tags(self) -> list[Tag]: @@ -262,8 +261,8 @@ def get_base_prefix(cls) -> Path: return Path(sys.prefix) - def get_marker_env(self) -> dict[str, Any]: - raise NotImplementedError() + @abstractmethod + def get_marker_env(self) -> dict[str, Any]: ... def get_pip_command(self, embedded: bool = False) -> list[str]: if embedded or not Path(self._bin(self._pip_executable)).exists(): @@ -271,11 +270,11 @@ def get_pip_command(self, embedded: bool = False) -> list[str]: # run as module so that pip can update itself on Windows return [str(self.python), "-m", "pip"] - def get_supported_tags(self) -> list[Tag]: - raise NotImplementedError() + @abstractmethod + def get_supported_tags(self) -> list[Tag]: ... - def get_paths(self) -> dict[str, str]: - raise NotImplementedError() + @abstractmethod + def get_paths(self) -> dict[str, str]: ... def is_valid_for_marker(self, marker: BaseMarker) -> bool: valid: bool = marker.validate(self.marker_env) @@ -351,8 +350,8 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int: exe.communicate() return exe.returncode - def is_venv(self) -> bool: - raise NotImplementedError() + @abstractmethod + def is_venv(self) -> bool: ... @property def script_dirs(self) -> list[Path]: diff --git a/src/poetry/utils/env/env_manager.py b/src/poetry/utils/env/env_manager.py index 60f49e99889..8d0835b0692 100644 --- a/src/poetry/utils/env/env_manager.py +++ b/src/poetry/utils/env/env_manager.py @@ -5,7 +5,6 @@ import os import plistlib import re -import shutil import subprocess import sys @@ -18,9 +17,7 @@ import virtualenv from cleo.io.null_io import NullIO -from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version -from poetry.core.constraints.version import parse_constraint from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS @@ -28,9 +25,10 @@ from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.exceptions import IncorrectEnvError from poetry.utils.env.exceptions import InvalidCurrentPythonVersionError -from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound -from poetry.utils.env.exceptions import PythonVersionNotFound +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFoundError +from poetry.utils.env.exceptions import PythonVersionNotFoundError from poetry.utils.env.generic_env import GenericEnv +from poetry.utils.env.python_manager import Python from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER from poetry.utils.env.system_env import SystemEnv @@ -97,70 +95,6 @@ def __init__(self, poetry: Poetry, io: None | IO = None) -> None: self._poetry = poetry self._io = io or NullIO() - @staticmethod - 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: - encoding = "locale" if sys.version_info >= (3, 10) else None - executable = subprocess.check_output( - [path_python, "-c", "import sys; print(sys.executable)"], - text=True, - encoding=encoding, - ).strip() - return Path(executable) - - except CalledProcessError: - return None - - @staticmethod - def _detect_active_python(io: None | IO = None) -> Path | None: - io = io or NullIO() - 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") - - if executable is not None: - io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) - else: - io.write_error_line( - "Unable to detect the current active python executable. Falling" - " back to default.", - verbosity=Verbosity.VERBOSE, - ) - - return executable - - @staticmethod - def get_python_version( - precision: int = 3, - prefer_active_python: bool = False, - io: None | IO = None, - ) -> Version: - version = ".".join(str(v) for v in sys.version_info[:precision]) - - if prefer_active_python: - executable = EnvManager._detect_active_python(io) - - if executable: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ).strip() - - version = ".".join(str(v) for v in python_patch.split(".")[:precision]) - - return Version.parse(version) - @property def in_project_venv(self) -> Path: venv: Path = self._poetry.file.path.parent / ".venv" @@ -189,23 +123,9 @@ def activate(self, python: str) -> Env: # Executable in PATH or full executable path pass - python_path = self._full_python_path(python) - if python_path is None: - raise PythonVersionNotFound(python) - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_version_string = subprocess.check_output( - [python_path, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ) - except CalledProcessError as e: - raise EnvCommandError(e) - - python_version = Version.parse(python_version_string.strip()) - minor = f"{python_version.major}.{python_version.minor}" - patch = python_version.text + python_ = Python.get_by_name(python) + if python_ is None: + raise PythonVersionNotFoundError(python) create = False # If we are required to create the virtual environment in the project directory, @@ -218,10 +138,10 @@ def activate(self, python: str) -> Env: _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) - if patch != current_patch: + if python_.patch_version.to_string() != current_patch: create = True - self.create_venv(executable=python_path, force=create) + self.create_venv(executable=python_.executable, force=create) return self.get(reload=True) @@ -233,11 +153,14 @@ def activate(self, python: str) -> Env: current_minor = current_env["minor"] current_patch = current_env["patch"] - if current_minor == minor and current_patch != patch: + if ( + current_minor == python_.minor_version.to_string() + and current_patch != python_.patch_version.to_string() + ): # We need to recreate create = True - name = f"{self.base_env_name}-py{minor}" + name = f"{self.base_env_name}-py{python_.minor_version.to_string()}" venv = venv_path / name # Create if needed @@ -251,13 +174,16 @@ def activate(self, python: str) -> Env: _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) - if patch != current_patch: + if python_.patch_version.to_string() != current_patch: create = True - self.create_venv(executable=python_path, force=create) + self.create_venv(executable=python_.executable, force=create) # Activate - envs[self.base_env_name] = {"minor": minor, "patch": patch} + envs[self.base_env_name] = { + "minor": python_.minor_version.to_string(), + "patch": python_.patch_version.to_string(), + } self.envs_file.write(envs) return self.get(reload=True) @@ -277,12 +203,8 @@ def get(self, reload: bool = False) -> Env: if self._env is not None and not reload: return self._env - prefer_active_python = self._poetry.config.get( - "virtualenvs.prefer-active-python" - ) - python_minor = self.get_python_version( - precision=2, prefer_active_python=prefer_active_python, io=self._io - ).to_string() + python = Python.get_preferred_python(config=self._poetry.config, io=self._io) + python_minor = python.minor_version.to_string() env = None envs = None @@ -480,8 +402,11 @@ def create_venv( ) venv_prompt = self._poetry.config.get("virtualenvs.prompt") - if not executable and prefer_active_python: - executable = self._detect_active_python() + python = ( + Python(executable) + if executable + else Python.get_preferred_python(config=self._poetry.config, io=self._io) + ) venv_path = ( self.in_project_venv @@ -491,19 +416,8 @@ def create_venv( if not name: name = self._poetry.package.name - python_patch = ".".join([str(v) for v in sys.version_info[:3]]) - python_minor = ".".join([str(v) for v in sys.version_info[:2]]) - if executable: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ).strip() - python_minor = ".".join(python_patch.split(".")[:2]) - supported_python = self._poetry.package.python_constraint - if not supported_python.allows(Version.parse(python_patch)): + if not supported_python.allows(python.patch_version): # The currently activated or chosen Python version # is not compatible with the Python constraint specified # for the project. @@ -511,72 +425,30 @@ def create_venv( # and notify the user of the incompatibility. # Otherwise, we try to find a compatible Python version. if executable and not prefer_active_python: - raise NoCompatiblePythonVersionFound( - self._poetry.package.python_versions, python_patch + raise NoCompatiblePythonVersionFoundError( + self._poetry.package.python_versions, + python.patch_version.to_string(), ) self._io.write_error_line( - f"The currently activated Python version {python_patch} is not" + f"The currently activated Python version {python.patch_version.to_string()} is not" f" supported by the project ({self._poetry.package.python_versions}).\n" "Trying to find and use a compatible version. " ) - for suffix in sorted( - self._poetry.package.AVAILABLE_PYTHONS, - key=lambda v: (v.startswith("3"), -len(v), v), - reverse=True, - ): - if len(suffix) == 1: - if not parse_constraint(f"^{suffix}.0").allows_any( - supported_python - ): - continue - elif not supported_python.allows_any(parse_constraint(suffix + ".*")): - continue - - python_name = f"python{suffix}" - if self._io.is_debug(): - self._io.write_error_line(f"Trying {python_name}") - - python = self._full_python_path(python_name) - if python is None: - continue - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [python, "-c", GET_PYTHON_VERSION_ONELINER], - stderr=subprocess.STDOUT, - text=True, - encoding=encoding, - ).strip() - except CalledProcessError: - continue - - if supported_python.allows(Version.parse(python_patch)): - self._io.write_error_line( - f"Using {python_name} ({python_patch})" - ) - executable = python - python_minor = ".".join(python_patch.split(".")[:2]) - break - - if not executable: - raise NoCompatiblePythonVersionFound( - self._poetry.package.python_versions - ) + python = Python.get_compatible_python(poetry=self._poetry, io=self._io) if in_project_venv: venv = venv_path else: name = self.generate_env_name(name, str(cwd)) - name = f"{name}-py{python_minor.strip()}" + name = f"{name}-py{python.minor_version.to_string()}" venv = venv_path / name if venv_prompt is not None: venv_prompt = venv_prompt.format( project_name=self._poetry.package.name or "virtualenv", - python_version=python_minor, + python_version=python.minor_version.to_string(), ) if not venv.exists(): @@ -613,7 +485,7 @@ def create_venv( if create_venv: self.build_venv( venv, - executable=executable, + executable=python.executable, flags=self._poetry.config.get("virtualenvs.options"), prompt=venv_prompt, ) @@ -643,8 +515,6 @@ def build_venv( executable: Path | None = None, flags: dict[str, str | bool] | None = None, with_pip: bool | None = None, - with_wheel: bool | None = None, - with_setuptools: bool | None = None, prompt: str | None = None, ) -> virtualenv.run.session.Session: flags = flags or {} @@ -652,25 +522,9 @@ def build_venv( if with_pip is not None: flags["no-pip"] = not with_pip - if with_wheel is not None: - wheel_flags: dict[str, str | bool] = ( - {"wheel": "bundle"} if with_wheel else {"no-wheel": True} - ) - flags.update(wheel_flags) - - if with_setuptools is not None: - setuptools_flags: dict[str, str | bool] = ( - {"setuptools": "bundle"} if with_setuptools else {"no-setuptools": True} - ) - flags.update(setuptools_flags) - flags.setdefault("no-pip", True) - - if "setuptools" not in flags and "no-setuptools" not in flags: - flags["no-setuptools"] = True - - if "wheel" not in flags and "no-wheel" not in flags: - flags["no-wheel"] = True + flags.setdefault("no-setuptools", True) + flags.setdefault("no-wheel", True) if WINDOWS: path = get_real_windows_path(path) diff --git a/src/poetry/utils/env/exceptions.py b/src/poetry/utils/env/exceptions.py index ece3b3924a0..78082d45932 100644 --- a/src/poetry/utils/env/exceptions.py +++ b/src/poetry/utils/env/exceptions.py @@ -33,12 +33,12 @@ def __init__(self, e: CalledProcessError) -> None: super().__init__("\n\n".join(message_parts)) -class PythonVersionNotFound(EnvError): +class PythonVersionNotFoundError(EnvError): def __init__(self, expected: str) -> None: super().__init__(f"Could not find the python executable {expected}") -class NoCompatiblePythonVersionFound(EnvError): +class NoCompatiblePythonVersionFoundError(EnvError): def __init__(self, expected: str, given: str | None = None) -> None: if given: message = ( diff --git a/src/poetry/utils/env/null_env.py b/src/poetry/utils/env/null_env.py index 7bd0a9e1c79..95de157862d 100644 --- a/src/poetry/utils/env/null_env.py +++ b/src/poetry/utils/env/null_env.py @@ -2,6 +2,7 @@ import sys +from functools import cached_property from pathlib import Path from typing import Any @@ -20,16 +21,14 @@ def __init__( self._execute = execute self.executed: list[list[str]] = [] - @property + @cached_property def paths(self) -> dict[str, str]: - if self._paths is None: - self._paths = self.get_paths() - self._paths["platlib"] = str(self._path / "platlib") - self._paths["purelib"] = str(self._path / "purelib") - self._paths["scripts"] = str(self._path / "scripts") - self._paths["data"] = str(self._path / "data") - - return self._paths + paths = self.get_paths() + paths["platlib"] = str(self._path / "platlib") + paths["purelib"] = str(self._path / "purelib") + paths["scripts"] = str(self._path / "scripts") + paths["data"] = str(self._path / "data") + return paths def _run(self, cmd: list[str], **kwargs: Any) -> str: self.executed.append(cmd) diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py new file mode 100644 index 00000000000..ce1d8103978 --- /dev/null +++ b/src/poetry/utils/env/python_manager.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import shutil +import subprocess +import sys + +from functools import cached_property +from pathlib import Path +from typing import TYPE_CHECKING + +from cleo.io.null_io import NullIO +from cleo.io.outputs.output import Verbosity +from poetry.core.constraints.version import Version +from poetry.core.constraints.version import parse_constraint + +from poetry.utils._compat import decode +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFoundError +from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER + + +if TYPE_CHECKING: + from cleo.io.io import IO + + from poetry.config.config import Config + from poetry.poetry import Poetry + + +class Python: + def __init__(self, executable: str | Path, version: Version | None = None) -> None: + self.executable = Path(executable) + self._version = version + + @property + def version(self) -> Version: + if not self._version: + if self.executable == Path(sys.executable): + python_version = ".".join(str(v) for v in sys.version_info[:3]) + else: + encoding = "locale" if sys.version_info >= (3, 10) else None + python_version = decode( + subprocess.check_output( + [str(self.executable), "-c", GET_PYTHON_VERSION_ONELINER], + text=True, + encoding=encoding, + ).strip() + ) + self._version = Version.parse(python_version) + + return self._version + + @cached_property + def patch_version(self) -> Version: + return Version.from_parts( + major=self.version.major, + minor=self.version.minor, + patch=self.version.patch, + ) + + @cached_property + def minor_version(self) -> Version: + return Version.from_parts(major=self.version.major, minor=self.version.minor) + + @staticmethod + 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: + encoding = "locale" if sys.version_info >= (3, 10) else None + executable = subprocess.check_output( + [path_python, "-c", "import sys; print(sys.executable)"], + text=True, + encoding=encoding, + ).strip() + return Path(executable) + + except subprocess.CalledProcessError: + return None + + @staticmethod + def _detect_active_python(io: IO) -> Path | None: + io.write_error_line( + "Trying to detect current active python executable as specified in" + " the config.", + verbosity=Verbosity.VERBOSE, + ) + + executable = Python._full_python_path("python") + + if executable is not None: + io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) + else: + io.write_error_line( + "Unable to detect the current active python executable. Falling" + " back to default.", + verbosity=Verbosity.VERBOSE, + ) + + return executable + + @classmethod + def get_system_python(cls) -> Python: + return cls(executable=sys.executable) + + @classmethod + def get_by_name(cls, python_name: str) -> Python | None: + executable = cls._full_python_path(python_name) + if not executable: + return None + + return cls(executable=executable) + + @classmethod + def get_preferred_python(cls, config: Config, io: IO | None = None) -> Python: + io = io or NullIO() + + if config.get("virtualenvs.prefer-active-python") and ( + active_python := Python._detect_active_python(io) + ): + return cls(executable=active_python) + + return cls.get_system_python() + + @classmethod + def get_compatible_python(cls, poetry: Poetry, io: IO | None = None) -> Python: + io = io or NullIO() + supported_python = poetry.package.python_constraint + python = None + + for suffix in [ + *sorted( + poetry.package.AVAILABLE_PYTHONS, + key=lambda v: (v.startswith("3"), -len(v), v), + reverse=True, + ), + "", + ]: + if len(suffix) == 1: + if not parse_constraint(f"^{suffix}.0").allows_any(supported_python): + continue + elif suffix and not supported_python.allows_any( + parse_constraint(suffix + ".*") + ): + continue + + python_name = f"python{suffix}" + if io.is_debug(): + io.write_error_line(f"Trying {python_name}") + + executable = cls._full_python_path(python_name) + if executable is None: + continue + + candidate = cls(executable) + if supported_python.allows(candidate.patch_version): + python = candidate + io.write_error_line( + f"Using {python_name} ({python.patch_version})" + ) + break + + if not python: + raise NoCompatiblePythonVersionFoundError(poetry.package.python_versions) + + return python diff --git a/src/poetry/utils/helpers.py b/src/poetry/utils/helpers.py index 07e0ce41687..22608f8c3eb 100644 --- a/src/poetry/utils/helpers.py +++ b/src/poetry/utils/helpers.py @@ -125,7 +125,7 @@ def merge_dicts(d1: dict[str, Any], d2: dict[str, Any]) -> None: d1[k] = d2[k] -class HTTPRangeRequestSupported(Exception): +class HTTPRangeRequestSupportedError(Exception): """Raised when server unexpectedly supports byte ranges.""" @@ -143,7 +143,7 @@ def download_file( downloader = Downloader(url, dest, session, max_retries=max_retries) if raise_accepts_ranges and downloader.accepts_ranges: - raise HTTPRangeRequestSupported(f"URL {url} supports range requests.") + raise HTTPRangeRequestSupportedError(f"URL {url} supports range requests.") set_indicator = False with Indicator.context() as update_context: diff --git a/src/poetry/utils/isolated_build.py b/src/poetry/utils/isolated_build.py index e78361f8480..5879676c691 100644 --- a/src/poetry/utils/isolated_build.py +++ b/src/poetry/utils/isolated_build.py @@ -136,7 +136,7 @@ def isolated_builder( with ephemeral_environment( executable=python_executable, - flags={"no-pip": True, "no-setuptools": True, "no-wheel": True}, + flags={"no-pip": True}, ) as venv: env = IsolatedEnv(venv, pool) stdout = StringIO() diff --git a/src/poetry/utils/pip.py b/src/poetry/utils/pip.py index b74294795ed..458e897855e 100644 --- a/src/poetry/utils/pip.py +++ b/src/poetry/utils/pip.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from poetry.exceptions import PoetryException +from poetry.exceptions import PoetryError from poetry.utils.env import EnvCommandError @@ -45,7 +45,7 @@ def pip_install( if editable: if not path.is_dir(): - raise PoetryException( + raise PoetryError( "Cannot install non directory dependencies in editable mode" ) args.append("-e") @@ -55,4 +55,4 @@ def pip_install( try: return environment.run_pip(*args) except EnvCommandError as e: - raise PoetryException(f"Failed to install {path}") from e + raise PoetryError(f"Failed to install {path}") from e diff --git a/src/poetry/utils/wheel.py b/src/poetry/utils/wheel.py index f45c50b3b35..bb4a70b7907 100644 --- a/src/poetry/utils/wheel.py +++ b/src/poetry/utils/wheel.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -class InvalidWheelName(Exception): +class InvalidWheelNameError(Exception): pass @@ -24,7 +24,7 @@ class Wheel: def __init__(self, filename: str) -> None: wheel_info = wheel_file_re.match(filename) if not wheel_info: - raise InvalidWheelName(f"{filename} is not a valid wheel filename.") + raise InvalidWheelNameError(f"{filename} is not a valid wheel filename.") self.filename = filename self.name = wheel_info.group("name").replace("_", "-") diff --git a/tests/config/test_source.py b/tests/config/test_source.py index 2331dcb4339..e639b81c91f 100644 --- a/tests/config/test_source.py +++ b/tests/config/test_source.py @@ -43,29 +43,11 @@ def test_source_default_is_primary() -> None: assert source.priority == Priority.PRIMARY -@pytest.mark.parametrize( - ("default", "secondary", "expected_priority"), - [ - (False, True, Priority.SECONDARY), - (True, False, Priority.DEFAULT), - (True, True, Priority.DEFAULT), - ], -) -def test_source_legacy_handling( - default: bool, secondary: bool, expected_priority: Priority -) -> None: - with pytest.warns(DeprecationWarning): - source = Source( - "foo", "https://example.com", default=default, secondary=secondary - ) - assert source.priority == expected_priority - - @pytest.mark.parametrize( ("priority", "expected_priority"), [ - ("secondary", Priority.SECONDARY), - ("SECONDARY", Priority.SECONDARY), + ("supplemental", Priority.SUPPLEMENTAL), + ("SUPPLEMENTAL", Priority.SUPPLEMENTAL), ], ) def test_source_priority_as_string(priority: str, expected_priority: Priority) -> None: @@ -74,4 +56,4 @@ def test_source_priority_as_string(priority: str, expected_priority: Priority) - "https://example.com", priority=priority, # type: ignore[arg-type] ) - assert source.priority == Priority.SECONDARY + assert source.priority == Priority.SUPPLEMENTAL diff --git a/tests/conftest.py b/tests/conftest.py index 1c443dbee30..19f260e58b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,7 +138,7 @@ def get_credential( if password is None: return None - return SimpleCredential(username, password) # type: ignore[no-untyped-call] + return SimpleCredential(username, password) def delete_password(self, service: str, username: str) -> None: if service in self._passwords and username in self._passwords[service]: @@ -534,7 +534,6 @@ def venv_flags_default() -> dict[str, bool]: "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, } diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py index 662e3434939..478ef414ef8 100644 --- a/tests/console/commands/env/test_use.py +++ b/tests/console/commands/env/test_use.py @@ -77,7 +77,6 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="simple-project-py3.7", ) diff --git a/tests/console/commands/source/conftest.py b/tests/console/commands/source/conftest.py index 7e44e280069..264ee640e6f 100644 --- a/tests/console/commands/source/conftest.py +++ b/tests/console/commands/source/conftest.py @@ -24,33 +24,11 @@ def source_two() -> Source: return Source(name="two", url="https://two.com") -@pytest.fixture -def source_default_deprecated() -> Source: - return Source(name="default", url="https://default.com", default=True) - - -@pytest.fixture -def source_secondary_deprecated() -> Source: - return Source(name="secondary", url="https://secondary.com", secondary=True) - - @pytest.fixture def source_primary() -> Source: return Source(name="primary", url="https://primary.com", priority=Priority.PRIMARY) -@pytest.fixture -def source_default() -> Source: - return Source(name="default", url="https://default.com", priority=Priority.DEFAULT) - - -@pytest.fixture -def source_secondary() -> Source: - return Source( - name="secondary", url="https://secondary.com", priority=Priority.SECONDARY - ) - - @pytest.fixture def source_supplemental() -> Source: return Source( @@ -158,16 +136,12 @@ def add_all_source_types( command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry, source_primary: Source, - source_default: Source, - source_secondary: Source, source_supplemental: Source, source_explicit: Source, ) -> None: add = command_tester_factory("source add", poetry=poetry_with_source) for source in [ source_primary, - source_default, - source_secondary, source_supplemental, source_explicit, ]: diff --git a/tests/console/commands/source/test_add.py b/tests/console/commands/source/test_add.py index 53ae876133a..91a2a8cb127 100644 --- a/tests/console/commands/source/test_add.py +++ b/tests/console/commands/source/test_add.py @@ -22,51 +22,13 @@ def tester( return command_tester_factory("source add", poetry=poetry_with_source) -def _get_source_warning(priority: Priority) -> str: - if priority is Priority.SECONDARY: - return ( - "Warning: Priority 'secondary' is deprecated. Consider changing the" - " priority to one of the non-deprecated values: 'primary'," - " 'supplemental', 'explicit'." - ) - elif priority is Priority.DEFAULT: - return ( - "Warning: Priority 'default' is deprecated. You can achieve" - " the same effect by changing the priority to 'primary' and putting" - " the source first." - ) - return "" - - -def assert_source_added_legacy( - tester: CommandTester, - poetry: Poetry, - source_existing: Source, - source_added: Source, -) -> None: - warning = ( - "Warning: Priority was set through a deprecated flag (--default or" - " --secondary). Consider using --priority next time.\n" - + _get_source_warning(source_added.priority) - ) - assert tester.io.fetch_error().strip() == warning - assert ( - tester.io.fetch_output().strip() - == f"Adding source with name {source_added.name}." - ) - poetry.pyproject.reload() - sources = poetry.get_sources() - assert sources == [source_existing, source_added] - assert tester.status_code == 0 - - def assert_source_added( tester: CommandTester, poetry: Poetry, source_existing: Source, source_added: Source, ) -> None: - assert tester.io.fetch_error().strip() == _get_source_warning(source_added.priority) + assert tester.io.fetch_error().strip() == "" assert ( tester.io.fetch_output().strip() == f"Adding source with name {source_added.name}." @@ -87,72 +49,6 @@ def test_source_add_simple( assert_source_added(tester, poetry_with_source, source_existing, source_one) -def test_source_add_default_legacy( - tester: CommandTester, - source_existing: Source, - source_default: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute(f"--default {source_default.name} {source_default.url}") - assert_source_added_legacy( - tester, poetry_with_source, source_existing, source_default - ) - - -def test_source_add_secondary_legacy( - tester: CommandTester, - source_existing: Source, - source_secondary: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}") - assert_source_added_legacy( - tester, poetry_with_source, source_existing, source_secondary - ) - - -def test_source_add_default( - tester: CommandTester, - source_existing: Source, - source_default: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute(f"--priority=default {source_default.name} {source_default.url}") - assert_source_added(tester, poetry_with_source, source_existing, source_default) - - -def test_source_add_second_default_fails( - tester: CommandTester, - source_existing: Source, - source_default: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute(f"--priority=default {source_default.name} {source_default.url}") - assert_source_added(tester, poetry_with_source, source_existing, source_default) - poetry_with_source.pyproject.reload() - - tester.execute(f"--priority=default {source_default.name}1 {source_default.url}") - assert ( - tester.io.fetch_error().strip() - == f"{_get_source_warning(source_default.priority)}\n" - f"Source with name {source_default.name} is already set to default." - " Only one default source can be configured at a time." - ) - assert tester.status_code == 1 - - -def test_source_add_secondary( - tester: CommandTester, - source_existing: Source, - source_secondary: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute( - f"--priority=secondary {source_secondary.name} {source_secondary.url}" - ) - assert_source_added(tester, poetry_with_source, source_existing, source_secondary) - - def test_source_add_supplemental( tester: CommandTester, source_existing: Source, @@ -177,26 +73,6 @@ def test_source_add_explicit( assert_source_added(tester, poetry_with_source, source_existing, source_explicit) -def test_source_add_error_default_and_secondary_legacy(tester: CommandTester) -> None: - tester.execute("--default --secondary error https://error.com") - assert ( - tester.io.fetch_error().strip() - == "Cannot configure a source as both default and secondary." - ) - assert tester.status_code == 1 - - -def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester) -> None: - tester.execute("--priority secondary --secondary error https://error.com") - assert ( - tester.io.fetch_error().strip() - == "Priority was passed through both --priority and a" - " deprecated flag (--default or --secondary). Please only provide" - " one of these." - ) - assert tester.status_code == 1 - - def test_source_add_error_no_url(tester: CommandTester) -> None: tester.execute("foo") assert ( @@ -238,32 +114,6 @@ def test_source_add_pypi_explicit( ) -def test_source_add_existing_legacy( - tester: CommandTester, source_existing: Source, poetry_with_source: Poetry -) -> None: - tester.execute(f"--default {source_existing.name} {source_existing.url}") - assert ( - tester.io.fetch_error().strip() - == "Warning: Priority was set through a deprecated flag" - " (--default or --secondary). Consider using --priority next" - f" time.\n{_get_source_warning(Priority.DEFAULT)}" - ) - assert ( - tester.io.fetch_output().strip() - == f"Source with name {source_existing.name} already exists. Updating." - ) - - poetry_with_source.pyproject.reload() - sources = poetry_with_source.get_sources() - - assert len(sources) == 1 - assert sources[0] != source_existing - expected_source = Source( - name=source_existing.name, url=source_existing.url, priority=Priority.DEFAULT - ) - assert sources[0] == expected_source - - @pytest.mark.parametrize("modifier", ["lower", "upper"]) def test_source_add_existing_no_change_except_case_of_name( modifier: str, @@ -295,7 +145,7 @@ def test_source_add_existing_updating( poetry_with_source: Poetry, ) -> None: name = getattr(source_existing.name, modifier)() - tester.execute(f"--priority=default {name} {source_existing.url}") + tester.execute(f"--priority=supplemental {name} {source_existing.url}") assert ( tester.io.fetch_output().strip() == f"Source with name {name} already exists. Updating." @@ -307,31 +157,6 @@ def test_source_add_existing_updating( assert len(sources) == 1 assert sources[0] != source_existing expected_source = Source( - name=name, url=source_existing.url, priority=Priority.DEFAULT + name=name, url=source_existing.url, priority=Priority.SUPPLEMENTAL ) assert sources[0] == expected_source - - -@pytest.mark.parametrize("modifier", ["lower", "upper"]) -def test_source_add_existing_fails_due_to_other_default( - modifier: str, - tester: CommandTester, - source_existing: Source, - source_default: Source, - poetry_with_source: Poetry, -) -> None: - tester.execute(f"--priority=default {source_default.name} {source_default.url}") - tester.io.fetch_error() - tester.io.fetch_output() - - name = getattr(source_existing.name, modifier)() - tester.execute(f"--priority=default {name} {source_existing.url}") - - assert ( - tester.io.fetch_error().strip() - == f"{_get_source_warning(source_default.priority)}\n" - f"Source with name {source_default.name} is already set to default." - " Only one default source can be configured at a time." - ) - assert tester.io.fetch_output().strip() == "" - assert tester.status_code == 1 diff --git a/tests/console/commands/source/test_show.py b/tests/console/commands/source/test_show.py index 91993a62557..bee08603d5c 100644 --- a/tests/console/commands/source/test_show.py +++ b/tests/console/commands/source/test_show.py @@ -119,8 +119,6 @@ def test_source_show_two( "source_str", ( "source_primary", - "source_default", - "source_secondary", "source_supplemental", "source_explicit", ), diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index ec88be4510d..0572c2c12a7 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -8,7 +8,7 @@ import pytest from deepdiff import DeepDiff -from poetry.core.pyproject.exceptions import PyProjectException +from poetry.core.pyproject.exceptions import PyProjectError from poetry.config.config_source import ConfigSource from poetry.console.commands.install import InstallCommand @@ -39,7 +39,7 @@ def test_show_config_with_local_config_file_empty( ) -> None: mocker.patch( "poetry.factory.Factory.create_poetry", - side_effect=PyProjectException("[tool.poetry] section not found"), + side_effect=PyProjectError("[tool.poetry] section not found"), ) tester.execute() @@ -66,7 +66,6 @@ def test_list_displays_default_value_if_not_set( virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} virtualenvs.prefer-active-python = false @@ -99,7 +98,6 @@ def test_list_displays_set_get_setting( virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} virtualenvs.prefer-active-python = false @@ -153,7 +151,6 @@ def test_unset_setting( virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} virtualenvs.prefer-active-python = false @@ -185,7 +182,6 @@ def test_unset_repo_setting( virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} virtualenvs.prefer-active-python = false @@ -315,7 +311,6 @@ def test_list_displays_set_get_local_setting( virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} virtualenvs.prefer-active-python = false @@ -333,7 +328,7 @@ def test_list_must_not_display_sources_from_pyproject_toml( command_tester_factory: CommandTesterFactory, config_cache_dir: Path, ) -> None: - source = fixture_dir("with_non_default_source_implicit") + source = fixture_dir("with_primary_source_implicit") pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") poetry = project_factory("foo", pyproject_content=pyproject_content) tester = command_tester_factory("config", poetry=poetry) @@ -356,7 +351,6 @@ def test_list_must_not_display_sources_from_pyproject_toml( virtualenvs.in-project = null virtualenvs.options.always-copy = false virtualenvs.options.no-pip = false -virtualenvs.options.no-setuptools = false virtualenvs.options.system-site-packages = false virtualenvs.path = {venv_path} # {config_cache_dir / 'virtualenvs'} virtualenvs.prefer-active-python = false diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index ec85d321f59..a13e07c15ec 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -1113,7 +1113,10 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: return result mocker.patch("subprocess.check_output", side_effect=mock_check_output) - + mocker.patch( + "poetry.utils.env.python_manager.Python._full_python_path", + return_value=Path(f"/usr/bin/python{python}"), + ) config.config["virtualenvs"]["prefer-active-python"] = prefer_active pyproject_file = source_dir / "pyproject.toml" diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 2692fa59f88..df7557749f8 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -6,11 +6,11 @@ import pytest -from poetry.core.masonry.utils.module import ModuleOrPackageNotFound +from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.installer_command import InstallerCommand -from poetry.console.exceptions import GroupNotFound +from poetry.console.exceptions import GroupNotFoundError from tests.helpers import TestLocker @@ -128,7 +128,7 @@ def test_group_options_are_passed_to_the_installer( mocker.patch.object(tester.command.installer, "run", return_value=0) editable_builder_mock = mocker.patch( "poetry.masonry.builders.editable.EditableBuilder", - side_effect=ModuleOrPackageNotFound(), + side_effect=ModuleOrPackageNotFoundError(), ) if not with_root: @@ -136,7 +136,7 @@ def test_group_options_are_passed_to_the_installer( status_code = tester.execute(options) - if options == "--no-root --only-root": + if options == "--no-root --only-root" or with_root: assert status_code == 1 return else: @@ -247,6 +247,20 @@ def test_extras_are_parsed_and_populate_installer( assert tester.command.installer._extras == ["first", "second", "third"] +def test_install_ensures_project_plugins( + tester: CommandTester, mocker: MockerFixture +) -> None: + assert isinstance(tester.command, InstallerCommand) + mocker.patch.object(tester.command.installer, "run", return_value=1) + ensure_project_plugins = mocker.patch( + "poetry.plugins.plugin_manager.PluginManager.ensure_project_plugins" + ) + + tester.execute("") + + ensure_project_plugins.assert_called_once() + + def test_extras_conflicts_all_extras( tester: CommandTester, mocker: MockerFixture ) -> None: @@ -322,9 +336,9 @@ def test_invalid_groups_with_without_only( if not should_raise: tester.execute(cmd_args) - assert tester.status_code == 0 + assert tester.status_code == 1 else: - with pytest.raises(GroupNotFound, match=r"^Group\(s\) not found:") as e: + with pytest.raises(GroupNotFoundError, match=r"^Group\(s\) not found:") as e: tester.execute(cmd_args) assert tester.status_code is None for opt, groups in options.items(): @@ -345,7 +359,7 @@ def test_remove_untracked_outputs_deprecation_warning( tester.execute("--remove-untracked") - assert tester.status_code == 0 + assert tester.status_code == 1 assert ( "The `--remove-untracked` option is deprecated, use the `--sync` option" " instead.\n" in tester.io.fetch_error() @@ -440,7 +454,11 @@ def test_install_warning_corrupt_root( tester = command_tester_factory("install", poetry=poetry) tester.execute("" if with_root else "--no-root") - assert tester.status_code == 0 + if error and with_root: + assert tester.status_code == 1 + else: + assert tester.status_code == 0 + if with_root and error: assert "The current project could not be installed: " in tester.io.fetch_error() else: diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py index 38d0c746412..428009a26c9 100644 --- a/tests/console/commands/test_lock.py +++ b/tests/console/commands/test_lock.py @@ -135,7 +135,7 @@ def test_lock_check_up_to_date_legacy( assert status_code == 0 -def test_lock_no_update( +def test_lock_does_not_update_if_not_necessary( command_tester_factory: CommandTesterFactory, poetry_with_old_lockfile: Poetry, repo: TestRepository, @@ -156,7 +156,7 @@ def test_lock_no_update( ) tester = command_tester_factory("lock", poetry=poetry_with_old_lockfile) - tester.execute("--no-update") + tester.execute() locker = Locker( lock=poetry_with_old_lockfile.pyproject.file.path.parent / "poetry.lock", @@ -172,10 +172,12 @@ def test_lock_no_update( assert locked_repository.find_packages(package.to_dependency()) -def test_lock_no_update_path_dependencies( +@pytest.mark.parametrize("regenerate", [True, False]) +def test_lock_always_updates_path_dependencies( command_tester_factory: CommandTesterFactory, poetry_with_nested_path_deps_old_lockfile: Poetry, repo: TestRepository, + regenerate: bool, ) -> None: """ The lock file contains a variant of the directory dependency "quix" that does @@ -195,14 +197,14 @@ def test_lock_no_update_path_dependencies( tester = command_tester_factory( "lock", poetry=poetry_with_nested_path_deps_old_lockfile ) - tester.execute("--no-update") + tester.execute("--regenerate" if regenerate else "") packages = locker.locked_repository().packages assert {p.name for p in packages} == {"quix", "sampleproject"} -@pytest.mark.parametrize("update", [True, False]) +@pytest.mark.parametrize("regenerate", [True, False]) @pytest.mark.parametrize( "project", ["missing_directory_dependency", "missing_file_dependency"] ) @@ -211,7 +213,7 @@ def test_lock_path_dependency_does_not_exist( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, - update: bool, + regenerate: bool, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) locker = Locker( @@ -219,10 +221,10 @@ def test_lock_path_dependency_does_not_exist( pyproject_data=poetry.locker._pyproject_data, ) poetry.set_locker(locker) - options = "" if update else "--no-update" + options = "--regenerate" if regenerate else "" tester = command_tester_factory("lock", poetry=poetry) - if update or "directory" in project: + if regenerate or "directory" in project: # directory dependencies are always updated with pytest.raises(ValueError, match="does not exist"): tester.execute(options) @@ -230,7 +232,7 @@ def test_lock_path_dependency_does_not_exist( tester.execute(options) -@pytest.mark.parametrize("update", [True, False]) +@pytest.mark.parametrize("regenerate", [True, False]) @pytest.mark.parametrize( "project", ["deleted_directory_dependency", "deleted_file_dependency"] ) @@ -239,7 +241,7 @@ def test_lock_path_dependency_deleted_from_pyproject( project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, - update: bool, + regenerate: bool, ) -> None: poetry = _project_factory(project, project_factory, fixture_dir) locker = Locker( @@ -249,22 +251,19 @@ def test_lock_path_dependency_deleted_from_pyproject( poetry.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry) - if update: - tester.execute("") - else: - tester.execute("--no-update") + tester.execute("--regenerate" if regenerate else "") packages = locker.locked_repository().packages assert {p.name for p in packages} == set() -@pytest.mark.parametrize("is_no_update", [False, True]) +@pytest.mark.parametrize("regenerate", [True, False]) def test_lock_with_incompatible_lockfile( command_tester_factory: CommandTesterFactory, poetry_with_incompatible_lockfile: Poetry, repo: TestRepository, - is_no_update: bool, + regenerate: bool, ) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) @@ -276,26 +275,26 @@ def test_lock_with_incompatible_lockfile( poetry_with_incompatible_lockfile.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry_with_incompatible_lockfile) - if is_no_update: + if regenerate: + # still possible because lock file is not required + status_code = tester.execute("--regenerate") + assert status_code == 0 + else: # not possible because of incompatible lock file expected = ( "(?s)lock file is not compatible .*" " regenerate the lock file with the `poetry lock` command" ) with pytest.raises(RuntimeError, match=expected): - tester.execute("--no-update") - else: - # still possible because lock file is not required - status_code = tester.execute() - assert status_code == 0 + tester.execute() -@pytest.mark.parametrize("is_no_update", [False, True]) +@pytest.mark.parametrize("regenerate", [True, False]) def test_lock_with_invalid_lockfile( command_tester_factory: CommandTesterFactory, poetry_with_invalid_lockfile: Poetry, repo: TestRepository, - is_no_update: bool, + regenerate: bool, ) -> None: repo.add_package(get_package("sampleproject", "1.3.1")) @@ -306,11 +305,11 @@ def test_lock_with_invalid_lockfile( poetry_with_invalid_lockfile.set_locker(locker) tester = command_tester_factory("lock", poetry=poetry_with_invalid_lockfile) - if is_no_update: - # not possible because of broken lock file - with pytest.raises(RuntimeError, match="Unable to read the lock file"): - tester.execute("--no-update") - else: + if regenerate: # still possible because lock file is not required - status_code = tester.execute() + status_code = tester.execute("--regenerate") assert status_code == 0 + else: + # not possible because of broken lock file + with pytest.raises(RuntimeError, match="Unable to read the lock file"): + tester.execute() diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index 72cb5654a66..b9b4579ba7e 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -215,6 +215,10 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: return output mocker.patch("subprocess.check_output", side_effect=mock_check_output) + mocker.patch( + "poetry.utils.env.python_manager.Python._full_python_path", + return_value=Path(f"/usr/bin/python{python}"), + ) config.config["virtualenvs"]["prefer-active-python"] = prefer_active diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py index 78216936409..ca841ad7392 100644 --- a/tests/console/commands/test_publish.py +++ b/tests/console/commands/test_publish.py @@ -10,7 +10,6 @@ import requests from poetry.factory import Factory -from poetry.publishing.uploader import UploadError if TYPE_CHECKING: @@ -82,9 +81,7 @@ def request_callback(*_: Any, **__: Any) -> None: assert exit_code == 1 - expected = str(UploadError(error=requests.ConnectionError())) - - assert expected in app_tester.io.fetch_error() + assert "Error connecting to repository" in app_tester.io.fetch_error() def test_publish_with_cert( diff --git a/tests/console/test_application.py b/tests/console/test_application.py index 4629d87267f..8fbc6d7dc67 100644 --- a/tests/console/test_application.py +++ b/tests/console/test_application.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import shutil from typing import TYPE_CHECKING from typing import ClassVar @@ -12,14 +13,20 @@ from poetry.console.application import Application from poetry.console.commands.command import Command from poetry.plugins.application_plugin import ApplicationPlugin +from poetry.plugins.plugin_manager import ProjectPluginCache from poetry.repositories.cached_repository import CachedRepository from poetry.utils.authenticator import Authenticator +from poetry.utils.env import EnvManager +from poetry.utils.env import MockEnv from tests.helpers import mock_metadata_entry_points if TYPE_CHECKING: + from pathlib import Path + from pytest_mock import MockerFixture + from tests.types import FixtureDirGetter from tests.types import SetProjectContext @@ -86,6 +93,43 @@ def test_application_execute_plugin_command_with_plugins_disabled( assert tester.status_code == 1 +@pytest.mark.parametrize("with_project_plugins", [False, True]) +@pytest.mark.parametrize("no_plugins", [False, True]) +def test_application_project_plugins( + fixture_dir: FixtureDirGetter, + tmp_path: Path, + no_plugins: bool, + with_project_plugins: bool, + mocker: MockerFixture, + set_project_context: SetProjectContext, +) -> None: + env = MockEnv( + path=tmp_path / "env", version_info=(3, 8, 0), sys_path=[str(tmp_path / "env")] + ) + mocker.patch.object(EnvManager, "get_system_env", return_value=env) + + orig_dir = fixture_dir("project_plugins") + project_path = tmp_path / "project" + project_path.mkdir() + shutil.copy(orig_dir / "pyproject.toml", project_path / "pyproject.toml") + project_plugin_path = project_path / ProjectPluginCache.PATH + if with_project_plugins: + project_plugin_path.mkdir(parents=True) + + with set_project_context(project_path, in_place=True): + app = Application() + + tester = ApplicationTester(app) + tester.execute("--no-plugins" if no_plugins else "") + + assert tester.status_code == 0 + sys_path = EnvManager.get_system_env(naive=True).sys_path + if with_project_plugins and not no_plugins: + assert sys_path[0] == str(project_plugin_path) + else: + assert sys_path[0] != str(project_plugin_path) + + @pytest.mark.parametrize("disable_cache", [True, False]) def test_application_verify_source_cache_flag( disable_cache: bool, set_project_context: SetProjectContext diff --git a/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/METADATA b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/METADATA new file mode 100644 index 00000000000..2ac98bcc428 --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/METADATA @@ -0,0 +1,7 @@ +Metadata-Version: 2.1 +Name: my-application-plugin +Version: 1.0 +Summary: description +Requires-Python: >=3.8,<4.0 +Requires-Dist: poetry (>=1.8.0,<3.0.0) +Requires-Dist: some-lib (>=1.7.0,<3.0.0) diff --git a/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt new file mode 100644 index 00000000000..b0dc8872d01 --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-1.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[poetry.application.plugin] +my-command=my_application_plugin.plugins:MyApplicationPlugin diff --git a/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA new file mode 100644 index 00000000000..616f2f02d9f --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/METADATA @@ -0,0 +1,7 @@ +Metadata-Version: 2.1 +Name: my-application-plugin +Version: 2.0 +Summary: description +Requires-Python: >=3.8,<4.0 +Requires-Dist: poetry (>=1.8.0,<3.0.0) +Requires-Dist: some-lib (>=1.7.0,<3.0.0) diff --git a/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt new file mode 100644 index 00000000000..b0dc8872d01 --- /dev/null +++ b/tests/fixtures/project_plugins/my_application_plugin-2.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[poetry.application.plugin] +my-command=my_application_plugin.plugins:MyApplicationPlugin diff --git a/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA new file mode 100644 index 00000000000..20721382dd0 --- /dev/null +++ b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/METADATA @@ -0,0 +1,7 @@ +Metadata-Version: 2.1 +Name: my-other-plugin +Version: 1.0 +Summary: description +Requires-Python: >=3.8,<4.0 +Requires-Dist: poetry (>=1.8.0,<3.0.0) +Requires-Dist: some-lib (>=1.7.0,<3.0.0) diff --git a/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt new file mode 100644 index 00000000000..a37ff9af31b --- /dev/null +++ b/tests/fixtures/project_plugins/my_other_plugin-1.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[poetry.plugin] +other-plugin=my_application_plugin.plugins:MyOtherPlugin diff --git a/tests/fixtures/project_plugins/pyproject.toml b/tests/fixtures/project_plugins/pyproject.toml new file mode 100644 index 00000000000..9f4453335bf --- /dev/null +++ b/tests/fixtures/project_plugins/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false + +[tool.poetry.requires-plugins] +my-application-plugin = ">=2.0" +my-other-plugin = ">=1.0" diff --git a/tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA b/tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA new file mode 100644 index 00000000000..f577113b83e --- /dev/null +++ b/tests/fixtures/project_plugins/some_lib-1.0.dist-info/METADATA @@ -0,0 +1,5 @@ +Metadata-Version: 2.1 +Name: some-lib +Version: 1.0 +Summary: description +Requires-Python: >=3.8,<4.0 diff --git a/tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA b/tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA new file mode 100644 index 00000000000..a5e948a8bca --- /dev/null +++ b/tests/fixtures/project_plugins/some_lib-2.0.dist-info/METADATA @@ -0,0 +1,5 @@ +Metadata-Version: 2.1 +Name: some-lib +Version: 2.0 +Summary: description +Requires-Python: >=3.8,<4.0 diff --git a/tests/fixtures/self_version_not_ok/pyproject.toml b/tests/fixtures/self_version_not_ok/pyproject.toml new file mode 100644 index 00000000000..19e752a0642 --- /dev/null +++ b/tests/fixtures/self_version_not_ok/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false +requires-poetry = "<1.2" + +[tool.poetry.dependencies] +python = "^3.8" diff --git a/tests/fixtures/self_version_ok/pyproject.toml b/tests/fixtures/self_version_ok/pyproject.toml new file mode 100644 index 00000000000..9347cb92342 --- /dev/null +++ b/tests/fixtures/self_version_ok/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false +requires-poetry = ">=1.2" + +[tool.poetry.dependencies] +python = "^3.8" diff --git a/tests/fixtures/with_default_source_and_pypi/README.rst b/tests/fixtures/with_default_source_and_pypi/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_default_source_and_pypi/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_default_source_and_pypi/pyproject.toml b/tests/fixtures/with_default_source_and_pypi/pyproject.toml deleted file mode 100644 index cf7ec689a8f..00000000000 --- a/tests/fixtures/with_default_source_and_pypi/pyproject.toml +++ /dev/null @@ -1,65 +0,0 @@ -[tool.poetry] -name = "with-default-source-and-pypi" -version = "1.2.3" -description = "Some description." -authors = [ - "Sébastien Eustace " -] -license = "MIT" - -readme = "README.rst" - -homepage = "https://python-poetry.org" -repository = "https://github.com/python-poetry/poetry" -documentation = "https://python-poetry.org/docs" - -keywords = ["packaging", "dependency", "poetry"] - -classifiers = [ - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules" -] - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" -cleo = "^0.6" -pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -requests = { version = "^2.18", optional = true, extras=[ "security" ] } -pathlib2 = { version = "^2.2", python = "~3.6" } - -orator = { version = "^0.9", optional = true } - -# File dependency -demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } - -# Dir dependency with setup.py -my-package = { path = "../project_with_setup/" } - -# Dir dependency with pyproject.toml -simple-project = { path = "../simple_project/" } - - -[tool.poetry.extras] -db = [ "orator" ] - -[tool.poetry.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "my_package:main" - - -[tool.poetry.plugins."blogtool.parsers"] -".rst" = "some_module::SomeClass" - - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -priority = "default" - - -[[tool.poetry.source]] -name = "PyPI" diff --git a/tests/fixtures/with_default_source_legacy/README.rst b/tests/fixtures/with_default_source_legacy/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_default_source_legacy/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_default_source_legacy/pyproject.toml b/tests/fixtures/with_default_source_legacy/pyproject.toml deleted file mode 100644 index b30cc53bda6..00000000000 --- a/tests/fixtures/with_default_source_legacy/pyproject.toml +++ /dev/null @@ -1,61 +0,0 @@ -[tool.poetry] -name = "default-source-legacy" -version = "1.2.3" -description = "Some description." -authors = [ - "Sébastien Eustace " -] -license = "MIT" - -readme = "README.rst" - -homepage = "https://python-poetry.org" -repository = "https://github.com/python-poetry/poetry" -documentation = "https://python-poetry.org/docs" - -keywords = ["packaging", "dependency", "poetry"] - -classifiers = [ - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules" -] - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" -cleo = "^0.6" -pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -requests = { version = "^2.18", optional = true, extras=[ "security" ] } -pathlib2 = { version = "^2.2", python = "~3.6" } - -orator = { version = "^0.9", optional = true } - -# File dependency -demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } - -# Dir dependency with setup.py -my-package = { path = "../project_with_setup/" } - -# Dir dependency with pyproject.toml -simple-project = { path = "../simple_project/" } - - -[tool.poetry.extras] -db = [ "orator" ] - -[tool.poetry.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "default_source_legacy:main" - - -[tool.poetry.plugins."blogtool.parsers"] -".rst" = "some_module::SomeClass" - - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -default = true diff --git a/tests/fixtures/with_default_source_pypi/README.rst b/tests/fixtures/with_default_source_pypi/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_default_source_pypi/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_default_source_pypi/pyproject.toml b/tests/fixtures/with_default_source_pypi/pyproject.toml deleted file mode 100644 index c3fcb9380a6..00000000000 --- a/tests/fixtures/with_default_source_pypi/pyproject.toml +++ /dev/null @@ -1,60 +0,0 @@ -[tool.poetry] -name = "with-default-source-pypi" -version = "1.2.3" -description = "Some description." -authors = [ - "Sébastien Eustace " -] -license = "MIT" - -readme = "README.rst" - -homepage = "https://python-poetry.org" -repository = "https://github.com/python-poetry/poetry" -documentation = "https://python-poetry.org/docs" - -keywords = ["packaging", "dependency", "poetry"] - -classifiers = [ - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules" -] - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" -cleo = "^0.6" -pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -requests = { version = "^2.18", optional = true, extras=[ "security" ] } -pathlib2 = { version = "^2.2", python = "~3.6" } - -orator = { version = "^0.9", optional = true } - -# File dependency -demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } - -# Dir dependency with setup.py -my-package = { path = "../project_with_setup/" } - -# Dir dependency with pyproject.toml -simple-project = { path = "../simple_project/" } - - -[tool.poetry.extras] -db = [ "orator" ] - -[tool.poetry.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "my_package:main" - - -[tool.poetry.plugins."blogtool.parsers"] -".rst" = "some_module::SomeClass" - - -[[tool.poetry.source]] -name = "PyPI" -priority = "default" diff --git a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml b/tests/fixtures/with_multiple_sources/pyproject.toml similarity index 93% rename from tests/fixtures/with_non_default_multiple_sources/pyproject.toml rename to tests/fixtures/with_multiple_sources/pyproject.toml index b3063e47827..ecce7e62590 100644 --- a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml +++ b/tests/fixtures/with_multiple_sources/pyproject.toml @@ -16,7 +16,7 @@ python = "^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "secondary" +priority = "supplemental" [[tool.poetry.source]] name = "bar" diff --git a/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml b/tests/fixtures/with_multiple_sources_pypi/pyproject.toml similarity index 94% rename from tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml rename to tests/fixtures/with_multiple_sources_pypi/pyproject.toml index 9e71ff764c8..1e4da135677 100644 --- a/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml +++ b/tests/fixtures/with_multiple_sources_pypi/pyproject.toml @@ -16,7 +16,7 @@ python = "^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "secondary" +priority = "supplemental" [[tool.poetry.source]] name = "bar" diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml b/tests/fixtures/with_multiple_supplemental_sources/pyproject.toml similarity index 88% rename from tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml rename to tests/fixtures/with_multiple_supplemental_sources/pyproject.toml index 517c37cc176..79f974dc780 100644 --- a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml +++ b/tests/fixtures/with_multiple_supplemental_sources/pyproject.toml @@ -16,9 +16,9 @@ python = "^3.6" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "secondary" +priority = "supplemental" [[tool.poetry.source]] name = "bar" url = "https://bar.baz/simple/" -priority = "secondary" +priority = "supplemental" diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml b/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml deleted file mode 100644 index 366db7461a8..00000000000 --- a/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.dev.dependencies] - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -secondary = true - -[[tool.poetry.source]] -name = "bar" -url = "https://bar.baz/simple/" -secondary = true diff --git a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml b/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml deleted file mode 100644 index 61f8e9bc59d..00000000000 --- a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.dev.dependencies] - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -secondary = true - -[[tool.poetry.source]] -name = "bar" -url = "https://bar.baz/simple/" diff --git a/tests/fixtures/with_non_default_secondary_source/pyproject.toml b/tests/fixtures/with_non_default_secondary_source/pyproject.toml deleted file mode 100644 index 7a8004cac32..00000000000 --- a/tests/fixtures/with_non_default_secondary_source/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.dev.dependencies] - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -priority = "secondary" diff --git a/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml b/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml deleted file mode 100644 index 980d78eecc9..00000000000 --- a/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[tool.poetry] -name = "my-package" -version = "1.2.3" -description = "Some description." -authors = [ - "Your Name " -] -license = "MIT" - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" - -[tool.poetry.group.dev.dependencies] - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -secondary = true diff --git a/tests/fixtures/with_non_default_source_explicit/pyproject.toml b/tests/fixtures/with_primary_source_explicit/pyproject.toml similarity index 100% rename from tests/fixtures/with_non_default_source_explicit/pyproject.toml rename to tests/fixtures/with_primary_source_explicit/pyproject.toml diff --git a/tests/fixtures/with_non_default_source_implicit/pyproject.toml b/tests/fixtures/with_primary_source_implicit/pyproject.toml similarity index 100% rename from tests/fixtures/with_non_default_source_implicit/pyproject.toml rename to tests/fixtures/with_primary_source_implicit/pyproject.toml diff --git a/tests/fixtures/with_default_source/README.rst b/tests/fixtures/with_source/README.rst similarity index 100% rename from tests/fixtures/with_default_source/README.rst rename to tests/fixtures/with_source/README.rst diff --git a/tests/fixtures/with_default_source/pyproject.toml b/tests/fixtures/with_source/pyproject.toml similarity index 96% rename from tests/fixtures/with_default_source/pyproject.toml rename to tests/fixtures/with_source/pyproject.toml index bc0c4ac42b1..49fa2f62d71 100644 --- a/tests/fixtures/with_default_source/pyproject.toml +++ b/tests/fixtures/with_source/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "with-default-source" +name = "with-source" version = "1.2.3" description = "Some description." authors = [ @@ -58,4 +58,3 @@ my-script = "with_default_source:main" [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -priority = "default" diff --git a/tests/fixtures/with_two_default_sources/README.rst b/tests/fixtures/with_two_default_sources/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_two_default_sources/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_two_default_sources/pyproject.toml b/tests/fixtures/with_two_default_sources/pyproject.toml deleted file mode 100644 index 6f05f22eba1..00000000000 --- a/tests/fixtures/with_two_default_sources/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[tool.poetry] -name = "two-default-sources" -version = "1.2.3" -description = "Some description." -authors = [ - "Sébastien Eustace " -] -license = "MIT" - -readme = "README.rst" - -homepage = "https://python-poetry.org" -repository = "https://github.com/python-poetry/poetry" -documentation = "https://python-poetry.org/docs" - -keywords = ["packaging", "dependency", "poetry"] - -classifiers = [ - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules" -] - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" -cleo = "^0.6" -pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -requests = { version = "^2.18", optional = true, extras=[ "security" ] } -pathlib2 = { version = "^2.2", python = "~3.6" } - -orator = { version = "^0.9", optional = true } - -# File dependency -demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } - -# Dir dependency with setup.py -my-package = { path = "../project_with_setup/" } - -# Dir dependency with pyproject.toml -simple-project = { path = "../simple_project/" } - - -[tool.poetry.extras] -db = [ "orator" ] - -[tool.poetry.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "two_default_sources:main" - - -[tool.poetry.plugins."blogtool.parsers"] -".rst" = "some_module::SomeClass" - - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -priority = "default" - -[[tool.poetry.source]] -name = "bar" -url = "https://bar.foo/simple/" -priority = "default" diff --git a/tests/fixtures/with_two_default_sources_legacy/README.rst b/tests/fixtures/with_two_default_sources_legacy/README.rst deleted file mode 100644 index f7fe15470f9..00000000000 --- a/tests/fixtures/with_two_default_sources_legacy/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -My Package -========== diff --git a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml b/tests/fixtures/with_two_default_sources_legacy/pyproject.toml deleted file mode 100644 index 0de036eb8c8..00000000000 --- a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[tool.poetry] -name = "two-default-sources-legacy" -version = "1.2.3" -description = "Some description." -authors = [ - "Sébastien Eustace " -] -license = "MIT" - -readme = "README.rst" - -homepage = "https://python-poetry.org" -repository = "https://github.com/python-poetry/poetry" -documentation = "https://python-poetry.org/docs" - -keywords = ["packaging", "dependency", "poetry"] - -classifiers = [ - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules" -] - -# Requirements -[tool.poetry.dependencies] -python = "^3.6" -cleo = "^0.6" -pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } -requests = { version = "^2.18", optional = true, extras=[ "security" ] } -pathlib2 = { version = "^2.2", python = "~3.6" } - -orator = { version = "^0.9", optional = true } - -# File dependency -demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } - -# Dir dependency with setup.py -my-package = { path = "../project_with_setup/" } - -# Dir dependency with pyproject.toml -simple-project = { path = "../simple_project/" } - - -[tool.poetry.extras] -db = [ "orator" ] - -[tool.poetry.group.dev.dependencies] -pytest = "~3.4" - - -[tool.poetry.scripts] -my-script = "two_default_sources_legacy:main" - - -[tool.poetry.plugins."blogtool.parsers"] -".rst" = "some_module::SomeClass" - - -[[tool.poetry.source]] -name = "foo" -url = "https://foo.bar/simple/" -default = true - -[[tool.poetry.source]] -name = "bar" -url = "https://bar.foo/simple/" -default = true diff --git a/tests/helpers.py b/tests/helpers.py index 431b52c5df9..491cec88a91 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -18,7 +18,7 @@ from poetry.installation.executor import Executor from poetry.packages import Locker from poetry.repositories import Repository -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.utils._compat import metadata @@ -215,7 +215,7 @@ class TestRepository(Repository): def find_packages(self, dependency: Dependency) -> list[Package]: packages = super().find_packages(dependency) if len(packages) == 0: - raise PackageNotFound(f"Package [{dependency.name}] not found.") + raise PackageNotFoundError(f"Package [{dependency.name}] not found.") return packages diff --git a/tests/inspection/test_lazy_wheel.py b/tests/inspection/test_lazy_wheel.py index 1a71cf0b441..4c149a7a0e7 100644 --- a/tests/inspection/test_lazy_wheel.py +++ b/tests/inspection/test_lazy_wheel.py @@ -14,9 +14,9 @@ from requests import codes -from poetry.inspection.lazy_wheel import HTTPRangeRequestNotRespected -from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported -from poetry.inspection.lazy_wheel import InvalidWheel +from poetry.inspection.lazy_wheel import HTTPRangeRequestNotRespectedError +from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupportedError +from poetry.inspection.lazy_wheel import InvalidWheelError from poetry.inspection.lazy_wheel import LazyWheelUnsupportedError from poetry.inspection.lazy_wheel import metadata_from_wheel_url from tests.helpers import http_setup_redirect @@ -377,7 +377,7 @@ def test_metadata_from_wheel_url_range_requests_not_supported_one_request( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - with pytest.raises(HTTPRangeRequestUnsupported): + with pytest.raises(HTTPRangeRequestUnsupportedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() @@ -407,7 +407,7 @@ def test_metadata_from_wheel_url_range_requests_not_supported_two_requests( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - with pytest.raises(HTTPRangeRequestUnsupported): + with pytest.raises(HTTPRangeRequestUnsupportedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() @@ -431,7 +431,7 @@ def test_metadata_from_wheel_url_range_requests_supported_but_not_respected( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - with pytest.raises(HTTPRangeRequestNotRespected): + with pytest.raises(HTTPRangeRequestNotRespectedError): metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() @@ -453,7 +453,7 @@ def test_metadata_from_wheel_url_invalid_wheel( url = f"https://{domain}/demo_missing_dist_info-0.1.0-py2.py3-none-any.whl" - with pytest.raises(InvalidWheel): + with pytest.raises(InvalidWheelError): metadata_from_wheel_url("demo-missing-dist-info", url, requests.Session()) latest_requests = http.latest_requests() diff --git a/tests/installation/fixtures/with-conditional-dependency.test b/tests/installation/fixtures/with-conditional-dependency.test index 090bee4025e..0c8c9c2b5a9 100644 --- a/tests/installation/fixtures/with-conditional-dependency.test +++ b/tests/installation/fixtures/with-conditional-dependency.test @@ -6,20 +6,6 @@ optional = false python-versions = ">=3.5" files = [] -[package.requirements] -python = ">=3.5,<4.0" - -[[package]] -name = "A" -version = "1.0.1" -description = "" -optional = false -python-versions = ">=3.6" -files = [] - -[package.requirements] -python = ">=3.6,<4.0" - [metadata] python-versions = "~2.7 || ^3.4" lock-version = "2.0" diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index b54bcdaf022..00f7f179cd7 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1081,6 +1081,49 @@ def test_executor_should_append_subdirectory_for_git( assert archive_arg == tmp_venv.path / "src/demo/subdirectories/two" +def test_executor_should_install_multiple_packages_from_same_git_repository( + mocker: MockerFixture, + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + artifact_cache: ArtifactCache, + io: BufferedIO, + wheel: Path, +) -> None: + package_a = Package( + "package_a", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="package_a", + ) + package_b = Package( + "package_b", + "0.1.2", + source_type="git", + source_reference="master", + source_resolved_reference="123456", + source_url="https://github.com/demo/subdirectories.git", + source_subdirectory="package_b", + ) + + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) + chef.set_directory_wheel(wheel) + spy = mocker.spy(chef, "prepare") + + executor = Executor(tmp_venv, pool, config, io) + executor._chef = chef + executor.execute([Install(package_a), Install(package_b)]) + + archive_arg = spy.call_args_list[0][0][0] + assert archive_arg == tmp_venv.path / "src/demo/subdirectories/package_a" + + archive_arg = spy.call_args_list[1][0][0] + assert archive_arg == tmp_venv.path / "src/demo/subdirectories/package_b" + + def test_executor_should_write_pep610_url_references_for_git_with_subdirectories( tmp_venv: VirtualEnv, pool: RepositoryPool, @@ -1299,7 +1342,7 @@ def test_build_system_requires_not_available( - Installing {package_name} ({package_version} {package_url}) - SolveFailure + SolveFailureError Because -root- depends on poetry-core (0.999) which doesn't match any versions,\ version solving failed. diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 681e882b19c..8776482aa4d 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -1791,9 +1791,6 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda assert installer.executor.removals_count == 0 -@pytest.mark.skip( - "This is not working at the moment due to limitations in the resolver" -) def test_installer_test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python( installer: Installer, locker: Locker, diff --git a/tests/json/fixtures/self_invalid_plugin.toml b/tests/json/fixtures/self_invalid_plugin.toml new file mode 100644 index 00000000000..3310785cac2 --- /dev/null +++ b/tests/json/fixtures/self_invalid_plugin.toml @@ -0,0 +1,8 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.requires-plugins] +foo = 5 diff --git a/tests/json/fixtures/self_invalid_version.toml b/tests/json/fixtures/self_invalid_version.toml new file mode 100644 index 00000000000..ddc07369b03 --- /dev/null +++ b/tests/json/fixtures/self_invalid_version.toml @@ -0,0 +1,6 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] +requires-poetry = 2 diff --git a/tests/json/fixtures/self_valid.toml b/tests/json/fixtures/self_valid.toml new file mode 100644 index 00000000000..44f9c200fbb --- /dev/null +++ b/tests/json/fixtures/self_valid.toml @@ -0,0 +1,9 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = ["Your Name "] +requires-poetry = ">=2.0" + +[tool.poetry.requires-plugins] +foo = ">=1.0" diff --git a/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml b/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml deleted file mode 100644 index 4e2789b49d8..00000000000 --- a/tests/json/fixtures/source/complete_invalid_priority_legacy_and_new.toml +++ /dev/null @@ -1,18 +0,0 @@ -[tool.poetry] -name = "foobar" -version = "0.1.0" -description = "" -authors = ["Your Name "] - -[tool.poetry.dependencies] -python = "^3.10" - -[[tool.poetry.source]] -name = "pypi-simple" -url = "https://pypi.org/simple/" -default = false -priority = "primary" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/tests/json/fixtures/source/complete_valid_legacy.toml b/tests/json/fixtures/source/complete_valid_legacy.toml deleted file mode 100644 index d0b4565ffa4..00000000000 --- a/tests/json/fixtures/source/complete_valid_legacy.toml +++ /dev/null @@ -1,18 +0,0 @@ -[tool.poetry] -name = "foobar" -version = "0.1.0" -description = "" -authors = ["Your Name "] - -[tool.poetry.dependencies] -python = "^3.10" - -[[tool.poetry.source]] -name = "pypi-simple" -url = "https://pypi.org/simple/" -default = false -secondary = false - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/tests/json/test_schema.py b/tests/json/test_schema.py new file mode 100644 index 00000000000..cb039bf409b --- /dev/null +++ b/tests/json/test_schema.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json + +from pathlib import Path +from typing import Any + +from poetry.core.json import SCHEMA_DIR as CORE_SCHEMA_DIR + +from poetry.factory import Factory +from poetry.json import SCHEMA_DIR +from poetry.toml import TOMLFile + + +FIXTURE_DIR = Path(__file__).parent / "fixtures" +SOURCE_FIXTURE_DIR = FIXTURE_DIR / "source" + + +def test_pyproject_toml_valid() -> None: + toml: dict[str, Any] = TOMLFile(SOURCE_FIXTURE_DIR / "complete_valid.toml").read() + assert Factory.validate(toml) == {"errors": [], "warnings": []} + + +def test_pyproject_toml_invalid_priority() -> None: + toml: dict[str, Any] = TOMLFile( + SOURCE_FIXTURE_DIR / "complete_invalid_priority.toml" + ).read() + assert Factory.validate(toml) == { + "errors": [ + "data.source[0].priority must be one of ['primary'," + " 'supplemental', 'explicit']" + ], + "warnings": [], + } + + +def test_self_valid() -> None: + toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_valid.toml").read() + assert Factory.validate(toml) == {"errors": [], "warnings": []} + + +def test_self_invalid_version() -> None: + toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_invalid_version.toml").read() + assert Factory.validate(toml) == { + "errors": ["data.requires-poetry must be string"], + "warnings": [], + } + + +def test_self_invalid_plugin() -> None: + toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "self_invalid_plugin.toml").read() + assert Factory.validate(toml) == { + "errors": [ + "data.requires-plugins.foo must be valid exactly by one definition" + " (0 matches found)" + ], + "warnings": [], + } + + +def test_dependencies_is_consistent_to_poetry_core_schema() -> None: + with (SCHEMA_DIR / "poetry.json").open(encoding="utf-8") as f: + schema = json.load(f) + dependency_definitions = { + key: value for key, value in schema["definitions"].items() if "depend" in key + } + with (CORE_SCHEMA_DIR / "poetry-schema.json").open(encoding="utf-8") as f: + core_schema = json.load(f) + core_dependency_definitions = { + key: value + for key, value in core_schema["definitions"].items() + if "depend" in key + } + assert dependency_definitions == core_dependency_definitions diff --git a/tests/json/test_schema_sources.py b/tests/json/test_schema_sources.py index 9e5d6fca50b..e69de29bb2d 100644 --- a/tests/json/test_schema_sources.py +++ b/tests/json/test_schema_sources.py @@ -1,43 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -from poetry.factory import Factory -from poetry.toml import TOMLFile - - -FIXTURE_DIR = Path(__file__).parent / "fixtures" / "source" - - -def test_pyproject_toml_valid_legacy() -> None: - toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "complete_valid_legacy.toml").read() - assert Factory.validate(toml) == {"errors": [], "warnings": []} - - -def test_pyproject_toml_valid() -> None: - toml: dict[str, Any] = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read() - assert Factory.validate(toml) == {"errors": [], "warnings": []} - - -def test_pyproject_toml_invalid_priority() -> None: - toml: dict[str, Any] = TOMLFile( - FIXTURE_DIR / "complete_invalid_priority.toml" - ).read() - assert Factory.validate(toml) == { - "errors": [ - "data.source[0].priority must be one of ['primary', 'default', " - "'secondary', 'supplemental', 'explicit']" - ], - "warnings": [], - } - - -def test_pyproject_toml_invalid_priority_legacy_and_new() -> None: - toml: dict[str, Any] = TOMLFile( - FIXTURE_DIR / "complete_invalid_priority_legacy_and_new.toml" - ).read() - assert Factory.validate(toml) == { - "errors": ["data.source[0] must NOT match a disallowed definition"], - "warnings": [], - } diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index 18551c5af6e..82c63426b90 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -272,7 +272,7 @@ def test_builder_setup_generation_runs_with_pip_editable( poetry = Factory().create_poetry(extended_project) # we need a venv with pip and setuptools since we are verifying setup.py builds - with ephemeral_environment(flags={"no-setuptools": False, "no-pip": False}) as venv: + with ephemeral_environment(flags={"no-pip": False}) as venv: builder = EditableBuilder(poetry, venv, NullIO()) builder.build() diff --git a/tests/mixology/helpers.py b/tests/mixology/helpers.py index c2c28e19853..c8dde3e5ae8 100644 --- a/tests/mixology/helpers.py +++ b/tests/mixology/helpers.py @@ -5,7 +5,7 @@ from poetry.core.packages.package import Package from poetry.factory import Factory -from poetry.mixology.failure import SolveFailure +from poetry.mixology.failure import SolveFailureError from poetry.mixology.version_solver import VersionSolver @@ -52,7 +52,7 @@ def check_solver_result( with provider.use_latest_for(use_latest or []): try: solution = solver.solve() - except SolveFailure as e: + except SolveFailureError as e: if error: assert str(e) == error diff --git a/tests/mixology/test_incompatibility.py b/tests/mixology/test_incompatibility.py index d3395f13796..f125abb6347 100644 --- a/tests/mixology/test_incompatibility.py +++ b/tests/mixology/test_incompatibility.py @@ -6,7 +6,7 @@ from poetry.core.packages.url_dependency import URLDependency from poetry.mixology.incompatibility import Incompatibility -from poetry.mixology.incompatibility_cause import DependencyCause +from poetry.mixology.incompatibility_cause import DependencyCauseError from poetry.mixology.term import Term @@ -45,6 +45,6 @@ def test_str_dependency_cause( dependency1: Dependency, dependency2: Dependency, expected: str ) -> None: incompatibility = Incompatibility( - [Term(dependency1, True), Term(dependency2, False)], DependencyCause() + [Term(dependency1, True), Term(dependency2, False)], DependencyCauseError() ) assert str(incompatibility) == expected diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py index 44e0d68343d..5d254b1ee6c 100644 --- a/tests/plugins/test_plugin_manager.py +++ b/tests/plugins/test_plugin_manager.py @@ -1,5 +1,7 @@ from __future__ import annotations +import shutil + from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar @@ -8,14 +10,28 @@ import pytest from cleo.io.buffered_io import BufferedIO +from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.file_dependency import FileDependency +from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage +from poetry.factory import Factory +from poetry.installation.wheel_installer import WheelInstaller from poetry.packages.locker import Locker from poetry.plugins import ApplicationPlugin from poetry.plugins import Plugin from poetry.plugins.plugin_manager import PluginManager +from poetry.plugins.plugin_manager import ProjectPluginCache from poetry.poetry import Poetry +from poetry.puzzle.exceptions import SolverProblemError +from poetry.repositories import Repository +from poetry.repositories import RepositoryPool +from poetry.repositories.installed_repository import InstalledRepository +from poetry.utils.env import Env +from poetry.utils.env import EnvManager +from poetry.utils.env import MockEnv from tests.helpers import mock_metadata_entry_points @@ -48,6 +64,33 @@ def activate(self, poetry: Poetry, io: IO) -> None: poetry.package.version = Version.parse("9.9.9") +@pytest.fixture +def repo() -> Repository: + repo = Repository("repo") + repo.add_package(Package("my-other-plugin", "1.0")) + for version in ("1.0", "2.0"): + package = Package("my-application-plugin", version) + package.add_dependency(Dependency("some-lib", version)) + repo.add_package(package) + repo.add_package(Package("some-lib", version)) + return repo + + +@pytest.fixture +def pool(repo: Repository) -> RepositoryPool: + pool = RepositoryPool() + pool.add_repository(repo) + + return pool + + +@pytest.fixture +def system_env(tmp_path: Path, mocker: MockerFixture) -> Env: + env = MockEnv(path=tmp_path, sys_path=[str(tmp_path / "purelib")]) + mocker.patch.object(EnvManager, "get_system_env", return_value=env) + return env + + @pytest.fixture def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry: project_path = fixture_dir("simple_project") @@ -62,8 +105,21 @@ def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry: return poetry +@pytest.fixture +def poetry_with_plugins( + fixture_dir: FixtureDirGetter, pool: RepositoryPool, tmp_path: Path +) -> Poetry: + orig_path = fixture_dir("project_plugins") + project_path = tmp_path / "project" + project_path.mkdir() + shutil.copy(orig_path / "pyproject.toml", project_path / "pyproject.toml") + poetry = Factory().create_poetry(project_path) + poetry.set_pool(pool) + return poetry + + @pytest.fixture() -def io() -> IO: +def io() -> BufferedIO: return BufferedIO() @@ -75,9 +131,14 @@ def _manager(group: str = Plugin.group) -> PluginManager: return _manager -@pytest.fixture() -def no_plugin_manager(poetry: Poetry, io: BufferedIO) -> PluginManager: - return PluginManager(Plugin.group, disable_plugins=True) +@pytest.fixture +def with_my_plugin(mocker: MockerFixture) -> None: + mock_metadata_entry_points(mocker, MyPlugin) + + +@pytest.fixture +def with_invalid_plugin(mocker: MockerFixture) -> None: + mock_metadata_entry_points(mocker, InvalidPlugin) def test_load_plugins_and_activate( @@ -94,16 +155,6 @@ def test_load_plugins_and_activate( assert io.fetch_output() == "Setting readmes\n" -@pytest.fixture -def with_my_plugin(mocker: MockerFixture) -> None: - mock_metadata_entry_points(mocker, MyPlugin) - - -@pytest.fixture -def with_invalid_plugin(mocker: MockerFixture) -> None: - mock_metadata_entry_points(mocker, InvalidPlugin) - - def test_load_plugins_with_invalid_plugin( manager_factory: ManagerFactory, poetry: Poetry, @@ -116,13 +167,408 @@ def test_load_plugins_with_invalid_plugin( manager.load_plugins() -def test_load_plugins_with_plugins_disabled( - no_plugin_manager: PluginManager, - poetry: Poetry, +def test_add_project_plugin_path( + poetry_with_plugins: Poetry, io: BufferedIO, - with_my_plugin: None, + system_env: Env, + fixture_dir: FixtureDirGetter, ) -> None: - no_plugin_manager.load_plugins() + dist_info_1 = "my_application_plugin-1.0.dist-info" + dist_info_2 = "my_application_plugin-2.0.dist-info" + cache = ProjectPluginCache(poetry_with_plugins, io) + shutil.copytree( + fixture_dir("project_plugins") / dist_info_1, cache._path / dist_info_1 + ) + shutil.copytree( + fixture_dir("project_plugins") / dist_info_2, system_env.purelib / dist_info_2 + ) - assert poetry.package.version.text == "1.2.3" + assert { + f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages + } == {"my-application-plugin 2.0"} + + PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent) + + assert { + f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages + } == {"my-application-plugin 1.0"} + + +def test_ensure_plugins_no_plugins_no_output(poetry: Poetry, io: BufferedIO) -> None: + PluginManager.ensure_project_plugins(poetry, io) + + assert not (poetry.pyproject_path.parent / ProjectPluginCache.PATH).exists() assert io.fetch_output() == "" + assert io.fetch_error() == "" + + +def test_ensure_plugins_no_plugins_existing_cache_is_removed( + poetry: Poetry, io: BufferedIO +) -> None: + plugin_path = poetry.pyproject_path.parent / ProjectPluginCache.PATH + plugin_path.mkdir(parents=True) + + PluginManager.ensure_project_plugins(poetry, io) + + assert not plugin_path.exists() + assert io.fetch_output() == ( + "No project plugins defined. Removing the project's plugin cache\n\n" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("debug_out", [False, True]) +def test_ensure_plugins_no_output_if_fresh( + poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool +) -> None: + io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) + cache = ProjectPluginCache(poetry_with_plugins, io) + cache._write_config() + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "The project's plugin cache is up to date.\n\n" if debug_out else "" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("debug_out", [False, True]) +def test_ensure_plugins_ignore_irrelevant_markers( + poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool +) -> None: + io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) + poetry_with_plugins.local_config["requires-plugins"] = { + "irrelevant": {"version": "1.0", "markers": "python_version < '3'"} + } + cache = ProjectPluginCache(poetry_with_plugins, io) + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "No relevant project plugins for Poetry's environment defined.\n\n" + if debug_out + else "" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_remove_outdated( + poetry_with_plugins: Poetry, io: BufferedIO, fixture_dir: FixtureDirGetter +) -> None: + # Test with irrelevant plugins because this is the first return + # where it is relevant that an existing cache is removed. + poetry_with_plugins.local_config["requires-plugins"] = { + "irrelevant": {"version": "1.0", "markers": "python_version < '3'"} + } + fixture_path = fixture_dir("project_plugins") + cache = ProjectPluginCache(poetry_with_plugins, io) + cache._path.mkdir(parents=True) + dist_info = "my_application_plugin-1.0.dist-info" + shutil.copytree(fixture_path / dist_info, cache._path / dist_info) + cache._config_file.touch() + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert not (cache._path / dist_info).exists() + assert io.fetch_output() == ( + "Removing the project's plugin cache because it is outdated\n" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_ignore_already_installed_in_system_env( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, +) -> None: + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-2.0.dist-info", + "my_other_plugin-1.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + + cache.ensure_plugins() + + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "All required plugins have already been installed in Poetry's environment.\n\n" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_install_missing_plugins( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [ + Dependency("my-application-plugin", ">=2.0"), + Dependency("my-other-plugin", ">=1.0"), + ], + system_env, + [], + ) + execute_mock.assert_called_once() + assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ + "", + "", + "", + ] + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-application-plugin (>=2.0)\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + "Updating dependencies\n" + "Resolving dependencies...\n\n" + "Writing lock file\n\n" + ) + assert io.fetch_error() == "" + + +def test_ensure_plugins_install_only_missing_plugins( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-2.0.dist-info", + "some_lib-2.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [Dependency("my-other-plugin", ">=1.0")], + system_env, + [Package("my-application-plugin", "2.0"), Package("some-lib", "2.0")], + ) + execute_mock.assert_called_once() + assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ + "" + ] + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + "Updating dependencies\n" + "Resolving dependencies...\n\n" + "Writing lock file\n\n" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("debug_out", [False, True]) +def test_ensure_plugins_install_overwrite_wrong_version_plugins( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, + debug_out: bool, +) -> None: + io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL) + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-1.0.dist-info", + "some_lib-2.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [ + Dependency("my-application-plugin", ">=2.0"), + Dependency("my-other-plugin", ">=1.0"), + ], + system_env, + [Package("some-lib", "2.0")], + ) + execute_mock.assert_called_once() + assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [ + "", + "", + ] + assert cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + start = ( + "Ensuring that the Poetry plugins required by the project are available...\n" + ) + opt = ( + "The following Poetry plugins are required by the project" + " but are not satisfied by the installed versions:\n" + " - my-application-plugin (>=2.0)\n" + " installed: my-application-plugin (1.0)\n" + ) + end = ( + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-application-plugin (>=2.0)\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + ) + expected = (start + opt + end) if debug_out else (start + end) + assert io.fetch_output().startswith(expected) + assert io.fetch_error() == "" + + +def test_ensure_plugins_pins_other_installed_packages( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + mocker: MockerFixture, +) -> None: + fixture_path = fixture_dir("project_plugins") + for dist_info in ( + "my_application_plugin-1.0.dist-info", + "some_lib-1.0.dist-info", + ): + shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info) + cache = ProjectPluginCache(poetry_with_plugins, io) + install_spy = mocker.spy(cache, "_install") + execute_mock = mocker.patch( + "poetry.plugins.plugin_manager.Installer._execute", return_value=0 + ) + + with pytest.raises(SolverProblemError): + cache.ensure_plugins() + + install_spy.assert_called_once_with( + [ + Dependency("my-application-plugin", ">=2.0"), + Dependency("my-other-plugin", ">=1.0"), + ], + system_env, + # pinned because it might be a dependency of another plugin or Poetry itself + [Package("some-lib", "1.0")], + ) + execute_mock.assert_not_called() + assert not cache._config_file.exists() + assert ( + cache._gitignore_file.exists() + and cache._gitignore_file.read_text(encoding="utf-8") == "*" + ) + assert io.fetch_output() == ( + "Ensuring that the Poetry plugins required by the project are available...\n" + "The following Poetry plugins are required by the project" + " but are not installed in Poetry's environment:\n" + " - my-application-plugin (>=2.0)\n" + " - my-other-plugin (>=1.0)\n" + "Installing Poetry plugins only for the current project...\n" + "Updating dependencies\n" + "Resolving dependencies...\n" + ) + assert io.fetch_error() == "" + + +@pytest.mark.parametrize("other_version", [False, True]) +def test_project_plugins_are_installed_in_project_folder( + poetry_with_plugins: Poetry, + io: BufferedIO, + system_env: Env, + fixture_dir: FixtureDirGetter, + tmp_path: Path, + other_version: bool, +) -> None: + orig_purelib = system_env.purelib + orig_platlib = system_env.platlib + + # make sure that the path dependency is on the same drive (for Windows tests in CI) + orig_wheel_path = ( + fixture_dir("wheel_with_no_requires_dist") / "demo-0.1.0-py2.py3-none-any.whl" + ) + wheel_path = tmp_path / orig_wheel_path.name + shutil.copy(orig_wheel_path, wheel_path) + + if other_version: + WheelInstaller(system_env).install(wheel_path) + dist_info = orig_purelib / "demo-0.1.0.dist-info" + metadata = dist_info / "METADATA" + metadata.write_text( + metadata.read_text(encoding="utf-8").replace("0.1.0", "0.1.2"), + encoding="utf-8", + ) + dist_info.rename(orig_purelib / "demo-0.1.2.dist-info") + + cache = ProjectPluginCache(poetry_with_plugins, io) + + # just use a file dependency so that we do not have to set up a repository + cache._install([FileDependency("demo", wheel_path)], system_env, []) + + project_site_packages = [p.name for p in cache._path.iterdir()] + assert "demo" in project_site_packages + assert "demo-0.1.0.dist-info" in project_site_packages + + orig_site_packages = [p.name for p in orig_purelib.iterdir()] + if other_version: + assert "demo" in orig_site_packages + assert "demo-0.1.2.dist-info" in orig_site_packages + assert "demo-0.1.0.dist-info" not in orig_site_packages + else: + assert not any(p.startswith("demo") for p in orig_site_packages) + if orig_platlib != orig_purelib: + assert not any(p.name.startswith("demo") for p in orig_platlib.iterdir()) diff --git a/tests/publishing/test_publisher.py b/tests/publishing/test_publisher.py index a68885e9a91..db1c7b4d62f 100644 --- a/tests/publishing/test_publisher.py +++ b/tests/publishing/test_publisher.py @@ -43,7 +43,7 @@ def test_publish_publishes_to_pypi_by_default( ] -@pytest.mark.parametrize("fixture_name", ["sample_project", "with_default_source"]) +@pytest.mark.parametrize("fixture_name", ["sample_project", "with_source"]) def test_publish_can_publish_to_given_repository( fixture_dir: FixtureDirGetter, mocker: MockerFixture, diff --git a/tests/puzzle/test_provider.py b/tests/puzzle/test_provider.py index d79ecdba92d..e4fa5c855a0 100644 --- a/tests/puzzle/test_provider.py +++ b/tests/puzzle/test_provider.py @@ -24,7 +24,7 @@ from poetry.packages import DependencyPackage from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.puzzle.provider import Provider -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.repository import Repository from poetry.repositories.repository_pool import Priority from poetry.repositories.repository_pool import RepositoryPool @@ -852,7 +852,7 @@ def test_complete_package_raises_packagenotfound_if_locked_source_not_available( locked = provider.get_locked(dependency) assert locked is not None - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): provider.complete_package(locked) diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 5c2fc8496ce..5c49557962d 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -663,6 +663,63 @@ def test_solver_returns_extras_when_multiple_extras_use_same_dependency( assert ops[0].package.marker.is_any() +def test_solver_locks_all_extras_when_multiple_extras_require_same_dependency( + solver: Solver, + repo: Repository, + package: ProjectPackage, +) -> None: + """ + - root depends on A[extra-b1] and C + - C depends on A[extra-b2] + - B is required by both extras + -> the locked dependency A on B must have both extra markers + """ + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + + dep_b1 = get_dependency("B", "*", optional=True) + dep_b1.marker = parse_marker("extra == 'extra-b1'") + + dep_b2 = get_dependency("B", "*", optional=True) + dep_b2.marker = parse_marker("extra == 'extra-b2'") + + package_a.extras = { + canonicalize_name("extra-b1"): [dep_b1], + canonicalize_name("extra-b2"): [dep_b2], + } + package_a.add_dependency(dep_b1) + package_a.add_dependency(dep_b2) + + package.add_dependency( + get_dependency("A", {"version": "*", "extras": ["extra-b1"]}) + ) + package.add_dependency(get_dependency("C", "*")) + package_c.add_dependency( + get_dependency("A", {"version": "*", "extras": ["extra-b2"]}) + ) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + transaction = solver.solve() + + expected = [ + {"job": "install", "package": package_b}, + {"job": "install", "package": package_a}, + {"job": "install", "package": package_c}, + ] + + ops = check_solver_result(transaction, expected) + locked_a_requires = ops[1].package.requires + assert len(locked_a_requires) == 2 + assert {str(r.marker) for r in locked_a_requires} == { + 'extra == "extra-b1"', + 'extra == "extra-b2"', + } + + @pytest.mark.parametrize("enabled_extra", ["one", "two", None]) def test_solver_returns_extras_only_requested_nested( solver: Solver, @@ -3179,7 +3236,7 @@ def test_solver_chooses_from_correct_repository_if_forced_and_transitive_depende assert ops[1].package.source_url is None -def test_solver_does_not_choose_from_secondary_repository_by_default( +def test_solver_does_not_choose_from_supplemental_repository_by_default( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, @@ -3189,7 +3246,7 @@ def test_solver_does_not_choose_from_secondary_repository_by_default( package.add_dependency(Factory.create_dependency("clikit", {"version": "^0.2.0"})) pool = RepositoryPool() - pool.add_repository(pypi_repository, priority=Priority.SECONDARY) + pool.add_repository(pypi_repository, priority=Priority.SUPPLEMENTAL) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) @@ -3229,7 +3286,7 @@ def test_solver_does_not_choose_from_secondary_repository_by_default( assert ops[2].package.source_url == legacy_repository.url -def test_solver_chooses_from_secondary_if_explicit( +def test_solver_chooses_from_supplemental_if_explicit( package: ProjectPackage, io: NullIO, legacy_repository: LegacyRepository, @@ -3241,7 +3298,7 @@ def test_solver_chooses_from_secondary_if_explicit( ) pool = RepositoryPool() - pool.add_repository(pypi_repository, priority=Priority.SECONDARY) + pool.add_repository(pypi_repository, priority=Priority.SUPPLEMENTAL) pool.add_repository(legacy_repository) solver = Solver(package, pool, [], [], io) @@ -3811,20 +3868,6 @@ def test_solver_synchronize_single( ) -@pytest.mark.skip(reason="Poetry no longer has critical package requirements") -def test_solver_with_synchronization_keeps_critical_package( - package: ProjectPackage, - pool: RepositoryPool, - io: NullIO, -) -> None: - package_pip = get_package("setuptools", "1.0") - - solver = Solver(package, pool, [package_pip], [], io) - transaction = solver.solve() - - check_solver_result(transaction, []) - - def test_solver_cannot_choose_another_version_for_directory_dependencies( solver: Solver, repo: Repository, diff --git a/tests/pyproject/test_pyproject_toml_file.py b/tests/pyproject/test_pyproject_toml_file.py index e8fea9000ec..f8cb40f2845 100644 --- a/tests/pyproject/test_pyproject_toml_file.py +++ b/tests/pyproject/test_pyproject_toml_file.py @@ -4,7 +4,7 @@ import pytest -from poetry.core.exceptions import PoetryCoreException +from poetry.core.exceptions import PoetryCoreError from poetry.toml import TOMLFile @@ -17,7 +17,7 @@ def test_pyproject_toml_file_invalid(pyproject_toml: Path) -> None: with pyproject_toml.open(mode="a", encoding="utf-8") as f: f.write("<<<<<<<<<<<") - with pytest.raises(PoetryCoreException) as excval: + with pytest.raises(PoetryCoreError) as excval: _ = TOMLFile(pyproject_toml).read() assert f"Invalid TOML file {pyproject_toml.as_posix()}" in str(excval.value) diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA new file mode 100644 index 00000000000..93113ecebfd --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir-2.3.4.dist-info/METADATA @@ -0,0 +1,22 @@ +Metadata-Version: 2.1 +Name: editable-src-dir +Version: 2.3.4 +Summary: Editable description. +License: MIT +Keywords: cli,commands +Author: Foo Bar +Author-email: foo@bar.com +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Description-Content-Type: text/x-rst + +Editable +#### diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth new file mode 100644 index 00000000000..c40b340604b --- /dev/null +++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-src-dir.pth @@ -0,0 +1 @@ +/path/to/editable/src diff --git a/tests/repositories/fixtures/pypi.org/json/attrs.json b/tests/repositories/fixtures/pypi.org/json/attrs.json index 3b0db9b129e..b6ca989bcf9 100644 --- a/tests/repositories/fixtures/pypi.org/json/attrs.json +++ b/tests/repositories/fixtures/pypi.org/json/attrs.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "attrs", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/black.json b/tests/repositories/fixtures/pypi.org/json/black.json index dd8177597f4..9b7eaf3205b 100644 --- a/tests/repositories/fixtures/pypi.org/json/black.json +++ b/tests/repositories/fixtures/pypi.org/json/black.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -67,7 +68,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "black", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/cleo.json b/tests/repositories/fixtures/pypi.org/json/cleo.json index 69ae3afc0d8..ea54aa71951 100644 --- a/tests/repositories/fixtures/pypi.org/json/cleo.json +++ b/tests/repositories/fixtures/pypi.org/json/cleo.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "cleo", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/clikit.json b/tests/repositories/fixtures/pypi.org/json/clikit.json index bd12deb0357..90754da338c 100644 --- a/tests/repositories/fixtures/pypi.org/json/clikit.json +++ b/tests/repositories/fixtures/pypi.org/json/clikit.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "clikit", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/colorama.json b/tests/repositories/fixtures/pypi.org/json/colorama.json index c58033e3674..87016aa6644 100644 --- a/tests/repositories/fixtures/pypi.org/json/colorama.json +++ b/tests/repositories/fixtures/pypi.org/json/colorama.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "colorama", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/discord-py.json b/tests/repositories/fixtures/pypi.org/json/discord-py.json index 1f5ab7ebab1..c51cb10a343 100644 --- a/tests/repositories/fixtures/pypi.org/json/discord-py.json +++ b/tests/repositories/fixtures/pypi.org/json/discord-py.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "discord-py", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/filecache.json b/tests/repositories/fixtures/pypi.org/json/filecache.json index a031d57165f..8c52b03c348 100644 --- a/tests/repositories/fixtures/pypi.org/json/filecache.json +++ b/tests/repositories/fixtures/pypi.org/json/filecache.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "filecache", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/funcsigs.json b/tests/repositories/fixtures/pypi.org/json/funcsigs.json index df218b6264d..9bd98b65abd 100644 --- a/tests/repositories/fixtures/pypi.org/json/funcsigs.json +++ b/tests/repositories/fixtures/pypi.org/json/funcsigs.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "funcsigs", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/futures.json b/tests/repositories/fixtures/pypi.org/json/futures.json index 6e889437873..d51da2b7b86 100644 --- a/tests/repositories/fixtures/pypi.org/json/futures.json +++ b/tests/repositories/fixtures/pypi.org/json/futures.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "futures", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/hbmqtt.json b/tests/repositories/fixtures/pypi.org/json/hbmqtt.json index 8efff2776ac..8f2b2ea5d04 100644 --- a/tests/repositories/fixtures/pypi.org/json/hbmqtt.json +++ b/tests/repositories/fixtures/pypi.org/json/hbmqtt.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -43,7 +44,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "hbmqtt", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json index 5ac034226d3..7b7b8326db2 100644 --- a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json +++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "importlib-metadata", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/ipython.json b/tests/repositories/fixtures/pypi.org/json/ipython.json index d4a0c25b84e..3923464b660 100644 --- a/tests/repositories/fixtures/pypi.org/json/ipython.json +++ b/tests/repositories/fixtures/pypi.org/json/ipython.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -128,7 +129,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "ipython", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/isort.json b/tests/repositories/fixtures/pypi.org/json/isort.json index d2c0cf86c28..97350f08598 100644 --- a/tests/repositories/fixtures/pypi.org/json/isort.json +++ b/tests/repositories/fixtures/pypi.org/json/isort.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -53,7 +54,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "isort", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/jupyter.json b/tests/repositories/fixtures/pypi.org/json/jupyter.json index 7bd87597f92..bc53690ec52 100644 --- a/tests/repositories/fixtures/pypi.org/json/jupyter.json +++ b/tests/repositories/fixtures/pypi.org/json/jupyter.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -49,7 +50,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "jupyter", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/more-itertools.json b/tests/repositories/fixtures/pypi.org/json/more-itertools.json index 9c3f884f82f..3495aa2675e 100644 --- a/tests/repositories/fixtures/pypi.org/json/more-itertools.json +++ b/tests/repositories/fixtures/pypi.org/json/more-itertools.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -53,7 +54,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "more-itertools", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pastel.json b/tests/repositories/fixtures/pypi.org/json/pastel.json index b5ef49183fe..53d3fbe4dcd 100644 --- a/tests/repositories/fixtures/pypi.org/json/pastel.json +++ b/tests/repositories/fixtures/pypi.org/json/pastel.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pastel", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pluggy.json b/tests/repositories/fixtures/pypi.org/json/pluggy.json index 112bbdd40e4..7fa6328f8aa 100644 --- a/tests/repositories/fixtures/pypi.org/json/pluggy.json +++ b/tests/repositories/fixtures/pypi.org/json/pluggy.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -53,7 +54,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pluggy", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core.json b/tests/repositories/fixtures/pypi.org/json/poetry-core.json index 8ae5d6a07aa..a2ba789ca6e 100644 --- a/tests/repositories/fixtures/pypi.org/json/poetry-core.json +++ b/tests/repositories/fixtures/pypi.org/json/poetry-core.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "poetry-core", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/py.json b/tests/repositories/fixtures/pypi.org/json/py.json index 4d0c746b260..a8505d8419a 100644 --- a/tests/repositories/fixtures/pypi.org/json/py.json +++ b/tests/repositories/fixtures/pypi.org/json/py.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "py", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pylev.json b/tests/repositories/fixtures/pypi.org/json/pylev.json index 80756bda9a1..938ebdbaf76 100644 --- a/tests/repositories/fixtures/pypi.org/json/pylev.json +++ b/tests/repositories/fixtures/pypi.org/json/pylev.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -33,7 +34,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pylev", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pytest.json b/tests/repositories/fixtures/pypi.org/json/pytest.json index 40cc11d5521..efd8fcf7df1 100644 --- a/tests/repositories/fixtures/pypi.org/json/pytest.json +++ b/tests/repositories/fixtures/pypi.org/json/pytest.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -67,7 +68,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pytest", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/python-language-server.json b/tests/repositories/fixtures/pypi.org/json/python-language-server.json index a8e3b08a408..90e914942d7 100644 --- a/tests/repositories/fixtures/pypi.org/json/python-language-server.json +++ b/tests/repositories/fixtures/pypi.org/json/python-language-server.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -17,7 +18,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "python-language-server", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/pyyaml.json b/tests/repositories/fixtures/pypi.org/json/pyyaml.json index cc61c6aec04..565a6a2be2c 100644 --- a/tests/repositories/fixtures/pypi.org/json/pyyaml.json +++ b/tests/repositories/fixtures/pypi.org/json/pyyaml.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -186,7 +187,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "pyyaml", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/requests.json b/tests/repositories/fixtures/pypi.org/json/requests.json index ab38c2b3721..593fda91f15 100644 --- a/tests/repositories/fixtures/pypi.org/json/requests.json +++ b/tests/repositories/fixtures/pypi.org/json/requests.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -185,7 +186,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "requests", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/setuptools.json b/tests/repositories/fixtures/pypi.org/json/setuptools.json index 0600461c7c1..7c53cc48f06 100644 --- a/tests/repositories/fixtures/pypi.org/json/setuptools.json +++ b/tests/repositories/fixtures/pypi.org/json/setuptools.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -65,7 +66,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "setuptools", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/six.json b/tests/repositories/fixtures/pypi.org/json/six.json index 2a0a55d8731..976db9ec16e 100644 --- a/tests/repositories/fixtures/pypi.org/json/six.json +++ b/tests/repositories/fixtures/pypi.org/json/six.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "six", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json b/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json index 23a407178d9..2bbabf10809 100644 --- a/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json +++ b/tests/repositories/fixtures/pypi.org/json/sqlalchemy.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -17,7 +18,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "sqlalchemy", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/toga.json b/tests/repositories/fixtures/pypi.org/json/toga.json index 04a26e3954c..d7561dcb2b5 100644 --- a/tests/repositories/fixtures/pypi.org/json/toga.json +++ b/tests/repositories/fixtures/pypi.org/json/toga.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -123,7 +124,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "toga", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/tomlkit.json b/tests/repositories/fixtures/pypi.org/json/tomlkit.json index d3ba9b77059..9cfa90929c2 100644 --- a/tests/repositories/fixtures/pypi.org/json/tomlkit.json +++ b/tests/repositories/fixtures/pypi.org/json/tomlkit.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -67,7 +68,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "tomlkit", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/twisted.json b/tests/repositories/fixtures/pypi.org/json/twisted.json index 66825247bab..efa14ce6d53 100644 --- a/tests/repositories/fixtures/pypi.org/json/twisted.json +++ b/tests/repositories/fixtures/pypi.org/json/twisted.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": false, @@ -17,7 +18,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "twisted", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/wheel.json b/tests/repositories/fixtures/pypi.org/json/wheel.json index 5138e55f85e..ddd882c8463 100644 --- a/tests/repositories/fixtures/pypi.org/json/wheel.json +++ b/tests/repositories/fixtures/pypi.org/json/wheel.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "wheel", "versions": [ diff --git a/tests/repositories/fixtures/pypi.org/json/zipp.json b/tests/repositories/fixtures/pypi.org/json/zipp.json index 04b282766a3..d34d5523ac4 100644 --- a/tests/repositories/fixtures/pypi.org/json/zipp.json +++ b/tests/repositories/fixtures/pypi.org/json/zipp.json @@ -1,4 +1,5 @@ { + "alternate-locations": [], "files": [ { "core-metadata": { @@ -35,7 +36,7 @@ ], "meta": { "_last-serial": 0, - "api-version": "1.1" + "api-version": "1.2" }, "name": "zipp", "versions": [ diff --git a/tests/repositories/test_http_repository.py b/tests/repositories/test_http_repository.py index 64a5ba6b176..fa1c9cc417c 100644 --- a/tests/repositories/test_http_repository.py +++ b/tests/repositories/test_http_repository.py @@ -14,9 +14,9 @@ from poetry.core.packages.utils.link import Link from poetry.inspection.info import PackageInfoError -from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported +from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupportedError from poetry.repositories.http_repository import HTTPRepository -from poetry.utils.helpers import HTTPRangeRequestSupported +from poetry.utils.helpers import HTTPRangeRequestSupportedError if TYPE_CHECKING: @@ -121,7 +121,7 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: repo = MockRepository() # 1. range request and download - mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupported + mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupportedError with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) @@ -140,7 +140,7 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: # 3. download and range request mock_metadata_from_wheel_url.side_effect = None - mock_download.side_effect = HTTPRangeRequestSupported + mock_download.side_effect = HTTPRangeRequestSupportedError with contextlib.suppress(PackageInfoError): repo._get_info_from_wheel(link) @@ -157,7 +157,7 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: assert mock_download.call_count == 3 # 5. range request and download - mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupported + mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupportedError mock_download.side_effect = None with contextlib.suppress(PackageInfoError): diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py index 0645f0256bf..204e8ca94af 100644 --- a/tests/repositories/test_installed_repository.py +++ b/tests/repositories/test_installed_repository.py @@ -4,6 +4,7 @@ import shutil import zipfile +from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from typing import Iterator @@ -69,6 +70,7 @@ def installed_results( ), metadata.PathDistribution(site_purelib / "standard-1.2.3.dist-info"), metadata.PathDistribution(site_purelib / "editable-2.3.4.dist-info"), + metadata.PathDistribution(site_purelib / "editable-src-dir-2.3.4.dist-info"), metadata.PathDistribution( site_purelib / "editable-with-import-2.3.4.dist-info" ), @@ -95,7 +97,7 @@ def env( env_dir: Path, site_purelib: Path, site_platlib: Path, src_dir: Path ) -> MockEnv: class _MockEnv(MockEnv): - @property + @cached_property def paths(self) -> dict[str, str]: return { "purelib": site_purelib.as_posix(), @@ -201,6 +203,30 @@ def test_load_successful_with_invalid_distribution( assert str(invalid_dist_info) in message +def test_loads_in_correct_sys_path_order( + tmp_path: Path, current_python: tuple[int, int, int], fixture_dir: FixtureDirGetter +) -> None: + path1 = tmp_path / "path1" + path1.mkdir() + path2 = tmp_path / "path2" + path2.mkdir() + env = MockEnv(path=tmp_path, sys_path=[str(path1), str(path2)]) + fixtures = fixture_dir("project_plugins") + dist_info_1 = "my_application_plugin-1.0.dist-info" + dist_info_2 = "my_application_plugin-2.0.dist-info" + dist_info_other = "my_other_plugin-1.0.dist-info" + shutil.copytree(fixtures / dist_info_1, path1 / dist_info_1) + shutil.copytree(fixtures / dist_info_2, path2 / dist_info_2) + shutil.copytree(fixtures / dist_info_other, path2 / dist_info_other) + + repo = InstalledRepository.load(env) + + assert {f"{p.name} {p.version}" for p in repo.packages} == { + "my-application-plugin 1.0", + "my-other-plugin 1.0", + } + + def test_load_ensure_isolation(repository: InstalledRepository) -> None: package = get_package_from_repository("attrs", repository) assert package is None @@ -262,6 +288,18 @@ def test_load_editable_package( assert editable.source_url == editable_source_directory_path +def test_load_editable_src_dir_package( + repository: InstalledRepository, editable_source_directory_path: str +) -> None: + # test editable package with src layout with text .pth file + editable = get_package_from_repository("editable-src-dir", repository) + assert editable is not None + assert editable.name == "editable-src-dir" + assert editable.version.text == "2.3.4" + assert editable.source_type == "directory" + assert editable.source_url == editable_source_directory_path + + def test_load_editable_with_import_package(repository: InstalledRepository) -> None: # test editable package with executable .pth file editable = get_package_from_repository("editable-with-import", repository) @@ -398,3 +436,30 @@ def test_system_site_packages_source_type( package.name: package.source_type for package in installed_repository.packages } assert source_types == {"cleo": None, "directory-pep-610": "directory"} + + +def test_pipx_shared_lib_site_packages( + tmp_path: Path, + poetry: Poetry, + site_purelib: Path, + caplog: LogCaptureFixture, +) -> None: + """ + Simulate pipx shared/lib/site-packages which is not relative to the venv path. + """ + venv_path = tmp_path / "venv" + shared_lib_site_path = tmp_path / "site" + env = MockEnv( + path=venv_path, sys_path=[str(venv_path / "purelib"), str(shared_lib_site_path)] + ) + dist_info = "cleo-0.7.6.dist-info" + shutil.copytree(site_purelib / dist_info, shared_lib_site_path / dist_info) + installed_repository = InstalledRepository.load(env) + + assert len(installed_repository.packages) == 1 + cleo_package = installed_repository.packages[0] + cleo_package.to_dependency() + # There must not be a warning + # that the package does not seem to be a valid Python package. + assert caplog.messages == [] + assert cleo_package.source_type is None diff --git a/tests/repositories/test_legacy_repository.py b/tests/repositories/test_legacy_repository.py index f4d955fbae6..3b122e19a53 100644 --- a/tests/repositories/test_legacy_repository.py +++ b/tests/repositories/test_legacy_repository.py @@ -14,7 +14,7 @@ from poetry.core.packages.utils.link import Link from poetry.factory import Factory -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.exceptions import RepositoryError from poetry.repositories.legacy_repository import LegacyRepository @@ -124,7 +124,7 @@ def test_sdist_format_support(legacy_repository: LegacyRepository) -> None: def test_missing_version(legacy_repository: LegacyRepository) -> None: repo = legacy_repository - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): repo._get_release_info( canonicalize_name("missing_version"), Version.parse("1.1.0") ) @@ -562,7 +562,7 @@ def test_get_40x_and_returns_none( ) -> None: repo = MockHttpRepository({"/foo/": status_code}, http) - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): repo.get_page("foo") diff --git a/tests/repositories/test_repository_pool.py b/tests/repositories/test_repository_pool.py index 87d5782170e..9c4036146a0 100644 --- a/tests/repositories/test_repository_pool.py +++ b/tests/repositories/test_repository_pool.py @@ -6,7 +6,7 @@ from poetry.repositories import Repository from poetry.repositories import RepositoryPool -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.repository_pool import Priority from tests.helpers import get_dependency @@ -17,7 +17,6 @@ def test_pool() -> None: pool = RepositoryPool() assert len(pool.repositories) == 0 - assert not pool.has_default() assert not pool.has_primary_repositories() @@ -26,7 +25,6 @@ def test_pool_with_initial_repositories() -> None: pool = RepositoryPool([repo]) assert len(pool.repositories) == 1 - assert not pool.has_default() assert pool.has_primary_repositories() assert pool.get_priority("repo") == Priority.PRIMARY @@ -71,30 +69,7 @@ def test_repository_from_single_repo_pool(priority: Priority) -> None: assert pool.get_priority("foo") == priority -@pytest.mark.parametrize( - ("default", "secondary", "expected_priority"), - [ - (False, True, Priority.SECONDARY), - (True, False, Priority.DEFAULT), - (True, True, Priority.DEFAULT), - ], -) -def test_repository_from_single_repo_pool_legacy( - default: bool, secondary: bool, expected_priority: Priority -) -> None: - repo = LegacyRepository("foo", "https://foo.bar") - pool = RepositoryPool() - - with pytest.warns(DeprecationWarning): - pool.add_repository(repo, default=default, secondary=secondary) - - assert pool.repository("foo") is repo - assert pool.get_priority("foo") == expected_priority - - def test_repository_with_all_prio_repositories() -> None: - secondary = LegacyRepository("secondary", "https://secondary.com") - default = LegacyRepository("default", "https://default.com") supplemental = LegacyRepository("supplemental", "https://supplemental.com") repo1 = LegacyRepository("foo", "https://foo.bar") repo2 = LegacyRepository("bar", "https://bar.baz") @@ -102,47 +77,39 @@ def test_repository_with_all_prio_repositories() -> None: pool = RepositoryPool() pool.add_repository(repo1) - pool.add_repository(secondary, priority=Priority.SECONDARY) pool.add_repository(repo2) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) pool.add_repository(explicit, priority=Priority.EXPLICIT) - pool.add_repository(default, priority=Priority.DEFAULT) - assert pool.repository("secondary") is secondary - assert pool.repository("default") is default assert pool.repository("foo") is repo1 assert pool.repository("bar") is repo2 assert pool.repository("supplemental") is supplemental assert pool.repository("explicit") is explicit - assert pool.has_default() assert pool.has_primary_repositories() -def test_repository_secondary_and_supplemental_repositories_do_show() -> None: - secondary = LegacyRepository("secondary", "https://secondary.com") +def test_repository_supplemental_repositories_do_show() -> None: supplemental = LegacyRepository("supplemental", "https://supplemental.com") pool = RepositoryPool() - pool.add_repository(secondary, priority=Priority.SECONDARY) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) - assert pool.repository("secondary") is secondary assert pool.repository("supplemental") is supplemental - assert pool.repositories == [secondary, supplemental] + assert pool.repositories == [supplemental] def test_repository_explicit_repositories_do_not_show() -> None: explicit = LegacyRepository("explicit", "https://explicit.com") - default = LegacyRepository("default", "https://default.com") + primary = LegacyRepository("primary", "https://primary.com") pool = RepositoryPool() pool.add_repository(explicit, priority=Priority.EXPLICIT) - pool.add_repository(default, priority=Priority.DEFAULT) + pool.add_repository(primary, priority=Priority.PRIMARY) assert pool.repository("explicit") is explicit - assert pool.repository("default") is default - assert pool.repositories == [default] - assert pool.all_repositories == [default, explicit] + assert pool.repository("primary") is primary + assert pool.repositories == [primary] + assert pool.all_repositories == [primary, explicit] def test_remove_non_existing_repository_raises_indexerror() -> None: @@ -168,65 +135,26 @@ def test_remove_existing_repository_successful() -> None: assert pool.repository("baz") is repo3 -def test_remove_default_repository() -> None: - default = LegacyRepository("default", "https://default.com") - repo1 = LegacyRepository("foo", "https://foo.bar") - repo2 = LegacyRepository("bar", "https://bar.baz") - new_default = LegacyRepository("new_default", "https://new.default.com") - - pool = RepositoryPool() - pool.add_repository(repo1) - pool.add_repository(repo2) - pool.add_repository(default, priority=Priority.DEFAULT) - - assert pool.has_default() - - pool.remove_repository("default") - - assert not pool.has_repository("default") - assert not pool.has_default() - - pool.add_repository(new_default, priority=Priority.DEFAULT) - - assert pool.get_priority("new_default") is Priority.DEFAULT - assert pool.has_default() - - def test_repository_ordering() -> None: - default1 = LegacyRepository("default1", "https://default1.com") - default2 = LegacyRepository("default2", "https://default2.com") primary1 = LegacyRepository("primary1", "https://primary1.com") primary2 = LegacyRepository("primary2", "https://primary2.com") primary3 = LegacyRepository("primary3", "https://primary3.com") - secondary1 = LegacyRepository("secondary1", "https://secondary1.com") - secondary2 = LegacyRepository("secondary2", "https://secondary2.com") - secondary3 = LegacyRepository("secondary3", "https://secondary3.com") supplemental = LegacyRepository("supplemental", "https://supplemental.com") pool = RepositoryPool() - pool.add_repository(secondary1, priority=Priority.SECONDARY) pool.add_repository(supplemental, priority=Priority.SUPPLEMENTAL) pool.add_repository(primary1) - pool.add_repository(default1, priority=Priority.DEFAULT) pool.add_repository(primary2) - pool.add_repository(secondary2, priority=Priority.SECONDARY) pool.remove_repository("primary2") - pool.remove_repository("secondary2") pool.add_repository(primary3) - pool.add_repository(secondary3, priority=Priority.SECONDARY) assert pool.repositories == [ - default1, primary1, primary3, - secondary1, - secondary3, supplemental, ] - with pytest.raises(ValueError): - pool.add_repository(default2, priority=Priority.DEFAULT) def test_pool_get_package_in_any_repository() -> None: @@ -281,7 +209,7 @@ def test_pool_no_package_from_any_repository_raises_package_not_found() -> None: pool = RepositoryPool() pool.add_repository(Repository("repo")) - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): pool.package("foo", Version.parse("1.0.0")) @@ -291,7 +219,7 @@ def test_pool_no_package_from_specified_repository_raises_package_not_found() -> repo2 = Repository("repo2", [package]) pool = RepositoryPool([repo1, repo2]) - with pytest.raises(PackageNotFound): + with pytest.raises(PackageNotFoundError): pool.package("foo", Version.parse("1.0.0"), repository_name="repo1") diff --git a/tests/repositories/test_single_page_repository.py b/tests/repositories/test_single_page_repository.py index 05789690874..61b8f48501b 100644 --- a/tests/repositories/test_single_page_repository.py +++ b/tests/repositories/test_single_page_repository.py @@ -7,7 +7,7 @@ from poetry.core.packages.dependency import Dependency -from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import PackageNotFoundError from poetry.repositories.link_sources.html import HTMLPage from poetry.repositories.single_page_repository import SinglePageRepository @@ -30,7 +30,7 @@ def __init__(self, page: str) -> None: def _get_page(self, name: NormalizedName) -> HTMLPage: fixture = self.FIXTURES / self.url.rsplit("/", 1)[-1] if not fixture.exists(): - raise PackageNotFound(f"Package [{name}] not found.") + raise PackageNotFoundError(f"Package [{name}] not found.") with fixture.open(encoding="utf-8") as f: return HTMLPage(self._url, f.read()) diff --git a/tests/test_factory.py b/tests/test_factory.py index dee66ba6a9f..a3c80ba5f4e 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -14,7 +14,8 @@ from poetry.core.packages.package import Package from poetry.core.packages.vcs_dependency import VCSDependency -from poetry.exceptions import PoetryException +from poetry.__version__ import __version__ +from poetry.exceptions import PoetryError from poetry.factory import Factory from poetry.plugins.plugin import Plugin from poetry.repositories.exceptions import InvalidSourceError @@ -230,67 +231,33 @@ def test_create_poetry_non_package_mode(fixture_dir: FixtureDirGetter) -> None: assert not poetry.is_package_mode -def test_poetry_with_default_source_legacy( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: +def test_create_poetry_version_ok(fixture_dir: FixtureDirGetter) -> None: io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source_legacy"), io=io) - - assert len(poetry.pool.repositories) == 1 - assert "Found deprecated key" in io.fetch_error() + Factory().create_poetry(fixture_dir("self_version_ok"), io=io) + assert io.fetch_output() == "" + assert io.fetch_error() == "" -def test_poetry_with_default_source( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source"), io=io) - assert len(poetry.pool.repositories) == 1 +def test_create_poetry_version_not_ok(fixture_dir: FixtureDirGetter) -> None: + with pytest.raises(PoetryError) as e: + Factory().create_poetry(fixture_dir("self_version_not_ok")) assert ( - io.fetch_error().strip() - == "Warning: Found deprecated priority 'default' for source 'foo' in" - " pyproject.toml. You can achieve the same effect by changing the priority" - " to 'primary' and putting the source first." + str(e.value) + == f"This project requires Poetry <1.2, but you are using Poetry {__version__}" ) -def test_poetry_with_default_source_and_pypi( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source_and_pypi"), io=io) - - assert len(poetry.pool.repositories) == 2 - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - assert "Warning: Found deprecated key" not in io.fetch_error() - - -def test_poetry_with_default_source_pypi( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - io = BufferedIO() - poetry = Factory().create_poetry(fixture_dir("with_default_source_pypi"), io=io) - - assert len(poetry.pool.repositories) == 1 - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT - - @pytest.mark.parametrize( "project", - ("with_non_default_source_implicit", "with_non_default_source_explicit"), + ("with_primary_source_implicit", "with_primary_source_explicit"), ) -def test_poetry_with_non_default_source( +def test_poetry_with_primary_source( project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() poetry = Factory().create_poetry(fixture_dir(project), io=io) - assert not poetry.pool.has_default() assert not poetry.pool.has_repository("PyPI") assert poetry.pool.has_repository("foo") assert poetry.pool.get_priority("foo") is Priority.PRIMARY @@ -298,58 +265,10 @@ def test_poetry_with_non_default_source( assert {repo.name for repo in poetry.pool.repositories} == {"foo"} -def test_poetry_with_non_default_secondary_source_legacy( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - poetry = Factory().create_poetry( - fixture_dir("with_non_default_secondary_source_legacy") - ) - - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - assert poetry.pool.has_repository("foo") - assert isinstance(poetry.pool.repository("foo"), LegacyRepository) - assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo"} - - -def test_poetry_with_non_default_secondary_source( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - poetry = Factory().create_poetry(fixture_dir("with_non_default_secondary_source")) - - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - assert poetry.pool.has_repository("foo") - assert isinstance(poetry.pool.repository("foo"), LegacyRepository) - assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo"} - - -def test_poetry_with_non_default_multiple_secondary_sources_legacy( - fixture_dir: FixtureDirGetter, - with_simple_keyring: None, -) -> None: - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_secondary_sources_legacy") - ) - - assert poetry.pool.has_repository("PyPI") - assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) - assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY - assert poetry.pool.has_repository("foo") - assert isinstance(poetry.pool.repository("foo"), LegacyRepository) - assert poetry.pool.has_repository("bar") - assert isinstance(poetry.pool.repository("bar"), LegacyRepository) - assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo", "bar"} - - -def test_poetry_with_non_default_multiple_secondary_sources( +def test_poetry_with_multiple_supplemental_sources( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_secondary_sources") - ) + poetry = Factory().create_poetry(fixture_dir("with_multiple_supplemental_sources")) assert poetry.pool.has_repository("PyPI") assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) @@ -361,28 +280,11 @@ def test_poetry_with_non_default_multiple_secondary_sources( assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo", "bar"} -def test_poetry_with_non_default_multiple_sources_legacy( +def test_poetry_with_multiple_sources( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_sources_legacy") - ) + poetry = Factory().create_poetry(fixture_dir("with_multiple_sources")) - assert not poetry.pool.has_default() - assert poetry.pool.has_repository("bar") - assert isinstance(poetry.pool.repository("bar"), LegacyRepository) - assert not poetry.pool.has_repository("PyPI") - assert poetry.pool.has_repository("foo") - assert isinstance(poetry.pool.repository("foo"), LegacyRepository) - assert {repo.name for repo in poetry.pool.repositories} == {"bar", "foo"} - - -def test_poetry_with_non_default_multiple_sources( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - poetry = Factory().create_poetry(fixture_dir("with_non_default_multiple_sources")) - - assert not poetry.pool.has_default() assert not poetry.pool.has_repository("PyPI") assert poetry.pool.has_repository("bar") assert isinstance(poetry.pool.repository("bar"), LegacyRepository) @@ -391,29 +293,19 @@ def test_poetry_with_non_default_multiple_sources( assert {repo.name for repo in poetry.pool.repositories} == {"bar", "foo"} -def test_poetry_with_non_default_multiple_sources_pypi( +def test_poetry_with_multiple_sources_pypi( fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: io = BufferedIO() - poetry = Factory().create_poetry( - fixture_dir("with_non_default_multiple_sources_pypi"), io=io - ) + poetry = Factory().create_poetry(fixture_dir("with_multiple_sources_pypi"), io=io) assert len(poetry.pool.repositories) == 4 - assert not poetry.pool.has_default() assert poetry.pool.has_repository("PyPI") assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository) assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY # PyPI must be between bar and baz! expected = ["bar", "PyPI", "baz", "foo"] assert [repo.name for repo in poetry.pool.repositories] == expected - error = io.fetch_error() - assert ( - error.strip() - == "Warning: Found deprecated priority 'secondary' for source 'foo' in" - " pyproject.toml. Consider changing the priority to one of the" - " non-deprecated values: 'default', 'primary', 'supplemental', 'explicit'." - ) def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None: @@ -476,29 +368,11 @@ def test_poetry_with_explicit_pypi_and_other( def test_poetry_with_pypi_explicit_only( project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None ) -> None: - with pytest.raises(PoetryException) as e: + with pytest.raises(PoetryError) as e: Factory().create_poetry(fixture_dir(project)) assert str(e.value) == "At least one source must not be configured as 'explicit'." -def test_poetry_with_two_default_sources_legacy( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - with pytest.raises(ValueError) as e: - Factory().create_poetry(fixture_dir("with_two_default_sources_legacy")) - - assert str(e.value) == "Only one repository can be the default." - - -def test_poetry_with_two_default_sources( - fixture_dir: FixtureDirGetter, with_simple_keyring: None -) -> None: - with pytest.raises(ValueError) as e: - Factory().create_poetry(fixture_dir("with_two_default_sources")) - - assert str(e.value) == "Only one repository can be the default." - - def test_validate(fixture_dir: FixtureDirGetter) -> None: complete = TOMLFile(fixture_dir("complete.toml")) pyproject: dict[str, Any] = complete.read() @@ -554,7 +428,6 @@ def test_create_poetry_with_local_config(fixture_dir: FixtureDirGetter) -> None: assert not poetry.config.get("virtualenvs.create") assert not poetry.config.get("virtualenvs.options.always-copy") assert not poetry.config.get("virtualenvs.options.no-pip") - assert not poetry.config.get("virtualenvs.options.no-setuptools") assert not poetry.config.get("virtualenvs.options.system-site-packages") diff --git a/tests/utils/env/test_env.py b/tests/utils/env/test_env.py index 43d30c81d89..15aaf4cfac8 100644 --- a/tests/utils/env/test_env.py +++ b/tests/utils/env/test_env.py @@ -313,15 +313,7 @@ def test_env_system_packages_are_relative_to_lib( ("flags", "packages"), [ ({"no-pip": False}, {"pip"}), - ({"no-pip": False, "no-wheel": True}, {"pip"}), - ({"no-pip": False, "no-wheel": False}, {"pip", "wheel"}), ({"no-pip": True}, set()), - ({"no-setuptools": False}, {"setuptools"}), - ({"no-setuptools": True}, set()), - ({"setuptools": "bundle"}, {"setuptools"}), - ({"no-pip": True, "no-setuptools": False}, {"setuptools"}), - ({"no-wheel": False}, {"wheel"}), - ({"wheel": "bundle"}, {"wheel"}), ({}, set()), ], ) @@ -339,14 +331,6 @@ def test_env_no_pip( if package.name != "sqlite3" } - # For python >= 3.12, virtualenv defaults to "--no-setuptools" and "--no-wheel" - # behaviour, so setting these values to False becomes meaningless. - if sys.version_info >= (3, 12): - if not flags.get("no-setuptools", True): - packages.discard("setuptools") - if not flags.get("no-wheel", True): - packages.discard("wheel") - assert installed_packages == packages @@ -508,35 +492,6 @@ def test_build_environment_not_called_without_build_script_specified( assert not env.executed # type: ignore[attr-defined] -def test_fallback_on_detect_active_python( - poetry: Poetry, mocker: MockerFixture -) -> None: - m = mocker.patch( - "subprocess.check_output", - side_effect=subprocess.CalledProcessError(1, "some command"), - ) - env_manager = EnvManager(poetry) - active_python = env_manager._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") - encoding = "locale" if sys.version_info >= (3, 10) else None - with python_wrapper.open("w", encoding=encoding) 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 - - def test_command_from_bin_preserves_relative_path(manager: EnvManager) -> None: # https://github.com/python-poetry/poetry/issues/7959 env = manager.get() diff --git a/tests/utils/env/test_env_manager.py b/tests/utils/env/test_env_manager.py index 88d973da3c7..fd673f8b167 100644 --- a/tests/utils/env/test_env_manager.py +++ b/tests/utils/env/test_env_manager.py @@ -19,8 +19,8 @@ from poetry.utils.env import EnvManager from poetry.utils.env import IncorrectEnvError from poetry.utils.env import InvalidCurrentPythonVersionError -from poetry.utils.env import NoCompatiblePythonVersionFound -from poetry.utils.env import PythonVersionNotFound +from poetry.utils.env import NoCompatiblePythonVersionFoundError +from poetry.utils.env import PythonVersionNotFoundError from poetry.utils.env.env_manager import EnvsFile from poetry.utils.helpers import remove_directory @@ -150,7 +150,6 @@ def test_activate_in_project_venv_no_explicit_config( "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="simple-project-py3.7", ) @@ -217,7 +216,7 @@ def test_activate_fails_when_python_cannot_be_found( mocker.patch("shutil.which", return_value=None) - with pytest.raises(PythonVersionNotFound) as e: + with pytest.raises(PythonVersionNotFoundError) as e: manager.activate("python3.7") expected_message = "Could not find the python executable python3.7" @@ -932,7 +931,7 @@ def test_create_venv_finds_no_python_executable( poetry.package.python_versions = "^999" - with pytest.raises(NoCompatiblePythonVersionFound) as e: + with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv() expected_message = ( @@ -993,12 +992,15 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( poetry.package.python_versions = "^4.8" - mocker.patch("subprocess.check_output", side_effect=[sys.base_prefix]) + mocker.patch( + "subprocess.check_output", + side_effect=[sys.base_prefix, "/usr/bin/python", "3.9.0"], + ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - with pytest.raises(NoCompatiblePythonVersionFound) as e: + with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv() expected_message = ( @@ -1024,7 +1026,7 @@ def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - with pytest.raises(NoCompatiblePythonVersionFound) as e: + with pytest.raises(NoCompatiblePythonVersionFoundError) as e: manager.create_venv(executable=Path("python3.8")) expected_message = ( @@ -1068,7 +1070,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py{version.major}.{version.minor}", - executable=None, + executable=Path(sys.executable), flags=venv_flags_default, prompt=f"simple-project-py{version.major}.{version.minor}", ) @@ -1179,7 +1181,6 @@ def test_create_venv_project_name_empty_sets_correct_prompt( "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="non-package-mode-py3.7", ) @@ -1229,7 +1230,6 @@ def mock_check_output(cmd: str, *args: Any, **kwargs: Any) -> str: "always-copy": False, "system-site-packages": False, "no-pip": False, - "no-setuptools": False, }, prompt="simple-project-py3.5", ) diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 23df1f97922..441d5c95678 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -190,7 +190,7 @@ def test_authenticator_falls_back_to_keyring_url( dummy_keyring.set_default_service_credential( "https://foo.bar/simple/", - SimpleCredential("foo", "bar"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "bar"), ) authenticator = Authenticator(config, NullIO()) @@ -217,7 +217,7 @@ def test_authenticator_falls_back_to_keyring_netloc( dummy_keyring.set_default_service_credential( "foo.bar", - SimpleCredential("foo", "bar"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "bar"), ) authenticator = Authenticator(config, NullIO()) @@ -483,11 +483,11 @@ def test_authenticator_falls_back_to_keyring_url_matched_by_path( dummy_keyring.set_default_service_credential( "https://foo.bar/alpha/files/simple/", - SimpleCredential("foo", "bar"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "bar"), ) dummy_keyring.set_default_service_credential( "https://foo.bar/beta/files/simple/", - SimpleCredential("foo", "baz"), # type: ignore[no-untyped-call] + SimpleCredential("foo", "baz"), ) authenticator = Authenticator(config, NullIO()) diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index a2aa909e936..462cfb636df 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -12,7 +12,7 @@ from requests.exceptions import ChunkedEncodingError from poetry.utils.helpers import Downloader -from poetry.utils.helpers import HTTPRangeRequestSupported +from poetry.utils.helpers import HTTPRangeRequestSupportedError from poetry.utils.helpers import download_file from poetry.utils.helpers import get_file_hash from poetry.utils.helpers import get_highest_priority_hash_type @@ -261,7 +261,7 @@ def handle_request( dest = tmp_path / filename if accepts_ranges and raise_accepts_ranges: - with pytest.raises(HTTPRangeRequestSupported): + with pytest.raises(HTTPRangeRequestSupportedError): download_file(url, dest, raise_accepts_ranges=raise_accepts_ranges) assert not dest.exists() else: diff --git a/tests/utils/test_python_manager.py b/tests/utils/test_python_manager.py new file mode 100644 index 00000000000..263cd4d572c --- /dev/null +++ b/tests/utils/test_python_manager.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import os +import subprocess +import sys + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from cleo.io.null_io import NullIO +from poetry.core.constraints.version import Version + +from poetry.utils.env.python_manager import Python +from tests.utils.env.test_env_manager import check_output_wrapper + + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + from poetry.config.config import Config + from tests.types import ProjectFactory + + +def test_python_get_version_on_the_fly() -> None: + python = Python(executable=sys.executable) + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join([str(s) for s in sys.version_info[:3]]) + ) + assert python.patch_version == Version.parse( + ".".join([str(s) for s in sys.version_info[:3]]) + ) + assert python.minor_version == Version.parse( + ".".join([str(s) for s in sys.version_info[:2]]) + ) + + +def test_python_get_system_python() -> None: + python = Python.get_system_python() + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join(str(v) for v in sys.version_info[:3]) + ) + + +def test_python_get_preferred_default(config: Config) -> None: + python = Python.get_preferred_python(config) + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join(str(v) for v in sys.version_info[:3]) + ) + + +def test_python_get_preferred_activated(config: Config, mocker: MockerFixture) -> None: + mocker.patch( + "subprocess.check_output", + side_effect=check_output_wrapper(Version.parse("3.7.1")), + ) + config.config["virtualenvs"]["prefer-active-python"] = True + python = Python.get_preferred_python(config) + + assert python.executable.as_posix().startswith("/usr/bin/python") + assert python.version == Version.parse("3.7.1") + + +def test_python_get_preferred_activated_fallback( + config: Config, mocker: MockerFixture +) -> None: + config.config["virtualenvs"]["prefer-active-python"] = True + with mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ): + python = Python.get_preferred_python(config) + + assert python.executable == Path(sys.executable) + + +def test_fallback_on_detect_active_python(mocker: MockerFixture) -> None: + m = mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ) + + active_python = Python._detect_active_python(NullIO()) + + 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(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") + encoding = "locale" if sys.version_info >= (3, 10) else None + with python_wrapper.open("w", encoding=encoding) as f: + f.write(f"@echo {wrapped_python}") + os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] + + active_python = Python._detect_active_python(NullIO()) + + assert active_python == wrapped_python + + +def test_python_find_compatible(project_factory: ProjectFactory) -> None: + # Note: This test may fail on Windows systems using Python from the Microsoft Store, + # as the executable is named `py.exe`, which is not currently recognized by + # Python.get_compatible_python. This issue will be resolved in #2117. + # However, this does not cause problems in our case because Poetry's own + # Python interpreter is used before attempting to find another compatible version. + fixture = Path(__file__).parent.parent / "fixtures" / "simple_project" + poetry = project_factory("simple-project", source=fixture) + python = Python.get_compatible_python(poetry) + + assert Version.from_parts(3, 4) <= python.version <= Version.from_parts(4, 0)