diff --git a/docs/configuration.md b/docs/configuration.md index 6e2a7fe1923..98f9bff2471 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -221,8 +221,7 @@ you encounter on the [issue tracker](https://github.com/python-poetry/poetry/iss *Introduced in 1.2.0* -When set this configuration allows users to configure package distribution format policy for all or -specific packages. +When set, this configuration allows users to disallow the use of binary distribution format for all, none or specific packages. | Configuration | Description | |------------------------|------------------------------------------------------------| @@ -259,6 +258,24 @@ Unless this is required system-wide, if configured globally, you could encounter across all your projects if incorrectly set. {{% /warning %}} +### `installer.only-binary` + +**Type**: `string | boolean` + +**Default**: `false` + +**Environment Variable**: `POETRY_INSTALLER_ONLY_BINARY` + +*Introduced in 1.9.0* + +When set, this configuration allows users to enforce the use of binary distribution format for all, none or +specific packages. + +{{% note %}} +Please refer to [`installer.no-binary`]({{< relref "configuration#installerno-binary" >}}) for information on allowed +values, usage instructions and warnings. +{{% /note %}} + ### `installer.parallel` **Type**: `boolean` diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index 1919aa1746b..4860f74772e 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -132,6 +132,7 @@ class Config: "parallel": True, "max-workers": None, "no-binary": None, + "only-binary": None, }, "solver": { "lazy-wheel": True, @@ -323,7 +324,7 @@ def _get_normalizer(name: str) -> Callable[[str], Any]: if name == "installer.max-workers": return int_normalizer - if name == "installer.no-binary": + if name in ["installer.no-binary", "installer.only-binary"]: return PackageFilterPolicy.normalize return lambda val: val diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 17296ec24ea..8b862226689 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -81,6 +81,10 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: PackageFilterPolicy.validator, PackageFilterPolicy.normalize, ), + "installer.only-binary": ( + PackageFilterPolicy.validator, + PackageFilterPolicy.normalize, + ), "solver.lazy-wheel": (boolean_validator, boolean_normalizer), "warnings.export": (boolean_validator, boolean_normalizer), "keyring.enabled": (boolean_validator, boolean_normalizer), diff --git a/src/poetry/installation/chooser.py b/src/poetry/installation/chooser.py index e60f03cc5ac..8805c0d2ac3 100644 --- a/src/poetry/installation/chooser.py +++ b/src/poetry/installation/chooser.py @@ -39,6 +39,9 @@ def __init__( self._no_binary_policy: PackageFilterPolicy = PackageFilterPolicy( self._config.get("installer.no-binary", []) ) + self._only_binary_policy: PackageFilterPolicy = PackageFilterPolicy( + self._config.get("installer.only-binary", []) + ) def choose_for(self, package: Package) -> Link: """ @@ -68,6 +71,15 @@ def choose_for(self, package: Package) -> Link: logger.debug("Skipping unsupported distribution %s", link.filename) continue + if link.is_sdist and not self._only_binary_policy.allows(package.name): + logger.debug( + "Skipping source distribution for %s as requested in only binary policy for" + " package (%s)", + link.filename, + package.name, + ) + continue + links.append(link) if not links: diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index 7c5a370132e..218d47f8032 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -58,6 +58,7 @@ def test_list_displays_default_value_if_not_set( installer.max-workers = null installer.modern-installation = true installer.no-binary = null +installer.only-binary = null installer.parallel = true keyring.enabled = true solver.lazy-wheel = true @@ -90,6 +91,7 @@ def test_list_displays_set_get_setting( installer.max-workers = null installer.modern-installation = true installer.no-binary = null +installer.only-binary = null installer.parallel = true keyring.enabled = true solver.lazy-wheel = true @@ -143,6 +145,7 @@ def test_unset_setting( installer.max-workers = null installer.modern-installation = true installer.no-binary = null +installer.only-binary = null installer.parallel = true keyring.enabled = true solver.lazy-wheel = true @@ -174,6 +177,7 @@ def test_unset_repo_setting( installer.max-workers = null installer.modern-installation = true installer.no-binary = null +installer.only-binary = null installer.parallel = true keyring.enabled = true solver.lazy-wheel = true @@ -303,6 +307,7 @@ def test_list_displays_set_get_local_setting( installer.max-workers = null installer.modern-installation = true installer.no-binary = null +installer.only-binary = null installer.parallel = true keyring.enabled = true solver.lazy-wheel = true @@ -342,6 +347,7 @@ def test_list_must_not_display_sources_from_pyproject_toml( installer.max-workers = null installer.modern-installation = true installer.no-binary = null +installer.only-binary = null installer.parallel = true keyring.enabled = true repositories.foo.url = "https://foo.bar/simple/" @@ -515,6 +521,13 @@ def test_config_installer_parallel( assert workers == 1 +@pytest.mark.parametrize( + ("setting",), + [ + ("installer.no-binary",), + ("installer.only-binary",), + ], +) @pytest.mark.parametrize( ("value", "expected"), [ @@ -528,11 +541,9 @@ def test_config_installer_parallel( ("", []), ], ) -def test_config_installer_no_binary( - tester: CommandTester, value: str, expected: list[str] +def test_config_installer_binary_filter_config( + tester: CommandTester, setting: str, value: str, expected: list[str] ) -> None: - setting = "installer.no-binary" - tester.execute(setting) assert tester.io.fetch_output().strip() == "null" diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py index 4ea8e1fdd69..f74400dc489 100644 --- a/tests/installation/test_chooser.py +++ b/tests/installation/test_chooser.py @@ -125,17 +125,18 @@ def pool() -> RepositoryPool: return pool -@pytest.mark.parametrize("source_type", ["", "legacy"]) -def test_chooser_chooses_universal_wheel_link_if_available( +def check_chosen_link_filename( env: MockEnv, - mock_pypi: None, - mock_legacy: None, source_type: str, pool: RepositoryPool, + filename: str | None, + config: Config | None = None, + package_name: str = "pytest", + package_version: str = "3.5.0", ) -> None: - chooser = Chooser(pool, env) + chooser = Chooser(pool, env, config) + package = Package(package_name, package_version) - package = Package("pytest", "3.5.0") if source_type == "legacy": package = Package( package.name, @@ -145,9 +146,31 @@ def test_chooser_chooses_universal_wheel_link_if_available( source_url="https://foo.bar/simple/", ) - link = chooser.choose_for(package) + try: + link = chooser.choose_for(package) + except RuntimeError as e: + if filename is None: + assert ( + str(e) + == f"Unable to find installation candidates for {package.name} ({package.version})" + ) + else: + pytest.fail("Package was not found") + else: + assert link.filename == filename - assert link.filename == "pytest-3.5.0-py2.py3-none-any.whl" + +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_chooses_universal_wheel_link_if_available( + env: MockEnv, + mock_pypi: None, + mock_legacy: None, + source_type: str, + pool: RepositoryPool, +) -> None: + check_chosen_link_filename( + env, source_type, pool, "pytest-3.5.0-py2.py3-none-any.whl" + ) @pytest.mark.parametrize( @@ -172,22 +195,69 @@ def test_chooser_no_binary_policy( config: Config, ) -> None: config.merge({"installer": {"no-binary": policy.split(",")}}) + check_chosen_link_filename(env, source_type, pool, filename, config) - chooser = Chooser(pool, env, config) - package = Package("pytest", "3.5.0") - if source_type == "legacy": - package = Package( - package.name, - package.version.text, - source_type="legacy", - source_reference="foo", - source_url="https://foo.bar/simple/", - ) +@pytest.mark.parametrize( + ("policy", "filename"), + [ + (":all:", "pytest-3.5.0-py2.py3-none-any.whl"), + (":none:", "pytest-3.5.0-py2.py3-none-any.whl"), + ("black", "pytest-3.5.0-py2.py3-none-any.whl"), + ("pytest", "pytest-3.5.0-py2.py3-none-any.whl"), + ("pytest,black", "pytest-3.5.0-py2.py3-none-any.whl"), + ], +) +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_only_binary_policy( + env: MockEnv, + mock_pypi: None, + mock_legacy: None, + source_type: str, + pool: RepositoryPool, + policy: str, + filename: str, + config: Config, +) -> None: + config.merge({"installer": {"only-binary": policy.split(",")}}) + check_chosen_link_filename(env, source_type, pool, filename, config) - link = chooser.choose_for(package) - assert link.filename == filename +@pytest.mark.parametrize( + ("no_binary", "only_binary", "filename"), + [ + (":all:", ":all:", None), + (":none:", ":none:", "pytest-3.5.0-py2.py3-none-any.whl"), + (":none:", ":all:", "pytest-3.5.0-py2.py3-none-any.whl"), + (":all:", ":none:", "pytest-3.5.0.tar.gz"), + ("black", "black", "pytest-3.5.0-py2.py3-none-any.whl"), + ("black", "pytest", "pytest-3.5.0-py2.py3-none-any.whl"), + ("pytest", "black", "pytest-3.5.0.tar.gz"), + ("pytest", "pytest", None), + ("pytest,black", "pytest,black", None), + ], +) +@pytest.mark.parametrize("source_type", ["", "legacy"]) +def test_chooser_multiple_binary_policy( + env: MockEnv, + mock_pypi: None, + mock_legacy: None, + source_type: str, + pool: RepositoryPool, + no_binary: str, + only_binary: str, + filename: str | None, + config: Config, +) -> None: + config.merge( + { + "installer": { + "no-binary": no_binary.split(","), + "only-binary": only_binary.split(","), + } + } + ) + check_chosen_link_filename(env, source_type, pool, filename, config) @pytest.mark.parametrize("source_type", ["", "legacy"]) @@ -198,21 +268,9 @@ def test_chooser_chooses_specific_python_universal_wheel_link_if_available( source_type: str, pool: RepositoryPool, ) -> None: - chooser = Chooser(pool, env) - - package = Package("isort", "4.3.4") - if source_type == "legacy": - package = Package( - package.name, - package.version.text, - source_type="legacy", - source_reference="foo", - source_url="https://foo.bar/simple/", - ) - - link = chooser.choose_for(package) - - assert link.filename == "isort-4.3.4-py3-none-any.whl" + check_chosen_link_filename( + env, source_type, pool, "isort-4.3.4-py3-none-any.whl", None, "isort", "4.3.4" + ) @pytest.mark.parametrize("source_type", ["", "legacy"]) @@ -222,21 +280,15 @@ def test_chooser_chooses_system_specific_wheel_link_if_available( env = MockEnv( supported_tags=[Tag("cp37", "cp37m", "win32"), Tag("py3", "none", "any")] ) - chooser = Chooser(pool, env) - - package = Package("pyyaml", "3.13.0") - if source_type == "legacy": - package = Package( - package.name, - package.version.text, - source_type="legacy", - source_reference="foo", - source_url="https://foo.bar/simple/", - ) - - link = chooser.choose_for(package) - - assert link.filename == "PyYAML-3.13-cp37-cp37m-win32.whl" + check_chosen_link_filename( + env, + source_type, + pool, + "PyYAML-3.13-cp37-cp37m-win32.whl", + None, + "pyyaml", + "3.13.0", + ) @pytest.mark.parametrize("source_type", ["", "legacy"]) @@ -247,21 +299,9 @@ def test_chooser_chooses_sdist_if_no_compatible_wheel_link_is_available( source_type: str, pool: RepositoryPool, ) -> None: - chooser = Chooser(pool, env) - - package = Package("pyyaml", "3.13.0") - if source_type == "legacy": - package = Package( - package.name, - package.version.text, - source_type="legacy", - source_reference="foo", - source_url="https://foo.bar/simple/", - ) - - link = chooser.choose_for(package) - - assert link.filename == "PyYAML-3.13.tar.gz" + check_chosen_link_filename( + env, source_type, pool, "PyYAML-3.13.tar.gz", None, "pyyaml", "3.13.0" + ) @pytest.mark.parametrize("source_type", ["", "legacy"])