dependabot-python 0.369.0 → 0.371.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/lib/parser.py +26 -0
- data/helpers/test/fixtures/pep621_spaced_specifiers.toml +10 -0
- data/helpers/test/test_parser.py +81 -0
- data/lib/dependabot/python/dependency_grapher/lockfile_generator.rb +13 -0
- data/lib/dependabot/python/file_parser/pyproject_files_parser.rb +46 -12
- 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 +48 -70
- data/lib/dependabot/python/file_updater/pyproject_preparer.rb +58 -52
- data/lib/dependabot/python/package_manager.rb +29 -11
- data/lib/dependabot/python/poetry_plugin_installer.rb +95 -0
- data/lib/dependabot/python/shared_file_fetcher.rb +33 -2
- data/lib/dependabot/python/update_checker/poetry_version_resolver.rb +13 -0
- data/lib/dependabot/python/update_checker/requirements_updater.rb +111 -28
- data/lib/dependabot/python/update_checker.rb +13 -5
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5c59beb6b677e51deac37d95023e448ad921265030b47910e4477249a07d1b4
|
|
4
|
+
data.tar.gz: 4c91924b433ac4f54c2f4f1bfd649e1124864b789cc459e4ab0aca281c02b603
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec8aa99e69214eec367ee3dff6e9e4b6f2031f75cca5e7b674eaa41369c8f40e5af4b8ba86dce39420e7affc0316301bacfc0cf2c832902d3d3da22558971d19
|
|
7
|
+
data.tar.gz: 586dea07cef16ec324974e7983dfbe42314e4782bfb388194bb524dc78ccb8a3448de97b52867e20be55b5304f674ce3eec26ec6f5e6be30632e4b3e1ffdff34
|
data/helpers/lib/parser.py
CHANGED
|
@@ -32,6 +32,30 @@ def parse_pep621_pep735_dependencies(pyproject_path):
|
|
|
32
32
|
next(iter(specifier_set)).operator in {"==", "==="}):
|
|
33
33
|
return next(iter(specifier_set)).version
|
|
34
34
|
|
|
35
|
+
def original_requirement_from_entry(entry, req):
|
|
36
|
+
"""Extract the original requirement string.
|
|
37
|
+
|
|
38
|
+
The packaging library normalizes specifiers
|
|
39
|
+
(removes spaces, reorders operators), but we
|
|
40
|
+
need the original so the file updater regex
|
|
41
|
+
can match the file content.
|
|
42
|
+
"""
|
|
43
|
+
# Strip the name (and any extras like [filecache]) from the start
|
|
44
|
+
remainder = entry[len(req.name):].strip()
|
|
45
|
+
|
|
46
|
+
# Strip extras bracket if present, e.g. [filecache]
|
|
47
|
+
if remainder.startswith("["):
|
|
48
|
+
close = remainder.index("]")
|
|
49
|
+
remainder = remainder[close + 1:].strip()
|
|
50
|
+
|
|
51
|
+
# Strip markers from the end, e.g. "; python_version < '3.4'"
|
|
52
|
+
if req.marker:
|
|
53
|
+
marker_pos = remainder.find(";")
|
|
54
|
+
if marker_pos != -1:
|
|
55
|
+
remainder = remainder[:marker_pos].strip()
|
|
56
|
+
|
|
57
|
+
return remainder
|
|
58
|
+
|
|
35
59
|
def parse_requirement(entry, pyproject_path, requirement_type=None):
|
|
36
60
|
try:
|
|
37
61
|
req = Requirement(entry)
|
|
@@ -45,6 +69,8 @@ def parse_pep621_pep735_dependencies(pyproject_path):
|
|
|
45
69
|
"markers": str(req.marker) or None,
|
|
46
70
|
"file": pyproject_path,
|
|
47
71
|
"requirement": str(req.specifier),
|
|
72
|
+
"source_requirement":
|
|
73
|
+
original_requirement_from_entry(entry, req),
|
|
48
74
|
"extras": sorted(list(req.extras)),
|
|
49
75
|
"requirement_type": requirement_type,
|
|
50
76
|
}
|
data/helpers/test/test_parser.py
CHANGED
|
@@ -182,3 +182,84 @@ class TestEdgeCases:
|
|
|
182
182
|
# Both groups should be processed without infinite recursion
|
|
183
183
|
assert "requests" in names
|
|
184
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"
|
|
@@ -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) }
|
|
@@ -68,7 +69,10 @@ module Dependabot
|
|
|
68
69
|
|
|
69
70
|
groups = T.must(poetry_root)["group"] || {}
|
|
70
71
|
groups.each do |group, group_spec|
|
|
71
|
-
|
|
72
|
+
deps = group_spec["dependencies"]
|
|
73
|
+
next unless deps
|
|
74
|
+
|
|
75
|
+
dependencies += parse_poetry_dependency_group(group, deps)
|
|
72
76
|
end
|
|
73
77
|
dependencies
|
|
74
78
|
end
|
|
@@ -84,16 +88,7 @@ module Dependabot
|
|
|
84
88
|
return dependencies if using_pdm?
|
|
85
89
|
|
|
86
90
|
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"
|
|
91
|
+
next if skip_pep621_dep?(dep)
|
|
97
92
|
|
|
98
93
|
dependencies <<
|
|
99
94
|
Dependency.new(
|
|
@@ -106,7 +101,9 @@ module Dependabot
|
|
|
106
101
|
groups: [dep["requirement_type"]].compact
|
|
107
102
|
}],
|
|
108
103
|
package_manager: "pip",
|
|
109
|
-
metadata: extras_metadata(dep["extras"])
|
|
104
|
+
metadata: extras_metadata(dep["extras"]).merge(
|
|
105
|
+
source_requirement: dep["source_requirement"]
|
|
106
|
+
).compact
|
|
110
107
|
)
|
|
111
108
|
end
|
|
112
109
|
|
|
@@ -229,6 +226,43 @@ module Dependabot
|
|
|
229
226
|
using_pep621? && pdm_lock
|
|
230
227
|
end
|
|
231
228
|
|
|
229
|
+
sig { returns(T::Array[String]) }
|
|
230
|
+
def dynamic_fields
|
|
231
|
+
@dynamic_fields ||= parsed_pyproject.dig("project", "dynamic") || []
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
sig { params(dep: T::Hash[String, T.untyped]).returns(T::Boolean) }
|
|
235
|
+
def skip_pep621_dep?(dep)
|
|
236
|
+
# If a requirement has a `<` or `<=` marker then updating it is
|
|
237
|
+
# probably blocked. Ignore it.
|
|
238
|
+
return true if dep["markers"]&.include?("<")
|
|
239
|
+
|
|
240
|
+
# If no requirement, don't add it
|
|
241
|
+
return true if dep["requirement"].empty?
|
|
242
|
+
|
|
243
|
+
# Skip build-system.requires dependencies when using Poetry
|
|
244
|
+
# Poetry manages its own build system dependencies
|
|
245
|
+
return true if using_poetry? && dep["requirement_type"] == "build-system.requires"
|
|
246
|
+
|
|
247
|
+
# When dependencies or optional-dependencies are listed in project.dynamic,
|
|
248
|
+
# they are managed by the build backend (e.g. Poetry) — skip the PEP 621 path
|
|
249
|
+
dynamic_pep621_dep?(dep["requirement_type"])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
sig { params(requirement_type: T.nilable(String)).returns(T::Boolean) }
|
|
253
|
+
def dynamic_pep621_dep?(requirement_type)
|
|
254
|
+
return false unless using_poetry?
|
|
255
|
+
return false unless requirement_type
|
|
256
|
+
|
|
257
|
+
if requirement_type == "dependencies"
|
|
258
|
+
dynamic_fields.include?("dependencies")
|
|
259
|
+
elsif parsed_pyproject.dig("project", "optional-dependencies")&.key?(requirement_type)
|
|
260
|
+
dynamic_fields.include?("optional-dependencies")
|
|
261
|
+
else
|
|
262
|
+
false
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
232
266
|
# Create a DependencySet where each element has no requirement. Any
|
|
233
267
|
# requirements will be added when combining the DependencySet with
|
|
234
268
|
# 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
|
|
@@ -13,12 +13,14 @@ require "dependabot/python/file_parser/python_requirement_parser"
|
|
|
13
13
|
require "dependabot/python/file_updater"
|
|
14
14
|
require "dependabot/python/native_helpers"
|
|
15
15
|
require "dependabot/python/name_normaliser"
|
|
16
|
+
require "dependabot/python/poetry_plugin_installer"
|
|
16
17
|
|
|
17
18
|
module Dependabot
|
|
18
19
|
module Python
|
|
19
20
|
class FileUpdater
|
|
20
21
|
class PoetryFileUpdater
|
|
21
22
|
require_relative "pyproject_preparer"
|
|
23
|
+
require_relative "poetry_file_updater/pep621_updater"
|
|
22
24
|
extend T::Sig
|
|
23
25
|
|
|
24
26
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
@@ -49,6 +51,7 @@ module Dependabot
|
|
|
49
51
|
@language_version_manager = T.let(nil, T.nilable(LanguageVersionManager))
|
|
50
52
|
@python_requirement_parser = T.let(nil, T.nilable(FileParser::PythonRequirementParser))
|
|
51
53
|
@updated_pyproject_content = T.let(nil, T.nilable(String))
|
|
54
|
+
@poetry_plugin_installer = T.let(nil, T.nilable(PoetryPluginInstaller))
|
|
52
55
|
@poetry_lock = T.let(nil, T.nilable(Dependabot::DependencyFile))
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -77,9 +80,7 @@ module Dependabot
|
|
|
77
80
|
)
|
|
78
81
|
end
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if lockfile
|
|
83
|
+
if lockfile && lockfile&.content != updated_lockfile_content
|
|
83
84
|
updated_files <<
|
|
84
85
|
updated_file(file: T.must(lockfile), content: updated_lockfile_content)
|
|
85
86
|
end
|
|
@@ -105,52 +106,51 @@ module Dependabot
|
|
|
105
106
|
updated_content
|
|
106
107
|
end
|
|
107
108
|
|
|
108
|
-
sig
|
|
109
|
-
params(
|
|
110
|
-
dep: Dependabot::Dependency,
|
|
111
|
-
content: String,
|
|
112
|
-
new_r: T::Hash[Symbol, T.untyped],
|
|
113
|
-
old_r: T::Hash[Symbol, T.untyped]
|
|
114
|
-
).returns(String)
|
|
115
|
-
end
|
|
109
|
+
sig { params(dep: Dependabot::Dependency, content: String, new_r: T::Hash[Symbol, T.untyped], old_r: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
116
110
|
def replace_dep(dep, content, new_r, old_r)
|
|
117
|
-
# Handle Git dependencies with tags
|
|
118
111
|
return update_git_tag(dep, content, new_r, old_r) if git_dependency?(new_r) && git_dependency?(old_r)
|
|
119
112
|
|
|
113
|
+
replace_poetry_dep(dep, content, new_r, old_r) ||
|
|
114
|
+
replace_poetry_table_dep(dep, content, new_r, old_r) ||
|
|
115
|
+
replace_pep621_dep(dep, content, new_r, old_r) ||
|
|
116
|
+
content
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { params(dep: Dependabot::Dependency, content: String, new_r: T::Hash[Symbol, T.untyped], old_r: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
|
|
120
|
+
def replace_poetry_dep(dep, content, new_r, old_r)
|
|
120
121
|
new_req = new_r[:requirement]
|
|
121
122
|
old_req = old_r[:requirement]
|
|
122
123
|
|
|
123
124
|
declaration_regex = declaration_regex(dep, old_r)
|
|
124
125
|
declaration_match = content.match(declaration_regex)
|
|
125
|
-
|
|
126
|
-
declaration = declaration_match[:declaration]
|
|
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
|
|
131
|
-
end
|
|
126
|
+
return unless declaration_match
|
|
132
127
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
128
|
+
declaration = declaration_match[:declaration]
|
|
129
|
+
return unless T.must(declaration).include?(old_req)
|
|
130
|
+
|
|
131
|
+
new_declaration = T.must(declaration).sub(old_req, new_req)
|
|
132
|
+
content.sub(T.must(declaration), new_declaration)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
sig { params(dep: Dependabot::Dependency, content: String, new_r: T::Hash[Symbol, T.untyped], old_r: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
|
|
136
|
+
def replace_poetry_table_dep(dep, content, new_r, old_r)
|
|
137
|
+
old_req = old_r[:requirement]
|
|
138
|
+
new_req = new_r[:requirement]
|
|
139
|
+
regex = table_declaration_regex(dep, new_r)
|
|
140
|
+
|
|
141
|
+
return unless content.match(regex)
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
new_declaration = T.must(declaration).sub(old_req, new_req)
|
|
150
|
-
return content.sub(T.must(declaration), new_declaration)
|
|
143
|
+
content.gsub(regex) do |match|
|
|
144
|
+
match.gsub(
|
|
145
|
+
/(\s*version\s*=\s*["'])#{Regexp.escape(old_req)}/,
|
|
146
|
+
'\1' + new_req
|
|
147
|
+
)
|
|
151
148
|
end
|
|
149
|
+
end
|
|
152
150
|
|
|
153
|
-
|
|
151
|
+
sig { params(dep: Dependabot::Dependency, content: String, new_r: T::Hash[Symbol, T.untyped], old_r: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
|
|
152
|
+
def replace_pep621_dep(dep, content, new_r, old_r)
|
|
153
|
+
Pep621Updater.new(dep: dep).replace(content, new_r, old_r)
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
|
|
@@ -158,14 +158,7 @@ module Dependabot
|
|
|
158
158
|
req.dig(:source, :type) == "git"
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
-
sig
|
|
162
|
-
params(
|
|
163
|
-
dep: Dependabot::Dependency,
|
|
164
|
-
content: String,
|
|
165
|
-
new_r: T::Hash[Symbol, T.untyped],
|
|
166
|
-
old_r: T::Hash[Symbol, T.untyped]
|
|
167
|
-
).returns(String)
|
|
168
|
-
end
|
|
161
|
+
sig { params(dep: Dependabot::Dependency, content: String, new_r: T::Hash[Symbol, T.untyped], old_r: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
169
162
|
def update_git_tag(dep, content, new_r, old_r)
|
|
170
163
|
old_tag = old_r.dig(:source, :ref)
|
|
171
164
|
new_tag = new_r.dig(:source, :ref)
|
|
@@ -302,6 +295,7 @@ module Dependabot
|
|
|
302
295
|
add_auth_env_vars
|
|
303
296
|
|
|
304
297
|
language_version_manager.install_required_python
|
|
298
|
+
poetry_plugin_installer.install_required_plugins
|
|
305
299
|
|
|
306
300
|
# use system git instead of the pure Python dulwich
|
|
307
301
|
run_poetry_command("pyenv exec poetry config system-git-client true")
|
|
@@ -350,17 +344,7 @@ module Dependabot
|
|
|
350
344
|
.add_auth_env_vars(credentials)
|
|
351
345
|
end
|
|
352
346
|
|
|
353
|
-
sig
|
|
354
|
-
params(
|
|
355
|
-
pyproject_content: String
|
|
356
|
-
).returns(T.nilable(
|
|
357
|
-
T.any(
|
|
358
|
-
T::Hash[String, T.untyped],
|
|
359
|
-
String,
|
|
360
|
-
T::Array[T::Hash[String, T.untyped]]
|
|
361
|
-
)
|
|
362
|
-
))
|
|
363
|
-
end
|
|
347
|
+
sig { params(pyproject_content: String).returns(T.nilable(T.any(T::Hash[String, T.untyped], String, T::Array[T::Hash[String, T.untyped]]))) }
|
|
364
348
|
def pyproject_hash_for(pyproject_content)
|
|
365
349
|
SharedHelpers.in_a_temporary_directory do |dir|
|
|
366
350
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
@@ -388,20 +372,6 @@ module Dependabot
|
|
|
388
372
|
/tool\.poetry\.#{old_req[:groups].first}\.#{escape(dep)}\](?:\r?\n).*?\s*version\s* =.*?(?:\r?\n)/m
|
|
389
373
|
end
|
|
390
374
|
|
|
391
|
-
sig { params(dep: Dependabot::Dependency, old_req: String).returns(Regexp) }
|
|
392
|
-
def pep621_declaration_regex(dep, old_req)
|
|
393
|
-
/(?<declaration>["']#{escape(dep)}#{extras_pattern(dep)}#{Regexp.escape(old_req)}["'])/mi
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Reconstructs extras from metadata for PEP 621 regex matching.
|
|
397
|
-
sig { params(dep: Dependabot::Dependency).returns(String) }
|
|
398
|
-
def extras_pattern(dep)
|
|
399
|
-
extras_str = dep.metadata[:extras]
|
|
400
|
-
return "" unless extras_str.is_a?(String) && !extras_str.empty?
|
|
401
|
-
|
|
402
|
-
"\\[" + extras_str.split(",").map { |e| Regexp.escape(e.strip) }.join(",\\s*") + "\\]"
|
|
403
|
-
end
|
|
404
|
-
|
|
405
375
|
sig { params(dep: Dependency).returns(String) }
|
|
406
376
|
def escape(dep)
|
|
407
377
|
Regexp.escape(dep.name).gsub("\\-", "[-_.]")
|
|
@@ -448,6 +418,14 @@ module Dependabot
|
|
|
448
418
|
)
|
|
449
419
|
end
|
|
450
420
|
|
|
421
|
+
sig { returns(PoetryPluginInstaller) }
|
|
422
|
+
def poetry_plugin_installer
|
|
423
|
+
@poetry_plugin_installer ||= T.let(
|
|
424
|
+
PoetryPluginInstaller.from_dependency_files(dependency_files),
|
|
425
|
+
T.nilable(PoetryPluginInstaller)
|
|
426
|
+
)
|
|
427
|
+
end
|
|
428
|
+
|
|
451
429
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
452
430
|
def pyproject
|
|
453
431
|
@pyproject ||=
|