Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6713 Introduce explicit package source #7658

Merged
merged 3 commits into from
Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" >}}), [`secondary`]({{< relref "repositories#secondary-package-sources" >}}), and [`explicit`]({{< relref "repositories#explicit-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
91 changes: 76 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,20 @@ 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, secondary and explicit 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),

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

Expand All @@ -148,10 +156,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 +172,94 @@ 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 %}}
#### Explicit Package Sources

*Introduced in 1.5.0*

If package sources are configured as explicit, these sources are only searched when a package configuration [explicitly indicates](#package-source-constraint) that it should be found on this package source.

You can configure a package source as an explicit source with `priority = "explicit` in your package source configuration.

```bash
poetry source add --priority=explicit foo https://foo.bar/simple/
```

There can be more than one explicit package source.

#### 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.
b-kamphorst marked this conversation as resolved.
Show resolved Hide resolved

```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.
In particular, it does not need to have priority `"explicit"`.
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 +292,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