From c33b0457e585c077381ee7ab21d11e1633a3090f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
<30527984+radoering@users.noreply.github.com>
Date: Sat, 15 Apr 2023 18:10:14 +0200
Subject: [PATCH] sources: allow to configure the priority of PyPI
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.
---
docs/cli.md | 12 ++-
docs/repositories.md | 35 ++++--
src/poetry/config/source.py | 6 +-
src/poetry/console/commands/source/add.py | 52 +++++----
src/poetry/console/commands/source/remove.py | 3 +-
src/poetry/console/commands/source/show.py | 17 ++-
src/poetry/console/commands/source/update.py | 0
src/poetry/factory.py | 73 +++++++++----
src/poetry/json/schemas/poetry.json | 3 +-
src/poetry/repositories/exceptions.py | 4 +
tests/console/commands/source/conftest.py | 34 ++++++
tests/console/commands/source/test_add.py | 86 +++++++++++----
tests/console/commands/source/test_remove.py | 61 ++++++++++-
tests/console/commands/source/test_show.py | 57 +++++++++-
.../with_default_source_and_pypi/README.rst | 2 +
.../pyproject.toml | 65 ++++++++++++
.../with_default_source_pypi/README.rst | 2 +
.../with_default_source_pypi/pyproject.toml | 60 +++++++++++
.../pyproject.toml | 22 ++++
.../pyproject.toml | 23 ++++
.../pyproject.toml | 18 ++++
.../pyproject.toml | 30 ++++++
tests/json/test_schema_sources.py | 9 --
tests/test_factory.py | 100 +++++++++++++++++-
24 files changed, 666 insertions(+), 108 deletions(-)
delete mode 100644 src/poetry/console/commands/source/update.py
create mode 100644 tests/fixtures/with_default_source_and_pypi/README.rst
create mode 100644 tests/fixtures/with_default_source_and_pypi/pyproject.toml
create mode 100644 tests/fixtures/with_default_source_pypi/README.rst
create mode 100644 tests/fixtures/with_default_source_pypi/pyproject.toml
create mode 100644 tests/fixtures/with_explicit_pypi_and_other/pyproject.toml
create mode 100644 tests/fixtures/with_explicit_pypi_and_other_explicit/pyproject.toml
create mode 100644 tests/fixtures/with_explicit_pypi_no_other/pyproject.toml
create mode 100644 tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml
diff --git a/docs/cli.md b/docs/cli.md
index e57b1548e2e..ab113a09d20 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -778,9 +778,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
@@ -807,7 +810,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
diff --git a/docs/repositories.md b/docs/repositories.md
index 38f993892f4..65ce97f6362 100644
--- a/docs/repositories.md
+++ b/docs/repositories.md
@@ -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).
@@ -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, 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](https://pypi.org) 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
@@ -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 with another priority than `explicit` configured.
+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.
diff --git a/src/poetry/config/source.py b/src/poetry/config/source.py
index aa0f9499b08..7a4043b45b9 100644
--- a/src/poetry/config/source.py
+++ b/src/poetry/config/source.py
@@ -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 = (
@@ -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
},
)
diff --git a/src/poetry/console/commands/source/add.py b/src/poetry/console/commands/source/add.py
index 6875d444be8..cb4b97bb00d 100644
--- a/src/poetry/console/commands/source/add.py
+++ b/src/poetry/console/commands/source/add.py
@@ -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 = [
@@ -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(
+ "The url of PyPI is fix and cannot be set."
+ )
+ return 1
+ elif not url:
+ self.line_error(
+ "A custom source cannot be added without a url."
+ )
+ return 1
if is_default and is_secondary:
self.line_error(
@@ -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(
"Priority was passed through both --priority and a"
" deprecated flag (--default or --secondary). Please only provide"
@@ -88,26 +109,17 @@ 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 {name} 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"Source with name {source.name} is already set to"
" default. Only one default source can be configured at a"
@@ -115,7 +127,7 @@ def handle(self) -> int:
)
return 1
- if source.name == name:
+ if source.name.lower() == lower_name:
source = new_source
is_new_source = False
diff --git a/src/poetry/console/commands/source/remove.py b/src/poetry/console/commands/source/remove.py
index 7d185bf1e73..cb667aa5ea1 100644
--- a/src/poetry/console/commands/source/remove.py
+++ b/src/poetry/console/commands/source/remove.py
@@ -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 {source.name}.")
removed = True
continue
diff --git a/src/poetry/console/commands/source/show.py b/src/poetry/console/commands/source/show.py
index 5014708d391..26b85544911 100644
--- a/src/poetry/console/commands/source/show.py
+++ b/src/poetry/console/commands/source/show.py
@@ -27,12 +27,13 @@ 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",
@@ -40,18 +41,14 @@ def handle(self) -> int:
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 = [
- ["name>", f" : {source.name}>"],
- ["url>", f" : {source.url}"],
- [
- "priority>",
- f" : {source.priority.name.lower()}",
- ],
- ]
+ rows: Rows = [["name>", f" : {source.name}>"]]
+ if source.url:
+ rows.append(["url>", f" : {source.url}"])
+ rows.append(["priority>", f" : {source.priority.name.lower()}"])
table.add_rows(rows)
table.render()
self.line("")
diff --git a/src/poetry/console/commands/source/update.py b/src/poetry/console/commands/source/update.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/src/poetry/factory.py b/src/poetry/factory.py
index d1a46a654aa..e807f2f980b 100644
--- a/src/poetry/factory.py
+++ b/src/poetry/factory.py
@@ -14,6 +14,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
@@ -31,7 +32,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__)
@@ -133,6 +134,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
@@ -162,21 +164,42 @@ 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: 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."
+ ""
+ )
+
+ 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
@@ -184,18 +207,28 @@ def create_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
diff --git a/src/poetry/json/schemas/poetry.json b/src/poetry/json/schemas/poetry.json
index c9191b03d23..930d73bffb4 100644
--- a/src/poetry/json/schemas/poetry.json
+++ b/src/poetry/json/schemas/poetry.json
@@ -20,8 +20,7 @@
"type": "object",
"additionalProperties": false,
"required": [
- "name",
- "url"
+ "name"
],
"properties": {
"name": {
diff --git a/src/poetry/repositories/exceptions.py b/src/poetry/repositories/exceptions.py
index 10ad3c460b8..c742f268a42 100644
--- a/src/poetry/repositories/exceptions.py
+++ b/src/poetry/repositories/exceptions.py
@@ -7,3 +7,7 @@ class RepositoryError(Exception):
class PackageNotFound(Exception):
pass
+
+
+class InvalidSourceError(Exception):
+ pass
diff --git a/tests/console/commands/source/conftest.py b/tests/console/commands/source/conftest.py
index 5ec79df2c9e..bc86ff400c5 100644
--- a/tests/console/commands/source/conftest.py
+++ b/tests/console/commands/source/conftest.py
@@ -58,6 +58,16 @@ def source_explicit() -> Source:
)
+@pytest.fixture
+def source_pypi() -> Source:
+ return Source(name="PyPI")
+
+
+@pytest.fixture
+def source_pypi_explicit() -> Source:
+ return Source(name="PyPI", priority=Priority.EXPLICIT)
+
+
_existing_source = Source(name="existing", url="https://existing.com")
@@ -88,6 +98,20 @@ def source_existing() -> Source:
"""
+PYPROJECT_WITH_PYPI = f"""{PYPROJECT_WITHOUT_SOURCES}
+
+[[tool.poetry.source]]
+name = "PyPI"
+"""
+
+
+PYPROJECT_WITH_PYPI_AND_OTHER = f"""{PYPROJECT_WITH_SOURCES}
+
+[[tool.poetry.source]]
+name = "PyPI"
+"""
+
+
@pytest.fixture
def poetry_without_source(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITHOUT_SOURCES)
@@ -98,6 +122,16 @@ def poetry_with_source(project_factory: ProjectFactory) -> Poetry:
return project_factory(pyproject_content=PYPROJECT_WITH_SOURCES)
+@pytest.fixture
+def poetry_with_pypi(project_factory: ProjectFactory) -> Poetry:
+ return project_factory(pyproject_content=PYPROJECT_WITH_PYPI)
+
+
+@pytest.fixture
+def poetry_with_pypi_and_other(project_factory: ProjectFactory) -> Poetry:
+ return project_factory(pyproject_content=PYPROJECT_WITH_PYPI_AND_OTHER)
+
+
@pytest.fixture
def add_multiple_sources(
command_tester_factory: CommandTesterFactory,
diff --git a/tests/console/commands/source/test_add.py b/tests/console/commands/source/test_add.py
index 468e9dec271..464fe7c0884 100644
--- a/tests/console/commands/source/test_add.py
+++ b/tests/console/commands/source/test_add.py
@@ -87,7 +87,7 @@ def test_source_add_secondary_legacy(
source_existing: Source,
source_secondary: Source,
poetry_with_source: Poetry,
-):
+) -> None:
tester.execute(f"--secondary {source_secondary.name} {source_secondary.url}")
assert_source_added_legacy(
tester, poetry_with_source, source_existing, source_secondary
@@ -99,7 +99,7 @@ def test_source_add_default(
source_existing: Source,
source_default: Source,
poetry_with_source: Poetry,
-):
+) -> None:
tester.execute(f"--priority=default {source_default.name} {source_default.url}")
assert_source_added(tester, poetry_with_source, source_existing, source_default)
@@ -109,7 +109,7 @@ def test_source_add_second_default_fails(
source_existing: Source,
source_default: Source,
poetry_with_source: Poetry,
-):
+) -> None:
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()
@@ -155,7 +155,7 @@ def test_source_add_error_default_and_secondary_legacy(tester: CommandTester) ->
assert tester.status_code == 1
-def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester):
+def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester) -> None:
tester.execute("--priority secondary --secondary error https://error.com")
assert (
tester.io.fetch_error().strip()
@@ -166,16 +166,47 @@ def test_source_add_error_priority_and_deprecated_legacy(tester: CommandTester):
assert tester.status_code == 1
+def test_source_add_error_no_url(tester: CommandTester) -> None:
+ tester.execute("foo")
+ assert (
+ tester.io.fetch_error().strip()
+ == "A custom source cannot be added without a url."
+ )
+ assert tester.status_code == 1
+
+
def test_source_add_error_pypi(tester: CommandTester) -> None:
tester.execute("pypi https://test.pypi.org/simple/")
assert (
- tester.io.fetch_error().strip()
- == "Failed to validate addition of pypi: The name [pypi] is reserved for"
- " repositories"
+ tester.io.fetch_error().strip() == "The url of PyPI is fix and cannot be set."
)
assert tester.status_code == 1
+@pytest.mark.parametrize("name", ["pypi", "PyPI"])
+def test_source_add_pypi(
+ name: str,
+ tester: CommandTester,
+ source_existing: Source,
+ source_pypi: Source,
+ poetry_with_source: Poetry,
+) -> None:
+ tester.execute(name)
+ assert_source_added(tester, poetry_with_source, source_existing, source_pypi)
+
+
+def test_source_add_pypi_explicit(
+ tester: CommandTester,
+ source_existing: Source,
+ source_pypi_explicit: Source,
+ poetry_with_source: Poetry,
+) -> None:
+ tester.execute("--priority=explicit PyPI")
+ assert_source_added(
+ tester, poetry_with_source, source_existing, source_pypi_explicit
+ )
+
+
def test_source_add_existing_legacy(
tester: CommandTester, source_existing: Source, poetry_with_source: Poetry
) -> None:
@@ -202,29 +233,41 @@ def test_source_add_existing_legacy(
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}")
+@pytest.mark.parametrize("modifier", ["lower", "upper"])
+def test_source_add_existing_no_change_except_case_of_name(
+ modifier: str,
+ tester: CommandTester,
+ source_existing: Source,
+ poetry_with_source: Poetry,
+) -> None:
+ name = getattr(source_existing.name, modifier)()
+ tester.execute(f"--priority=primary {name} {source_existing.url}")
assert (
tester.io.fetch_output().strip()
- == f"Source with name {source_existing.name} already exists. Skipping addition."
+ == f"Source with name {name} already exists. Updating."
)
poetry_with_source.pyproject.reload()
sources = poetry_with_source.get_sources()
assert len(sources) == 1
- assert sources[0] == source_existing
+ assert sources[0].name == getattr(source_existing.name, modifier)()
+ assert sources[0].url == source_existing.url
+ assert sources[0].priority == source_existing.priority
+@pytest.mark.parametrize("modifier", ["lower", "upper"])
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}")
+ modifier: str,
+ tester: CommandTester,
+ source_existing: Source,
+ poetry_with_source: Poetry,
+) -> None:
+ name = getattr(source_existing.name, modifier)()
+ tester.execute(f"--priority=default {name} {source_existing.url}")
assert (
tester.io.fetch_output().strip()
- == f"Source with name {source_existing.name} already exists. Updating."
+ == f"Source with name {name} already exists. Updating."
)
poetry_with_source.pyproject.reload()
@@ -233,21 +276,24 @@ def test_source_add_existing_updating(
assert len(sources) == 1
assert sources[0] != source_existing
expected_source = Source(
- name=source_existing.name, url=source_existing.url, priority=Priority.DEFAULT
+ name=name, url=source_existing.url, priority=Priority.DEFAULT
)
assert sources[0] == expected_source
+@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_add_existing_fails_due_to_other_default(
+ modifier: str,
tester: CommandTester,
source_existing: Source,
source_default: Source,
poetry_with_source: Poetry,
-):
+) -> None:
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}")
+ name = getattr(source_existing.name, modifier)()
+ tester.execute(f"--priority=default {name} {source_existing.url}")
assert (
tester.io.fetch_error().strip()
diff --git a/tests/console/commands/source/test_remove.py b/tests/console/commands/source/test_remove.py
index 49d881328a8..7237a6897f3 100644
--- a/tests/console/commands/source/test_remove.py
+++ b/tests/console/commands/source/test_remove.py
@@ -22,14 +22,32 @@ def tester(
return command_tester_factory("source remove", poetry=poetry_with_source)
+@pytest.fixture
+def tester_pypi(
+ command_tester_factory: CommandTesterFactory,
+ poetry_with_pypi: Poetry,
+) -> CommandTester:
+ return command_tester_factory("source remove", poetry=poetry_with_pypi)
+
+
+@pytest.fixture
+def tester_pypi_and_other(
+ command_tester_factory: CommandTesterFactory,
+ poetry_with_pypi_and_other: Poetry,
+) -> CommandTester:
+ return command_tester_factory("source remove", poetry=poetry_with_pypi_and_other)
+
+
+@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_remove_simple(
tester: CommandTester,
poetry_with_source: Poetry,
source_existing: Source,
source_one: Source,
source_two: Source,
+ modifier: str,
) -> None:
- tester.execute(f"{source_existing.name}")
+ tester.execute(getattr(f"{source_existing.name}", modifier)())
assert (
tester.io.fetch_output().strip()
== f"Removing source with name {source_existing.name}."
@@ -42,7 +60,42 @@ def test_source_remove_simple(
assert tester.status_code == 0
-def test_source_remove_error(tester: CommandTester) -> None:
- tester.execute("error")
- assert tester.io.fetch_error().strip() == "Source with name error was not found."
+@pytest.mark.parametrize("name", ["pypi", "PyPI"])
+def test_source_remove_pypi(
+ name: str, tester_pypi: CommandTester, poetry_with_pypi: Poetry
+) -> None:
+ tester_pypi.execute(name)
+ assert tester_pypi.io.fetch_output().strip() == "Removing source with name PyPI."
+
+ poetry_with_pypi.pyproject.reload()
+ sources = poetry_with_pypi.get_sources()
+ assert sources == []
+
+ assert tester_pypi.status_code == 0
+
+
+@pytest.mark.parametrize("name", ["pypi", "PyPI"])
+def test_source_remove_pypi_and_other(
+ name: str,
+ tester_pypi_and_other: CommandTester,
+ poetry_with_pypi_and_other: Poetry,
+ source_existing: Source,
+) -> None:
+ tester_pypi_and_other.execute(name)
+ assert (
+ tester_pypi_and_other.io.fetch_output().strip()
+ == "Removing source with name PyPI."
+ )
+
+ poetry_with_pypi_and_other.pyproject.reload()
+ sources = poetry_with_pypi_and_other.get_sources()
+ assert sources == [source_existing]
+
+ assert tester_pypi_and_other.status_code == 0
+
+
+@pytest.mark.parametrize("name", ["foo", "pypi", "PyPI"])
+def test_source_remove_error(name: str, tester: CommandTester) -> None:
+ tester.execute(name)
+ assert tester.io.fetch_error().strip() == f"Source with name {name} was not found."
assert tester.status_code == 1
diff --git a/tests/console/commands/source/test_show.py b/tests/console/commands/source/test_show.py
index d3c94682650..2f7c278dfa7 100644
--- a/tests/console/commands/source/test_show.py
+++ b/tests/console/commands/source/test_show.py
@@ -30,6 +30,22 @@ def tester_no_sources(
return command_tester_factory("source show", poetry=poetry_without_source)
+@pytest.fixture
+def tester_pypi(
+ command_tester_factory: CommandTesterFactory,
+ poetry_with_pypi: Poetry,
+) -> CommandTester:
+ return command_tester_factory("source show", poetry=poetry_with_pypi)
+
+
+@pytest.fixture
+def tester_pypi_and_other(
+ command_tester_factory: CommandTesterFactory,
+ poetry_with_pypi_and_other: Poetry,
+) -> CommandTester:
+ return command_tester_factory("source show", poetry=poetry_with_pypi_and_other)
+
+
@pytest.fixture
def tester_all_types(
command_tester_factory: CommandTesterFactory,
@@ -61,8 +77,11 @@ def test_source_show_simple(tester: CommandTester) -> None:
assert tester.status_code == 0
-def test_source_show_one(tester: CommandTester, source_one: Source) -> None:
- tester.execute(f"{source_one.name}")
+@pytest.mark.parametrize("modifier", ["lower", "upper"])
+def test_source_show_one(
+ tester: CommandTester, source_one: Source, modifier: str
+) -> None:
+ tester.execute(getattr(f"{source_one.name}", modifier)())
expected = """\
name : one
@@ -75,10 +94,11 @@ def test_source_show_one(tester: CommandTester, source_one: Source) -> None:
assert tester.status_code == 0
+@pytest.mark.parametrize("modifier", ["lower", "upper"])
def test_source_show_two(
- tester: CommandTester, source_one: Source, source_two: Source
+ tester: CommandTester, source_one: Source, source_two: Source, modifier: str
) -> None:
- tester.execute(f"{source_one.name} {source_two.name}")
+ tester.execute(getattr(f"{source_one.name} {source_two.name}", modifier)())
expected = """\
name : one
@@ -121,6 +141,35 @@ def test_source_show_given_priority(
assert tester_all_types.status_code == 0
+def test_source_show_pypi(tester_pypi: CommandTester) -> None:
+ tester_pypi.execute("")
+ expected = """\
+name : PyPI
+priority : primary
+""".splitlines()
+ assert [
+ line.strip() for line in tester_pypi.io.fetch_output().strip().splitlines()
+ ] == expected
+ assert tester_pypi.status_code == 0
+
+
+def test_source_show_pypi_and_other(tester_pypi_and_other: CommandTester) -> None:
+ tester_pypi_and_other.execute("")
+ expected = """\
+name : existing
+url : https://existing.com
+priority : primary
+
+name : PyPI
+priority : primary
+""".splitlines()
+ assert [
+ line.strip()
+ for line in tester_pypi_and_other.io.fetch_output().strip().splitlines()
+ ] == expected
+ assert tester_pypi_and_other.status_code == 0
+
+
def test_source_show_no_sources(tester_no_sources: CommandTester) -> None:
tester_no_sources.execute("error")
assert (
diff --git a/tests/fixtures/with_default_source_and_pypi/README.rst b/tests/fixtures/with_default_source_and_pypi/README.rst
new file mode 100644
index 00000000000..f7fe15470f9
--- /dev/null
+++ b/tests/fixtures/with_default_source_and_pypi/README.rst
@@ -0,0 +1,2 @@
+My Package
+==========
diff --git a/tests/fixtures/with_default_source_and_pypi/pyproject.toml b/tests/fixtures/with_default_source_and_pypi/pyproject.toml
new file mode 100644
index 00000000000..9ca9786cfa0
--- /dev/null
+++ b/tests/fixtures/with_default_source_and_pypi/pyproject.toml
@@ -0,0 +1,65 @@
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Sébastien Eustace "
+]
+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/"
+priority = "default"
+
+
+[[tool.poetry.source]]
+name = "PyPI"
diff --git a/tests/fixtures/with_default_source_pypi/README.rst b/tests/fixtures/with_default_source_pypi/README.rst
new file mode 100644
index 00000000000..f7fe15470f9
--- /dev/null
+++ b/tests/fixtures/with_default_source_pypi/README.rst
@@ -0,0 +1,2 @@
+My Package
+==========
diff --git a/tests/fixtures/with_default_source_pypi/pyproject.toml b/tests/fixtures/with_default_source_pypi/pyproject.toml
new file mode 100644
index 00000000000..2d0f9baaccd
--- /dev/null
+++ b/tests/fixtures/with_default_source_pypi/pyproject.toml
@@ -0,0 +1,60 @@
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Sébastien Eustace "
+]
+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 = "PyPI"
+priority = "default"
diff --git a/tests/fixtures/with_explicit_pypi_and_other/pyproject.toml b/tests/fixtures/with_explicit_pypi_and_other/pyproject.toml
new file mode 100644
index 00000000000..49fda759132
--- /dev/null
+++ b/tests/fixtures/with_explicit_pypi_and_other/pyproject.toml
@@ -0,0 +1,22 @@
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Your Name "
+]
+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/"
+
+[[tool.poetry.source]]
+name = "PyPI"
+priority = "explicit"
diff --git a/tests/fixtures/with_explicit_pypi_and_other_explicit/pyproject.toml b/tests/fixtures/with_explicit_pypi_and_other_explicit/pyproject.toml
new file mode 100644
index 00000000000..1f94700b6f5
--- /dev/null
+++ b/tests/fixtures/with_explicit_pypi_and_other_explicit/pyproject.toml
@@ -0,0 +1,23 @@
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Your Name "
+]
+license = "MIT"
+
+# Requirements
+[tool.poetry.dependencies]
+python = "~2.7 || ^3.6"
+
+[tool.poetry.dev-dependencies]
+
+[[tool.poetry.source]]
+name = "explicit"
+url = "https://explicit.com/simple/"
+priority = "explicit"
+
+[[tool.poetry.source]]
+name = "PyPI"
+priority = "explicit"
diff --git a/tests/fixtures/with_explicit_pypi_no_other/pyproject.toml b/tests/fixtures/with_explicit_pypi_no_other/pyproject.toml
new file mode 100644
index 00000000000..e7c7403933d
--- /dev/null
+++ b/tests/fixtures/with_explicit_pypi_no_other/pyproject.toml
@@ -0,0 +1,18 @@
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Your Name "
+]
+license = "MIT"
+
+# Requirements
+[tool.poetry.dependencies]
+python = "~2.7 || ^3.6"
+
+[tool.poetry.dev-dependencies]
+
+[[tool.poetry.source]]
+name = "PyPI"
+priority = "explicit"
diff --git a/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml b/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml
new file mode 100644
index 00000000000..63053b4836f
--- /dev/null
+++ b/tests/fixtures/with_non_default_multiple_sources_pypi/pyproject.toml
@@ -0,0 +1,30 @@
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Your Name "
+]
+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 = "secondary"
+
+[[tool.poetry.source]]
+name = "bar"
+url = "https://bar.baz/simple/"
+
+[[tool.poetry.source]]
+name = "PyPI"
+
+[[tool.poetry.source]]
+name = "baz"
+url = "https://baz.bar/simple/"
diff --git a/tests/json/test_schema_sources.py b/tests/json/test_schema_sources.py
index 78e446bc6b3..643d66f596c 100644
--- a/tests/json/test_schema_sources.py
+++ b/tests/json/test_schema_sources.py
@@ -21,15 +21,6 @@ def test_pyproject_toml_valid() -> None:
assert Factory.validate(content) == {"errors": [], "warnings": []}
-def test_pyproject_toml_invalid_url() -> None:
- toml = TOMLFile(FIXTURE_DIR / "complete_invalid_url.toml").read()
- content = toml["tool"]["poetry"]
- assert Factory.validate(content) == {
- "errors": ["[source.0] 'url' is a required property"],
- "warnings": [],
- }
-
-
def test_pyproject_toml_invalid_priority() -> None:
toml = TOMLFile(FIXTURE_DIR / "complete_invalid_priority.toml").read()
content = toml["tool"]["poetry"]
diff --git a/tests/test_factory.py b/tests/test_factory.py
index d3a39620e86..f987cea0c54 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -9,8 +9,10 @@
from packaging.utils import canonicalize_name
from poetry.core.constraints.version import parse_constraint
+from poetry.exceptions import PoetryException
from poetry.factory import Factory
from poetry.plugins.plugin import Plugin
+from poetry.repositories.exceptions import InvalidSourceError
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.repository_pool import Priority
@@ -22,6 +24,7 @@
from cleo.io.io import IO
from pytest_mock import MockerFixture
+ from poetry.config.config import Config
from poetry.poetry import Poetry
from tests.types import FixtureDirGetter
@@ -231,6 +234,31 @@ def test_poetry_with_default_source(
assert io.fetch_error() == ""
+def test_poetry_with_default_source_and_pypi(
+ fixture_dir: FixtureDirGetter, with_simple_keyring: None
+) -> None:
+ io = BufferedIO()
+ poetry = Factory().create_poetry(fixture_dir("with_default_source_and_pypi"), io=io)
+
+ 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.PRIMARY
+ assert "Warning: Found deprecated key" not in io.fetch_error()
+
+
+def test_poetry_with_default_source_pypi(
+ fixture_dir: FixtureDirGetter, with_simple_keyring: None
+) -> None:
+ io = BufferedIO()
+ poetry = Factory().create_poetry(fixture_dir("with_default_source_pypi"), io=io)
+
+ assert len(poetry.pool.repositories) == 1
+ assert poetry.pool.has_repository("PyPI")
+ assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
+ assert poetry.pool.get_priority("PyPI") is Priority.DEFAULT
+
+
@pytest.mark.parametrize(
"project",
("with_non_default_source_implicit", "with_non_default_source_explicit"),
@@ -238,7 +266,8 @@ def test_poetry_with_default_source(
def test_poetry_with_non_default_source(
project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
- poetry = Factory().create_poetry(fixture_dir(project))
+ io = BufferedIO()
+ poetry = Factory().create_poetry(fixture_dir(project), io=io)
assert not poetry.pool.has_default()
assert poetry.pool.has_repository("PyPI")
@@ -248,6 +277,8 @@ def test_poetry_with_non_default_source(
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"}
+ error = io.fetch_error()
+ assert "Warning: In a future version of Poetry, PyPI will be disabled" in error
def test_poetry_with_non_default_secondary_source_legacy(
@@ -347,6 +378,26 @@ def test_poetry_with_non_default_multiple_sources(
assert {repo.name for repo in poetry.pool.repositories} == {"PyPI", "bar", "foo"}
+def test_poetry_with_non_default_multiple_sources_pypi(
+ fixture_dir: FixtureDirGetter, with_simple_keyring: None
+) -> None:
+ io = BufferedIO()
+ poetry = Factory().create_poetry(
+ fixture_dir("with_non_default_multiple_sources_pypi"), io=io
+ )
+
+ assert len(poetry.pool.repositories) == 4
+ assert not poetry.pool.has_default()
+ assert poetry.pool.has_repository("PyPI")
+ assert isinstance(poetry.pool.repository("PyPI"), PyPiRepository)
+ assert poetry.pool.get_priority("PyPI") is Priority.PRIMARY
+ # PyPI must be between bar and baz!
+ expected = ["bar", "PyPI", "baz", "foo"]
+ assert [repo.name for repo in poetry.pool.repositories] == expected
+ error = io.fetch_error()
+ assert error == ""
+
+
def test_poetry_with_no_default_source(fixture_dir: FixtureDirGetter) -> None:
poetry = Factory().create_poetry(fixture_dir("sample_project"))
@@ -371,6 +422,29 @@ def test_poetry_with_explicit_source(
assert [repo.name for repo in poetry.pool.repositories] == ["PyPI"]
+def test_poetry_with_explicit_pypi_and_other(
+ fixture_dir: FixtureDirGetter, with_simple_keyring: None
+) -> None:
+ io = BufferedIO()
+ poetry = Factory().create_poetry(fixture_dir("with_explicit_pypi_and_other"), io=io)
+
+ assert len(poetry.pool.repositories) == 1
+ assert len(poetry.pool.all_repositories) == 2
+ error = io.fetch_error()
+ assert error == ""
+
+
+@pytest.mark.parametrize(
+ "project", ["with_explicit_pypi_no_other", "with_explicit_pypi_and_other_explicit"]
+)
+def test_poetry_with_pypi_explicit_only(
+ project: str, fixture_dir: FixtureDirGetter, with_simple_keyring: None
+) -> None:
+ with pytest.raises(PoetryException) as e:
+ Factory().create_poetry(fixture_dir(project))
+ assert str(e.value) == "At least one source must not be configured as 'explicit'."
+
+
def test_poetry_with_two_default_sources_legacy(
fixture_dir: FixtureDirGetter, with_simple_keyring: None
) -> None:
@@ -441,3 +515,27 @@ def test_create_poetry_with_plugins(
poetry = Factory().create_poetry(fixture_dir("sample_project"))
assert poetry.package.readmes == ("README.md",)
+
+
+@pytest.mark.parametrize(
+ ("source", "expected"),
+ [
+ ({}, "Missing [name] in source."),
+ ({"name": "foo"}, "Missing [url] in source 'foo'."),
+ (
+ {"name": "PyPI", "url": "https://example.com"},
+ "The PyPI repository cannot be configured with a custom url.",
+ ),
+ ],
+)
+def test_create_package_source_invalid(
+ source: dict[str, str],
+ expected: str,
+ config: Config,
+ fixture_dir: FixtureDirGetter,
+) -> None:
+ with pytest.raises(InvalidSourceError) as e:
+ Factory.create_package_source(source, auth_config=config)
+ Factory().create_poetry(fixture_dir("with_source_pypi_url"))
+
+ assert str(e.value) == expected