dependabot-python 0.367.0 → 0.369.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/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/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 +184 -0
- data/helpers/test/test_run.py +49 -0
- data/lib/dependabot/python/file_updater/poetry_file_updater.rb +16 -11
- data/lib/dependabot/python/file_updater/pyproject_preparer.rb +109 -0
- data/lib/dependabot/python/file_updater/requirement_file_updater.rb +36 -13
- data/lib/dependabot/python/file_updater/requirement_replacer.rb +29 -21
- data/lib/dependabot/python/metadata_finder.rb +152 -20
- data/lib/dependabot/python/shared_file_fetcher.rb +10 -5
- data/lib/dependabot/python/update_checker/latest_version_finder.rb +4 -2
- metadata +29 -4
|
@@ -0,0 +1,184 @@
|
|
|
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
|
|
@@ -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
|
|
@@ -124,8 +124,10 @@ module Dependabot
|
|
|
124
124
|
declaration_match = content.match(declaration_regex)
|
|
125
125
|
if declaration_match
|
|
126
126
|
declaration = declaration_match[:declaration]
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
if T.must(declaration).include?(old_req)
|
|
128
|
+
new_declaration = T.must(declaration).sub(old_req, new_req)
|
|
129
|
+
return content.sub(T.must(declaration), new_declaration)
|
|
130
|
+
end
|
|
129
131
|
end
|
|
130
132
|
|
|
131
133
|
# Try Poetry table format
|
|
@@ -220,9 +222,8 @@ module Dependabot
|
|
|
220
222
|
|
|
221
223
|
sig { params(pyproject_content: String).returns(String) }
|
|
222
224
|
def freeze_other_dependencies(pyproject_content)
|
|
223
|
-
PyprojectPreparer
|
|
224
|
-
|
|
225
|
-
.freeze_top_level_dependencies_except(dependencies)
|
|
225
|
+
PyprojectPreparer.new(pyproject_content: pyproject_content, lockfile: lockfile)
|
|
226
|
+
.freeze_top_level_dependencies_except(dependencies)
|
|
226
227
|
end
|
|
227
228
|
|
|
228
229
|
sig { params(pyproject_content: String).returns(String) }
|
|
@@ -244,14 +245,18 @@ module Dependabot
|
|
|
244
245
|
end
|
|
245
246
|
end
|
|
246
247
|
|
|
248
|
+
# Freeze PEP 621 project.dependencies and project.optional-dependencies
|
|
249
|
+
PyprojectPreparer.freeze_pep621_deps!(pyproject_object, dependencies) do |dep|
|
|
250
|
+
!git_dependency_being_updated?(dep)
|
|
251
|
+
end
|
|
252
|
+
|
|
247
253
|
TomlRB.dump(pyproject_object)
|
|
248
254
|
end
|
|
249
255
|
|
|
250
256
|
sig { params(pyproject_content: String).returns(String) }
|
|
251
257
|
def update_python_requirement(pyproject_content)
|
|
252
|
-
PyprojectPreparer
|
|
253
|
-
|
|
254
|
-
.update_python_requirement(language_version_manager.python_version)
|
|
258
|
+
PyprojectPreparer.new(pyproject_content: pyproject_content)
|
|
259
|
+
.update_python_requirement(language_version_manager.python_version)
|
|
255
260
|
end
|
|
256
261
|
|
|
257
262
|
sig { params(poetry_object: T::Hash[String, T.untyped], dep: Dependabot::Dependency).returns(T::Array[String]) }
|
|
@@ -262,6 +267,8 @@ module Dependabot
|
|
|
262
267
|
next unless pkg_name
|
|
263
268
|
|
|
264
269
|
if poetry_object[type][pkg_name].is_a?(Hash)
|
|
270
|
+
next unless poetry_object[type][pkg_name].key?("version") # skip enrichment-only entries
|
|
271
|
+
|
|
265
272
|
poetry_object[type][pkg_name]["version"] = dep.version
|
|
266
273
|
else
|
|
267
274
|
poetry_object[type][pkg_name] = dep.version
|
|
@@ -284,9 +291,7 @@ module Dependabot
|
|
|
284
291
|
|
|
285
292
|
sig { params(pyproject_content: String).returns(String) }
|
|
286
293
|
def sanitize(pyproject_content)
|
|
287
|
-
PyprojectPreparer
|
|
288
|
-
.new(pyproject_content: pyproject_content)
|
|
289
|
-
.sanitize
|
|
294
|
+
PyprojectPreparer.new(pyproject_content: pyproject_content).sanitize
|
|
290
295
|
end
|
|
291
296
|
|
|
292
297
|
sig { params(pyproject_content: String).returns(String) }
|
|
@@ -16,6 +16,80 @@ module Dependabot
|
|
|
16
16
|
class PyprojectPreparer
|
|
17
17
|
extend T::Sig
|
|
18
18
|
|
|
19
|
+
# Builds a regex pattern that matches a PEP 508 package name,
|
|
20
|
+
# treating hyphens, underscores, and dots as interchangeable per PEP 508.
|
|
21
|
+
sig { params(name: String).returns(String) }
|
|
22
|
+
def self.pep508_name_pattern(name)
|
|
23
|
+
Regexp.escape(name).gsub("\\-", "[-_.]").gsub("_", "[-_.]").gsub("\\.", "[-_.]")
|
|
24
|
+
end
|
|
25
|
+
private_class_method :pep508_name_pattern
|
|
26
|
+
|
|
27
|
+
# Pins a single PEP 508 dependency entry string to a specific version,
|
|
28
|
+
# preserving extras and environment markers.
|
|
29
|
+
sig { params(entry: String, name_pattern: String, version: String).returns(String) }
|
|
30
|
+
def self.pin_pep508_entry(entry, name_pattern, version)
|
|
31
|
+
if entry.match?(/\A#{name_pattern}(\[.*?\])?\s*[><=!~]/i)
|
|
32
|
+
entry.sub(
|
|
33
|
+
/(?<pre>#{name_pattern}(?:\[.*?\])?)\s*[><=!~][^;]*?(?=\s*;|\s*\z)/i,
|
|
34
|
+
"\\k<pre>==#{version}"
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
entry.sub(
|
|
38
|
+
/(?<pre>#{name_pattern}(?:\[.*?\])?)(?<rest>\s*(?:;.*)?)/i,
|
|
39
|
+
"\\k<pre>==#{version}\\k<rest>"
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
private_class_method :pin_pep508_entry
|
|
44
|
+
|
|
45
|
+
# Freezes PEP 621 dependencies in-place within a parsed pyproject object.
|
|
46
|
+
# Replaces version specifiers with ==dep.version for each matching dep.
|
|
47
|
+
# Accepts an optional block to filter which dependencies to freeze.
|
|
48
|
+
sig do
|
|
49
|
+
params(
|
|
50
|
+
pyproject_object: T::Hash[String, T.untyped],
|
|
51
|
+
deps: T::Array[Dependabot::Dependency],
|
|
52
|
+
blk: T.nilable(T.proc.params(dep: Dependabot::Dependency).returns(T::Boolean))
|
|
53
|
+
).void
|
|
54
|
+
end
|
|
55
|
+
def self.freeze_pep621_deps!(pyproject_object, deps, &blk)
|
|
56
|
+
dep_arrays = collect_pep621_dep_arrays(pyproject_object)
|
|
57
|
+
return if dep_arrays.empty?
|
|
58
|
+
|
|
59
|
+
deps.each do |dep|
|
|
60
|
+
next if blk && !yield(dep)
|
|
61
|
+
next unless dep.version
|
|
62
|
+
|
|
63
|
+
pin_pep621_dep_in_arrays!(dep_arrays, dep)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(pyproject_object: T::Hash[String, T.untyped]).returns(T::Array[T::Array[String]]) }
|
|
68
|
+
def self.collect_pep621_dep_arrays(pyproject_object)
|
|
69
|
+
project_object = pyproject_object["project"]
|
|
70
|
+
return [] unless project_object
|
|
71
|
+
|
|
72
|
+
dep_arrays = [project_object["dependencies"]]
|
|
73
|
+
project_object["optional-dependencies"]&.each_value { |opt| dep_arrays << opt }
|
|
74
|
+
dep_arrays.compact!
|
|
75
|
+
dep_arrays.select! { |arr| arr.is_a?(Array) && arr.all?(String) }
|
|
76
|
+
dep_arrays
|
|
77
|
+
end
|
|
78
|
+
private_class_method :collect_pep621_dep_arrays
|
|
79
|
+
|
|
80
|
+
sig { params(dep_arrays: T::Array[T::Array[String]], dep: Dependabot::Dependency).void }
|
|
81
|
+
def self.pin_pep621_dep_in_arrays!(dep_arrays, dep)
|
|
82
|
+
name_pattern = pep508_name_pattern(dep.name)
|
|
83
|
+
dep_arrays.each do |arr|
|
|
84
|
+
arr.each_with_index do |entry, i|
|
|
85
|
+
next unless entry.match?(/\A#{name_pattern}(\[.*?\])?\s*(\z|[><=!~;,])/i)
|
|
86
|
+
|
|
87
|
+
arr[i] = pin_pep508_entry(entry, name_pattern, T.must(dep.version))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private_class_method :pin_pep621_dep_in_arrays!
|
|
92
|
+
|
|
19
93
|
sig { params(pyproject_content: String, lockfile: T.nilable(Dependabot::DependencyFile)).void }
|
|
20
94
|
def initialize(pyproject_content:, lockfile: nil)
|
|
21
95
|
@pyproject_content = pyproject_content
|
|
@@ -109,6 +183,9 @@ module Dependabot
|
|
|
109
183
|
end
|
|
110
184
|
end
|
|
111
185
|
|
|
186
|
+
# Freeze PEP 621 project.dependencies and project.optional-dependencies
|
|
187
|
+
freeze_pep621_top_level_deps!(pyproject_object, excluded_names)
|
|
188
|
+
|
|
112
189
|
TomlRB.dump(pyproject_object)
|
|
113
190
|
end
|
|
114
191
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -122,6 +199,38 @@ module Dependabot
|
|
|
122
199
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
123
200
|
attr_reader :lockfile
|
|
124
201
|
|
|
202
|
+
sig { params(pyproject_object: T::Hash[String, T.untyped], excluded_names: T::Array[String]).void }
|
|
203
|
+
def freeze_pep621_top_level_deps!(pyproject_object, excluded_names)
|
|
204
|
+
project_object = pyproject_object["project"]
|
|
205
|
+
return unless project_object
|
|
206
|
+
|
|
207
|
+
freeze_pep621_dep_array!(project_object["dependencies"], excluded_names)
|
|
208
|
+
|
|
209
|
+
project_object["optional-dependencies"]&.each_value do |opt_deps|
|
|
210
|
+
freeze_pep621_dep_array!(opt_deps, excluded_names)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
sig { params(dep_array: T.nilable(T::Array[String]), excluded_names: T::Array[String]).void }
|
|
215
|
+
def freeze_pep621_dep_array!(dep_array, excluded_names)
|
|
216
|
+
return unless dep_array
|
|
217
|
+
|
|
218
|
+
dep_array.each_with_index do |entry, index|
|
|
219
|
+
# Extract dependency name from PEP 508 string
|
|
220
|
+
match = entry.match(/\A([a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?)/i)
|
|
221
|
+
next unless match
|
|
222
|
+
|
|
223
|
+
dep_name = normalise(T.must(match[1]))
|
|
224
|
+
next if excluded_names.include?(dep_name)
|
|
225
|
+
|
|
226
|
+
locked_details = locked_details(dep_name)
|
|
227
|
+
next unless (locked_version = locked_details&.fetch("version"))
|
|
228
|
+
|
|
229
|
+
name_pattern = self.class.send(:pep508_name_pattern, T.must(match[1]))
|
|
230
|
+
dep_array[index] = self.class.send(:pin_pep508_entry, entry, name_pattern, locked_version)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
125
234
|
sig { params(dep_name: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
126
235
|
def locked_details(dep_name)
|
|
127
236
|
parsed_lockfile.fetch("package")
|
|
@@ -55,33 +55,56 @@ module Dependabot
|
|
|
55
55
|
|
|
56
56
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
57
57
|
def fetch_updated_dependency_files
|
|
58
|
-
|
|
58
|
+
updated_contents = T.let({}, T::Hash[String, String])
|
|
59
|
+
|
|
60
|
+
unique_requirement_changes.each do |pair|
|
|
61
|
+
new_req = T.must(pair[0])
|
|
62
|
+
old_req = pair[1]
|
|
63
|
+
filename = new_req.fetch(:file)
|
|
64
|
+
content = updated_contents[filename] || T.must(T.must(get_original_file(filename)).content)
|
|
65
|
+
updated_contents[filename] = updated_requirement_or_setup_file_content(content, new_req, old_req)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
updated_contents.filter_map do |filename, content|
|
|
69
|
+
file = T.must(get_original_file(filename)).dup
|
|
70
|
+
next if content == T.must(file.content)
|
|
71
|
+
|
|
72
|
+
file.content = content
|
|
73
|
+
file
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Deduplicates requirements that share the same file and requirement strings.
|
|
78
|
+
# The replacer's regex matches all extras variants at once, so one call per
|
|
79
|
+
# unique (file, old_requirement, new_requirement) is sufficient.
|
|
80
|
+
sig { returns(T::Array[T::Array[T.nilable(T::Hash[Symbol, T.untyped])]]) }
|
|
81
|
+
def unique_requirement_changes
|
|
82
|
+
previous_reqs = dependency.previous_requirements || []
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
changes = dependency.requirements.filter_map do |new_req|
|
|
85
|
+
old_req = previous_reqs.find { |r| r[:file] == new_req[:file] && r[:groups] == new_req[:groups] }
|
|
61
86
|
next if new_req == old_req
|
|
62
87
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
updated_requirement_or_setup_file_content(new_req, old_req)
|
|
66
|
-
next if updated_content == file.content
|
|
88
|
+
[new_req, old_req]
|
|
89
|
+
end
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
changes.uniq do |pair|
|
|
92
|
+
new_req = pair[0]
|
|
93
|
+
old_req = pair[1]
|
|
94
|
+
[new_req[:file], old_req&.fetch(:requirement), new_req.fetch(:requirement)]
|
|
70
95
|
end
|
|
71
96
|
end
|
|
72
97
|
|
|
73
98
|
sig do
|
|
74
99
|
params(
|
|
100
|
+
content: String,
|
|
75
101
|
new_req: T::Hash[Symbol, T.untyped],
|
|
76
102
|
old_req: T.nilable(T::Hash[Symbol, T.untyped])
|
|
77
103
|
).returns(String)
|
|
78
104
|
end
|
|
79
|
-
def updated_requirement_or_setup_file_content(new_req, old_req)
|
|
80
|
-
original_file = get_original_file(new_req.fetch(:file))
|
|
81
|
-
raise "Could not find a dependency file for #{new_req}" unless original_file
|
|
82
|
-
|
|
105
|
+
def updated_requirement_or_setup_file_content(content, new_req, old_req)
|
|
83
106
|
RequirementReplacer.new(
|
|
84
|
-
content:
|
|
107
|
+
content: content,
|
|
85
108
|
dependency_name: dependency.name,
|
|
86
109
|
old_requirement: old_req&.fetch(:requirement),
|
|
87
110
|
new_requirement: new_req.fetch(:requirement),
|
|
@@ -53,7 +53,7 @@ module Dependabot
|
|
|
53
53
|
# ignore it, since it isn't actually a declaration
|
|
54
54
|
next mtch if Regexp.last_match&.pre_match&.match?(/--.*\z/)
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
updated_matched_declaration(mtch)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
raise "Expected content to change!" if old_requirement != new_requirement && content == updated_content
|
|
@@ -98,29 +98,30 @@ module Dependabot
|
|
|
98
98
|
new_req_string
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
# Builds updated declaration from the actual matched text, preserving
|
|
102
|
+
# whatever extras (or lack thereof) appeared in the original match.
|
|
103
|
+
sig { params(matched_declaration: String).returns(String) }
|
|
104
|
+
def updated_matched_declaration(matched_declaration)
|
|
105
|
+
updated = if old_requirement
|
|
106
|
+
matched_declaration
|
|
107
|
+
.sub(RequirementParser::REQUIREMENTS, updated_requirement_string || "")
|
|
108
|
+
else
|
|
109
|
+
matched_declaration
|
|
110
|
+
.sub(RequirementParser::NAME_WITH_EXTRAS) { |nm| nm + (updated_requirement_string || "") }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
return updated unless update_hashes? && matched_declaration.match?(RequirementParser::HASHES)
|
|
114
|
+
|
|
115
|
+
algorithm = T.must(matched_declaration.match(RequirementParser::HASHES))
|
|
116
|
+
.named_captures.fetch("algorithm")
|
|
117
|
+
separator = hash_separator(old_requirement)
|
|
118
|
+
updated.sub(
|
|
118
119
|
RequirementParser::HASHES,
|
|
119
120
|
package_hashes_for(
|
|
120
121
|
name: dependency_name,
|
|
121
122
|
version: new_hash_version,
|
|
122
|
-
algorithm:
|
|
123
|
-
).join(
|
|
123
|
+
algorithm: algorithm
|
|
124
|
+
).join(separator)
|
|
124
125
|
)
|
|
125
126
|
end
|
|
126
127
|
|
|
@@ -142,7 +143,14 @@ module Dependabot
|
|
|
142
143
|
def original_declaration_replacement_regex
|
|
143
144
|
original_string =
|
|
144
145
|
original_dependency_declaration_string(old_requirement)
|
|
145
|
-
|
|
146
|
+
match_data = T.must(original_string.match(RequirementParser::NAME_WITH_EXTRAS))
|
|
147
|
+
name_escaped = Regexp.escape(T.must(match_data[:name]))
|
|
148
|
+
# Everything after name+extras (the requirement/markers/hashes portion)
|
|
149
|
+
after_name_extras = T.must(original_string[T.must(match_data[0]).length..]).strip
|
|
150
|
+
after_escaped = Regexp.escape(after_name_extras)
|
|
151
|
+
# Match the dependency name with any extras (or none), followed by the requirement.
|
|
152
|
+
# This ensures a single gsub handles all extras variants of the same dependency.
|
|
153
|
+
/(?<![\-\w\.\[])#{name_escaped}\s*\\?\s*(?:\[[^\]]*\])?\s*\\?\s*#{after_escaped}(?![\-\w\.])/
|
|
146
154
|
end
|
|
147
155
|
|
|
148
156
|
sig { params(requirement: T.nilable(String)).returns(T::Boolean) }
|