Skip to content

Commit ed26721

Browse files
abnneersighted
authored andcommitted
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.
1 parent feb11b1 commit ed26721

File tree

11 files changed

+170
-90
lines changed

11 files changed

+170
-90
lines changed

docs/cli.md

-6
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ option is used.
223223
* `--default`: Only include the main dependencies. (**Deprecated**)
224224
* `--sync`: Synchronize the environment with the locked packages and the specified groups.
225225
* `--no-root`: Do not install the root package (your project).
226-
* `--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.
227226
* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose).
228227
* `--extras (-E)`: Features to install (multiple values allowed).
229228
* `--no-dev`: Do not install dev dependencies. (**Deprecated**)
@@ -234,11 +233,6 @@ option is used.
234233
When `--only` is specified, `--with` and `--without` options are ignored.
235234
{{% /note %}}
236235

237-
{{% note %}}
238-
The `--no-binary` option will only work with the new installer. For the old installer,
239-
this is ignored.
240-
{{% /note %}}
241-
242236

243237
## update
244238

docs/configuration.md

+48
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,54 @@ the number of maximum workers is still limited at `number_of_cores + 4`.
141141
This configuration will be ignored when `installer.parallel` is set to false.
142142
{{% /note %}}
143143

144+
### `installer.no-binary`
145+
146+
**Type**: string | bool
147+
148+
*Introduced in 1.2.0*
149+
150+
When set this configuration allows users to configure package distribution format policy for all or
151+
specific packages.
152+
153+
| Configuration | Description |
154+
|------------------------|------------------------------------------------------------|
155+
| `:all:` or `true` | Disallow binary distributions for all packages. |
156+
| `:none:` or `false` | Allow binary distributions for all packages. |
157+
| `package[,package,..]` | Disallow binary distributions for specified packages only. |
158+
159+
{{% note %}}
160+
This configuration is only respected when using the new installer. If you have disabled it please
161+
consider re-enabling it.
162+
163+
As with all configurations described here, this is a user specific configuration. This means that this
164+
is not taken into consideration when a lockfile is generated or dependencies are resolved. This is
165+
applied only when selecting which distribution for dependency should be installed into a Poetry managed
166+
environment.
167+
{{% /note %}}
168+
169+
{{% note %}}
170+
For project specific usage, it is recommended that this be configured with the `--local`.
171+
172+
```bash
173+
poetry config --local installer.no-binary :all:
174+
```
175+
{{% /note %}}
176+
177+
{{% note %}}
178+
For CI or container environments using [environment variable](#using-environment-variables)
179+
to configure this might be useful.
180+
181+
```bash
182+
export POETRY_INSTALLER_NO_BINARY=:all:
183+
```
184+
{{% /note %}}
185+
186+
{{% warning %}}
187+
Unless this is required system-wide, if configured globally, you could encounter slower install times
188+
across all your projects if incorrectly set.
189+
{{% /warning %}}
190+
191+
144192
### `virtualenvs.create`
145193

146194
**Type**: boolean

src/poetry/config/config.py

+67-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import logging
45
import os
56
import re
@@ -11,6 +12,7 @@
1112
from typing import Callable
1213

1314
from poetry.core.toml import TOMLFile
15+
from poetry.core.utils.helpers import canonicalize_name
1416

1517
from poetry.config.dict_config_source import DictConfigSource
1618
from poetry.config.file_config_source import FileConfigSource
@@ -34,6 +36,67 @@ def int_normalizer(val: str) -> int:
3436
return int(val)
3537

3638

39+
@dataclasses.dataclass
40+
class PackageFilterPolicy:
41+
policy: dataclasses.InitVar[str | list[str] | None]
42+
packages: list[str] = dataclasses.field(init=False)
43+
44+
def __post_init__(self, policy: str | list[str] | None) -> None:
45+
if not policy:
46+
policy = []
47+
elif isinstance(policy, str):
48+
policy = self.normalize(policy)
49+
self.packages = policy
50+
51+
def allows(self, package_name: str) -> bool:
52+
if ":all:" in self.packages:
53+
return False
54+
55+
return (
56+
not self.packages
57+
or ":none:" in self.packages
58+
or canonicalize_name(package_name) not in self.packages
59+
)
60+
61+
@classmethod
62+
def is_reserved(cls, name: str) -> bool:
63+
return bool(re.match(r":(all|none):", name))
64+
65+
@classmethod
66+
def normalize(cls, policy: str) -> list[str]:
67+
if boolean_validator(policy):
68+
if boolean_normalizer(policy):
69+
return [":all:"]
70+
else:
71+
return [":none:"]
72+
73+
return list(
74+
{
75+
name.strip() if cls.is_reserved(name) else canonicalize_name(name)
76+
for name in policy.strip().split(",")
77+
if name
78+
}
79+
)
80+
81+
@classmethod
82+
def validator(cls, policy: str) -> bool:
83+
if boolean_validator(policy):
84+
return True
85+
86+
names = policy.strip().split(",")
87+
88+
for name in names:
89+
if (
90+
not name
91+
or (cls.is_reserved(name) and len(names) == 1)
92+
or re.match(r"^[a-zA-Z\d_-]+$", name)
93+
):
94+
continue
95+
return False
96+
97+
return True
98+
99+
37100
logger = logging.getLogger(__name__)
38101

39102

@@ -61,7 +124,7 @@ class Config:
61124
"prefer-active-python": False,
62125
},
63126
"experimental": {"new-installer": True, "system-git-client": False},
64-
"installer": {"parallel": True, "max-workers": None},
127+
"installer": {"parallel": True, "max-workers": None, "no-binary": None},
65128
}
66129

67130
def __init__(
@@ -196,6 +259,9 @@ def _get_normalizer(name: str) -> Callable[[str], Any]:
196259
if name == "installer.max-workers":
197260
return int_normalizer
198261

262+
if name == "installer.no-binary":
263+
return PackageFilterPolicy.normalize
264+
199265
return lambda val: val
200266

201267
@classmethod

src/poetry/console/commands/config.py

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from cleo.helpers import argument
1111
from cleo.helpers import option
1212

13+
from poetry.config.config import PackageFilterPolicy
1314
from poetry.console.commands.command import Command
1415

1516

@@ -107,6 +108,11 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]:
107108
int_normalizer,
108109
None,
109110
),
111+
"installer.no-binary": (
112+
PackageFilterPolicy.validator,
113+
PackageFilterPolicy.normalize,
114+
None,
115+
),
110116
}
111117

112118
return unique_config_values

src/poetry/console/commands/install.py

-21
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,6 @@ class InstallCommand(InstallerCommand):
3333
option(
3434
"no-root", None, "Do not install the root package (the current project)."
3535
),
36-
option(
37-
"no-binary",
38-
None,
39-
"Do not use binary distributions for packages matching given policy.\n"
40-
"Use package name to disallow a specific package; or <b>:all:</b> to\n"
41-
"disallow and <b>:none:</b> to force binary for all packages. Multiple\n"
42-
"packages can be specified separated by commas.",
43-
flag=False,
44-
multiple=True,
45-
),
4636
option(
4737
"dry-run",
4838
None,
@@ -108,17 +98,6 @@ def handle(self) -> int:
10898

10999
with_synchronization = True
110100

111-
if self.option("no-binary"):
112-
policy = ",".join(self.option("no-binary", []))
113-
try:
114-
self._installer.no_binary(policy=policy)
115-
except ValueError as e:
116-
self.line_error(
117-
f"<warning>Invalid value (<c1>{policy}</>) for"
118-
f" `<b>--no-binary</b>`</>.\n\n<error>{e}</>"
119-
)
120-
return 1
121-
122101
self._installer.only_groups(self.activated_groups)
123102
self._installer.dry_run(self.option("dry-run"))
124103
self._installer.requires_synchronization(with_synchronization)

src/poetry/installation/chooser.py

+7-24
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from packaging.tags import Tag
99

10-
from poetry.utils.helpers import canonicalize_name
10+
from poetry.config.config import Config
11+
from poetry.config.config import PackageFilterPolicy
1112
from poetry.utils.patterns import wheel_file_re
1213

1314

@@ -58,30 +59,12 @@ class Chooser:
5859
A Chooser chooses an appropriate release archive for packages.
5960
"""
6061

61-
def __init__(self, pool: Pool, env: Env) -> None:
62+
def __init__(self, pool: Pool, env: Env, config: Config | None = None) -> None:
6263
self._pool = pool
6364
self._env = env
64-
self._no_binary_policy: set[str] = set()
65-
66-
def set_no_binary_policy(self, policy: str) -> None:
67-
self._no_binary_policy = {
68-
name.strip() if re.match(r":(all|none):", name) else canonicalize_name(name)
69-
for name in policy.split(",")
70-
}
71-
72-
if {":all:", ":none:"} <= self._no_binary_policy:
73-
raise ValueError(
74-
"Ambiguous binary policy containing :all: and :none: given."
75-
)
76-
77-
def allow_binary(self, package_name: str) -> bool:
78-
if ":all:" in self._no_binary_policy:
79-
return False
80-
81-
return (
82-
not self._no_binary_policy
83-
or ":none:" in self._no_binary_policy
84-
or canonicalize_name(package_name) not in self._no_binary_policy
65+
self._config = config or Config.create()
66+
self._no_binary_policy: PackageFilterPolicy = PackageFilterPolicy(
67+
self._config.get("installer.no-binary", [])
8568
)
8669

8770
def choose_for(self, package: Package) -> Link:
@@ -91,7 +74,7 @@ def choose_for(self, package: Package) -> Link:
9174
links = []
9275
for link in self._get_links(package):
9376
if link.is_wheel:
94-
if not self.allow_binary(package.name):
77+
if not self._no_binary_policy.allows(package.name):
9578
logger.debug(
9679
"Skipping wheel for %s as requested in no binary policy for"
9780
" package (%s)",

src/poetry/installation/executor.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __init__(
5858
self._verbose = False
5959
self._authenticator = Authenticator(config, self._io)
6060
self._chef = Chef(config, self._env)
61-
self._chooser = Chooser(pool, self._env)
61+
self._chooser = Chooser(pool, self._env, config)
6262

6363
if parallel is None:
6464
parallel = config.get("installer.parallel", True)
@@ -92,9 +92,6 @@ def updates_count(self) -> int:
9292
def removals_count(self) -> int:
9393
return self._executed["uninstall"]
9494

95-
def set_no_binary_policy(self, policy: str) -> None:
96-
self._chooser.set_no_binary_policy(policy)
97-
9895
def supports_fancy_output(self) -> bool:
9996
return self._io.output.is_decorated() and not self._dry_run
10097

src/poetry/installation/installer.py

-5
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,6 @@ def verbose(self, verbose: bool = True) -> Installer:
135135
def is_verbose(self) -> bool:
136136
return self._verbose
137137

138-
def no_binary(self, policy: str) -> Installer:
139-
if self._executor:
140-
self._executor.set_no_binary_policy(policy=policy)
141-
return self
142-
143138
def only_groups(self, groups: Iterable[str]) -> Installer:
144139
self._groups = groups
145140

tests/console/commands/test_config.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
import pytest
99

10+
from deepdiff import DeepDiff
1011
from poetry.core.pyproject.exceptions import PyProjectException
1112

1213
from poetry.config.config_source import ConfigSource
1314
from poetry.factory import Factory
15+
from tests.conftest import Config
1416

1517

1618
if TYPE_CHECKING:
@@ -20,7 +22,6 @@
2022
from pytest_mock import MockerFixture
2123

2224
from poetry.config.dict_config_source import DictConfigSource
23-
from tests.conftest import Config
2425
from tests.types import CommandTesterFactory
2526
from tests.types import FixtureDirGetter
2627

@@ -53,6 +54,7 @@ def test_list_displays_default_value_if_not_set(
5354
experimental.new-installer = true
5455
experimental.system-git-client = false
5556
installer.max-workers = null
57+
installer.no-binary = null
5658
installer.parallel = true
5759
virtualenvs.create = true
5860
virtualenvs.in-project = null
@@ -80,6 +82,7 @@ def test_list_displays_set_get_setting(
8082
experimental.new-installer = true
8183
experimental.system-git-client = false
8284
installer.max-workers = null
85+
installer.no-binary = null
8386
installer.parallel = true
8487
virtualenvs.create = false
8588
virtualenvs.in-project = null
@@ -131,6 +134,7 @@ def test_list_displays_set_get_local_setting(
131134
experimental.new-installer = true
132135
experimental.system-git-client = false
133136
installer.max-workers = null
137+
installer.no-binary = null
134138
installer.parallel = true
135139
virtualenvs.create = false
136140
virtualenvs.in-project = null
@@ -200,3 +204,33 @@ def test_config_installer_parallel(
200204
"install"
201205
)._command._installer._executor._max_workers
202206
assert workers == 1
207+
208+
209+
@pytest.mark.parametrize(
210+
("value", "expected"),
211+
[
212+
("true", [":all:"]),
213+
("1", [":all:"]),
214+
("false", [":none:"]),
215+
("0", [":none:"]),
216+
("pytest", ["pytest"]),
217+
("PyTest", ["pytest"]),
218+
("pytest,black", ["pytest", "black"]),
219+
("", []),
220+
],
221+
)
222+
def test_config_installer_no_binary(
223+
tester: CommandTester, value: str, expected: list[str]
224+
) -> None:
225+
setting = "installer.no-binary"
226+
227+
tester.execute(setting)
228+
assert tester.io.fetch_output().strip() == "null"
229+
230+
config = Config.create()
231+
assert not config.get(setting)
232+
233+
tester.execute(f"{setting} '{value}'")
234+
235+
config = Config.create(reload=True)
236+
assert not DeepDiff(config.get(setting), expected, ignore_order=True)

0 commit comments

Comments
 (0)