Skip to content

Commit

Permalink
sources: introduce "priority" key for sources and deprecate flags "de…
Browse files Browse the repository at this point in the history
…fault" and "secondary", adjust cli accordingly (python-poetry#7658)

Co-authored-by: Randy Döring <[email protected]>
  • Loading branch information
b-kamphorst and radoering committed Apr 16, 2023
1 parent 3a31f2d commit 5806b42
Show file tree
Hide file tree
Showing 38 changed files with 1,014 additions and 199 deletions.
7 changes: 4 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,11 +784,12 @@ You cannot use the name `pypi` as it is reserved for use by the default PyPI sou

#### Options

* `--default`: Set this source as the [default]({{< relref "repositories#disabling-the-pypi-repository" >}}) (disable PyPI).
* `--secondary`: Set this source as a [secondary]({{< relref "repositories#install-dependencies-from-a-private-repository" >}}) source.
* `--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" >}}), and [`secondary`]({{< relref "repositories#secondary-package-sources" >}}). Refer to the dedicated sections in [Repositories]({{< relref "repositories" >}}) for more information.

{{% 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 %}}

### source show
Expand Down
2 changes: 1 addition & 1 deletion docs/dependency-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ you can use the `source` property:
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
secondary = true
priority = "secondary"

[tool.poetry.dependencies]
my-cool-package = { version = "*", source = "foo" }
Expand Down
74 changes: 59 additions & 15 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ First, [configure](#project-configuration) the [package source](#package-source)
project.

```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.
Expand Down Expand Up @@ -120,12 +120,18 @@ This will generate the following configuration snippet in your
[[tool.poetry.source]]
name = "foo"
url = "https://foo.bar/simple/"
default = false
secondary = false
priority = "primary"
```

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 %}}

Expand All @@ -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
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
poetry source add --default foo https://foo.bar/simple/
poetry source add --priority=default foo https://foo.bar/simple/
```

{{% warning %}}
Expand All @@ -164,41 +170,79 @@ as a package source for your project.
#### Secondary Package Sources

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
source.
priority when selecting compatible package distribution that also exists in your default and primary package sources.

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.

```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.

{{% note %}}
#### Package Source Constraint

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
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
poetry add --source internal-pypi httpx
```

This results in the following configuration in `pyproject.toml`:

```toml
[tool.poetry.dependencies]
...
httpx = { version = "^0.22", source = "internal-pypi" }

[[tool.poetry.source]]
name = "internal-pypi"
url = "https://foo.bar/simple/"
secondary = true
url = ...
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 %}}

### Supported Package Sources
Expand Down Expand Up @@ -231,7 +275,7 @@ httpx = {version = "^0.22.0", source = "pypi"}

{{% 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.

{{% /warning %}}
Expand Down
35 changes: 32 additions & 3 deletions src/poetry/config/source.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
from __future__ import annotations

import dataclasses
import warnings

from poetry.repositories.repository_pool import Priority


@dataclasses.dataclass(order=True, eq=True)
class Source:
name: str
url: str
default: bool = dataclasses.field(default=False)
secondary: bool = dataclasses.field(default=False)
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:
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(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
},
)
76 changes: 60 additions & 16 deletions src/poetry/console/commands/source/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from poetry.config.source import Source
from poetry.console.commands.command import Command
from poetry.repositories.repository_pool import Priority


class SourceAddCommand(Command):
Expand All @@ -28,60 +29,103 @@ class SourceAddCommand(Command):
(
"Set this source as the default (disable PyPI). A "
"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:
from poetry.factory import Factory
from poetry.utils.source import source_to_table

name = self.argument("name")
url = self.argument("url")
is_default = self.option("default")
is_secondary = self.option("secondary")
name: str = self.argument("name")
url: str = self.argument("url")
is_default: bool = self.option("default", False)
is_secondary: bool = self.option("secondary", False)
priority: Priority | None = self.option("priority", None)

if is_default and is_secondary:
self.line_error(
"Cannot configure a source as both <c1>default</c1> and"
" <c1>secondary</c1>."
"<error>Cannot configure a source as both <c1>default</c1> and"
" <c1>secondary</c1>.</error>"
)
return 1

new_source: Source | None = Source(
name=name, url=url, default=is_default, secondary=is_secondary
)
if is_default or 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()

sources = AoT([])

is_new_source = True
for source in existing_sources:
if source == new_source:
self.line(
f"Source with name <c1>{name}</c1> already exists. Skipping"
" addition."
)
return 0
elif source.default and is_default:
elif (
source.priority is Priority.DEFAULT
and new_source.priority is Priority.DEFAULT
):
self.line_error(
f"<error>Source with name <c1>{source.name}</c1> is already set to"
" default. Only one default source can be configured at a"
" time.</error>"
)
return 1

if new_source and source.name == name:
self.line(f"Source with name <c1>{name}</c1> already exists. Updating.")
if source.name == name:
source = new_source
new_source = None
is_new_source = False

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>.")
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.
try:
Expand Down
18 changes: 6 additions & 12 deletions src/poetry/console/commands/source/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@ def handle(self) -> int:
return 0

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

bool_string = {
True: "yes",
False: "no",
}

for source in sources:
if names and source.name not in names:
continue
Expand All @@ -50,12 +48,8 @@ def handle(self) -> int:
["<info>name</>", f" : <c1>{source.name}</>"],
["<info>url</>", f" : {source.url}"],
[
"<info>default</>",
f" : {bool_string.get(source.default, False)}",
],
[
"<info>secondary</>",
f" : {bool_string.get(source.secondary, False)}",
"<info>priority</>",
f" : {source.priority.name.lower()}",
],
]
table.add_rows(rows)
Expand Down
Loading

0 comments on commit 5806b42

Please sign in to comment.