Commit ed26721c by Arun Babu Neelicattu Committed by Bjorn Neergaard

config: introduce `installer.no-binary`

This change replaces the `--no-binary` option introduced in #5600 as
the original implementation could cause inconsistent results when the
add, update or lock commands were used.

This implementation makes use of a new configuration
`installer.no-binary` to allow for user specification of sdist
preference for select packages.
parent feb11b13
...@@ -223,7 +223,6 @@ option is used. ...@@ -223,7 +223,6 @@ option is used.
* `--default`: Only include the main dependencies. (**Deprecated**) * `--default`: Only include the main dependencies. (**Deprecated**)
* `--sync`: Synchronize the environment with the locked packages and the specified groups. * `--sync`: Synchronize the environment with the locked packages and the specified groups.
* `--no-root`: Do not install the root package (your project). * `--no-root`: Do not install the root package (your project).
* `--no-binary`: Do not use binary distributions for packages matching given policy. Use package name to disallow a specific package; or `:all:` to disallow and `:none:` to force binary for all packages.
* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). * `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose).
* `--extras (-E)`: Features to install (multiple values allowed). * `--extras (-E)`: Features to install (multiple values allowed).
* `--no-dev`: Do not install dev dependencies. (**Deprecated**) * `--no-dev`: Do not install dev dependencies. (**Deprecated**)
...@@ -234,11 +233,6 @@ option is used. ...@@ -234,11 +233,6 @@ option is used.
When `--only` is specified, `--with` and `--without` options are ignored. When `--only` is specified, `--with` and `--without` options are ignored.
{{% /note %}} {{% /note %}}
{{% note %}}
The `--no-binary` option will only work with the new installer. For the old installer,
this is ignored.
{{% /note %}}
## update ## update
......
...@@ -141,6 +141,54 @@ the number of maximum workers is still limited at `number_of_cores + 4`. ...@@ -141,6 +141,54 @@ the number of maximum workers is still limited at `number_of_cores + 4`.
This configuration will be ignored when `installer.parallel` is set to false. This configuration will be ignored when `installer.parallel` is set to false.
{{% /note %}} {{% /note %}}
### `installer.no-binary`
**Type**: string | bool
*Introduced in 1.2.0*
When set this configuration allows users to configure package distribution format policy for all or
specific packages.
| Configuration | Description |
|------------------------|------------------------------------------------------------|
| `:all:` or `true` | Disallow binary distributions for all packages. |
| `:none:` or `false` | Allow binary distributions for all packages. |
| `package[,package,..]` | Disallow binary distributions for specified packages only. |
{{% note %}}
This configuration is only respected when using the new installer. If you have disabled it please
consider re-enabling it.
As with all configurations described here, this is a user specific configuration. This means that this
is not taken into consideration when a lockfile is generated or dependencies are resolved. This is
applied only when selecting which distribution for dependency should be installed into a Poetry managed
environment.
{{% /note %}}
{{% note %}}
For project specific usage, it is recommended that this be configured with the `--local`.
```bash
poetry config --local installer.no-binary :all:
```
{{% /note %}}
{{% note %}}
For CI or container environments using [environment variable](#using-environment-variables)
to configure this might be useful.
```bash
export POETRY_INSTALLER_NO_BINARY=:all:
```
{{% /note %}}
{{% warning %}}
Unless this is required system-wide, if configured globally, you could encounter slower install times
across all your projects if incorrectly set.
{{% /warning %}}
### `virtualenvs.create` ### `virtualenvs.create`
**Type**: boolean **Type**: boolean
......
from __future__ import annotations from __future__ import annotations
import dataclasses
import logging import logging
import os import os
import re import re
...@@ -11,6 +12,7 @@ from typing import Any ...@@ -11,6 +12,7 @@ from typing import Any
from typing import Callable from typing import Callable
from poetry.core.toml import TOMLFile from poetry.core.toml import TOMLFile
from poetry.core.utils.helpers import canonicalize_name
from poetry.config.dict_config_source import DictConfigSource from poetry.config.dict_config_source import DictConfigSource
from poetry.config.file_config_source import FileConfigSource from poetry.config.file_config_source import FileConfigSource
...@@ -34,6 +36,67 @@ def int_normalizer(val: str) -> int: ...@@ -34,6 +36,67 @@ def int_normalizer(val: str) -> int:
return int(val) return int(val)
@dataclasses.dataclass
class PackageFilterPolicy:
policy: dataclasses.InitVar[str | list[str] | None]
packages: list[str] = dataclasses.field(init=False)
def __post_init__(self, policy: str | list[str] | None) -> None:
if not policy:
policy = []
elif isinstance(policy, str):
policy = self.normalize(policy)
self.packages = policy
def allows(self, package_name: str) -> bool:
if ":all:" in self.packages:
return False
return (
not self.packages
or ":none:" in self.packages
or canonicalize_name(package_name) not in self.packages
)
@classmethod
def is_reserved(cls, name: str) -> bool:
return bool(re.match(r":(all|none):", name))
@classmethod
def normalize(cls, policy: str) -> list[str]:
if boolean_validator(policy):
if boolean_normalizer(policy):
return [":all:"]
else:
return [":none:"]
return list(
{
name.strip() if cls.is_reserved(name) else canonicalize_name(name)
for name in policy.strip().split(",")
if name
}
)
@classmethod
def validator(cls, policy: str) -> bool:
if boolean_validator(policy):
return True
names = policy.strip().split(",")
for name in names:
if (
not name
or (cls.is_reserved(name) and len(names) == 1)
or re.match(r"^[a-zA-Z\d_-]+$", name)
):
continue
return False
return True
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -61,7 +124,7 @@ class Config: ...@@ -61,7 +124,7 @@ class Config:
"prefer-active-python": False, "prefer-active-python": False,
}, },
"experimental": {"new-installer": True, "system-git-client": False}, "experimental": {"new-installer": True, "system-git-client": False},
"installer": {"parallel": True, "max-workers": None}, "installer": {"parallel": True, "max-workers": None, "no-binary": None},
} }
def __init__( def __init__(
...@@ -196,6 +259,9 @@ class Config: ...@@ -196,6 +259,9 @@ class Config:
if name == "installer.max-workers": if name == "installer.max-workers":
return int_normalizer return int_normalizer
if name == "installer.no-binary":
return PackageFilterPolicy.normalize
return lambda val: val return lambda val: val
@classmethod @classmethod
......
...@@ -10,6 +10,7 @@ from typing import cast ...@@ -10,6 +10,7 @@ from typing import cast
from cleo.helpers import argument from cleo.helpers import argument
from cleo.helpers import option from cleo.helpers import option
from poetry.config.config import PackageFilterPolicy
from poetry.console.commands.command import Command from poetry.console.commands.command import Command
...@@ -107,6 +108,11 @@ To remove a repository (repo is a short alias for repositories): ...@@ -107,6 +108,11 @@ To remove a repository (repo is a short alias for repositories):
int_normalizer, int_normalizer,
None, None,
), ),
"installer.no-binary": (
PackageFilterPolicy.validator,
PackageFilterPolicy.normalize,
None,
),
} }
return unique_config_values return unique_config_values
......
...@@ -34,16 +34,6 @@ class InstallCommand(InstallerCommand): ...@@ -34,16 +34,6 @@ class InstallCommand(InstallerCommand):
"no-root", None, "Do not install the root package (the current project)." "no-root", None, "Do not install the root package (the current project)."
), ),
option( option(
"no-binary",
None,
"Do not use binary distributions for packages matching given policy.\n"
"Use package name to disallow a specific package; or <b>:all:</b> to\n"
"disallow and <b>:none:</b> to force binary for all packages. Multiple\n"
"packages can be specified separated by commas.",
flag=False,
multiple=True,
),
option(
"dry-run", "dry-run",
None, None,
"Output the operations but do not execute anything " "Output the operations but do not execute anything "
...@@ -108,17 +98,6 @@ dependencies and not including the current project, run the command with the ...@@ -108,17 +98,6 @@ dependencies and not including the current project, run the command with the
with_synchronization = True with_synchronization = True
if self.option("no-binary"):
policy = ",".join(self.option("no-binary", []))
try:
self._installer.no_binary(policy=policy)
except ValueError as e:
self.line_error(
f"<warning>Invalid value (<c1>{policy}</>) for"
f" `<b>--no-binary</b>`</>.\n\n<error>{e}</>"
)
return 1
self._installer.only_groups(self.activated_groups) self._installer.only_groups(self.activated_groups)
self._installer.dry_run(self.option("dry-run")) self._installer.dry_run(self.option("dry-run"))
self._installer.requires_synchronization(with_synchronization) self._installer.requires_synchronization(with_synchronization)
......
...@@ -7,7 +7,8 @@ from typing import TYPE_CHECKING ...@@ -7,7 +7,8 @@ from typing import TYPE_CHECKING
from packaging.tags import Tag from packaging.tags import Tag
from poetry.utils.helpers import canonicalize_name from poetry.config.config import Config
from poetry.config.config import PackageFilterPolicy
from poetry.utils.patterns import wheel_file_re from poetry.utils.patterns import wheel_file_re
...@@ -58,30 +59,12 @@ class Chooser: ...@@ -58,30 +59,12 @@ class Chooser:
A Chooser chooses an appropriate release archive for packages. A Chooser chooses an appropriate release archive for packages.
""" """
def __init__(self, pool: Pool, env: Env) -> None: def __init__(self, pool: Pool, env: Env, config: Config | None = None) -> None:
self._pool = pool self._pool = pool
self._env = env self._env = env
self._no_binary_policy: set[str] = set() self._config = config or Config.create()
self._no_binary_policy: PackageFilterPolicy = PackageFilterPolicy(
def set_no_binary_policy(self, policy: str) -> None: self._config.get("installer.no-binary", [])
self._no_binary_policy = {
name.strip() if re.match(r":(all|none):", name) else canonicalize_name(name)
for name in policy.split(",")
}
if {":all:", ":none:"} <= self._no_binary_policy:
raise ValueError(
"Ambiguous binary policy containing :all: and :none: given."
)
def allow_binary(self, package_name: str) -> bool:
if ":all:" in self._no_binary_policy:
return False
return (
not self._no_binary_policy
or ":none:" in self._no_binary_policy
or canonicalize_name(package_name) not in self._no_binary_policy
) )
def choose_for(self, package: Package) -> Link: def choose_for(self, package: Package) -> Link:
...@@ -91,7 +74,7 @@ class Chooser: ...@@ -91,7 +74,7 @@ class Chooser:
links = [] links = []
for link in self._get_links(package): for link in self._get_links(package):
if link.is_wheel: if link.is_wheel:
if not self.allow_binary(package.name): if not self._no_binary_policy.allows(package.name):
logger.debug( logger.debug(
"Skipping wheel for %s as requested in no binary policy for" "Skipping wheel for %s as requested in no binary policy for"
" package (%s)", " package (%s)",
......
...@@ -58,7 +58,7 @@ class Executor: ...@@ -58,7 +58,7 @@ class Executor:
self._verbose = False self._verbose = False
self._authenticator = Authenticator(config, self._io) self._authenticator = Authenticator(config, self._io)
self._chef = Chef(config, self._env) self._chef = Chef(config, self._env)
self._chooser = Chooser(pool, self._env) self._chooser = Chooser(pool, self._env, config)
if parallel is None: if parallel is None:
parallel = config.get("installer.parallel", True) parallel = config.get("installer.parallel", True)
...@@ -92,9 +92,6 @@ class Executor: ...@@ -92,9 +92,6 @@ class Executor:
def removals_count(self) -> int: def removals_count(self) -> int:
return self._executed["uninstall"] return self._executed["uninstall"]
def set_no_binary_policy(self, policy: str) -> None:
self._chooser.set_no_binary_policy(policy)
def supports_fancy_output(self) -> bool: def supports_fancy_output(self) -> bool:
return self._io.output.is_decorated() and not self._dry_run return self._io.output.is_decorated() and not self._dry_run
......
...@@ -135,11 +135,6 @@ class Installer: ...@@ -135,11 +135,6 @@ class Installer:
def is_verbose(self) -> bool: def is_verbose(self) -> bool:
return self._verbose return self._verbose
def no_binary(self, policy: str) -> Installer:
if self._executor:
self._executor.set_no_binary_policy(policy=policy)
return self
def only_groups(self, groups: Iterable[str]) -> Installer: def only_groups(self, groups: Iterable[str]) -> Installer:
self._groups = groups self._groups = groups
......
...@@ -7,10 +7,12 @@ from typing import TYPE_CHECKING ...@@ -7,10 +7,12 @@ from typing import TYPE_CHECKING
import pytest import pytest
from deepdiff import DeepDiff
from poetry.core.pyproject.exceptions import PyProjectException from poetry.core.pyproject.exceptions import PyProjectException
from poetry.config.config_source import ConfigSource from poetry.config.config_source import ConfigSource
from poetry.factory import Factory from poetry.factory import Factory
from tests.conftest import Config
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -20,7 +22,6 @@ if TYPE_CHECKING: ...@@ -20,7 +22,6 @@ if TYPE_CHECKING:
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from poetry.config.dict_config_source import DictConfigSource from poetry.config.dict_config_source import DictConfigSource
from tests.conftest import Config
from tests.types import CommandTesterFactory from tests.types import CommandTesterFactory
from tests.types import FixtureDirGetter from tests.types import FixtureDirGetter
...@@ -53,6 +54,7 @@ def test_list_displays_default_value_if_not_set( ...@@ -53,6 +54,7 @@ def test_list_displays_default_value_if_not_set(
experimental.new-installer = true experimental.new-installer = true
experimental.system-git-client = false experimental.system-git-client = false
installer.max-workers = null installer.max-workers = null
installer.no-binary = null
installer.parallel = true installer.parallel = true
virtualenvs.create = true virtualenvs.create = true
virtualenvs.in-project = null virtualenvs.in-project = null
...@@ -80,6 +82,7 @@ def test_list_displays_set_get_setting( ...@@ -80,6 +82,7 @@ def test_list_displays_set_get_setting(
experimental.new-installer = true experimental.new-installer = true
experimental.system-git-client = false experimental.system-git-client = false
installer.max-workers = null installer.max-workers = null
installer.no-binary = null
installer.parallel = true installer.parallel = true
virtualenvs.create = false virtualenvs.create = false
virtualenvs.in-project = null virtualenvs.in-project = null
...@@ -131,6 +134,7 @@ def test_list_displays_set_get_local_setting( ...@@ -131,6 +134,7 @@ def test_list_displays_set_get_local_setting(
experimental.new-installer = true experimental.new-installer = true
experimental.system-git-client = false experimental.system-git-client = false
installer.max-workers = null installer.max-workers = null
installer.no-binary = null
installer.parallel = true installer.parallel = true
virtualenvs.create = false virtualenvs.create = false
virtualenvs.in-project = null virtualenvs.in-project = null
...@@ -200,3 +204,33 @@ def test_config_installer_parallel( ...@@ -200,3 +204,33 @@ def test_config_installer_parallel(
"install" "install"
)._command._installer._executor._max_workers )._command._installer._executor._max_workers
assert workers == 1 assert workers == 1
@pytest.mark.parametrize(
("value", "expected"),
[
("true", [":all:"]),
("1", [":all:"]),
("false", [":none:"]),
("0", [":none:"]),
("pytest", ["pytest"]),
("PyTest", ["pytest"]),
("pytest,black", ["pytest", "black"]),
("", []),
],
)
def test_config_installer_no_binary(
tester: CommandTester, value: str, expected: list[str]
) -> None:
setting = "installer.no-binary"
tester.execute(setting)
assert tester.io.fetch_output().strip() == "null"
config = Config.create()
assert not config.get(setting)
tester.execute(f"{setting} '{value}'")
config = Config.create(reload=True)
assert not DeepDiff(config.get(setting), expected, ignore_order=True)
...@@ -136,29 +136,3 @@ def test_sync_option_is_passed_to_the_installer( ...@@ -136,29 +136,3 @@ def test_sync_option_is_passed_to_the_installer(
tester.execute("--sync") tester.execute("--sync")
assert tester.command.installer._requires_synchronization assert tester.command.installer._requires_synchronization
@pytest.mark.parametrize(
("options", "policy"),
[
(
"--no-binary :all:",
{":all:"},
),
("--no-binary :none:", {":none:"}),
("--no-binary pytest", {"pytest"}),
("--no-binary pytest,black", {"black", "pytest"}),
("--no-binary pytest --no-binary black", {"black", "pytest"}),
],
)
def test_no_binary_option_is_passed_to_the_installer(
tester: CommandTester, mocker: MockerFixture, options: str, policy: set[str]
) -> None:
"""
The --no-binary option is passed properly to the installer.
"""
mocker.patch.object(tester.command.installer, "run", return_value=1)
tester.execute(options)
assert tester.command.installer.executor._chooser._no_binary_policy == policy
...@@ -23,6 +23,8 @@ if TYPE_CHECKING: ...@@ -23,6 +23,8 @@ if TYPE_CHECKING:
from httpretty.core import HTTPrettyRequest from httpretty.core import HTTPrettyRequest
from tests.conftest import Config
JSON_FIXTURES = ( JSON_FIXTURES = (
Path(__file__).parent.parent / "repositories" / "fixtures" / "pypi.org" / "json" Path(__file__).parent.parent / "repositories" / "fixtures" / "pypi.org" / "json"
...@@ -140,9 +142,11 @@ def test_chooser_no_binary_policy( ...@@ -140,9 +142,11 @@ def test_chooser_no_binary_policy(
pool: Pool, pool: Pool,
policy: str, policy: str,
filename: str, filename: str,
config: Config,
): ):
chooser = Chooser(pool, env) config.merge({"installer": {"no-binary": policy.split(",")}})
chooser.set_no_binary_policy(policy)
chooser = Chooser(pool, env, config)
package = Package("pytest", "3.5.0") package = Package("pytest", "3.5.0")
if source_type == "legacy": if source_type == "legacy":
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment