dependabot-python 0.368.0 → 0.370.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/helpers/build +4 -0
- data/helpers/lib/parser.py +26 -0
- data/helpers/requirements.txt +1 -0
- data/helpers/test/fixtures/no_dependencies.toml +3 -0
- data/helpers/test/fixtures/pep621_arbitrary_equality.toml +7 -0
- data/helpers/test/fixtures/pep621_dependencies.toml +21 -0
- data/helpers/test/fixtures/pep621_empty_deps.toml +8 -0
- data/helpers/test/fixtures/pep621_extras.toml +8 -0
- data/helpers/test/fixtures/pep621_markers.toml +7 -0
- data/helpers/test/fixtures/pep621_multiple_extras.toml +7 -0
- data/helpers/test/fixtures/pep621_no_version.toml +8 -0
- data/helpers/test/fixtures/pep621_only_build_system.toml +3 -0
- data/helpers/test/fixtures/pep621_spaced_specifiers.toml +10 -0
- data/helpers/test/fixtures/pep735_cycle.toml +13 -0
- data/helpers/test/fixtures/pep735_dependency_groups.toml +18 -0
- data/helpers/test/fixtures/requirements/constraints.txt +1 -0
- data/helpers/test/fixtures/requirements/markers.txt +1 -0
- data/helpers/test/fixtures/requirements/requirements-dev.txt +2 -0
- data/helpers/test/fixtures/requirements/requirements.txt +5 -0
- data/helpers/test/fixtures/requirements/with_constraints.txt +2 -0
- data/helpers/test/fixtures/requirements_empty/.gitkeep +0 -0
- data/helpers/test/fixtures/setup_cfg/setup.cfg +16 -0
- data/helpers/test/fixtures/setup_py/setup.py +20 -0
- data/helpers/test/fixtures/setup_py_comments/setup.py +9 -0
- data/helpers/test/test_hasher.py +114 -0
- data/helpers/test/test_parse_requirements.py +103 -0
- data/helpers/test/test_parse_setup.py +127 -0
- data/helpers/test/test_parser.py +265 -0
- data/helpers/test/test_run.py +49 -0
- data/lib/dependabot/python/dependency_grapher/lockfile_generator.rb +13 -0
- data/lib/dependabot/python/file_parser/pyproject_files_parser.rb +42 -11
- data/lib/dependabot/python/file_parser.rb +21 -1
- data/lib/dependabot/python/file_updater/poetry_file_updater/pep621_updater.rb +162 -0
- data/lib/dependabot/python/file_updater/poetry_file_updater.rb +60 -77
- data/lib/dependabot/python/file_updater/pyproject_preparer.rb +139 -27
- data/lib/dependabot/python/package_manager.rb +16 -0
- data/lib/dependabot/python/poetry_plugin_installer.rb +95 -0
- data/lib/dependabot/python/update_checker/latest_version_finder.rb +4 -2
- data/lib/dependabot/python/update_checker/poetry_version_resolver.rb +13 -0
- data/lib/dependabot/python/update_checker/requirements_updater.rb +86 -15
- data/lib/dependabot/python/update_checker.rb +6 -2
- metadata +32 -4
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Add the helpers lib directory to the Python path so we can import parser
|
|
6
|
+
sys.path.insert(
|
|
7
|
+
0, os.path.join(os.path.dirname(__file__), os.pardir, "lib")
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from parser import parse_pep621_pep735_dependencies # noqa: E402
|
|
11
|
+
|
|
12
|
+
FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse(fixture_name):
|
|
16
|
+
path = os.path.join(FIXTURES, fixture_name)
|
|
17
|
+
result = json.loads(parse_pep621_pep735_dependencies(path))
|
|
18
|
+
return result["result"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_dep(deps, name):
|
|
22
|
+
return next((d for d in deps if d["name"] == name), None)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# PEP 621 project.dependencies
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
class TestPep621Dependencies:
|
|
29
|
+
def test_parses_runtime_dependencies(self):
|
|
30
|
+
deps = parse("pep621_dependencies.toml")
|
|
31
|
+
requests = find_dep(deps, "requests")
|
|
32
|
+
assert requests is not None
|
|
33
|
+
# packaging normalises specifier order alphabetically by operator
|
|
34
|
+
assert requests["requirement"] == "<3.0,>=2.13.0"
|
|
35
|
+
assert requests["requirement_type"] == "dependencies"
|
|
36
|
+
|
|
37
|
+
def test_parses_exact_version(self):
|
|
38
|
+
deps = parse("pep621_dependencies.toml")
|
|
39
|
+
urllib3 = find_dep(deps, "urllib3")
|
|
40
|
+
assert urllib3 is not None
|
|
41
|
+
assert urllib3["version"] == "1.26.0"
|
|
42
|
+
assert urllib3["requirement"] == "==1.26.0"
|
|
43
|
+
|
|
44
|
+
def test_parses_optional_dependencies(self):
|
|
45
|
+
deps = parse("pep621_dependencies.toml")
|
|
46
|
+
pysocks = find_dep(deps, "PySocks")
|
|
47
|
+
assert pysocks is not None
|
|
48
|
+
assert pysocks["requirement_type"] == "socks"
|
|
49
|
+
|
|
50
|
+
def test_optional_dependency_specifiers(self):
|
|
51
|
+
deps = parse("pep621_dependencies.toml")
|
|
52
|
+
pysocks = find_dep(deps, "PySocks")
|
|
53
|
+
# packaging normalises specifiers: sorted, no spaces
|
|
54
|
+
assert "!=" in pysocks["requirement"]
|
|
55
|
+
assert ">=" in pysocks["requirement"]
|
|
56
|
+
|
|
57
|
+
def test_parses_multiple_optional_groups(self):
|
|
58
|
+
deps = parse("pep621_dependencies.toml")
|
|
59
|
+
group_types = {d["requirement_type"] for d in deps}
|
|
60
|
+
assert "socks" in group_types
|
|
61
|
+
assert "tests" in group_types
|
|
62
|
+
|
|
63
|
+
def test_parses_build_system_requires(self):
|
|
64
|
+
deps = parse("pep621_dependencies.toml")
|
|
65
|
+
setuptools = find_dep(deps, "setuptools")
|
|
66
|
+
assert setuptools is not None
|
|
67
|
+
assert setuptools["requirement_type"] == "build-system.requires"
|
|
68
|
+
assert setuptools["requirement"] == ">=68.0"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# PEP 621 extras
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
class TestPep621Extras:
|
|
75
|
+
def test_parses_extras(self):
|
|
76
|
+
deps = parse("pep621_extras.toml")
|
|
77
|
+
cc = find_dep(deps, "cachecontrol")
|
|
78
|
+
assert cc is not None
|
|
79
|
+
assert cc["extras"] == ["filecache"]
|
|
80
|
+
|
|
81
|
+
def test_extras_requirement(self):
|
|
82
|
+
deps = parse("pep621_extras.toml")
|
|
83
|
+
cc = find_dep(deps, "cachecontrol")
|
|
84
|
+
assert cc["requirement"] == ">=0.14.0"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# PEP 735 dependency-groups
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
class TestPep735DependencyGroups:
|
|
91
|
+
def test_parses_dependency_group(self):
|
|
92
|
+
deps = parse("pep735_dependency_groups.toml")
|
|
93
|
+
pytest_dep = find_dep(deps, "pytest")
|
|
94
|
+
assert pytest_dep is not None
|
|
95
|
+
assert pytest_dep["requirement_type"] == "dev"
|
|
96
|
+
assert pytest_dep["version"] == "7.1.3"
|
|
97
|
+
|
|
98
|
+
def test_include_group_resolves(self):
|
|
99
|
+
deps = parse("pep735_dependency_groups.toml")
|
|
100
|
+
# "lint" group includes "dev" via include-group, plus flake8
|
|
101
|
+
lint_deps = [d for d in deps if d["requirement_type"] == "lint"]
|
|
102
|
+
lint_names = {d["name"] for d in lint_deps}
|
|
103
|
+
assert "flake8" in lint_names
|
|
104
|
+
# include-group pulls dev deps into lint but they keep their
|
|
105
|
+
# original group name, so they appear under "dev" requirement_type
|
|
106
|
+
all_names = {d["name"] for d in deps}
|
|
107
|
+
assert "pytest" in all_names
|
|
108
|
+
assert "black" in all_names
|
|
109
|
+
|
|
110
|
+
def test_include_group_lists_all_resolved_deps(self):
|
|
111
|
+
deps = parse("pep735_dependency_groups.toml")
|
|
112
|
+
# pytest appears twice: once from direct "dev" processing and
|
|
113
|
+
# once from "lint" including "dev" (both with requirement_type="dev"
|
|
114
|
+
# because included deps keep their original group name)
|
|
115
|
+
pytests = [d for d in deps if d["name"] == "pytest"]
|
|
116
|
+
assert len(pytests) == 2
|
|
117
|
+
assert all(d["requirement_type"] == "dev" for d in pytests)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Markers
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
class TestMarkers:
|
|
124
|
+
def test_parses_markers(self):
|
|
125
|
+
deps = parse("pep621_markers.toml")
|
|
126
|
+
requests = find_dep(deps, "requests")
|
|
127
|
+
assert requests is not None
|
|
128
|
+
assert requests["markers"] == 'python_version >= "3.8"'
|
|
129
|
+
|
|
130
|
+
def test_requirement_with_markers(self):
|
|
131
|
+
deps = parse("pep621_markers.toml")
|
|
132
|
+
requests = find_dep(deps, "requests")
|
|
133
|
+
assert requests["requirement"] == ">=2.13.0"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Edge cases
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
class TestEdgeCases:
|
|
140
|
+
def test_no_dependencies(self):
|
|
141
|
+
deps = parse("no_dependencies.toml")
|
|
142
|
+
assert deps == []
|
|
143
|
+
|
|
144
|
+
def test_only_build_system(self):
|
|
145
|
+
deps = parse("pep621_only_build_system.toml")
|
|
146
|
+
assert len(deps) == 2
|
|
147
|
+
names = {d["name"] for d in deps}
|
|
148
|
+
assert "setuptools" in names
|
|
149
|
+
assert "wheel" in names
|
|
150
|
+
assert all(
|
|
151
|
+
d["requirement_type"] == "build-system.requires" for d in deps
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def test_empty_dependency_lists(self):
|
|
155
|
+
deps = parse("pep621_empty_deps.toml")
|
|
156
|
+
assert deps == []
|
|
157
|
+
|
|
158
|
+
def test_multiple_extras_on_single_dep(self):
|
|
159
|
+
deps = parse("pep621_multiple_extras.toml")
|
|
160
|
+
boto3 = find_dep(deps, "boto3")
|
|
161
|
+
assert boto3 is not None
|
|
162
|
+
assert boto3["extras"] == ["crt", "s3"]
|
|
163
|
+
assert boto3["requirement"] == ">=1.28.0"
|
|
164
|
+
|
|
165
|
+
def test_arbitrary_equality_operator(self):
|
|
166
|
+
deps = parse("pep621_arbitrary_equality.toml")
|
|
167
|
+
numpy = find_dep(deps, "numpy")
|
|
168
|
+
assert numpy is not None
|
|
169
|
+
assert numpy["version"] == "1.24.0rc1"
|
|
170
|
+
assert numpy["requirement"] == "===1.24.0rc1"
|
|
171
|
+
|
|
172
|
+
def test_no_version_specifier(self):
|
|
173
|
+
deps = parse("pep621_no_version.toml")
|
|
174
|
+
requests = find_dep(deps, "requests")
|
|
175
|
+
assert requests is not None
|
|
176
|
+
assert requests["version"] is None
|
|
177
|
+
assert requests["requirement"] == ""
|
|
178
|
+
|
|
179
|
+
def test_cyclic_include_group_does_not_loop(self):
|
|
180
|
+
deps = parse("pep735_cycle.toml")
|
|
181
|
+
names = [d["name"] for d in deps]
|
|
182
|
+
# Both groups should be processed without infinite recursion
|
|
183
|
+
assert "requests" in names
|
|
184
|
+
assert "flask" in names
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# source_requirement — preserves original specifier string from the TOML
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
class TestSourceRequirement:
|
|
191
|
+
def test_simple_specifier_preserves_order(self):
|
|
192
|
+
deps = parse("pep621_dependencies.toml")
|
|
193
|
+
requests = find_dep(deps, "requests")
|
|
194
|
+
# Original: "requests>=2.13.0,<3.0" — order preserved
|
|
195
|
+
assert requests["source_requirement"] == ">=2.13.0,<3.0"
|
|
196
|
+
# Normalized by packaging: "<3.0,>=2.13.0" (alphabetical by operator)
|
|
197
|
+
assert requests["requirement"] == "<3.0,>=2.13.0"
|
|
198
|
+
|
|
199
|
+
def test_exact_version(self):
|
|
200
|
+
deps = parse("pep621_dependencies.toml")
|
|
201
|
+
urllib3 = find_dep(deps, "urllib3")
|
|
202
|
+
assert urllib3["source_requirement"] == "==1.26.0"
|
|
203
|
+
|
|
204
|
+
def test_extras_stripped_from_source(self):
|
|
205
|
+
deps = parse("pep621_extras.toml")
|
|
206
|
+
cc = find_dep(deps, "cachecontrol")
|
|
207
|
+
assert cc["source_requirement"] == ">=0.14.0"
|
|
208
|
+
|
|
209
|
+
def test_markers_stripped_from_source(self):
|
|
210
|
+
deps = parse("pep621_markers.toml")
|
|
211
|
+
requests = find_dep(deps, "requests")
|
|
212
|
+
assert requests["source_requirement"] == ">=2.13.0"
|
|
213
|
+
|
|
214
|
+
def test_no_version_specifier(self):
|
|
215
|
+
deps = parse("pep621_no_version.toml")
|
|
216
|
+
requests = find_dep(deps, "requests")
|
|
217
|
+
assert requests["source_requirement"] == ""
|
|
218
|
+
|
|
219
|
+
def test_multiple_extras_stripped(self):
|
|
220
|
+
deps = parse("pep621_multiple_extras.toml")
|
|
221
|
+
boto3 = find_dep(deps, "boto3")
|
|
222
|
+
assert boto3["source_requirement"] == ">=1.28.0"
|
|
223
|
+
|
|
224
|
+
def test_arbitrary_equality(self):
|
|
225
|
+
deps = parse("pep621_arbitrary_equality.toml")
|
|
226
|
+
numpy = find_dep(deps, "numpy")
|
|
227
|
+
assert numpy["source_requirement"] == "===1.24.0rc1"
|
|
228
|
+
|
|
229
|
+
def test_spaced_specifiers_preserved(self):
|
|
230
|
+
deps = parse("pep621_spaced_specifiers.toml")
|
|
231
|
+
requests = find_dep(deps, "requests")
|
|
232
|
+
# Spaces are preserved from the original entry
|
|
233
|
+
assert requests["source_requirement"] == ">= 2.13.0, < 3.0"
|
|
234
|
+
|
|
235
|
+
def test_spaced_exact_version_preserved(self):
|
|
236
|
+
deps = parse("pep621_spaced_specifiers.toml")
|
|
237
|
+
urllib3 = find_dep(deps, "urllib3")
|
|
238
|
+
assert urllib3["source_requirement"] == "== 1.26.0"
|
|
239
|
+
|
|
240
|
+
def test_spaced_extras_and_specifier(self):
|
|
241
|
+
deps = parse("pep621_spaced_specifiers.toml")
|
|
242
|
+
cc = find_dep(deps, "cachecontrol")
|
|
243
|
+
assert cc["source_requirement"] == ">= 0.14.0"
|
|
244
|
+
|
|
245
|
+
def test_spaced_specifier_with_markers(self):
|
|
246
|
+
deps = parse("pep621_spaced_specifiers.toml")
|
|
247
|
+
idna = find_dep(deps, "idna")
|
|
248
|
+
# Markers stripped, original spacing kept
|
|
249
|
+
assert idna["source_requirement"] == ">= 2.5"
|
|
250
|
+
|
|
251
|
+
def test_optional_deps_with_spaces(self):
|
|
252
|
+
deps = parse("pep621_dependencies.toml")
|
|
253
|
+
pysocks = find_dep(deps, "PySocks")
|
|
254
|
+
# Original: "PySocks >= 1.5.6, != 1.5.7, < 2"
|
|
255
|
+
assert pysocks["source_requirement"] == ">= 1.5.6, != 1.5.7, < 2"
|
|
256
|
+
|
|
257
|
+
def test_build_system_source_requirement(self):
|
|
258
|
+
deps = parse("pep621_dependencies.toml")
|
|
259
|
+
setuptools = find_dep(deps, "setuptools")
|
|
260
|
+
assert setuptools["source_requirement"] == ">=68.0"
|
|
261
|
+
|
|
262
|
+
def test_dependency_group_source_requirement(self):
|
|
263
|
+
deps = parse("pep735_dependency_groups.toml")
|
|
264
|
+
pytest_dep = find_dep(deps, "pytest")
|
|
265
|
+
assert pytest_dep["source_requirement"] == "==7.1.3"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
|
|
7
|
+
HELPERS_DIR = os.path.join(os.path.dirname(__file__), os.pardir)
|
|
8
|
+
RUN_PY = os.path.join(HELPERS_DIR, "run.py")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_helper(function, args):
|
|
12
|
+
input_json = json.dumps({"function": function, "args": args})
|
|
13
|
+
result = subprocess.run(
|
|
14
|
+
[sys.executable, RUN_PY],
|
|
15
|
+
input=input_json,
|
|
16
|
+
capture_output=True,
|
|
17
|
+
text=True,
|
|
18
|
+
cwd=HELPERS_DIR,
|
|
19
|
+
)
|
|
20
|
+
assert result.returncode == 0, (
|
|
21
|
+
f"run.py failed: {result.stderr}"
|
|
22
|
+
)
|
|
23
|
+
return json.loads(result.stdout)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestRunRouting:
|
|
27
|
+
def test_parse_pep621_routing(self):
|
|
28
|
+
fixture = os.path.join(FIXTURES, "pep621_dependencies.toml")
|
|
29
|
+
result = run_helper("parse_pep621_pep735_dependencies", [fixture])
|
|
30
|
+
|
|
31
|
+
assert "result" in result
|
|
32
|
+
names = {d["name"] for d in result["result"]}
|
|
33
|
+
assert "requests" in names
|
|
34
|
+
|
|
35
|
+
def test_parse_setup_routing(self):
|
|
36
|
+
fixture_dir = os.path.join(FIXTURES, "setup_py")
|
|
37
|
+
result = run_helper("parse_setup", [fixture_dir])
|
|
38
|
+
|
|
39
|
+
assert "result" in result
|
|
40
|
+
names = {d["name"] for d in result["result"]}
|
|
41
|
+
assert "requests" in names
|
|
42
|
+
|
|
43
|
+
def test_parse_requirements_routing(self):
|
|
44
|
+
fixture_dir = os.path.join(FIXTURES, "requirements")
|
|
45
|
+
result = run_helper("parse_requirements", [fixture_dir])
|
|
46
|
+
|
|
47
|
+
assert "result" in result
|
|
48
|
+
names = {d["name"] for d in result["result"]}
|
|
49
|
+
assert "requests" in names
|
|
@@ -7,6 +7,7 @@ require "dependabot/dependency_file"
|
|
|
7
7
|
require "dependabot/shared_helpers"
|
|
8
8
|
require "dependabot/python/file_parser/python_requirement_parser"
|
|
9
9
|
require "dependabot/python/language_version_manager"
|
|
10
|
+
require "dependabot/python/poetry_plugin_installer"
|
|
10
11
|
|
|
11
12
|
module Dependabot
|
|
12
13
|
module Python
|
|
@@ -33,6 +34,10 @@ module Dependabot
|
|
|
33
34
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
34
35
|
write_temporary_files
|
|
35
36
|
language_version_manager.install_required_python
|
|
37
|
+
|
|
38
|
+
# Install any required Poetry plugins declared in pyproject.toml
|
|
39
|
+
poetry_plugin_installer.install_required_plugins
|
|
40
|
+
|
|
36
41
|
run_poetry_lock
|
|
37
42
|
read_generated_lockfile
|
|
38
43
|
end
|
|
@@ -119,6 +124,14 @@ module Dependabot
|
|
|
119
124
|
)
|
|
120
125
|
end
|
|
121
126
|
|
|
127
|
+
sig { returns(PoetryPluginInstaller) }
|
|
128
|
+
def poetry_plugin_installer
|
|
129
|
+
@poetry_plugin_installer ||= T.let(
|
|
130
|
+
PoetryPluginInstaller.from_dependency_files(dependency_files),
|
|
131
|
+
T.nilable(PoetryPluginInstaller)
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
122
135
|
sig { params(error: SharedHelpers::HelperSubprocessFailed).void }
|
|
123
136
|
def handle_generation_error(error)
|
|
124
137
|
Dependabot.logger.error(
|
|
@@ -25,6 +25,7 @@ module Dependabot
|
|
|
25
25
|
sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
|
|
26
26
|
def initialize(dependency_files:)
|
|
27
27
|
@dependency_files = dependency_files
|
|
28
|
+
@dynamic_fields = T.let(nil, T.nilable(T::Array[String]))
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
|
@@ -84,16 +85,7 @@ module Dependabot
|
|
|
84
85
|
return dependencies if using_pdm?
|
|
85
86
|
|
|
86
87
|
parse_pep621_pep735_dependencies.each do |dep|
|
|
87
|
-
|
|
88
|
-
# probably blocked. Ignore it.
|
|
89
|
-
next if dep["markers"]&.include?("<")
|
|
90
|
-
|
|
91
|
-
# If no requirement, don't add it
|
|
92
|
-
next if dep["requirement"].empty?
|
|
93
|
-
|
|
94
|
-
# Skip build-system.requires dependencies when using Poetry
|
|
95
|
-
# Poetry manages its own build system dependencies
|
|
96
|
-
next if using_poetry? && dep["requirement_type"] == "build-system.requires"
|
|
88
|
+
next if skip_pep621_dep?(dep)
|
|
97
89
|
|
|
98
90
|
dependencies <<
|
|
99
91
|
Dependency.new(
|
|
@@ -106,7 +98,9 @@ module Dependabot
|
|
|
106
98
|
groups: [dep["requirement_type"]].compact
|
|
107
99
|
}],
|
|
108
100
|
package_manager: "pip",
|
|
109
|
-
metadata: extras_metadata(dep["extras"])
|
|
101
|
+
metadata: extras_metadata(dep["extras"]).merge(
|
|
102
|
+
source_requirement: dep["source_requirement"]
|
|
103
|
+
).compact
|
|
110
104
|
)
|
|
111
105
|
end
|
|
112
106
|
|
|
@@ -229,6 +223,43 @@ module Dependabot
|
|
|
229
223
|
using_pep621? && pdm_lock
|
|
230
224
|
end
|
|
231
225
|
|
|
226
|
+
sig { returns(T::Array[String]) }
|
|
227
|
+
def dynamic_fields
|
|
228
|
+
@dynamic_fields ||= parsed_pyproject.dig("project", "dynamic") || []
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
sig { params(dep: T::Hash[String, T.untyped]).returns(T::Boolean) }
|
|
232
|
+
def skip_pep621_dep?(dep)
|
|
233
|
+
# If a requirement has a `<` or `<=` marker then updating it is
|
|
234
|
+
# probably blocked. Ignore it.
|
|
235
|
+
return true if dep["markers"]&.include?("<")
|
|
236
|
+
|
|
237
|
+
# If no requirement, don't add it
|
|
238
|
+
return true if dep["requirement"].empty?
|
|
239
|
+
|
|
240
|
+
# Skip build-system.requires dependencies when using Poetry
|
|
241
|
+
# Poetry manages its own build system dependencies
|
|
242
|
+
return true if using_poetry? && dep["requirement_type"] == "build-system.requires"
|
|
243
|
+
|
|
244
|
+
# When dependencies or optional-dependencies are listed in project.dynamic,
|
|
245
|
+
# they are managed by the build backend (e.g. Poetry) — skip the PEP 621 path
|
|
246
|
+
dynamic_pep621_dep?(dep["requirement_type"])
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sig { params(requirement_type: T.nilable(String)).returns(T::Boolean) }
|
|
250
|
+
def dynamic_pep621_dep?(requirement_type)
|
|
251
|
+
return false unless using_poetry?
|
|
252
|
+
return false unless requirement_type
|
|
253
|
+
|
|
254
|
+
if requirement_type == "dependencies"
|
|
255
|
+
dynamic_fields.include?("dependencies")
|
|
256
|
+
elsif parsed_pyproject.dig("project", "optional-dependencies")&.key?(requirement_type)
|
|
257
|
+
dynamic_fields.include?("optional-dependencies")
|
|
258
|
+
else
|
|
259
|
+
false
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
232
263
|
# Create a DependencySet where each element has no requirement. Any
|
|
233
264
|
# requirements will be added when combining the DependencySet with
|
|
234
265
|
# other DependencySets.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "toml-rb"
|
|
4
5
|
require "dependabot/dependency"
|
|
5
6
|
require "dependabot/file_parsers"
|
|
6
7
|
require "dependabot/file_parsers/base"
|
|
@@ -110,7 +111,13 @@ module Dependabot
|
|
|
110
111
|
|
|
111
112
|
return PipenvPackageManager.new(T.must(detect_pipenv_version)) if detect_pipenv_version
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
poetry_version = detect_poetry_version
|
|
115
|
+
if poetry_version
|
|
116
|
+
return PoetryPackageManager.new(
|
|
117
|
+
poetry_version,
|
|
118
|
+
requires_poetry_version_constraint
|
|
119
|
+
)
|
|
120
|
+
end
|
|
114
121
|
|
|
115
122
|
return PipCompilePackageManager.new(T.must(detect_pipcompile_version)) if detect_pipcompile_version
|
|
116
123
|
|
|
@@ -135,6 +142,19 @@ module Dependabot
|
|
|
135
142
|
nil
|
|
136
143
|
end
|
|
137
144
|
|
|
145
|
+
sig { returns(T.nilable(Dependabot::Python::Requirement)) }
|
|
146
|
+
def requires_poetry_version_constraint
|
|
147
|
+
return nil unless pyproject&.content
|
|
148
|
+
|
|
149
|
+
parsed = TomlRB.parse(T.must(pyproject).content)
|
|
150
|
+
constraint = parsed.dig("tool", "poetry", "requires-poetry")
|
|
151
|
+
return nil unless constraint.is_a?(String) && !constraint.strip.empty?
|
|
152
|
+
|
|
153
|
+
Dependabot::Python::Requirement.new(constraint.strip)
|
|
154
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError, Gem::Requirement::BadRequirementError
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
138
158
|
# Detects the version of pip-compile. If the version cannot be detected, it returns nil
|
|
139
159
|
sig { returns(T.nilable(String)) }
|
|
140
160
|
def detect_pipcompile_version
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/python/file_updater/poetry_file_updater"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module Python
|
|
9
|
+
class FileUpdater
|
|
10
|
+
class PoetryFileUpdater
|
|
11
|
+
class Pep621Updater
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { params(dep: Dependabot::Dependency).void }
|
|
15
|
+
def initialize(dep:)
|
|
16
|
+
@dep = dep
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig do
|
|
20
|
+
params(
|
|
21
|
+
content: String,
|
|
22
|
+
new_r: T::Hash[Symbol, T.untyped],
|
|
23
|
+
old_r: T::Hash[Symbol, T.untyped]
|
|
24
|
+
).returns(T.nilable(String))
|
|
25
|
+
end
|
|
26
|
+
def replace(content, new_r, old_r)
|
|
27
|
+
source_req = dep.metadata[:source_requirement]
|
|
28
|
+
|
|
29
|
+
if source_req
|
|
30
|
+
replace_with_source_requirement(content, source_req, new_r, old_r)
|
|
31
|
+
else
|
|
32
|
+
replace_with_normalized_requirement(content, new_r, old_r)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(source_req: String, old_req: String, new_req: String).returns(String) }
|
|
37
|
+
def rewrite_pep508_requirement(source_req, old_req, new_req)
|
|
38
|
+
old_specifiers = parse_specifiers(old_req)
|
|
39
|
+
new_specifiers = parse_specifiers(new_req)
|
|
40
|
+
|
|
41
|
+
old_versions_by_op = group_versions_by_operator(old_specifiers)
|
|
42
|
+
new_versions_by_op = group_versions_by_operator(new_specifiers)
|
|
43
|
+
|
|
44
|
+
replacements = T.let([], T::Array[T::Hash[Symbol, String]])
|
|
45
|
+
new_versions_by_op.each do |operator, new_versions|
|
|
46
|
+
old_versions = old_versions_by_op[operator]
|
|
47
|
+
next unless old_versions
|
|
48
|
+
next unless old_versions.length == new_versions.length
|
|
49
|
+
|
|
50
|
+
old_versions.zip(new_versions).each do |old_version, new_version|
|
|
51
|
+
next if old_version == new_version
|
|
52
|
+
|
|
53
|
+
replacements << {
|
|
54
|
+
operator: operator,
|
|
55
|
+
old_version: old_version,
|
|
56
|
+
new_version: T.must(new_version)
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
result = source_req.dup
|
|
62
|
+
replacements.each do |replacement|
|
|
63
|
+
op = Regexp.escape(T.must(replacement[:operator]))
|
|
64
|
+
ver = Regexp.escape(T.must(replacement[:old_version]))
|
|
65
|
+
result = result.sub(/(#{op}\s*)#{ver}/, "\\1#{replacement[:new_version]}")
|
|
66
|
+
end
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(specifiers: T::Array[T::Hash[Symbol, String]]).returns(T::Hash[String, T::Array[String]]) }
|
|
71
|
+
def group_versions_by_operator(specifiers)
|
|
72
|
+
specifiers.each_with_object(
|
|
73
|
+
T.let({}, T::Hash[String, T::Array[String]])
|
|
74
|
+
) do |specifier, grouped_versions|
|
|
75
|
+
operator = T.must(specifier[:operator])
|
|
76
|
+
version = T.must(specifier[:version])
|
|
77
|
+
|
|
78
|
+
grouped_versions[operator] ||= []
|
|
79
|
+
T.must(grouped_versions[operator]) << version
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(req: String).returns(T::Array[T::Hash[Symbol, String]]) }
|
|
84
|
+
def parse_specifiers(req)
|
|
85
|
+
req.scan(/([!<>=~]+)\s*([^\s,]+)/).map do |op, ver|
|
|
86
|
+
{ operator: op, version: ver }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
sig { returns(Dependabot::Dependency) }
|
|
93
|
+
attr_reader :dep
|
|
94
|
+
|
|
95
|
+
sig do
|
|
96
|
+
params(
|
|
97
|
+
content: String,
|
|
98
|
+
source_req: String,
|
|
99
|
+
new_r: T::Hash[Symbol, T.untyped],
|
|
100
|
+
old_r: T::Hash[Symbol, T.untyped]
|
|
101
|
+
).returns(T.nilable(String))
|
|
102
|
+
end
|
|
103
|
+
def replace_with_source_requirement(content, source_req, new_r, old_r)
|
|
104
|
+
match = content.match(declaration_regex(source_req))
|
|
105
|
+
return unless match
|
|
106
|
+
|
|
107
|
+
declaration = T.must(match[:declaration])
|
|
108
|
+
new_req_str = rewrite_pep508_requirement(source_req, old_r[:requirement], new_r[:requirement])
|
|
109
|
+
content.sub(declaration, declaration.sub(source_req, new_req_str))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Fallback when source_requirement metadata is absent (e.g. after
|
|
113
|
+
# DependencySet merge or deserialization). Matches using the
|
|
114
|
+
# normalized requirement string, which may fail on whitespace
|
|
115
|
+
# differences but is better than skipping the update entirely.
|
|
116
|
+
sig do
|
|
117
|
+
params(
|
|
118
|
+
content: String,
|
|
119
|
+
new_r: T::Hash[Symbol, T.untyped],
|
|
120
|
+
old_r: T::Hash[Symbol, T.untyped]
|
|
121
|
+
).returns(T.nilable(String))
|
|
122
|
+
end
|
|
123
|
+
def replace_with_normalized_requirement(content, new_r, old_r)
|
|
124
|
+
old_req = old_r[:requirement]
|
|
125
|
+
new_req = new_r[:requirement]
|
|
126
|
+
|
|
127
|
+
match = content.match(normalized_declaration_regex(old_req))
|
|
128
|
+
return unless match
|
|
129
|
+
|
|
130
|
+
declaration = T.must(match[:declaration])
|
|
131
|
+
return unless declaration.include?(old_req)
|
|
132
|
+
|
|
133
|
+
content.sub(declaration, declaration.sub(old_req, new_req))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { params(old_req: String).returns(Regexp) }
|
|
137
|
+
def declaration_regex(old_req)
|
|
138
|
+
/(?<declaration>["']#{escape}\s*#{extras_pattern}\s*#{Regexp.escape(old_req)}["'])/mi
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
sig { params(old_req: String).returns(Regexp) }
|
|
142
|
+
def normalized_declaration_regex(old_req)
|
|
143
|
+
/(?<declaration>["']#{escape}#{extras_pattern}#{Regexp.escape(old_req)}["'])/mi
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
sig { returns(String) }
|
|
147
|
+
def extras_pattern
|
|
148
|
+
extras_str = dep.metadata[:extras]
|
|
149
|
+
return "" unless extras_str.is_a?(String) && !extras_str.empty?
|
|
150
|
+
|
|
151
|
+
"\\[" + extras_str.split(",").map { |e| Regexp.escape(e.strip) }.join(",\\s*") + "\\]"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sig { returns(String) }
|
|
155
|
+
def escape
|
|
156
|
+
Regexp.escape(dep.name).gsub("\\-", "[-_.]")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|