Skip to content

Commit ae319a1

Browse files
authored
Add support for poetry update (#83)
1 parent edd541c commit ae319a1

File tree

4 files changed

+122
-28
lines changed

4 files changed

+122
-28
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ $ scfw configure
5454

5555
### Compatibility and limitations
5656

57-
| Package manager | Compatible versions | Inspected subcommands |
58-
| :---------------: | :-------------------: | :---------------------------: |
59-
| npm | >= 7.0 | `install` (including aliases) |
60-
| pip | >= 22.2 | `install` |
61-
| poetry | >= 1.7 | `add`, `install`, `sync` |
57+
| Package manager | Compatible versions | Inspected subcommands |
58+
| :---------------: | :-------------------: | :--------------------------------: |
59+
| npm | >= 7.0 | `install` (including aliases) |
60+
| pip | >= 22.2 | `install` |
61+
| poetry | >= 1.7 | `add`, `install`, `sync`, `update` |
6262

6363
In keeping with its goal of blocking 100% of known-malicious package installations, `scfw` will refuse to run with an incompatible version of a supported package manager. Please upgrade to or verify that you are running a compatible version before using this tool.
6464

scfw/commands/poetry_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
MIN_POETRY_VERSION = version_parse("1.7.0")
2121

22-
INSPECTED_SUBCOMMANDS = {"add", "install", "sync"}
22+
INSPECTED_SUBCOMMANDS = {"add", "install", "sync", "update"}
2323

2424

2525
class PoetryCommand(PackageManagerCommand):

tests/commands/test_poetry.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ def poetry_project_target_previous_lock_latest():
9999
tempdir.cleanup()
100100

101101

102+
@pytest.fixture
103+
def poetry_project_lock_latest():
104+
"""
105+
Initialize a Poetry project where the latest version of `TARGET` is an as-yet
106+
uninstalled dependency.
107+
"""
108+
tempdir = TemporaryDirectory()
109+
_init_poetry_project(tempdir.name, TEST_PROJECT_NAME)
110+
subprocess.run(["poetry", "add", "--lock", f"{TARGET}=={TARGET_LATEST}"], check=True, cwd=tempdir.name)
111+
112+
yield tempdir.name
113+
114+
tempdir.cleanup()
115+
116+
102117
def test_poetry_version_output():
103118
"""
104119
Test that `poetry --version` has the required format and parses correctly.
@@ -183,6 +198,29 @@ def test_poetry_sync_no_change(new_poetry_project):
183198
assert all(_test_poetry_no_change(new_poetry_project, init_state, command) for command in test_cases)
184199

185200

201+
def test_poetry_update_no_change(new_poetry_project):
202+
"""
203+
Test that certain `poetry update` commands relied on by Supply-Chain Firewall
204+
not to error or modify the local installation state indeed have these properties.
205+
"""
206+
test_cases = [
207+
["poetry", "-V", "update"],
208+
["poetry", "update", "-V"],
209+
["poetry", "--version", "update"],
210+
["poetry", "update", "--version"],
211+
["poetry", "-h", "update"],
212+
["poetry", "update", "-h"],
213+
["poetry", "--help", "update"],
214+
["poetry", "update", "--help"],
215+
["poetry", "--dry-run", "update"],
216+
["poetry", "update", "--dry-run"],
217+
]
218+
219+
init_state = poetry_show(new_poetry_project)
220+
221+
assert all(_test_poetry_no_change(new_poetry_project, init_state, command) for command in test_cases)
222+
223+
186224
def _test_poetry_no_change(project, init_state, command) -> bool:
187225
"""
188226
Tests that a given Poetry command does not encounter any errors and does not
@@ -273,6 +311,35 @@ def test_poetry_sync_error_no_change(new_poetry_project):
273311
assert all(_test_poetry_error_no_change(new_poetry_project, init_state, command) for command in test_cases)
274312

275313

314+
def test_poetry_update_error_no_change(new_poetry_project):
315+
"""
316+
Tests that certain `poetry update` commands encounter an error and do not
317+
modify the local installation state when run in the context of a given project.
318+
"""
319+
test_cases = [
320+
["poetry", "update", "--nonexistent-option"],
321+
["poetry", "update", "--nonexistent-option", TARGET],
322+
["poetry", "update", "--without"],
323+
["poetry", "update", TARGET, "--without"],
324+
["poetry", "update", "--with"],
325+
["poetry", "update", TARGET, "--with"],
326+
["poetry", "update", "--only"],
327+
["poetry", "update", TARGET, "--only"],
328+
["poetry", "update", "-P"],
329+
["poetry", "update", TARGET, "-P"],
330+
["poetry", "update", "--project"],
331+
["poetry", "update", TARGET, "--project"],
332+
["poetry", "update", "-C"],
333+
["poetry", "update", TARGET, "-C"],
334+
["poetry", "update", "--directory"],
335+
["poetry", "update", TARGET, "--directory"],
336+
]
337+
338+
init_state = poetry_show(new_poetry_project)
339+
340+
assert all(_test_poetry_error_no_change(new_poetry_project, init_state, command) for command in test_cases)
341+
342+
276343
def _test_poetry_error_no_change(project, init_state, command) -> bool:
277344
"""
278345
Tests that a given Poetry command does encounter an error and does not modify
@@ -283,7 +350,7 @@ def _test_poetry_error_no_change(project, init_state, command) -> bool:
283350
return poetry_show(project) == init_state
284351

285352

286-
def test_poetry_dry_run_output_install(new_poetry_project):
353+
def test_poetry_dry_run_output_install(new_poetry_project, poetry_project_lock_latest):
287354
"""
288355
Tests that a dry-run of an installish Poetry command that results in a
289356
dependency installation has the expected format.
@@ -293,16 +360,17 @@ def is_install_line(target: str, version: str, line: str) -> bool:
293360
return match is not None and match.group(1) == target and match.group(2) == version and "Skipped" not in line
294361

295362
test_cases = [
296-
(["poetry", "add", "--dry-run", TARGET], TARGET, TARGET_LATEST, None),
297-
(["poetry", "install", "--dry-run"], TEST_PROJECT_NAME, "0.1.0", None),
298-
(["poetry", "sync", "--dry-run"], TEST_PROJECT_NAME, "0.1.0", POETRY_V2),
363+
(new_poetry_project, ["poetry", "add", "--dry-run", TARGET], TARGET, TARGET_LATEST, None),
364+
(new_poetry_project, ["poetry", "install", "--dry-run"], TEST_PROJECT_NAME, "0.1.0", None),
365+
(new_poetry_project, ["poetry", "sync", "--dry-run"], TEST_PROJECT_NAME, "0.1.0", POETRY_V2),
366+
(poetry_project_lock_latest, ["poetry", "update", "--dry-run"], TARGET, TARGET_LATEST, None)
299367
]
300368

301-
for command, target, version, min_poetry_version in test_cases:
369+
for poetry_project, command, target, version, min_poetry_version in test_cases:
302370
if min_poetry_version and poetry_version() < min_poetry_version:
303371
continue
304372

305-
dry_run = subprocess.run(command, check=True, cwd=new_poetry_project, text=True, capture_output=True)
373+
dry_run = subprocess.run(command, check=True, cwd=poetry_project, text=True, capture_output=True)
306374
assert any(is_install_line(target, version, line) for line in dry_run.stdout.split('\n'))
307375

308376

@@ -322,6 +390,7 @@ def is_update_line(target: str, line: str) -> bool:
322390
(poetry_project_target_previous, ["poetry", "add", "--dry-run", f"{TARGET}=={TARGET_LATEST}"], None),
323391
(poetry_project_target_previous_lock_latest, ["poetry", "install", "--dry-run"], None),
324392
(poetry_project_target_previous_lock_latest, ["poetry", "sync", "--dry-run"], POETRY_V2),
393+
(poetry_project_target_previous_lock_latest, ["poetry", "update", "--dry-run"], None),
325394
]
326395

327396
for poetry_project, command, min_poetry_version in test_cases:
@@ -348,6 +417,7 @@ def is_downgrade_line(target: str, line: str) -> bool:
348417
(poetry_project_target_latest, ["poetry", "add", "--dry-run", f"{TARGET}=={TARGET_PREVIOUS}"], None),
349418
(poetry_project_target_latest_lock_previous, ["poetry", "install", "--dry-run"], None),
350419
(poetry_project_target_latest_lock_previous, ["poetry", "sync", "--dry-run"], POETRY_V2),
420+
(poetry_project_target_latest_lock_previous, ["poetry", "update", "--dry-run"], None),
351421
]
352422

353423
for poetry_project, command, min_poetry_version in test_cases:

tests/commands/test_poetry_command.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from scfw.target import InstallTarget
88

99
from .test_poetry import (
10-
POETRY_V2, TARGET, TARGET_LATEST, TARGET_PREVIOUS, TEST_PROJECT_NAME, new_poetry_project,
10+
POETRY_V2, TARGET, TARGET_LATEST, TARGET_PREVIOUS, TEST_PROJECT_NAME,
11+
new_poetry_project, poetry_project_lock_latest,
1112
poetry_project_target_latest, poetry_project_target_latest_lock_previous,
1213
poetry_project_target_previous, poetry_project_target_previous_lock_latest,
1314
poetry_show, poetry_version,
@@ -83,15 +84,10 @@ def test_poetry_command_would_install_install(
8384
(poetry_project_target_previous_lock_latest, [(TARGET, TARGET_LATEST), (TEST_PROJECT_NAME, "0.1.0")]),
8485
]
8586

86-
for poetry_project, true_targets in test_cases:
87-
init_state = poetry_show(poetry_project)
88-
89-
true_targets = [InstallTarget(ECOSYSTEM.PyPI, package, version) for package, version in true_targets]
90-
91-
command = PoetryCommand(["poetry", "install", "--directory", poetry_project])
92-
93-
assert command.would_install() == true_targets
94-
assert poetry_show(poetry_project) == init_state
87+
assert all(
88+
_test_poetry_command_would_install(["poetry", "install", "--directory", project], project, targets)
89+
for project, targets in test_cases
90+
)
9591

9692

9793
def test_poetry_command_would_install_sync(
@@ -116,12 +112,40 @@ def test_poetry_command_would_install_sync(
116112
(poetry_project_target_previous_lock_latest, [(TARGET, TARGET_LATEST), (TEST_PROJECT_NAME, "0.1.0")]),
117113
]
118114

119-
for poetry_project, true_targets in test_cases:
120-
init_state = poetry_show(poetry_project)
115+
assert all(
116+
_test_poetry_command_would_install(["poetry", "sync", "--directory", project], project, targets)
117+
for project, targets in test_cases
118+
)
119+
120+
121+
def test_poetry_command_would_install_update(
122+
poetry_project_lock_latest,
123+
poetry_project_target_latest_lock_previous,
124+
poetry_project_target_previous_lock_latest,
125+
):
126+
"""
127+
Tests that `PoetryCommand.would_install()` for a `poetry update` command
128+
correctly resolves installation targets without installing anything.
129+
"""
130+
test_cases = [
131+
(poetry_project_lock_latest, [(TARGET, TARGET_LATEST)]),
132+
(poetry_project_target_latest_lock_previous, [(TARGET, TARGET_PREVIOUS)]),
133+
(poetry_project_target_previous_lock_latest, [(TARGET, TARGET_LATEST)]),
134+
]
121135

122-
true_targets = [InstallTarget(ECOSYSTEM.PyPI, package, version) for package, version in true_targets]
136+
assert all(
137+
_test_poetry_command_would_install(["poetry", "update", "--directory", project], project, targets)
138+
for project, targets in test_cases
139+
)
123140

124-
command = PoetryCommand(["poetry", "sync", "--directory", poetry_project])
125141

126-
assert command.would_install() == true_targets
127-
assert poetry_show(poetry_project) == init_state
142+
def _test_poetry_command_would_install(command, project, targets) -> bool:
143+
"""
144+
Tests that a `PoetryCommand` initialized from `command` when run in `project`
145+
correctly resolves installation targets without installing anything.
146+
"""
147+
init_state = poetry_show(project)
148+
149+
targets = [InstallTarget(ECOSYSTEM.PyPI, package, version) for package, version in targets]
150+
151+
return PoetryCommand(command).would_install() == targets and poetry_show(project) == init_state

0 commit comments

Comments
 (0)