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",