Skip to content

Commit f8535fc

Browse files
committed
install: add --no-binary option
1 parent 754199e commit f8535fc

7 files changed

Lines changed: 140 additions & 9 deletions

File tree

docs/cli.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ 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.
226227
* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose).
227228
* `--extras (-E)`: Features to install (multiple values allowed).
228229
* `--no-dev`: Do not install dev dependencies. (**Deprecated**)
@@ -233,6 +234,12 @@ option is used.
233234
When `--only` is specified, `--with` and `--without` options are ignored.
234235
{{% /note %}}
235236

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+
242+
236243
## update
237244

238245
In order to get the latest versions of the dependencies and to update the `poetry.lock` file,

src/poetry/console/commands/install.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ 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+
),
3646
option(
3747
"dry-run",
3848
None,
@@ -98,6 +108,17 @@ def handle(self) -> int:
98108

99109
with_synchronization = True
100110

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+
101122
self._installer.only_groups(self.activated_groups)
102123
self._installer.dry_run(self.option("dry-run"))
103124
self._installer.requires_synchronization(with_synchronization)

src/poetry/installation/chooser.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from packaging.tags import Tag
99

10+
from poetry.utils.helpers import canonicalize_name
1011
from poetry.utils.patterns import wheel_file_re
1112

1213

@@ -60,22 +61,52 @@ class Chooser:
6061
def __init__(self, pool: Pool, env: Env) -> None:
6162
self._pool = pool
6263
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
85+
)
6386

6487
def choose_for(self, package: Package) -> Link:
6588
"""
6689
Return the url of the selected archive for a given package.
6790
"""
6891
links = []
6992
for link in self._get_links(package):
70-
if link.is_wheel and not Wheel(link.filename).is_supported_by_environment(
71-
self._env
72-
):
73-
logger.debug(
74-
"Skipping wheel %s as this is not supported by the current"
75-
" environment",
76-
link.filename,
77-
)
78-
continue
93+
if link.is_wheel:
94+
if not self.allow_binary(package.name):
95+
logger.debug(
96+
"Skipping wheel for %s as requested in no binary policy for"
97+
" package (%s)",
98+
link.filename,
99+
package.name,
100+
)
101+
continue
102+
103+
if not Wheel(link.filename).is_supported_by_environment(self._env):
104+
logger.debug(
105+
"Skipping wheel %s as this is not supported by the current"
106+
" environment",
107+
link.filename,
108+
)
109+
continue
79110

80111
if link.ext in {".egg", ".exe", ".msi", ".rpm", ".srpm"}:
81112
logger.debug("Skipping unsupported distribution %s", link.filename)

src/poetry/installation/executor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ 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+
9598
def supports_fancy_output(self) -> bool:
9699
return self._io.output.is_decorated() and not self._dry_run
97100

src/poetry/installation/installer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ 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+
138143
def only_groups(self, groups: Iterable[str]) -> Installer:
139144
self._groups = groups
140145

tests/console/commands/test_install.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,29 @@ def test_sync_option_is_passed_to_the_installer(
136136
tester.execute("--sync")
137137

138138
assert tester.command.installer._requires_synchronization
139+
140+
141+
@pytest.mark.parametrize(
142+
("options", "policy"),
143+
[
144+
(
145+
"--no-binary :all:",
146+
{":all:"},
147+
),
148+
("--no-binary :none:", {":none:"}),
149+
("--no-binary pytest", {"pytest"}),
150+
("--no-binary pytest,black", {"black", "pytest"}),
151+
("--no-binary pytest --no-binary black", {"black", "pytest"}),
152+
],
153+
)
154+
def test_no_binary_option_is_passed_to_the_installer(
155+
tester: CommandTester, mocker: MockerFixture, options: str, policy: set[str]
156+
) -> None:
157+
"""
158+
The --no-binary option is passed properly to the installer.
159+
"""
160+
mocker.patch.object(tester.command.installer, "run", return_value=1)
161+
162+
tester.execute(options)
163+
164+
assert tester.command.installer.executor._chooser._no_binary_policy == policy

tests/installation/test_chooser.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,44 @@ def test_chooser_chooses_universal_wheel_link_if_available(
121121
assert link.filename == "pytest-3.5.0-py2.py3-none-any.whl"
122122

123123

124+
@pytest.mark.parametrize(
125+
("policy", "filename"),
126+
[
127+
(":all:", "pytest-3.5.0.tar.gz"),
128+
(":none:", "pytest-3.5.0-py2.py3-none-any.whl"),
129+
("black", "pytest-3.5.0-py2.py3-none-any.whl"),
130+
("pytest", "pytest-3.5.0.tar.gz"),
131+
("pytest,black", "pytest-3.5.0.tar.gz"),
132+
],
133+
)
134+
@pytest.mark.parametrize("source_type", ["", "legacy"])
135+
def test_chooser_no_binary_policy(
136+
env: MockEnv,
137+
mock_pypi: None,
138+
mock_legacy: None,
139+
source_type: str,
140+
pool: Pool,
141+
policy: str,
142+
filename: str,
143+
):
144+
chooser = Chooser(pool, env)
145+
chooser.set_no_binary_policy(policy)
146+
147+
package = Package("pytest", "3.5.0")
148+
if source_type == "legacy":
149+
package = Package(
150+
package.name,
151+
package.version.text,
152+
source_type="legacy",
153+
source_reference="foo",
154+
source_url="https://foo.bar/simple/",
155+
)
156+
157+
link = chooser.choose_for(package)
158+
159+
assert link.filename == filename
160+
161+
124162
@pytest.mark.parametrize("source_type", ["", "legacy"])
125163
def test_chooser_chooses_specific_python_universal_wheel_link_if_available(
126164
env: MockEnv, mock_pypi: None, mock_legacy: None, source_type: str, pool: Pool

0 commit comments

Comments
 (0)