diff --git a/docs/cli.md b/docs/cli.md index f09ffb97273..f717b3f9ab1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -774,7 +774,7 @@ You cannot use the name `pypi` as it is reserved for use by the default PyPI sou #### Options -* `--default`: Set this source as the [default]({{< relref "repositories#disabling-the-pypi-repository" >}}) (disable PyPI). +* `--default`: Set this source as the default (**Deprecated**, use `poetry source default --disable-pypi` to disable PyPI). * `--secondary`: Set this source as a [secondary]({{< relref "repositories#install-dependencies-from-a-private-repository" >}}) source. {{% note %}} @@ -807,6 +807,19 @@ The `source remove` command removes a configured source from your `pyproject.tom poetry source remove pypi-test ``` +### source default + +The `source default` command enables or disables the implicit default source PyPI for the project. + +```bash +poetry source default --disable-pypi +``` + +#### Options + +* `--disable-pypi`: Disable PyPI as implicit default source. +* `--enable-pypi`: Enable PyPI as implicit default source. + ## about The `about` command displays global information about Poetry, including the current version and version of `poetry-core`. diff --git a/docs/repositories.md b/docs/repositories.md index 8d52a10994f..22681cd1b94 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -120,7 +120,6 @@ This will generate the following configuration snippet in your [[tool.poetry.source]] name = "foo" url = "https://foo.bar/simple/" -default = false secondary = false ``` @@ -129,7 +128,9 @@ Any package source not marked as `secondary` will take precedence over [PyPI](ht {{% 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 prefer to disable [PyPI](https://pypi.org) completely, +you can run `poetry source default --disable-pypi`. +If you disable PyPI, you have to configure at least one other source. If you prefer to specify a package source for a specific dependency, see [Secondary Package Sources](#secondary-package-sources). @@ -146,18 +147,39 @@ you must declare **all** package sources to be [secondary](#secondary-package-so #### Default Package Source -By default, Poetry configures [PyPI](https://pypi.org) as the default package source for your -project. You can alter this behaviour and exclusively look up packages only from the configured -package sources by adding a **single** source with `default = true`. +By default, Poetry configures [PyPI](https://pypi.org) as the default package source for +your project. If you configure additional sources, these are preferred to PyPI unless +they are configured as secondary. Nevertheless, packages are looked up on PyPI. + +If you want to exclusively look up packages only from the configured package sources, +you can disable the implicit default source PyPI: ```bash -poetry source add --default foo https://foo.bar/simple/ +poetry source default --disable-pypi +``` + +This will generate the following entry in your `pyproject.toml` file: + +```toml +[tool.poetry] +... +default-source-pypi = false ``` {{% warning %}} -Configuring a custom package source as default, will effectively disable [PyPI](https://pypi.org) -as a package source for your project. +Configuring a custom package source as default (`default = true`, **deprecated**), +will effectively disable [PyPI](https://pypi.org) as a package source for your project. +Whereas the implicit default source PyPI is looked up before secondary but after other +repositories, an explicit default source will be looked up before all other repositories. + +{{% /warning %}} + +{{% warning %}} + +Strictly speaking, PyPI is only configured as default package source if there is at +least one other non-secondary source. Otherwise PyPI is configured as last secondary +source. {{% /warning %}} @@ -229,10 +251,12 @@ This will generate the following configuration snippet in your `pyproject.toml` httpx = {version = "^0.22.0", source = "pypi"} ``` +If you want to disable PyPI completely, see [Default Package Source](#default-package-source). + {{% warning %}} -If any source within a project is configured with `default = true`, The implicit `pypi` source will -be disabled and not used for any packages. +If any source within a project is configured with `default = true` (**deprecated**), +the implicit `pypi` source will be disabled and not used for any packages. {{% /warning %}} diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 8c246b369bf..fe8a44986a8 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -88,6 +88,7 @@ def _load() -> Command: "self show plugins", # Source commands "source add", + "source default", "source remove", "source show", ] diff --git a/src/poetry/console/commands/source/add.py b/src/poetry/console/commands/source/add.py index 81a0ca66e91..c416c2ac866 100644 --- a/src/poetry/console/commands/source/add.py +++ b/src/poetry/console/commands/source/add.py @@ -2,7 +2,6 @@ from cleo.helpers import argument from cleo.helpers import option -from cleo.io.null_io import NullIO from tomlkit.items import AoT from poetry.config.source import Source @@ -85,7 +84,7 @@ def handle(self) -> int: # ensure new source is valid. eg: invalid name etc. try: - pool = Factory.create_pool(self.poetry.config, sources, NullIO()) + pool = Factory.create_pool(self.poetry.config, sources, self._io) pool.repository(name) except ValueError as e: self.line_error( diff --git a/src/poetry/console/commands/source/default.py b/src/poetry/console/commands/source/default.py new file mode 100644 index 00000000000..beec5a55fc2 --- /dev/null +++ b/src/poetry/console/commands/source/default.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from cleo.helpers import option + +from poetry.console.commands.command import Command + + +class SourceDefaultCommand(Command): + name = "source default" + description = "Enable or disable the implicit default source PyPI for the project." + + options = [ + option("enable-pypi", None, "Enable PyPI as implicit default source."), + option("disable-pypi", None, "Disable PyPI as implicit default source."), + ] + + def handle(self) -> int: + enable_pypi = self.option("enable-pypi") + disable_pypi = self.option("disable-pypi") + + if enable_pypi and disable_pypi: + self.line_error("Cannot enable and disable PyPI.") + return 1 + + if enable_pypi or disable_pypi: + self.poetry.pyproject.poetry_config["default-source-pypi"] = enable_pypi + self.poetry.pyproject.save() + + else: + state = ( + "enabled" + if self.poetry.pyproject.poetry_config.get("default-source-pypi", True) + else "disabled" + ) + self.line(f"PyPI is {state} as implicit default source.") + + return 0 diff --git a/src/poetry/console/commands/source/show.py b/src/poetry/console/commands/source/show.py index 8a89a39a55c..35c9a52afbc 100644 --- a/src/poetry/console/commands/source/show.py +++ b/src/poetry/console/commands/source/show.py @@ -61,5 +61,12 @@ def handle(self) -> int: table.add_rows(rows) table.render() self.line("") + if not names: + state = ( + "enabled" + if self.poetry.pyproject.poetry_config.get("default-source-pypi", True) + else "disabled" + ) + self.line(f"PyPI is {state} as implicit default source.") return 0 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 43960b43285..80623d24b3c 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -96,6 +96,9 @@ def create_poetry( poetry.local_config.get("source", []), io, disable_cache=disable_cache, + default_source_pypi=poetry.local_config.get( + "default-source-pypi", True + ), ) ) @@ -117,12 +120,13 @@ def create_pool( sources: Iterable[dict[str, Any]] = (), io: IO | None = None, disable_cache: bool = False, + *, + default_source_pypi: bool = True, ) -> RepositoryPool: from poetry.repositories import RepositoryPool if io is None: io = NullIO() - if disable_cache: logger.debug("Disabling source caches") @@ -142,13 +146,29 @@ def create_pool( message += " and setting it as secondary" io.write_line(message) + if is_default: + # TODO: replace command + io.write_error_line( + "" + "The 'default' option is deprecated.\n" + " If you want to disable PyPI, TODO command\n" + " If you want to set a source as the first source to be searched" + " for packages, just make it the first source in your" + " pyproject.toml." + "" + ) pool.add_repository(repository, is_default, secondary=is_secondary) # Put PyPI last to prefer private repositories # unless we have no default source AND no primary sources # (default = false, secondary = false) - if pool.has_default(): + if pool.has_default() or not default_source_pypi: + if not sources: + raise RuntimeError( + "If no sources are configured," + ' "default-source-pypi" must not be set to false!' + ) if io.is_debug(): io.write_line("Deactivating the PyPI repository") else: diff --git a/src/poetry/json/schemas/poetry.json b/src/poetry/json/schemas/poetry.json index 7532fd836b4..ac983288d41 100644 --- a/src/poetry/json/schemas/poetry.json +++ b/src/poetry/json/schemas/poetry.json @@ -4,6 +4,10 @@ "type": "object", "required": [], "properties": { + "default-source-pypi": { + "type": "boolean", + "description": "Whether PyPI is implicitly used to search for packages." + }, "source": { "type": "array", "description": "A set of additional repositories where packages can be found.", diff --git a/src/poetry/utils/source.py b/src/poetry/utils/source.py index dc8e1c8c92f..c3c843dd384 100644 --- a/src/poetry/utils/source.py +++ b/src/poetry/utils/source.py @@ -15,6 +15,9 @@ def source_to_table(source: Source) -> Table: source_table: Table = table() for key, value in source.to_dict().items(): + if key == "default" and not value: + # default is deprecated, so we don't add it if it is not set + continue source_table.add(key, value) source_table.add(nl()) return source_table diff --git a/tests/console/commands/self/conftest.py b/tests/console/commands/self/conftest.py index 381e22e2f90..baaafcfc85a 100644 --- a/tests/console/commands/self/conftest.py +++ b/tests/console/commands/self/conftest.py @@ -47,12 +47,14 @@ def pool(repo: TestRepository) -> RepositoryPool: def create_pool_factory( repo: Repository, -) -> Callable[[Config, Iterable[dict[str, Any]], IO, bool], RepositoryPool]: +) -> Callable[[Config, Iterable[dict[str, Any]], IO, bool, bool], RepositoryPool]: def _create_pool( config: Config, sources: Iterable[dict[str, Any]] = (), io: IO | None = None, disable_cache: bool = False, + *, + default_source_pypi: bool = True, ) -> RepositoryPool: pool = RepositoryPool() pool.add_repository(repo) diff --git a/tests/console/commands/source/test_add.py b/tests/console/commands/source/test_add.py index 7e43b9e2014..4c9ca0dde0a 100644 --- a/tests/console/commands/source/test_add.py +++ b/tests/console/commands/source/test_add.py @@ -33,6 +33,9 @@ def assert_source_added( == f"Adding source with name {source_added.name}." ) poetry.pyproject.reload() + for source_table in poetry.pyproject.poetry_config["source"]: + # "default" is deprecated and should only be written if set to True + assert "default" not in source_table or source_table["default"] is True sources = poetry.get_sources() assert sources == [source_existing, source_added] assert tester.status_code == 0 @@ -55,6 +58,7 @@ def test_source_add_default( poetry_with_source: Poetry, ): tester.execute(f"--default {source_default.name} {source_default.url}") + assert "deprecated" in tester.io.fetch_error() assert_source_added(tester, poetry_with_source, source_existing, source_default) @@ -95,6 +99,7 @@ def test_source_add_existing( tester.io.fetch_output().strip() == f"Source with name {source_existing.name} already exists. Updating." ) + assert "deprecated" in tester.io.fetch_error() poetry_with_source.pyproject.reload() sources = poetry_with_source.get_sources() diff --git a/tests/console/commands/source/test_default.py b/tests/console/commands/source/test_default.py new file mode 100644 index 00000000000..60d9d742df7 --- /dev/null +++ b/tests/console/commands/source/test_default.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + + from poetry.config.source import Source + from poetry.poetry import Poetry + from tests.types import CommandTesterFactory + + +@pytest.fixture +def tester( + command_tester_factory: CommandTesterFactory, poetry_with_source: Poetry +) -> CommandTester: + return command_tester_factory("source default", poetry=poetry_with_source) + + +def test_source_default_enabled_by_default( + tester: CommandTester, + source_existing: Source, + source_default: Source, + poetry_with_source: Poetry, +) -> None: + tester.execute("") + assert "enabled" in tester.io.fetch_output() + poetry_with_source.pyproject.reload() + assert "default-source-pypi" not in poetry_with_source.pyproject.poetry_config + + +@pytest.mark.parametrize("enable", [True, False]) +def test_source_default_disable( + tester: CommandTester, + source_existing: Source, + source_default: Source, + poetry_with_source: Poetry, + enable: bool, +) -> None: + tester.execute("--enable-pypi" if enable else "--disable-pypi") + poetry_with_source.pyproject.reload() + assert poetry_with_source.pyproject.poetry_config["default-source-pypi"] is enable + + tester.execute("") + output = tester.io.fetch_output() + assert ("enabled" in output) is enable + assert ("disabled" in output) is not enable + poetry_with_source.pyproject.reload() + assert poetry_with_source.pyproject.poetry_config["default-source-pypi"] is enable diff --git a/tests/console/commands/source/test_show.py b/tests/console/commands/source/test_show.py index 8b975cf4b92..572ab4b9ed0 100644 --- a/tests/console/commands/source/test_show.py +++ b/tests/console/commands/source/test_show.py @@ -40,6 +40,32 @@ def test_source_show_simple(tester: CommandTester): url : https://two.com default : no secondary : no + +PyPI is enabled as implicit default source. +""".splitlines() + assert [ + line.strip() for line in tester.io.fetch_output().strip().splitlines() + ] == expected + assert tester.status_code == 0 + + +def test_source_show_default_pypi_disabled( + command_tester_factory: CommandTesterFactory, + poetry_with_source: Poetry, +) -> None: + tester = command_tester_factory("source default", poetry=poetry_with_source) + tester.execute("--disable-pypi") + + tester = command_tester_factory("source show", poetry=poetry_with_source) + tester.execute("") + + expected = """\ +name : existing +url : https://existing.com +default : no +secondary : no + +PyPI is disabled as implicit default source. """.splitlines() assert [ line.strip() for line in tester.io.fetch_output().strip().splitlines() diff --git a/tests/fixtures/with_no_explicit_source_pypi_disabled/pyproject.toml b/tests/fixtures/with_no_explicit_source_pypi_disabled/pyproject.toml new file mode 100644 index 00000000000..637eb32589c --- /dev/null +++ b/tests/fixtures/with_no_explicit_source_pypi_disabled/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" +default-source-pypi = false + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" + +[tool.poetry.dev-dependencies] diff --git a/tests/fixtures/with_non_default_source_pypi_disabled/pyproject.toml b/tests/fixtures/with_non_default_source_pypi_disabled/pyproject.toml new file mode 100644 index 00000000000..7362d2d2c06 --- /dev/null +++ b/tests/fixtures/with_non_default_source_pypi_disabled/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "my-package" +version = "1.2.3" +description = "Some description." +authors = [ + "Your Name " +] +license = "MIT" +default-source-pypi = false + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.6" + +[tool.poetry.dev-dependencies] + +[[tool.poetry.source]] +name = "foo" +url = "https://foo.bar/simple/" diff --git a/tests/test_factory.py b/tests/test_factory.py index 7eafc85d210..098be5e3606 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -5,6 +5,7 @@ import pytest +from cleo.io.buffered_io import BufferedIO from deepdiff import DeepDiff from packaging.utils import canonicalize_name from poetry.core.constraints.version import parse_constraint @@ -207,10 +208,25 @@ def test_create_poetry_with_multi_constraints_dependency(): def test_poetry_with_default_source(with_simple_keyring: None): - poetry = Factory().create_poetry(fixtures_dir / "with_default_source") + io = BufferedIO() + poetry = Factory().create_poetry(fixtures_dir / "with_default_source", io=io) assert len(poetry.pool.repositories) == 1 + assert poetry.pool.has_default() + + assert poetry.pool.repositories[0].name == "foo" + assert isinstance(poetry.pool.repositories[0], LegacyRepository) + + assert "deprecated" in io.fetch_error() + + +def test_poetry_with_two_default_sources(with_simple_keyring: None): + with pytest.raises(ValueError) as e: + Factory().create_poetry(fixtures_dir / "with_two_default_sources") + + assert str(e.value) == "Only one repository can be the default." + def test_poetry_with_non_default_source(with_simple_keyring: None): poetry = Factory().create_poetry(fixtures_dir / "with_non_default_source") @@ -226,6 +242,19 @@ def test_poetry_with_non_default_source(with_simple_keyring: None): assert isinstance(poetry.pool.repositories[1], PyPiRepository) +def test_poetry_with_nondefault_source_pypi_disabled(with_simple_keyring: None): + poetry = Factory().create_poetry( + fixtures_dir / "with_non_default_source_pypi_disabled" + ) + + assert len(poetry.pool.repositories) == 1 + + assert not poetry.pool.has_default() + + assert poetry.pool.repositories[0].name == "foo" + assert isinstance(poetry.pool.repositories[0], LegacyRepository) + + def test_poetry_with_non_default_secondary_source(with_simple_keyring: None): poetry = Factory().create_poetry(fixtures_dir / "with_non_default_secondary_source") @@ -284,7 +313,7 @@ def test_poetry_with_non_default_multiple_sources(with_simple_keyring: None): assert isinstance(repository, PyPiRepository) -def test_poetry_with_no_default_source(): +def test_poetry_with_no_explicit_source(): poetry = Factory().create_poetry(fixtures_dir / "sample_project") assert len(poetry.pool.repositories) == 1 @@ -295,11 +324,9 @@ def test_poetry_with_no_default_source(): assert isinstance(poetry.pool.repositories[0], PyPiRepository) -def test_poetry_with_two_default_sources(with_simple_keyring: None): - with pytest.raises(ValueError) as e: - Factory().create_poetry(fixtures_dir / "with_two_default_sources") - - assert str(e.value) == "Only one repository can be the default." +def test_poetry_with_no_explicit_source_pypi_disabled(): + with pytest.raises(RuntimeError, match="no sources"): + Factory().create_poetry(fixtures_dir / "with_no_explicit_source_pypi_disabled") def test_validate(): diff --git a/tests/utils/test_source.py b/tests/utils/test_source.py index a970b7262ca..862a2235fc4 100644 --- a/tests/utils/test_source.py +++ b/tests/utils/test_source.py @@ -16,7 +16,6 @@ ( Source("foo", "https://example.com"), { - "default": False, "name": "foo", "secondary": False, "url": "https://example.com",