Commit 5806b42e by Bart Kamphorst Committed by Randy Döring

sources: introduce "priority" key for sources and deprecate flags "default" and…

sources: introduce "priority" key for sources and deprecate flags "default" and "secondary", adjust cli accordingly (#7658)

Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
parent 3a31f2de
...@@ -784,11 +784,12 @@ You cannot use the name `pypi` as it is reserved for use by the default PyPI sou ...@@ -784,11 +784,12 @@ You cannot use the name `pypi` as it is reserved for use by the default PyPI sou
#### Options #### Options
* `--default`: Set this source as the [default]({{< relref "repositories#disabling-the-pypi-repository" >}}) (disable PyPI). * `--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#install-dependencies-from-a-private-repository" >}}) source. * `--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" >}}), and [`secondary`]({{< relref "repositories#secondary-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information.
{{% note %}} {{% note %}}
You cannot set a source as both `default` and `secondary`. At most one of the options above can be provided. See [package sources]({{< relref "repositories#package-sources" >}}) for more information.
{{% /note %}} {{% /note %}}
### source show ### source show
......
...@@ -258,7 +258,7 @@ you can use the `source` property: ...@@ -258,7 +258,7 @@ you can use the `source` property:
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
secondary = true priority = "secondary"
[tool.poetry.dependencies] [tool.poetry.dependencies]
my-cool-package = { version = "*", source = "foo" } my-cool-package = { version = "*", source = "foo" }
......
...@@ -33,7 +33,7 @@ First, [configure](#project-configuration) the [package source](#package-source) ...@@ -33,7 +33,7 @@ First, [configure](#project-configuration) the [package source](#package-source)
project. project.
```bash ```bash
poetry source add --secondary foo https://pypi.example.org/simple/ poetry source add --priority=secondary foo https://pypi.example.org/simple/
``` ```
Then, assuming the repository requires authentication, configure credentials for it. Then, assuming the repository requires authentication, configure credentials for it.
...@@ -120,12 +120,18 @@ This will generate the following configuration snippet in your ...@@ -120,12 +120,18 @@ This will generate the following configuration snippet in your
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
default = false priority = "primary"
secondary = false
``` ```
Any package source not marked as `secondary` will take precedence over [PyPI](https://pypi.org). If `priority` is undefined, the source is considered a primary source that takes precedence over PyPI and secondary sources.
Package sources are considered in the following order:
1. [default source](#default-package-source),
2. primary sources,
3. PyPI (unless disabled by another default source),
4. [secondary sources](#secondary-package-sources),
Within each priority class, package sources are considered in order of appearance in `pyproject.toml`.
{{% note %}} {{% note %}}
...@@ -148,10 +154,10 @@ you must declare **all** package sources to be [secondary](#secondary-package-so ...@@ -148,10 +154,10 @@ you must declare **all** package sources to be [secondary](#secondary-package-so
By default, Poetry configures [PyPI](https://pypi.org) as the default package source for your By default, Poetry configures [PyPI](https://pypi.org) as the default package source for your
project. You can alter this behaviour and exclusively look up packages only from the configured project. You can alter this behaviour and exclusively look up packages only from the configured
package sources by adding a **single** source with `default = true`. package sources by adding a **single** source with `priority = "default"`.
```bash ```bash
poetry source add --default foo https://foo.bar/simple/ poetry source add --priority=default foo https://foo.bar/simple/
``` ```
{{% warning %}} {{% warning %}}
...@@ -164,30 +170,31 @@ as a package source for your project. ...@@ -164,30 +170,31 @@ as a package source for your project.
#### Secondary Package Sources #### Secondary Package Sources
If package sources are configured as secondary, all it means is that these will be given a lower 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 package priority when selecting compatible package distribution that also exists in your default and primary package sources.
source.
You can configure a package source as a secondary source with `secondary = true` in your package You can configure a package source as a secondary source with `priority = "secondary"` in your package
source configuration. source configuration.
```bash ```bash
poetry source add --secondary foo https://foo.bar/simple/ poetry source add --priority=secondary https://foo.bar/simple/
``` ```
There can be more than one secondary package source. There can be more than one secondary package source.
{{% note %}} #### Package Source Constraint
All package sources (including secondary sources) will be searched during the package lookup All package sources (including secondary sources) will be searched during the package lookup
process. These network requests will occur for all sources, regardless of if the package is process. These network requests will occur for all sources, regardless of if the package is
found at one or more sources. found at one or more sources.
In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly. This is strongly suggested for all private packages to avoid dependency confusion attacks. In order to limit the search for a specific package to a particular package repository, you can specify the source explicitly.
```bash ```bash
poetry add --source internal-pypi httpx poetry add --source internal-pypi httpx
``` ```
This results in the following configuration in `pyproject.toml`:
```toml ```toml
[tool.poetry.dependencies] [tool.poetry.dependencies]
... ...
...@@ -195,10 +202,47 @@ httpx = { version = "^0.22", source = "internal-pypi" } ...@@ -195,10 +202,47 @@ httpx = { version = "^0.22", source = "internal-pypi" }
[[tool.poetry.source]] [[tool.poetry.source]]
name = "internal-pypi" name = "internal-pypi"
url = "https://foo.bar/simple/" url = ...
secondary = true priority = ...
``` ```
{{% note %}}
A repository that is configured to be the only source for retrieving a certain package can itself have any priority.
If a repository is configured to be the source of a package, it will be the only source that is considered for that package
and the repository priority will have no effect on the resolution.
{{% /note %}}
{{% note %}}
Package `source` keys are not inherited by their dependencies.
In particular, if `package-A` is configured to be found in `source = internal-pypi`,
and `package-A` depends on `package-B` that is also to be found on `internal-pypi`,
then `package-B` needs to be configured as such in `pyproject.toml`.
The easiest way to achieve this is to add `package-B` with a wildcard constraint:
```bash
poetry add --source internal-pypi package-B@*
```
This will ensure that `package-B` is searched only in the `internal-pypi` package source.
The version constraints on `package-B` are derived from `package-A` (and other client packages), as usual.
If you want to avoid additional main dependencies,
you can add `package-B` to a dedicated [dependency group]({{< relref "managing-dependencies#dependency-groups" >}}):
```bash
poetry add --group explicit --source internal-pypi package-B@*
```
{{% /note %}}
{{% note %}}
Package source constraints are strongly suggested for all packages that are expected
to be provided only by one specific source to avoid dependency confusion attacks.
{{% /note %}} {{% /note %}}
### Supported Package Sources ### Supported Package Sources
...@@ -231,7 +275,7 @@ httpx = {version = "^0.22.0", source = "pypi"} ...@@ -231,7 +275,7 @@ httpx = {version = "^0.22.0", source = "pypi"}
{{% warning %}} {{% warning %}}
If any source within a project is configured with `default = true`, The implicit `pypi` source will If any source within a project is configured with `priority = "default"`, The implicit `pypi` source will
be disabled and not used for any packages. be disabled and not used for any packages.
{{% /warning %}} {{% /warning %}}
......
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import warnings
from poetry.repositories.repository_pool import Priority
@dataclasses.dataclass(order=True, eq=True) @dataclasses.dataclass(order=True, eq=True)
class Source: class Source:
name: str name: str
url: str url: str
default: bool = dataclasses.field(default=False) default: dataclasses.InitVar[bool] = False
secondary: bool = dataclasses.field(default=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:
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]: def to_dict(self) -> dict[str, str | bool]:
return dataclasses.asdict(self) return dataclasses.asdict(
self,
dict_factory=lambda x: {
k: v if not isinstance(v, Priority) else v.name.lower() for (k, v) in x
},
)
...@@ -7,6 +7,7 @@ from tomlkit.items import AoT ...@@ -7,6 +7,7 @@ from tomlkit.items import AoT
from poetry.config.source import Source from poetry.config.source import Source
from poetry.console.commands.command import Command from poetry.console.commands.command import Command
from poetry.repositories.repository_pool import Priority
class SourceAddCommand(Command): class SourceAddCommand(Command):
...@@ -28,35 +29,74 @@ class SourceAddCommand(Command): ...@@ -28,35 +29,74 @@ class SourceAddCommand(Command):
( (
"Set this source as the default (disable PyPI). A " "Set this source as the default (disable PyPI). A "
"default source will also be the fallback source if " "default source will also be the fallback source if "
"you add other sources." "you add other sources. (<warning>Deprecated</warning>, use --priority)"
), ),
), ),
option("secondary", "s", "Set this source as secondary."), option(
"secondary",
"s",
(
"Set this source as secondary. (<warning>Deprecated</warning>, use"
" --priority)"
),
),
option(
"priority",
"p",
(
"Set the priority of this source. One of:"
f" {', '.join(p.name.lower() for p in Priority)}. Defaults to"
f" {Priority.PRIMARY.name.lower()}."
),
flag=False,
),
] ]
def handle(self) -> int: def handle(self) -> int:
from poetry.factory import Factory from poetry.factory import Factory
from poetry.utils.source import source_to_table from poetry.utils.source import source_to_table
name = self.argument("name") name: str = self.argument("name")
url = self.argument("url") url: str = self.argument("url")
is_default = self.option("default") is_default: bool = self.option("default", False)
is_secondary = self.option("secondary") is_secondary: bool = self.option("secondary", False)
priority: Priority | None = self.option("priority", None)
if is_default and is_secondary: if is_default and is_secondary:
self.line_error( self.line_error(
"Cannot configure a source as both <c1>default</c1> and" "<error>Cannot configure a source as both <c1>default</c1> and"
" <c1>secondary</c1>." " <c1>secondary</c1>.</error>"
) )
return 1 return 1
new_source: Source | None = Source( if is_default or is_secondary:
name=name, url=url, default=is_default, secondary=is_secondary if priority is not None:
) self.line_error(
"<error>Priority was passed through both --priority and a"
" deprecated flag (--default or --secondary). Please only provide"
" one of these.</error>"
)
return 1
else:
self.line_error(
"<warning>Warning: Priority was set through a deprecated flag"
" (--default or --secondary). Consider using --priority next"
" time.</warning>"
)
if is_default:
priority = Priority.DEFAULT
elif is_secondary:
priority = Priority.SECONDARY
elif priority is None:
priority = Priority.PRIMARY
new_source = Source(name=name, url=url, priority=priority)
existing_sources = self.poetry.get_sources() existing_sources = self.poetry.get_sources()
sources = AoT([]) sources = AoT([])
is_new_source = True
for source in existing_sources: for source in existing_sources:
if source == new_source: if source == new_source:
self.line( self.line(
...@@ -64,7 +104,10 @@ class SourceAddCommand(Command): ...@@ -64,7 +104,10 @@ class SourceAddCommand(Command):
" addition." " addition."
) )
return 0 return 0
elif source.default and is_default: elif (
source.priority is Priority.DEFAULT
and new_source.priority is Priority.DEFAULT
):
self.line_error( self.line_error(
f"<error>Source with name <c1>{source.name}</c1> is already set to" f"<error>Source with name <c1>{source.name}</c1> is already set to"
" default. Only one default source can be configured at a" " default. Only one default source can be configured at a"
...@@ -72,16 +115,17 @@ class SourceAddCommand(Command): ...@@ -72,16 +115,17 @@ class SourceAddCommand(Command):
) )
return 1 return 1
if new_source and source.name == name: if source.name == name:
self.line(f"Source with name <c1>{name}</c1> already exists. Updating.")
source = new_source source = new_source
new_source = None is_new_source = False
sources.append(source_to_table(source)) sources.append(source_to_table(source))
if new_source is not None: if is_new_source:
self.line(f"Adding source with name <c1>{name}</c1>.") self.line(f"Adding source with name <c1>{name}</c1>.")
sources.append(source_to_table(new_source)) sources.append(source_to_table(new_source))
else:
self.line(f"Source with name <c1>{name}</c1> already exists. Updating.")
# ensure new source is valid. eg: invalid name etc. # ensure new source is valid. eg: invalid name etc.
try: try:
......
...@@ -33,14 +33,12 @@ class SourceShowCommand(Command): ...@@ -33,14 +33,12 @@ class SourceShowCommand(Command):
return 0 return 0
if names and not any(s.name in names for s in sources): if names and not any(s.name in names for s in sources):
self.line_error(f"No source found with name(s): {', '.join(names)}") self.line_error(
f"No source found with name(s): {', '.join(names)}",
style="error",
)
return 1 return 1
bool_string = {
True: "yes",
False: "no",
}
for source in sources: for source in sources:
if names and source.name not in names: if names and source.name not in names:
continue continue
...@@ -50,12 +48,8 @@ class SourceShowCommand(Command): ...@@ -50,12 +48,8 @@ class SourceShowCommand(Command):
["<info>name</>", f" : <c1>{source.name}</>"], ["<info>name</>", f" : <c1>{source.name}</>"],
["<info>url</>", f" : {source.url}"], ["<info>url</>", f" : {source.url}"],
[ [
"<info>default</>", "<info>priority</>",
f" : {bool_string.get(source.default, False)}", f" : {source.priority.name.lower()}",
],
[
"<info>secondary</>",
f" : {bool_string.get(source.secondary, False)}",
], ],
] ]
table.add_rows(rows) table.add_rows(rows)
......
...@@ -123,6 +123,7 @@ class Factory(BaseFactory): ...@@ -123,6 +123,7 @@ class Factory(BaseFactory):
disable_cache: bool = False, disable_cache: bool = False,
) -> RepositoryPool: ) -> RepositoryPool:
from poetry.repositories import RepositoryPool from poetry.repositories import RepositoryPool
from poetry.repositories.repository_pool import Priority
if io is None: if io is None:
io = NullIO() io = NullIO()
...@@ -136,31 +137,46 @@ class Factory(BaseFactory): ...@@ -136,31 +137,46 @@ class Factory(BaseFactory):
repository = cls.create_package_source( repository = cls.create_package_source(
source, auth_config, disable_cache=disable_cache source, auth_config, disable_cache=disable_cache
) )
is_default = source.get("default", False) priority = Priority[source.get("priority", Priority.PRIMARY.name).upper()]
is_secondary = source.get("secondary", False) 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: {warning}</warning>")
if source.get("default"):
priority = Priority.DEFAULT
elif source.get("secondary"):
priority = Priority.SECONDARY
if io.is_debug(): if io.is_debug():
message = f"Adding repository {repository.name} ({repository.url})" message = f"Adding repository {repository.name} ({repository.url})"
if is_default: if priority is Priority.DEFAULT:
message += " and setting it as the default one" message += " and setting it as the default one"
elif is_secondary: else:
message += " and setting it as secondary" message += f" and setting it as {priority.name.lower()}"
io.write_line(message) io.write_line(message)
pool.add_repository(repository, is_default, secondary=is_secondary) pool.add_repository(repository, priority=priority)
# Put PyPI last to prefer private repositories # Only add PyPI if no default repository is configured
# unless we have no default source AND no primary sources
# (default = false, secondary = false)
if pool.has_default(): if pool.has_default():
if io.is_debug(): if io.is_debug():
io.write_line("Deactivating the PyPI repository") io.write_line("Deactivating the PyPI repository")
else: else:
from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.pypi_repository import PyPiRepository
default = not pool.has_primary_repositories() if pool.has_primary_repositories():
pypi_priority = Priority.SECONDARY
else:
pypi_priority = Priority.DEFAULT
pool.add_repository( pool.add_repository(
PyPiRepository(disable_cache=disable_cache), default, not default PyPiRepository(disable_cache=disable_cache), priority=pypi_priority
) )
return pool return pool
......
...@@ -26,20 +26,28 @@ ...@@ -26,20 +26,28 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"description": "The name of the repository" "description": "The name of the repository."
}, },
"url": { "url": {
"type": "string", "type": "string",
"description": "The url of the repository", "description": "The url of the repository.",
"format": "uri" "format": "uri"
}, },
"default": { "default": {
"type": "boolean", "type": "boolean",
"description": "Make this repository the default (disable PyPI)" "description": "Make this repository the default (disable PyPI). (deprecated, see priority)"
}, },
"secondary": { "secondary": {
"type": "boolean", "type": "boolean",
"description": "Declare this repository as secondary, i.e. it will only be looked up last for packages." "description": "Declare this repository as secondary, i.e. default repositories take precedence. (deprecated, see priority)"
},
"priority": {
"enum": [
"primary",
"default",
"secondary"
],
"description": "Declare the priority of this repository."
}, },
"links": { "links": {
"type": "boolean", "type": "boolean",
...@@ -49,6 +57,22 @@ ...@@ -49,6 +57,22 @@
"type": "boolean", "type": "boolean",
"description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)" "description": "For PEP 503 simple API repositories, pre-fetch and index the available packages. (experimental)"
} }
},
"not": {
"anyOf": [
{
"required": [
"priority",
"default"
]
},
{
"required": [
"priority",
"secondary"
]
}
]
} }
} }
} }
......
from __future__ import annotations from __future__ import annotations
import enum import enum
import warnings
from collections import OrderedDict from collections import OrderedDict
from dataclasses import dataclass from dataclasses import dataclass
...@@ -71,13 +72,24 @@ class RepositoryPool(AbstractRepository): ...@@ -71,13 +72,24 @@ class RepositoryPool(AbstractRepository):
return name.lower() in self._repositories return name.lower() in self._repositories
def repository(self, name: str) -> Repository: def repository(self, name: str) -> Repository:
return self._get_prioritized_repository(name).repository
def get_priority(self, name: str) -> Priority:
return self._get_prioritized_repository(name).priority
def _get_prioritized_repository(self, name: str) -> PrioritizedRepository:
name = name.lower() name = name.lower()
if self.has_repository(name): if self.has_repository(name):
return self._repositories[name].repository return self._repositories[name]
raise IndexError(f'Repository "{name}" does not exist.') raise IndexError(f'Repository "{name}" does not exist.')
def add_repository( def add_repository(
self, repository: Repository, default: bool = False, secondary: bool = False self,
repository: Repository,
default: bool = False,
secondary: bool = False,
*,
priority: Priority = Priority.PRIMARY,
) -> RepositoryPool: ) -> RepositoryPool:
""" """
Adds a repository to the pool. Adds a repository to the pool.
...@@ -88,14 +100,24 @@ class RepositoryPool(AbstractRepository): ...@@ -88,14 +100,24 @@ class RepositoryPool(AbstractRepository):
f"A repository with name {repository_name} was already added." f"A repository with name {repository_name} was already added."
) )
if default and self.has_default(): 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,
)
if default:
priority = Priority.DEFAULT
else:
priority = Priority.SECONDARY
if priority is Priority.DEFAULT and self.has_default():
raise ValueError("Only one repository can be the default.") raise ValueError("Only one repository can be the default.")
priority = Priority.PRIMARY
if default:
priority = Priority.DEFAULT
elif secondary:
priority = Priority.SECONDARY
self._repositories[repository_name] = PrioritizedRepository( self._repositories[repository_name] = PrioritizedRepository(
repository, priority repository, priority
) )
...@@ -103,7 +125,9 @@ class RepositoryPool(AbstractRepository): ...@@ -103,7 +125,9 @@ class RepositoryPool(AbstractRepository):
def remove_repository(self, name: str) -> RepositoryPool: def remove_repository(self, name: str) -> RepositoryPool:
if not self.has_repository(name): if not self.has_repository(name):
raise IndexError(f"Pool can not remove unknown repository '{name}'.") raise IndexError(
f"RepositoryPool can not remove unknown repository '{name}'."
)
del self._repositories[name.lower()] del self._repositories[name.lower()]
return self return self
......
...@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING ...@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
import pytest import pytest
from poetry.config.source import Source from poetry.config.source import Source
from poetry.repositories.repository_pool import Priority
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -24,15 +25,32 @@ def source_two() -> Source: ...@@ -24,15 +25,32 @@ def source_two() -> Source:
@pytest.fixture @pytest.fixture
def source_default() -> Source: def source_default_deprecated() -> Source:
return Source(name="default", url="https://default.com", default=True) return Source(name="default", url="https://default.com", default=True)
@pytest.fixture @pytest.fixture
def source_secondary() -> Source: def source_secondary_deprecated() -> Source:
return Source(name="secondary", url="https://secondary.com", secondary=True) 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
)
_existing_source = Source(name="existing", url="https://existing.com") _existing_source = Source(name="existing", url="https://existing.com")
...@@ -41,7 +59,7 @@ def source_existing() -> Source: ...@@ -41,7 +59,7 @@ def source_existing() -> Source:
return _existing_source return _existing_source
PYPROJECT_WITH_SOURCES = f""" PYPROJECT_WITHOUT_SOURCES = """
[tool.poetry] [tool.poetry]
name = "source-command-test" name = "source-command-test"
version = "0.1.0" version = "0.1.0"
...@@ -52,6 +70,10 @@ authors = ["Poetry Tester <tester@poetry.org>"] ...@@ -52,6 +70,10 @@ authors = ["Poetry Tester <tester@poetry.org>"]
python = "^3.9" python = "^3.9"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
"""
PYPROJECT_WITH_SOURCES = f"""{PYPROJECT_WITHOUT_SOURCES}
[[tool.poetry.source]] [[tool.poetry.source]]
name = "{_existing_source.name}" name = "{_existing_source.name}"
...@@ -60,6 +82,11 @@ url = "{_existing_source.url}" ...@@ -60,6 +82,11 @@ url = "{_existing_source.url}"
@pytest.fixture @pytest.fixture
def poetry_without_source(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITHOUT_SOURCES)
@pytest.fixture
def poetry_with_source(project_factory: ProjectFactory) -> Poetry: def poetry_with_source(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES) return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES)
...@@ -74,3 +101,20 @@ def add_multiple_sources( ...@@ -74,3 +101,20 @@ def add_multiple_sources(
add = command_tester_factory("source add", poetry=poetry_with_source) add = command_tester_factory("source add", poetry=poetry_with_source)
for source in [source_one, source_two]: for source in [source_one, source_two]:
add.execute(f"{source.name} {source.url}") add.execute(f"{source.name} {source.url}")
@pytest.fixture
def add_all_source_types(
command_tester_factory: CommandTesterFactory,
poetry_with_source: Poetry,
source_primary: Source,
source_default: Source,
source_secondary: Source,
) -> None:
add = command_tester_factory("source add", poetry=poetry_with_source)
for source in [
source_primary,
source_default,
source_secondary,
]:
add.execute(f"{source.name} {source.url} --priority={source.name}")
from __future__ import annotations from __future__ import annotations
import dataclasses
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
from poetry.config.source import Source
from poetry.repositories.repository_pool import Priority
if TYPE_CHECKING: if TYPE_CHECKING:
from cleo.testers.command_tester import CommandTester from cleo.testers.command_tester import CommandTester
from poetry.config.source import Source
from poetry.poetry import Poetry from poetry.poetry import Poetry
from tests.types import CommandTesterFactory from tests.types import CommandTesterFactory
...@@ -22,6 +22,28 @@ def tester( ...@@ -22,6 +22,28 @@ def tester(
return command_tester_factory("source add", poetry=poetry_with_source) return command_tester_factory("source add", poetry=poetry_with_source)
def assert_source_added_legacy(
tester: CommandTester,
poetry: Poetry,
source_existing: Source,
source_added: Source,
) -> None:
assert (
tester.io.fetch_error().strip()
== "Warning: Priority was set through a deprecated flag"
" (--default or --secondary). Consider using --priority next"
" time."
)
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( def assert_source_added(
tester: CommandTester, tester: CommandTester,
poetry: Poetry, poetry: Poetry,
...@@ -48,27 +70,73 @@ def test_source_add_simple( ...@@ -48,27 +70,73 @@ def test_source_add_simple(
assert_source_added(tester, poetry_with_source, source_existing, source_one) assert_source_added(tester, poetry_with_source, source_existing, source_one)
def test_source_add_default( def test_source_add_default_legacy(
tester: CommandTester, tester: CommandTester,
source_existing: Source, source_existing: Source,
source_default: Source, source_default: Source,
poetry_with_source: Poetry, poetry_with_source: Poetry,
) -> None: ) -> None:
tester.execute(f"--default {source_default.name} {source_default.url}") 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,
):
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,
):
tester.execute(f"--priority=default {source_default.name} {source_default.url}")
assert_source_added(tester, poetry_with_source, source_existing, source_default) 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,
):
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"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( def test_source_add_secondary(
tester: CommandTester, tester: CommandTester,
source_existing: Source, source_existing: Source,
source_secondary: Source, source_secondary: Source,
poetry_with_source: Poetry, poetry_with_source: Poetry,
) -> None: ) -> None:
tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}") tester.execute(
f"--priority=secondary {source_secondary.name} {source_secondary.url}"
)
assert_source_added(tester, poetry_with_source, source_existing, source_secondary) assert_source_added(tester, poetry_with_source, source_existing, source_secondary)
def test_source_add_error_default_and_secondary(tester: CommandTester) -> None: def test_source_add_error_default_and_secondary_legacy(tester: CommandTester) -> None:
tester.execute("--default --secondary error https://error.com") tester.execute("--default --secondary error https://error.com")
assert ( assert (
tester.io.fetch_error().strip() tester.io.fetch_error().strip()
...@@ -77,6 +145,17 @@ def test_source_add_error_default_and_secondary(tester: CommandTester) -> None: ...@@ -77,6 +145,17 @@ def test_source_add_error_default_and_secondary(tester: CommandTester) -> None:
assert tester.status_code == 1 assert tester.status_code == 1
def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester):
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_pypi(tester: CommandTester) -> None: def test_source_add_error_pypi(tester: CommandTester) -> None:
tester.execute("pypi https://test.pypi.org/simple/") tester.execute("pypi https://test.pypi.org/simple/")
assert ( assert (
...@@ -87,11 +166,17 @@ def test_source_add_error_pypi(tester: CommandTester) -> None: ...@@ -87,11 +166,17 @@ def test_source_add_error_pypi(tester: CommandTester) -> None:
assert tester.status_code == 1 assert tester.status_code == 1
def test_source_add_existing( def test_source_add_existing_legacy(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
) -> None: ) -> None:
tester.execute(f"--default {source_existing.name} {source_existing.url}") tester.execute(f"--default {source_existing.name} {source_existing.url}")
assert ( assert (
tester.io.fetch_error().strip()
== "Warning: Priority was set through a deprecated flag"
" (--default or --secondary). Consider using --priority next"
" time."
)
assert (
tester.io.fetch_output().strip() tester.io.fetch_output().strip()
== f"Source with name {source_existing.name} already exists. Updating." == f"Source with name {source_existing.name} already exists. Updating."
) )
...@@ -101,4 +186,64 @@ def test_source_add_existing( ...@@ -101,4 +186,64 @@ def test_source_add_existing(
assert len(sources) == 1 assert len(sources) == 1
assert sources[0] != source_existing assert sources[0] != source_existing
assert sources[0] == dataclasses.replace(source_existing, default=True) expected_source = Source(
name=source_existing.name, url=source_existing.url, priority=Priority.DEFAULT
)
assert sources[0] == expected_source
def test_source_add_existing_no_change(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
):
tester.execute(f"--priority=primary {source_existing.name} {source_existing.url}")
assert (
tester.io.fetch_output().strip()
== f"Source with name {source_existing.name} already exists. Skipping addition."
)
poetry_with_source.pyproject.reload()
sources = poetry_with_source.get_sources()
assert len(sources) == 1
assert sources[0] == source_existing
def test_source_add_existing_updating(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
):
tester.execute(f"--priority=default {source_existing.name} {source_existing.url}")
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
def test_source_add_existing_fails_due_to_other_default(
tester: CommandTester,
source_existing: Source,
source_default: Source,
poetry_with_source: Poetry,
):
tester.execute(f"--priority=default {source_default.name} {source_default.url}")
tester.io.fetch_output()
tester.execute(f"--priority=default {source_existing.name} {source_existing.url}")
assert (
tester.io.fetch_error().strip()
== 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
...@@ -22,24 +22,38 @@ def tester( ...@@ -22,24 +22,38 @@ def tester(
return command_tester_factory("source show", poetry=poetry_with_source) return command_tester_factory("source show", poetry=poetry_with_source)
@pytest.fixture
def tester_no_sources(
command_tester_factory: CommandTesterFactory,
poetry_without_source: Poetry,
) -> CommandTester:
return command_tester_factory("source show", poetry=poetry_without_source)
@pytest.fixture
def tester_all_types(
command_tester_factory: CommandTesterFactory,
poetry_with_source: Poetry,
add_all_source_types: None,
) -> CommandTester:
return command_tester_factory("source show", poetry=poetry_with_source)
def test_source_show_simple(tester: CommandTester) -> None: def test_source_show_simple(tester: CommandTester) -> None:
tester.execute("") tester.execute("")
expected = """\ expected = """\
name : existing name : existing
url : https://existing.com url : https://existing.com
default : no priority : primary
secondary : no
name : one
name : one url : https://one.com
url : https://one.com priority : primary
default : no
secondary : no name : two
url : https://two.com
name : two priority : primary
url : https://two.com
default : no
secondary : no
""".splitlines() """.splitlines()
assert [ assert [
line.strip() for line in tester.io.fetch_output().strip().splitlines() line.strip() for line in tester.io.fetch_output().strip().splitlines()
...@@ -51,10 +65,9 @@ def test_source_show_one(tester: CommandTester, source_one: Source) -> None: ...@@ -51,10 +65,9 @@ def test_source_show_one(tester: CommandTester, source_one: Source) -> None:
tester.execute(f"{source_one.name}") tester.execute(f"{source_one.name}")
expected = """\ expected = """\
name : one name : one
url : https://one.com url : https://one.com
default : no priority : primary
secondary : no
""".splitlines() """.splitlines()
assert [ assert [
line.strip() for line in tester.io.fetch_output().strip().splitlines() line.strip() for line in tester.io.fetch_output().strip().splitlines()
...@@ -68,15 +81,13 @@ def test_source_show_two( ...@@ -68,15 +81,13 @@ def test_source_show_two(
tester.execute(f"{source_one.name} {source_two.name}") tester.execute(f"{source_one.name} {source_two.name}")
expected = """\ expected = """\
name : one name : one
url : https://one.com url : https://one.com
default : no priority : primary
secondary : no
name : two
name : two url : https://two.com
url : https://two.com priority : primary
default : no
secondary : no
""".splitlines() """.splitlines()
assert [ assert [
line.strip() for line in tester.io.fetch_output().strip().splitlines() line.strip() for line in tester.io.fetch_output().strip().splitlines()
...@@ -84,6 +95,40 @@ secondary : no ...@@ -84,6 +95,40 @@ secondary : no
assert tester.status_code == 0 assert tester.status_code == 0
@pytest.mark.parametrize(
"source_str",
(
"source_primary",
"source_default",
"source_secondary",
),
)
def test_source_show_given_priority(
tester_all_types: CommandTester, source_str: Source, request: pytest.FixtureRequest
) -> None:
source = request.getfixturevalue(source_str)
tester_all_types.execute(f"{source.name}")
expected = f"""\
name : {source.name}
url : {source.url}
priority : {source.name}
""".splitlines()
assert [
line.strip() for line in tester_all_types.io.fetch_output().strip().splitlines()
] == expected
assert tester_all_types.status_code == 0
def test_source_show_no_sources(tester_no_sources: CommandTester) -> None:
tester_no_sources.execute("error")
assert (
tester_no_sources.io.fetch_output().strip()
== "No sources configured for this project."
)
assert tester_no_sources.status_code == 0
def test_source_show_error(tester: CommandTester) -> None: def test_source_show_error(tester: CommandTester) -> None:
tester.execute("error") tester.execute("error")
assert tester.io.fetch_error().strip() == "No source found with name(s): error" assert tester.io.fetch_error().strip() == "No source found with name(s): error"
......
...@@ -164,7 +164,7 @@ def test_list_must_not_display_sources_from_pyproject_toml( ...@@ -164,7 +164,7 @@ def test_list_must_not_display_sources_from_pyproject_toml(
config: Config, config: Config,
config_cache_dir: Path, config_cache_dir: Path,
): ):
source = fixture_dir("with_non_default_source") source = fixture_dir("with_non_default_source_implicit")
pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8")
poetry = project_factory("foo", pyproject_content=pyproject_content) poetry = project_factory("foo", pyproject_content=pyproject_content)
tester = command_tester_factory("config", poetry=poetry) tester = command_tester_factory("config", poetry=poetry)
......
...@@ -58,4 +58,4 @@ my-script = "my_package:main" ...@@ -58,4 +58,4 @@ my-script = "my_package:main"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
default = true priority = "default"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
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 = "~2.7 || ^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 = "~2.7" }
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.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/"
default = true
...@@ -16,9 +16,9 @@ python = "~2.7 || ^3.6" ...@@ -16,9 +16,9 @@ python = "~2.7 || ^3.6"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
secondary = true priority = "secondary"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "bar" name = "bar"
url = "https://bar.baz/simple/" url = "https://bar.baz/simple/"
secondary = true priority = "secondary"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
[tool.poetry.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
...@@ -16,7 +16,7 @@ python = "~2.7 || ^3.6" ...@@ -16,7 +16,7 @@ python = "~2.7 || ^3.6"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
secondary = true priority = "secondary"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "bar" name = "bar"
......
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
secondary = true
[[tool.poetry.source]]
name = "bar"
url = "https://bar.baz/simple/"
...@@ -16,4 +16,4 @@ python = "~2.7 || ^3.6" ...@@ -16,4 +16,4 @@ python = "~2.7 || ^3.6"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
secondary = true priority = "secondary"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
secondary = true
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Your Name <you@example.com>"
]
license = "MIT"
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
[tool.poetry.dev-dependencies]
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
priority = "primary"
...@@ -58,9 +58,9 @@ my-script = "my_package:main" ...@@ -58,9 +58,9 @@ my-script = "my_package:main"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "foo" name = "foo"
url = "https://foo.bar/simple/" url = "https://foo.bar/simple/"
default = true priority = "default"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "bar" name = "bar"
url = "https://bar.foo/simple/" url = "https://bar.foo/simple/"
default = true priority = "default"
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
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 = "~2.7 || ^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 = "~2.7" }
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.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/"
default = true
[[tool.poetry.source]]
name = "bar"
url = "https://bar.foo/simple/"
default = true
...@@ -13,6 +13,7 @@ from poetry.core.packages.package import Package ...@@ -13,6 +13,7 @@ from poetry.core.packages.package import Package
from poetry.installation.pip_installer import PipInstaller from poetry.installation.pip_installer import PipInstaller
from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.repository_pool import Priority
from poetry.repositories.repository_pool import RepositoryPool from poetry.repositories.repository_pool import RepositoryPool
from poetry.utils.authenticator import RepositoryCertificateConfig from poetry.utils.authenticator import RepositoryCertificateConfig
from poetry.utils.env import NullEnv from poetry.utils.env import NullEnv
...@@ -137,7 +138,7 @@ def test_install_with_non_pypi_default_repository( ...@@ -137,7 +138,7 @@ def test_install_with_non_pypi_default_repository(
default = LegacyRepository("default", "https://default.com") default = LegacyRepository("default", "https://default.com")
another = LegacyRepository("another", "https://another.com") another = LegacyRepository("another", "https://another.com")
pool.add_repository(default, default=True) pool.add_repository(default, priority=Priority.DEFAULT)
pool.add_repository(another) pool.add_repository(another)
foo = Package( foo = Package(
...@@ -177,7 +178,7 @@ def test_install_with_certs( ...@@ -177,7 +178,7 @@ def test_install_with_certs(
default = LegacyRepository("default", "https://foo.bar") default = LegacyRepository("default", "https://foo.bar")
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(default, default=True) pool.add_repository(default, priority=Priority.DEFAULT)
installer = PipInstaller(env, NullIO(), pool) installer = PipInstaller(env, NullIO(), pool)
...@@ -255,7 +256,7 @@ def test_install_with_trusted_host(config: Config, env: NullEnv) -> None: ...@@ -255,7 +256,7 @@ def test_install_with_trusted_host(config: Config, env: NullEnv) -> None:
default = LegacyRepository("default", "https://foo.bar") default = LegacyRepository("default", "https://foo.bar")
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(default, default=True) pool.add_repository(default, priority=Priority.DEFAULT)
installer = PipInstaller(env, NullIO(), pool) installer = PipInstaller(env, NullIO(), pool)
......
[tool.poetry]
name = "foobar"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.10"
[[tool.poetry.source]]
name = "pypi-simple"
url = "https://pypi.org/simple/"
priority = "arbitrary"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "foobar"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
[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"
[tool.poetry]
name = "foobar"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.10"
[[tool.poetry.source]]
name = "pypi-simple"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
...@@ -10,8 +10,7 @@ python = "^3.10" ...@@ -10,8 +10,7 @@ python = "^3.10"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "pypi-simple" name = "pypi-simple"
url = "https://pypi.org/simple/" url = "https://pypi.org/simple/"
default = false priority = "primary"
secondary = false
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
......
...@@ -9,6 +9,7 @@ python = "^3.10" ...@@ -9,6 +9,7 @@ python = "^3.10"
[[tool.poetry.source]] [[tool.poetry.source]]
name = "pypi-simple" name = "pypi-simple"
url = "https://pypi.org/simple/"
default = false default = false
secondary = false secondary = false
......
...@@ -9,16 +9,51 @@ from poetry.toml import TOMLFile ...@@ -9,16 +9,51 @@ from poetry.toml import TOMLFile
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "source" FIXTURE_DIR = Path(__file__).parent / "fixtures" / "source"
def test_pyproject_toml_valid_legacy() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_valid_legacy.toml").read()
content = toml["tool"]["poetry"]
assert Factory.validate(content) == {"errors": [], "warnings": []}
def test_pyproject_toml_valid() -> None: def test_pyproject_toml_valid() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read() toml = TOMLFile(FIXTURE_DIR / "complete_valid.toml").read()
content = toml["tool"]["poetry"] content = toml["tool"]["poetry"]
assert Factory.validate(content) == {"errors": [], "warnings": []} assert Factory.validate(content) == {"errors": [], "warnings": []}
def test_pyproject_toml_invalid() -> None: def test_pyproject_toml_invalid_url() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_invalid.toml").read() toml = TOMLFile(FIXTURE_DIR / "complete_invalid_url.toml").read()
content = toml["tool"]["poetry"] content = toml["tool"]["poetry"]
assert Factory.validate(content) == { assert Factory.validate(content) == {
"errors": ["[source.0] 'url' is a required property"], "errors": ["[source.0] 'url' is a required property"],
"warnings": [], "warnings": [],
} }
def test_pyproject_toml_invalid_priority() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_invalid_priority.toml").read()
content = toml["tool"]["poetry"]
assert Factory.validate(content) == {
"errors": [
"[source.0.priority] 'arbitrary' is not one of ['primary', 'default',"
" 'secondary']"
],
"warnings": [],
}
def test_pyproject_toml_invalid_priority_legacy_and_new() -> None:
toml = TOMLFile(
FIXTURE_DIR / "complete_invalid_priority_legacy_and_new.toml"
).read()
content = toml["tool"]["poetry"]
assert Factory.validate(content) == {
"errors": [
"[source.0] {'name': 'pypi-simple', 'url': "
"'https://pypi.org/simple/', 'default': False, 'priority': "
"'primary'} should not be valid under {'anyOf': [{'required': "
"['priority', 'default']}, {'required': ['priority', "
"'secondary']}]}"
],
"warnings": [],
}
...@@ -23,6 +23,7 @@ from poetry.puzzle import Solver ...@@ -23,6 +23,7 @@ from poetry.puzzle import Solver
from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.exceptions import SolverProblemError
from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.puzzle.provider import IncompatibleConstraintsError
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
from poetry.repositories.repository_pool import Priority
from poetry.repositories.repository_pool import RepositoryPool from poetry.repositories.repository_pool import RepositoryPool
from poetry.utils.env import MockEnv from poetry.utils.env import MockEnv
from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import MOCK_DEFAULT_GIT_REVISION
...@@ -2915,7 +2916,7 @@ def test_solver_does_not_choose_from_secondary_repository_by_default( ...@@ -2915,7 +2916,7 @@ def test_solver_does_not_choose_from_secondary_repository_by_default(
package.add_dependency(Factory.create_dependency("clikit", {"version": "^0.2.0"})) package.add_dependency(Factory.create_dependency("clikit", {"version": "^0.2.0"}))
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(MockPyPIRepository(), secondary=True) pool.add_repository(MockPyPIRepository(), priority=Priority.SECONDARY)
pool.add_repository(MockLegacyRepository()) pool.add_repository(MockLegacyRepository())
solver = Solver(package, pool, [], [], io) solver = Solver(package, pool, [], [], io)
...@@ -2965,7 +2966,7 @@ def test_solver_chooses_from_secondary_if_explicit( ...@@ -2965,7 +2966,7 @@ def test_solver_chooses_from_secondary_if_explicit(
) )
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(MockPyPIRepository(), secondary=True) pool.add_repository(MockPyPIRepository(), priority=Priority.SECONDARY)
pool.add_repository(MockLegacyRepository()) pool.add_repository(MockLegacyRepository())
solver = Solver(package, pool, [], [], io) solver = Solver(package, pool, [], [], io)
......
...@@ -8,6 +8,7 @@ from poetry.repositories import Repository ...@@ -8,6 +8,7 @@ from poetry.repositories import Repository
from poetry.repositories import RepositoryPool from poetry.repositories import RepositoryPool
from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import PackageNotFound
from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.repository_pool import Priority
from tests.helpers import get_dependency from tests.helpers import get_dependency
from tests.helpers import get_package from tests.helpers import get_package
...@@ -27,6 +28,7 @@ def test_pool_with_initial_repositories() -> None: ...@@ -27,6 +28,7 @@ def test_pool_with_initial_repositories() -> None:
assert len(pool.repositories) == 1 assert len(pool.repositories) == 1
assert not pool.has_default() assert not pool.has_default()
assert pool.has_primary_repositories() assert pool.has_primary_repositories()
assert pool.get_priority("repo") == Priority.PRIMARY
def test_repository_no_repository() -> None: def test_repository_no_repository() -> None:
...@@ -47,20 +49,36 @@ def test_adding_repositories_with_same_name_twice_raises_value_error() -> None: ...@@ -47,20 +49,36 @@ def test_adding_repositories_with_same_name_twice_raises_value_error() -> None:
RepositoryPool([repo1]).add_repository(repo2) RepositoryPool([repo1]).add_repository(repo2)
def test_repository_from_normal_pool() -> None: @pytest.mark.parametrize("priority", (p for p in Priority))
def test_repository_from_single_repo_pool(priority: Priority) -> None:
repo = LegacyRepository("foo", "https://foo.bar") repo = LegacyRepository("foo", "https://foo.bar")
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(repo)
assert pool.repository("foo") is repo
pool.add_repository(repo, priority=priority)
def test_repository_from_secondary_pool() -> None: assert pool.repository("foo") is repo
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") repo = LegacyRepository("foo", "https://foo.bar")
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(repo, secondary=True)
with pytest.warns(DeprecationWarning):
pool.add_repository(repo, default=default, secondary=secondary)
assert pool.repository("foo") is repo assert pool.repository("foo") is repo
assert pool.get_priority("foo") == expected_priority
def test_repository_with_normal_default_and_secondary_repositories() -> None: def test_repository_with_normal_default_and_secondary_repositories() -> None:
...@@ -71,9 +89,9 @@ def test_repository_with_normal_default_and_secondary_repositories() -> None: ...@@ -71,9 +89,9 @@ def test_repository_with_normal_default_and_secondary_repositories() -> None:
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(repo1) pool.add_repository(repo1)
pool.add_repository(secondary, secondary=True) pool.add_repository(secondary, priority=Priority.SECONDARY)
pool.add_repository(repo2) pool.add_repository(repo2)
pool.add_repository(default, default=True) pool.add_repository(default, priority=Priority.DEFAULT)
assert pool.repository("secondary") is secondary assert pool.repository("secondary") is secondary
assert pool.repository("default") is default assert pool.repository("default") is default
...@@ -115,19 +133,19 @@ def test_remove_default_repository() -> None: ...@@ -115,19 +133,19 @@ def test_remove_default_repository() -> None:
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(repo1) pool.add_repository(repo1)
pool.add_repository(repo2) pool.add_repository(repo2)
pool.add_repository(default, default=True) pool.add_repository(default, priority=Priority.DEFAULT)
assert pool.has_default() assert pool.has_default()
pool.remove_repository("default") pool.remove_repository("default")
assert not pool.has_repository("default")
assert not pool.has_default() assert not pool.has_default()
pool.add_repository(new_default, default=True) pool.add_repository(new_default, priority=Priority.DEFAULT)
assert pool.get_priority("new_default") is Priority.DEFAULT
assert pool.has_default() assert pool.has_default()
assert pool.repositories[0] is new_default
assert not pool.has_repository("default")
def test_repository_ordering() -> None: def test_repository_ordering() -> None:
...@@ -141,21 +159,21 @@ def test_repository_ordering() -> None: ...@@ -141,21 +159,21 @@ def test_repository_ordering() -> None:
secondary3 = LegacyRepository("secondary3", "https://secondary3.com") secondary3 = LegacyRepository("secondary3", "https://secondary3.com")
pool = RepositoryPool() pool = RepositoryPool()
pool.add_repository(secondary1, secondary=True) pool.add_repository(secondary1, priority=Priority.SECONDARY)
pool.add_repository(primary1) pool.add_repository(primary1)
pool.add_repository(default1, default=True) pool.add_repository(default1, priority=Priority.DEFAULT)
pool.add_repository(primary2) pool.add_repository(primary2)
pool.add_repository(secondary2, secondary=True) pool.add_repository(secondary2, priority=Priority.SECONDARY)
pool.remove_repository("primary2") pool.remove_repository("primary2")
pool.remove_repository("secondary2") pool.remove_repository("secondary2")
pool.add_repository(primary3) pool.add_repository(primary3)
pool.add_repository(secondary3, secondary=True) pool.add_repository(secondary3, priority=Priority.SECONDARY)
assert pool.repositories == [default1, primary1, primary3, secondary1, secondary3] assert pool.repositories == [default1, primary1, primary3, secondary1, secondary3]
with pytest.raises(ValueError): with pytest.raises(ValueError):
pool.add_repository(default2, default=True) pool.add_repository(default2, priority=Priority.DEFAULT)
def test_pool_get_package_in_any_repository() -> None: def test_pool_get_package_in_any_repository() -> None:
......
...@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING ...@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
import pytest import pytest
from cleo.io.buffered_io import BufferedIO
from deepdiff import DeepDiff from deepdiff import DeepDiff
from packaging.utils import canonicalize_name from packaging.utils import canonicalize_name
from poetry.core.constraints.version import parse_constraint from poetry.core.constraints.version import parse_constraint
...@@ -13,6 +14,7 @@ from poetry.factory import Factory ...@@ -13,6 +14,7 @@ from poetry.factory import Factory
from poetry.plugins.plugin import Plugin from poetry.plugins.plugin import Plugin
from poetry.repositories.legacy_repository import LegacyRepository from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.repository_pool import Priority
from poetry.toml.file import TOMLFile from poetry.toml.file import TOMLFile
from tests.helpers import mock_metadata_entry_points from tests.helpers import mock_metadata_entry_points
...@@ -206,40 +208,80 @@ def test_create_poetry_with_multi_constraints_dependency(): ...@@ -206,40 +208,80 @@ def test_create_poetry_with_multi_constraints_dependency():
assert len(package.requires) == 2 assert len(package.requires) == 2
def test_poetry_with_default_source(with_simple_keyring: None): def test_poetry_with_default_source_legacy(with_simple_keyring: None):
poetry = Factory().create_poetry(fixtures_dir / "with_default_source") io = BufferedIO()
poetry = Factory().create_poetry(fixtures_dir / "with_default_source_legacy", io=io)
assert len(poetry.pool.repositories) == 1 assert len(poetry.pool.repositories) == 1
assert "Found deprecated key" in io.fetch_error()
def test_poetry_with_default_source(with_simple_keyring: None):
io = BufferedIO()
poetry = Factory().create_poetry(fixtures_dir / "with_default_source", io=io)
assert len(poetry.pool.repositories) == 1
assert io.fetch_error() == ""
def test_poetry_with_non_default_source(with_simple_keyring: None):
poetry = Factory().create_poetry(fixtures_dir / "with_non_default_source")
assert len(poetry.pool.repositories) == 2 @pytest.mark.parametrize(
"fixture_filename",
("with_non_default_source_implicit", "with_non_default_source_explicit"),
)
def test_poetry_with_non_default_source(
fixture_filename: str, with_simple_keyring: None
):
poetry = Factory().create_poetry(fixtures_dir / fixture_filename)
assert not poetry.pool.has_default() assert not poetry.pool.has_default()
assert poetry.pool.has_repository("PyPI")
assert poetry.pool.get_priority("PyPI") is Priority.SECONDARY
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.has_repository("foo")
assert poetry.pool.get_priority("foo") is Priority.PRIMARY
assert isinstance(poetry.pool.repository("foo"), LegacyRepository)
assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "foo"}
assert poetry.pool.repositories[0].name == "foo" def test_poetry_with_non_default_secondary_source_legacy(with_simple_keyring: None):
assert isinstance(poetry.pool.repositories[0], LegacyRepository) poetry = Factory().create_poetry(
fixtures_dir / "with_non_default_secondary_source_legacy"
)
assert poetry.pool.repositories[1].name == "PyPI" assert poetry.pool.has_repository("PyPI")
assert isinstance(poetry.pool.repositories[1], PyPiRepository) assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT
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(with_simple_keyring: None): def test_poetry_with_non_default_secondary_source(with_simple_keyring: None):
poetry = Factory().create_poetry(fixtures_dir / "with_non_default_secondary_source") poetry = Factory().create_poetry(fixtures_dir / "with_non_default_secondary_source")
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.DEFAULT
assert poetry.pool.has_repository("foo")
assert isinstance(poetry.pool.repository("foo"), LegacyRepository)
assert [repo.name for repo in poetry.pool.repositories] == ["PyPI", "foo"]
assert poetry.pool.has_default()
repository = poetry.pool.repositories[0] def test_poetry_with_non_default_multiple_secondary_sources_legacy(
assert repository.name == "PyPI" with_simple_keyring: None,
assert isinstance(repository, PyPiRepository) ):
poetry = Factory().create_poetry(
fixtures_dir / "with_non_default_multiple_secondary_sources_legacy"
)
repository = poetry.pool.repositories[1] assert poetry.pool.has_repository("PyPI")
assert repository.name == "foo" assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert isinstance(repository, LegacyRepository) assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT
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(with_simple_keyring: None): def test_poetry_with_non_default_multiple_secondary_sources(with_simple_keyring: None):
...@@ -247,52 +289,60 @@ def test_poetry_with_non_default_multiple_secondary_sources(with_simple_keyring: ...@@ -247,52 +289,60 @@ def test_poetry_with_non_default_multiple_secondary_sources(with_simple_keyring:
fixtures_dir / "with_non_default_multiple_secondary_sources" fixtures_dir / "with_non_default_multiple_secondary_sources"
) )
assert len(poetry.pool.repositories) == 3 assert poetry.pool.has_repository("PyPI")
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT
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"}
assert poetry.pool.has_default()
repository = poetry.pool.repositories[0] def test_poetry_with_non_default_multiple_sources_legacy(with_simple_keyring: None):
assert repository.name == "PyPI" poetry = Factory().create_poetry(
assert isinstance(repository, PyPiRepository) fixtures_dir / "with_non_default_multiple_sources_legacy"
)
repository = poetry.pool.repositories[1]
assert repository.name == "foo"
assert isinstance(repository, LegacyRepository)
repository = poetry.pool.repositories[2] assert not poetry.pool.has_default()
assert repository.name == "bar" assert poetry.pool.has_repository("bar")
assert isinstance(repository, LegacyRepository) assert isinstance(poetry.pool.repository("bar"), LegacyRepository)
assert poetry.pool.has_repository("PyPI")
assert poetry.pool.get_priority("PyPI") is Priority.SECONDARY
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert poetry.pool.has_repository("foo")
assert isinstance(poetry.pool.repository("foo"), LegacyRepository)
assert {repo.name for repo in poetry.pool.repositories} == {"bar", "PyPI", "foo"}
def test_poetry_with_non_default_multiple_sources(with_simple_keyring: None): def test_poetry_with_non_default_multiple_sources(with_simple_keyring: None):
poetry = Factory().create_poetry(fixtures_dir / "with_non_default_multiple_sources") poetry = Factory().create_poetry(fixtures_dir / "with_non_default_multiple_sources")
assert len(poetry.pool.repositories) == 3
assert not poetry.pool.has_default() assert not poetry.pool.has_default()
assert poetry.pool.has_repository("PyPI")
repository = poetry.pool.repositories[0] assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert repository.name == "bar" assert poetry.pool.get_priority("PyPI") is Priority.SECONDARY
assert isinstance(repository, LegacyRepository) assert poetry.pool.has_repository("bar")
assert isinstance(poetry.pool.repository("bar"), LegacyRepository)
repository = poetry.pool.repositories[1] assert poetry.pool.has_repository("foo")
assert repository.name == "foo" assert isinstance(poetry.pool.repository("foo"), LegacyRepository)
assert isinstance(repository, LegacyRepository) assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "bar", "foo"}
repository = poetry.pool.repositories[2]
assert repository.name == "PyPI"
assert isinstance(repository, PyPiRepository)
def test_poetry_with_no_default_source(): def test_poetry_with_no_default_source():
poetry = Factory().create_poetry(fixtures_dir / "sample_project") poetry = Factory().create_poetry(fixtures_dir / "sample_project")
assert len(poetry.pool.repositories) == 1 assert poetry.pool.has_repository("PyPI")
assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT
assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
assert {repo.name for repo in poetry.pool.repositories} == {"PyPI"}
assert poetry.pool.has_default()
assert poetry.pool.repositories[0].name == "PyPI" def test_poetry_with_two_default_sources_legacy(with_simple_keyring: None):
assert isinstance(poetry.pool.repositories[0], PyPiRepository) with pytest.raises(ValueError) as e:
Factory().create_poetry(fixtures_dir / "with_two_default_sources_legacy")
assert str(e.value) == "Only one repository can be the default."
def test_poetry_with_two_default_sources(with_simple_keyring: None): def test_poetry_with_two_default_sources(with_simple_keyring: None):
......
...@@ -7,6 +7,7 @@ from tomlkit.items import Table ...@@ -7,6 +7,7 @@ from tomlkit.items import Table
from tomlkit.items import Trivia from tomlkit.items import Trivia
from poetry.config.source import Source from poetry.config.source import Source
from poetry.repositories.repository_pool import Priority
from poetry.utils.source import source_to_table from poetry.utils.source import source_to_table
...@@ -16,25 +17,58 @@ from poetry.utils.source import source_to_table ...@@ -16,25 +17,58 @@ from poetry.utils.source import source_to_table
( (
Source("foo", "https://example.com"), Source("foo", "https://example.com"),
{ {
"default": False,
"name": "foo", "name": "foo",
"secondary": False, "priority": "primary",
"url": "https://example.com", "url": "https://example.com",
}, },
), ),
( (
Source("bar", "https://example.com/bar", True, True), Source("bar", "https://example.com/bar", priority=Priority.SECONDARY),
{ {
"default": True,
"name": "bar", "name": "bar",
"secondary": True, "priority": "secondary",
"url": "https://example.com/bar", "url": "https://example.com/bar",
}, },
), ),
], ],
) )
def test_source_to_table(source: Source, table_body: dict[str, str | bool]): def test_source_to_table(source: Source, table_body: dict[str, str | bool]) -> None:
table = Table(Container(), Trivia(), False) table = Table(Container(), Trivia(), False)
table._value = table_body table._value = table_body
assert source_to_table(source) == table assert source_to_table(source) == table
def test_source_default_is_primary() -> None:
source = Source("foo", "https://example.com")
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),
],
)
def test_source_priority_as_string(priority: str, expected_priority: Priority) -> None:
source = Source("foo", "https://example.com", priority=priority)
assert source.priority == Priority.SECONDARY
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment