Skip to content

Commit

Permalink
sources: allow to configure the priority of PyPI
Browse files Browse the repository at this point in the history
Add a warning that PyPI will be disabled automatically in a future version of Poetry if there is at least one custom source with another priority than `explicit` configured and that it should be configured explicitly with a certain priority for forward compatibility.
  • Loading branch information
radoering committed Apr 24, 2023
1 parent bc6e84e commit 1dfed7e
Show file tree
Hide file tree
Showing 37 changed files with 676 additions and 118 deletions.
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"
],
"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

0 comments on commit 1dfed7e

Please sign in to comment.