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