From 0f09086cef703c9e75e4d52c2682860fdd847bb8 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 | 72 ++++++++++--- tests/console/commands/source/test_remove.py | 61 ++++++++++- tests/console/commands/source/test_show.py | 57 +++++++++- .../with_default_source/pyproject.toml | 4 +- .../with_default_source_and_pypi/README.rst | 2 + .../pyproject.toml | 65 ++++++++++++ .../with_default_source_legacy/pyproject.toml | 4 +- .../with_default_source_pypi/README.rst | 2 + .../with_default_source_pypi/pyproject.toml | 60 +++++++++++ .../pyproject.toml | 22 ++++ .../pyproject.toml | 23 ++++ .../pyproject.toml | 18 ++++ .../with_explicit_source/pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../pyproject.toml | 30 ++++++ .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- .../with_two_default_sources/pyproject.toml | 4 +- .../pyproject.toml | 4 +- tests/json/test_schema_sources.py | 9 -- tests/test_factory.py | 100 +++++++++++++++++- 37 files changed, 676 insertions(+), 118 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 ada0a8764ac..baf5557e4d3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 @@ -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 diff --git a/docs/repositories.md b/docs/repositories.md index 38f993892f4..d79d88ac7c1 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](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 @@ -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. 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..3752f340990 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 fixed 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 a9a488d6bf7..68c51bdf2c3 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -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 @@ -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__) @@ -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 @@ -163,21 +165,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 @@ -185,18 +208,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 5ca52540fb4..2c3cda9afdd 100644 --- a/tests/console/commands/source/test_add.py +++ b/tests/console/commands/source/test_add.py @@ -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 fixed 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 +@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: - tester.execute(f"--priority=primary {source_existing.name} {source_existing.url}") + 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 + modifier: str, + tester: CommandTester, + source_existing: Source, + poetry_with_source: Poetry, ) -> None: - 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_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,12 +276,14 @@ 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, @@ -247,7 +292,8 @@ def test_source_add_existing_fails_due_to_other_default( 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/pyproject.toml b/tests/fixtures/with_default_source/pyproject.toml index b572591b6e1..ccda6306bd5 100644 --- a/tests/fixtures/with_default_source/pyproject.toml +++ b/tests/fixtures/with_default_source/pyproject.toml @@ -22,11 +22,11 @@ classifiers = [ # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^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" } +pathlib2 = { version = "^2.2", python = "~3.6" } orator = { version = "^0.9", optional = true } 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..eb949e6b607 --- /dev/null +++ b/tests/fixtures/with_default_source_and_pypi/pyproject.toml @@ -0,0 +1,65 @@ +[tool.poetry] +name = "with-default-source-and-pypi" +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 = "^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 = "~3.6" } + +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_legacy/pyproject.toml b/tests/fixtures/with_default_source_legacy/pyproject.toml index 0c8a0977d64..ca27c0ce032 100644 --- a/tests/fixtures/with_default_source_legacy/pyproject.toml +++ b/tests/fixtures/with_default_source_legacy/pyproject.toml @@ -22,11 +22,11 @@ classifiers = [ # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^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" } +pathlib2 = { version = "^2.2", python = "~3.6" } orator = { version = "^0.9", optional = true } 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..84fba4d7d26 --- /dev/null +++ b/tests/fixtures/with_default_source_pypi/pyproject.toml @@ -0,0 +1,60 @@ +[tool.poetry] +name = "with-default-source-pypi" +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 = "^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 = "~3.6" } + +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..ae588647a56 --- /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 = "^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..d7666f8836d --- /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 = "^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..19b38da7904 --- /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 = "^3.6" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "PyPI" +priority = "explicit" diff --git a/tests/fixtures/with_explicit_source/pyproject.toml b/tests/fixtures/with_explicit_source/pyproject.toml index bc0f8e17830..c1fde4fffdc 100644 --- a/tests/fixtures/with_explicit_source/pyproject.toml +++ b/tests/fixtures/with_explicit_source/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml b/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml index d5db76c6094..1ee7df9444e 100644 --- a/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml +++ b/tests/fixtures/with_non_default_multiple_secondary_sources/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml b/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml index 933bee96912..35bcfd58126 100644 --- a/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml +++ b/tests/fixtures/with_non_default_multiple_secondary_sources_legacy/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml b/tests/fixtures/with_non_default_multiple_sources/pyproject.toml index e40dd03e66e..f83aeda2da4 100644 --- a/tests/fixtures/with_non_default_multiple_sources/pyproject.toml +++ b/tests/fixtures/with_non_default_multiple_sources/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml b/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml index 6cacb602e8b..e2da526ef39 100644 --- a/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml +++ b/tests/fixtures/with_non_default_multiple_sources_legacy/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] 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..66db604c4f1 --- /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 = "^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/fixtures/with_non_default_secondary_source/pyproject.toml b/tests/fixtures/with_non_default_secondary_source/pyproject.toml index 5cce04b0591..8dbca2435b7 100644 --- a/tests/fixtures/with_non_default_secondary_source/pyproject.toml +++ b/tests/fixtures/with_non_default_secondary_source/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml b/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml index 453e3f9747f..2e997cefec7 100644 --- a/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml +++ b/tests/fixtures/with_non_default_secondary_source_legacy/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_source_explicit/pyproject.toml b/tests/fixtures/with_non_default_source_explicit/pyproject.toml index 23e7733cf34..3a83099313b 100644 --- a/tests/fixtures/with_non_default_source_explicit/pyproject.toml +++ b/tests/fixtures/with_non_default_source_explicit/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_source_implicit/pyproject.toml b/tests/fixtures/with_non_default_source_implicit/pyproject.toml index d36abb55a25..73f95041e67 100644 --- a/tests/fixtures/with_non_default_source_implicit/pyproject.toml +++ b/tests/fixtures/with_non_default_source_implicit/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^3.6" [tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_two_default_sources/pyproject.toml b/tests/fixtures/with_two_default_sources/pyproject.toml index 3f91eff4664..d7f5b258ae4 100644 --- a/tests/fixtures/with_two_default_sources/pyproject.toml +++ b/tests/fixtures/with_two_default_sources/pyproject.toml @@ -22,11 +22,11 @@ classifiers = [ # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^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" } +pathlib2 = { version = "^2.2", python = "~3.6" } orator = { version = "^0.9", optional = true } diff --git a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml b/tests/fixtures/with_two_default_sources_legacy/pyproject.toml index 66a333f543c..ffbb22fc70a 100644 --- a/tests/fixtures/with_two_default_sources_legacy/pyproject.toml +++ b/tests/fixtures/with_two_default_sources_legacy/pyproject.toml @@ -22,11 +22,11 @@ classifiers = [ # Requirements [tool.poetry.dependencies] -python = "~2.7 || ^3.6" +python = "^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" } +pathlib2 = { version = "^2.2", python = "~3.6" } orator = { version = "^0.9", optional = true } 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 0530bfee66a..565f3003d37 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: @@ -442,3 +516,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