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

sources: allow to configure the priority of PyPI #7801

Merged
merged 1 commit into from
Apr 26, 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
12 changes: 8 additions & 4 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,9 +779,12 @@ For example, to add the `pypi-test` source, you can run:
poetry source add pypi-test https://test.pypi.org/simple/
```

{{% note %}}
You cannot use the name `pypi` as it is reserved for use by the default PyPI source.
{{% /note %}}
You cannot use the name `pypi` for a custom repository as it is reserved for use by
the default PyPI source. However, you can set the priority of PyPI:

```bash
poetry source add --priority=explicit pypi
```

#### Options

Expand All @@ -808,7 +811,8 @@ poetry source show pypi-test
```

{{% note %}}
This command will only show sources configured via the `pyproject.toml` and does not include PyPI.
This command will only show sources configured via the `pyproject.toml`
and does not include the implicit default PyPI.
{{% /note %}}

### source remove
Expand Down
35 changes: 24 additions & 11 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ If `priority` is undefined, the source is considered a primary source that takes
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),
3. implicit PyPI (unless disabled by another [default source](#default-package-source) or configured explicitly),
4. [secondary sources](#secondary-package-sources),

[Explicit sources](#explicit-package-sources) are considered only for packages that explicitly [indicate their source](#package-source-constraint).
Expand All @@ -137,19 +137,17 @@ Within each priority class, package sources are considered in order of appearanc

{{% note %}}

If you prefer to disable [PyPI](https://pypi.org) completely, you may choose to set one of your package sources to be the [default](#default-package-source).
If you want to change the priority of [PyPI](https://pypi.org), you can set it explicitly, e.g.

If you prefer to specify a package source for a specific dependency, see [Secondary Package Sources](#secondary-package-sources).

{{% /note %}}


{{% warning %}}
```bash
poetry source add --priority=primary PyPI
```

If you do not want any of the custom sources to take precedence over [PyPI](https://pypi.org),
you must declare **all** package sources to be [secondary](#secondary-package-sources).
If you prefer to disable PyPI completely,
you may choose to set one of your package sources to be the [default](#default-package-source)
or configure PyPI as [explicit source](#explicit-package-sources).

{{% /warning %}}
{{% /note %}}


#### Default Package Source
Expand All @@ -164,6 +162,21 @@ poetry source add --priority=default foo https://foo.bar/simple/

{{% warning %}}

In a future version of Poetry, PyPI will be disabled automatically
if there is at least one custom source configured with another priority than `explicit`.
If you are using custom sources in addition to PyPI, you should configure PyPI explicitly
with a certain priority, e.g.

```bash
poetry source add --priority=primary PyPI
```

This way, the priority of PyPI can be set in a fine-granular way.

{{% /warning %}}

{{% warning %}}

Configuring a custom package source as default, will effectively disable [PyPI](https://pypi.org)
as a package source for your project.

Expand Down
6 changes: 4 additions & 2 deletions src/poetry/config/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@dataclasses.dataclass(order=True, eq=True)
class Source:
name: str
url: str
url: str = ""
default: dataclasses.InitVar[bool] = False
secondary: dataclasses.InitVar[bool] = False
priority: Priority = (
Expand Down Expand Up @@ -38,6 +38,8 @@ def to_dict(self) -> dict[str, str | bool]:
return dataclasses.asdict(
self,
dict_factory=lambda x: {
k: v if not isinstance(v, Priority) else v.name.lower() for (k, v) in x
k: v if not isinstance(v, Priority) else v.name.lower()
for (k, v) in x
if v
},
)
52 changes: 32 additions & 20 deletions src/poetry/console/commands/source/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ class SourceAddCommand(Command):
"name",
"Source repository name.",
),
argument("url", "Source repository url."),
argument(
"url",
(
"Source repository URL."
" Required, except for PyPI, for which it is not allowed."
),
optional=True,
),
]

options = [
Expand Down Expand Up @@ -57,10 +64,24 @@ def handle(self) -> int:
from poetry.utils.source import source_to_table

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

if lower_name == "pypi":
name = "PyPI"
if url:
self.line_error(
"<error>The URL of PyPI is fixed and cannot be set.</error>"
)
return 1
elif not url:
self.line_error(
"<error>A custom source cannot be added without a URL.</error>"
)
return 1

if is_default and is_secondary:
self.line_error(
Expand All @@ -70,7 +91,7 @@ def handle(self) -> int:
return 1

if is_default or is_secondary:
if priority is not None:
if priority_str is not None:
self.line_error(
"<error>Priority was passed through both --priority and a"
" deprecated flag (--default or --secondary). Please only provide"
Expand All @@ -88,34 +109,25 @@ def handle(self) -> int:
priority = Priority.DEFAULT
elif is_secondary:
priority = Priority.SECONDARY
elif priority is None:
elif priority_str is None:
priority = Priority.PRIMARY

new_source = Source(name=name, url=url, priority=priority)
existing_sources = self.poetry.get_sources()
else:
priority = Priority[priority_str.upper()]

sources = AoT([])

new_source = Source(name=name, url=url, priority=priority)
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.priority is Priority.DEFAULT
and new_source.priority is Priority.DEFAULT
):

for source in self.poetry.get_sources():
if source.priority is Priority.DEFAULT and 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 source.name == name:
if source.name.lower() == lower_name:
source = new_source
is_new_source = False

Expand Down
3 changes: 2 additions & 1 deletion src/poetry/console/commands/source/remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ def handle(self) -> int:
from poetry.utils.source import source_to_table

name = self.argument("name")
lower_name = name.lower()

sources = AoT([])
removed = False

for source in self.poetry.get_sources():
if source.name == name:
if source.name.lower() == lower_name:
self.line(f"Removing source with name <c1>{source.name}</c1>.")
removed = True
continue
Expand Down
17 changes: 7 additions & 10 deletions src/poetry/console/commands/source/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,28 @@ class SourceShowCommand(Command):
def handle(self) -> int:
sources = self.poetry.get_sources()
names = self.argument("source")
lower_names = [name.lower() for name in names]

if not sources:
self.line("No sources configured for this project.")
return 0

if names and not any(s.name in names for s in sources):
if names and not any(s.name.lower() in lower_names for s in sources):
self.line_error(
f"No source found with name(s): {', '.join(names)}",
style="error",
)
return 1

for source in sources:
if names and source.name not in names:
if names and source.name.lower() not in lower_names:
continue

table = self.table(style="compact")
rows: Rows = [
["<info>name</>", f" : <c1>{source.name}</>"],
["<info>url</>", f" : {source.url}"],
[
"<info>priority</>",
f" : {source.priority.name.lower()}",
],
]
rows: Rows = [["<info>name</>", f" : <c1>{source.name}</>"]]
if source.url:
rows.append(["<info>url</>", f" : {source.url}"])
rows.append(["<info>priority</>", f" : {source.priority.name.lower()}"])
table.add_rows(rows)
table.render()
self.line("")
Expand Down
Empty file.
73 changes: 53 additions & 20 deletions src/poetry/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from poetry.core.packages.project_package import ProjectPackage

from poetry.config.config import Config
from poetry.exceptions import PoetryException
from poetry.json import validate_object
from poetry.packages.locker import Locker
from poetry.plugins.plugin import Plugin
Expand All @@ -32,7 +33,7 @@
from tomlkit.toml_document import TOMLDocument

from poetry.repositories import RepositoryPool
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.http_repository import HTTPRepository
from poetry.utils.dependency_specification import DependencySpec

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -134,6 +135,7 @@ def create_pool(

pool = RepositoryPool()

explicit_pypi = False
for source in sources:
repository = cls.create_package_source(
source, auth_config, disable_cache=disable_cache
Expand Down Expand Up @@ -163,40 +165,71 @@ def create_pool(
io.write_line(message)

pool.add_repository(repository, priority=priority)
if repository.name.lower() == "pypi":
explicit_pypi = True

# Only add PyPI if no default repository is configured
if pool.has_default():
if io.is_debug():
io.write_line("Deactivating the PyPI repository")
else:
from poetry.repositories.pypi_repository import PyPiRepository

if pool.has_primary_repositories():
pypi_priority = Priority.SECONDARY
if not explicit_pypi:
if pool.has_default():
if io.is_debug():
io.write_line("Deactivating the PyPI repository")
else:
pypi_priority = Priority.DEFAULT
from poetry.repositories.pypi_repository import PyPiRepository

if pool.repositories:
io.write_error_line(
"<warning>"
"Warning: In a future version of Poetry, PyPI will be disabled"
" automatically if at least one custom source is configured"
" with another priority than 'explicit'. In order to avoid"
" a breaking change and make your pyproject.toml forward"
" compatible, add PyPI explicitly via 'poetry source add pypi'."
" By the way, this has the advantage that you can set the"
" priority of PyPI as with any other source."
"</warning>"
)

if pool.has_primary_repositories():
pypi_priority = Priority.SECONDARY
else:
pypi_priority = Priority.DEFAULT

pool.add_repository(
PyPiRepository(disable_cache=disable_cache), priority=pypi_priority
pool.add_repository(
PyPiRepository(disable_cache=disable_cache), priority=pypi_priority
)

if not pool.repositories:
raise PoetryException(
"At least one source must not be configured as 'explicit'."
)

return pool

@classmethod
def create_package_source(
cls, source: dict[str, str], auth_config: Config, disable_cache: bool = False
) -> LegacyRepository:
) -> HTTPRepository:
from poetry.repositories.exceptions import InvalidSourceError
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.single_page_repository import SinglePageRepository

if "url" not in source:
raise RuntimeError("Unsupported source specified")
try:
name = source["name"]
except KeyError:
raise InvalidSourceError("Missing [name] in source.")

if name.lower() == "pypi":
if "url" in source:
raise InvalidSourceError(
"The PyPI repository cannot be configured with a custom url."
)
return PyPiRepository(disable_cache=disable_cache)

# PyPI-like repository
if "name" not in source:
raise RuntimeError("Missing [name] in source.")
name = source["name"]
url = source["url"]
try:
url = source["url"]
except KeyError:
raise InvalidSourceError(f"Missing [url] in source {name!r}.")

repository_class = LegacyRepository

Expand Down
3 changes: 1 addition & 2 deletions src/poetry/json/schemas/poetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
"type": "object",
"additionalProperties": false,
"required": [
"name",
"url"
"name"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also tried some fancy anyOf not pattern magic, which worked correctly, but always gave the following message for invalid combinations: [source.0] {'name': 'x'} is not valid under any of the given schemas

Thus, I decided to be permissive here and check for invalid combinations in create_package_source() in factory.py. This way, we get way better error messages if there is a PyPI with a url or another source without a url.

],
"properties": {
"name": {
Expand Down
4 changes: 4 additions & 0 deletions src/poetry/repositories/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ class RepositoryError(Exception):

class PackageNotFound(Exception):
pass


class InvalidSourceError(Exception):
pass
Loading