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
|
@@ -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,50 +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
|
-
new_declaration = T.must(declaration).sub(old_req, new_req)
|
|
128
|
-
return content.sub(T.must(declaration), new_declaration)
|
|
129
|
-
end
|
|
126
|
+
return unless declaration_match
|
|
130
127
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
'\1' + new_req
|
|
138
|
-
)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
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
|
|
141
134
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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)
|
|
142
|
+
|
|
143
|
+
content.gsub(regex) do |match|
|
|
144
|
+
match.gsub(
|
|
145
|
+
/(\s*version\s*=\s*["'])#{Regexp.escape(old_req)}/,
|
|
146
|
+
'\1' + new_req
|
|
147
|
+
)
|
|
149
148
|
end
|
|
149
|
+
end
|
|
150
150
|
|
|
151
|
-
|
|
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)
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
|
|
@@ -156,14 +158,7 @@ module Dependabot
|
|
|
156
158
|
req.dig(:source, :type) == "git"
|
|
157
159
|
end
|
|
158
160
|
|
|
159
|
-
sig
|
|
160
|
-
params(
|
|
161
|
-
dep: Dependabot::Dependency,
|
|
162
|
-
content: String,
|
|
163
|
-
new_r: T::Hash[Symbol, T.untyped],
|
|
164
|
-
old_r: T::Hash[Symbol, T.untyped]
|
|
165
|
-
).returns(String)
|
|
166
|
-
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) }
|
|
167
162
|
def update_git_tag(dep, content, new_r, old_r)
|
|
168
163
|
old_tag = old_r.dig(:source, :ref)
|
|
169
164
|
new_tag = new_r.dig(:source, :ref)
|
|
@@ -220,9 +215,8 @@ module Dependabot
|
|
|
220
215
|
|
|
221
216
|
sig { params(pyproject_content: String).returns(String) }
|
|
222
217
|
def freeze_other_dependencies(pyproject_content)
|
|
223
|
-
PyprojectPreparer
|
|
224
|
-
|
|
225
|
-
.freeze_top_level_dependencies_except(dependencies)
|
|
218
|
+
PyprojectPreparer.new(pyproject_content: pyproject_content, lockfile: lockfile)
|
|
219
|
+
.freeze_top_level_dependencies_except(dependencies)
|
|
226
220
|
end
|
|
227
221
|
|
|
228
222
|
sig { params(pyproject_content: String).returns(String) }
|
|
@@ -244,14 +238,18 @@ module Dependabot
|
|
|
244
238
|
end
|
|
245
239
|
end
|
|
246
240
|
|
|
241
|
+
# Freeze PEP 621 project.dependencies and project.optional-dependencies
|
|
242
|
+
PyprojectPreparer.freeze_pep621_deps!(pyproject_object, dependencies) do |dep|
|
|
243
|
+
!git_dependency_being_updated?(dep)
|
|
244
|
+
end
|
|
245
|
+
|
|
247
246
|
TomlRB.dump(pyproject_object)
|
|
248
247
|
end
|
|
249
248
|
|
|
250
249
|
sig { params(pyproject_content: String).returns(String) }
|
|
251
250
|
def update_python_requirement(pyproject_content)
|
|
252
|
-
PyprojectPreparer
|
|
253
|
-
|
|
254
|
-
.update_python_requirement(language_version_manager.python_version)
|
|
251
|
+
PyprojectPreparer.new(pyproject_content: pyproject_content)
|
|
252
|
+
.update_python_requirement(language_version_manager.python_version)
|
|
255
253
|
end
|
|
256
254
|
|
|
257
255
|
sig { params(poetry_object: T::Hash[String, T.untyped], dep: Dependabot::Dependency).returns(T::Array[String]) }
|
|
@@ -262,6 +260,8 @@ module Dependabot
|
|
|
262
260
|
next unless pkg_name
|
|
263
261
|
|
|
264
262
|
if poetry_object[type][pkg_name].is_a?(Hash)
|
|
263
|
+
next unless poetry_object[type][pkg_name].key?("version") # skip enrichment-only entries
|
|
264
|
+
|
|
265
265
|
poetry_object[type][pkg_name]["version"] = dep.version
|
|
266
266
|
else
|
|
267
267
|
poetry_object[type][pkg_name] = dep.version
|
|
@@ -284,9 +284,7 @@ module Dependabot
|
|
|
284
284
|
|
|
285
285
|
sig { params(pyproject_content: String).returns(String) }
|
|
286
286
|
def sanitize(pyproject_content)
|
|
287
|
-
PyprojectPreparer
|
|
288
|
-
.new(pyproject_content: pyproject_content)
|
|
289
|
-
.sanitize
|
|
287
|
+
PyprojectPreparer.new(pyproject_content: pyproject_content).sanitize
|
|
290
288
|
end
|
|
291
289
|
|
|
292
290
|
sig { params(pyproject_content: String).returns(String) }
|
|
@@ -297,6 +295,7 @@ module Dependabot
|
|
|
297
295
|
add_auth_env_vars
|
|
298
296
|
|
|
299
297
|
language_version_manager.install_required_python
|
|
298
|
+
poetry_plugin_installer.install_required_plugins
|
|
300
299
|
|
|
301
300
|
# use system git instead of the pure Python dulwich
|
|
302
301
|
run_poetry_command("pyenv exec poetry config system-git-client true")
|
|
@@ -345,17 +344,7 @@ module Dependabot
|
|
|
345
344
|
.add_auth_env_vars(credentials)
|
|
346
345
|
end
|
|
347
346
|
|
|
348
|
-
sig
|
|
349
|
-
params(
|
|
350
|
-
pyproject_content: String
|
|
351
|
-
).returns(T.nilable(
|
|
352
|
-
T.any(
|
|
353
|
-
T::Hash[String, T.untyped],
|
|
354
|
-
String,
|
|
355
|
-
T::Array[T::Hash[String, T.untyped]]
|
|
356
|
-
)
|
|
357
|
-
))
|
|
358
|
-
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]]))) }
|
|
359
348
|
def pyproject_hash_for(pyproject_content)
|
|
360
349
|
SharedHelpers.in_a_temporary_directory do |dir|
|
|
361
350
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
@@ -383,20 +372,6 @@ module Dependabot
|
|
|
383
372
|
/tool\.poetry\.#{old_req[:groups].first}\.#{escape(dep)}\](?:\r?\n).*?\s*version\s* =.*?(?:\r?\n)/m
|
|
384
373
|
end
|
|
385
374
|
|
|
386
|
-
sig { params(dep: Dependabot::Dependency, old_req: String).returns(Regexp) }
|
|
387
|
-
def pep621_declaration_regex(dep, old_req)
|
|
388
|
-
/(?<declaration>["']#{escape(dep)}#{extras_pattern(dep)}#{Regexp.escape(old_req)}["'])/mi
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
# Reconstructs extras from metadata for PEP 621 regex matching.
|
|
392
|
-
sig { params(dep: Dependabot::Dependency).returns(String) }
|
|
393
|
-
def extras_pattern(dep)
|
|
394
|
-
extras_str = dep.metadata[:extras]
|
|
395
|
-
return "" unless extras_str.is_a?(String) && !extras_str.empty?
|
|
396
|
-
|
|
397
|
-
"\\[" + extras_str.split(",").map { |e| Regexp.escape(e.strip) }.join(",\\s*") + "\\]"
|
|
398
|
-
end
|
|
399
|
-
|
|
400
375
|
sig { params(dep: Dependency).returns(String) }
|
|
401
376
|
def escape(dep)
|
|
402
377
|
Regexp.escape(dep.name).gsub("\\-", "[-_.]")
|
|
@@ -443,6 +418,14 @@ module Dependabot
|
|
|
443
418
|
)
|
|
444
419
|
end
|
|
445
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
|
+
|
|
446
429
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
447
430
|
def pyproject
|
|
448
431
|
@pyproject ||=
|
|
@@ -16,6 +16,81 @@ module Dependabot
|
|
|
16
16
|
class PyprojectPreparer
|
|
17
17
|
extend T::Sig
|
|
18
18
|
|
|
19
|
+
# Fixed regex for extracting the name+extras prefix from a PEP 508 entry.
|
|
20
|
+
# Does not interpolate library input, avoiding polynomial backtracking.
|
|
21
|
+
PEP508_PREFIX = T.let(
|
|
22
|
+
/\A(?<prefix>(?<name>[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?)(?:\[[^\]]*\])?)/i,
|
|
23
|
+
Regexp
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Pins a single PEP 508 dependency entry string to a specific version,
|
|
27
|
+
# preserving extras and environment markers.
|
|
28
|
+
sig { params(entry: String, version: String).returns(String) }
|
|
29
|
+
def self.pin_pep508_entry(entry, version)
|
|
30
|
+
prefix_match = entry.match(PEP508_PREFIX)
|
|
31
|
+
return entry unless prefix_match
|
|
32
|
+
|
|
33
|
+
prefix = T.must(prefix_match[:prefix])
|
|
34
|
+
rest = T.must(entry[prefix.length..])
|
|
35
|
+
|
|
36
|
+
# Extract the environment marker ("; ..." suffix) if present
|
|
37
|
+
marker_match = rest.match(/(\s*;.*)/)
|
|
38
|
+
marker = marker_match ? marker_match[1] : ""
|
|
39
|
+
|
|
40
|
+
"#{prefix}==#{version}#{marker}"
|
|
41
|
+
end
|
|
42
|
+
private_class_method :pin_pep508_entry
|
|
43
|
+
|
|
44
|
+
# Freezes PEP 621 dependencies in-place within a parsed pyproject object.
|
|
45
|
+
# Replaces version specifiers with ==dep.version for each matching dep.
|
|
46
|
+
# Accepts an optional block to filter which dependencies to freeze.
|
|
47
|
+
sig do
|
|
48
|
+
params(
|
|
49
|
+
pyproject_object: T::Hash[String, T.untyped],
|
|
50
|
+
deps: T::Array[Dependabot::Dependency],
|
|
51
|
+
blk: T.nilable(T.proc.params(dep: Dependabot::Dependency).returns(T::Boolean))
|
|
52
|
+
).void
|
|
53
|
+
end
|
|
54
|
+
def self.freeze_pep621_deps!(pyproject_object, deps, &blk)
|
|
55
|
+
dep_arrays = collect_pep621_dep_arrays(pyproject_object)
|
|
56
|
+
return if dep_arrays.empty?
|
|
57
|
+
|
|
58
|
+
deps.each do |dep|
|
|
59
|
+
next if blk && !yield(dep)
|
|
60
|
+
next unless dep.version
|
|
61
|
+
|
|
62
|
+
pin_pep621_dep_in_arrays!(dep_arrays, dep)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { params(pyproject_object: T::Hash[String, T.untyped]).returns(T::Array[T::Array[String]]) }
|
|
67
|
+
def self.collect_pep621_dep_arrays(pyproject_object)
|
|
68
|
+
project_object = pyproject_object["project"]
|
|
69
|
+
return [] unless project_object
|
|
70
|
+
|
|
71
|
+
dep_arrays = [project_object["dependencies"]]
|
|
72
|
+
project_object["optional-dependencies"]&.each_value { |opt| dep_arrays << opt }
|
|
73
|
+
dep_arrays.compact!
|
|
74
|
+
dep_arrays.select! { |arr| arr.is_a?(Array) && arr.all?(String) }
|
|
75
|
+
dep_arrays
|
|
76
|
+
end
|
|
77
|
+
private_class_method :collect_pep621_dep_arrays
|
|
78
|
+
|
|
79
|
+
sig { params(dep_arrays: T::Array[T::Array[String]], dep: Dependabot::Dependency).void }
|
|
80
|
+
def self.pin_pep621_dep_in_arrays!(dep_arrays, dep)
|
|
81
|
+
normalised_name = NameNormaliser.normalise(dep.name)
|
|
82
|
+
dep_arrays.each do |arr|
|
|
83
|
+
arr.each_with_index do |entry, i|
|
|
84
|
+
match = entry.match(PEP508_PREFIX)
|
|
85
|
+
next unless match
|
|
86
|
+
next unless NameNormaliser.normalise(T.must(match[:name])) == normalised_name
|
|
87
|
+
|
|
88
|
+
arr[i] = pin_pep508_entry(entry, T.must(dep.version))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
private_class_method :pin_pep621_dep_in_arrays!
|
|
93
|
+
|
|
19
94
|
sig { params(pyproject_content: String, lockfile: T.nilable(Dependabot::DependencyFile)).void }
|
|
20
95
|
def initialize(pyproject_content:, lockfile: nil)
|
|
21
96
|
@pyproject_content = pyproject_content
|
|
@@ -64,8 +139,8 @@ module Dependabot
|
|
|
64
139
|
.gsub('#{', "{")
|
|
65
140
|
end
|
|
66
141
|
|
|
67
|
-
|
|
68
|
-
|
|
142
|
+
UNSUPPORTED_SOURCE_TYPES = T.let(%w(directory file url).freeze, T::Array[String])
|
|
143
|
+
|
|
69
144
|
sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(String) }
|
|
70
145
|
def freeze_top_level_dependencies_except(dependencies)
|
|
71
146
|
return pyproject_content unless lockfile
|
|
@@ -80,39 +155,18 @@ module Dependabot
|
|
|
80
155
|
Dependabot::Python::FileParser::PyprojectFilesParser::POETRY_DEPENDENCY_TYPES.each do |key|
|
|
81
156
|
next unless poetry_object[key]
|
|
82
157
|
|
|
83
|
-
source_types = %w(directory file url)
|
|
84
158
|
poetry_object.fetch(key).each do |dep_name, _|
|
|
85
159
|
next if excluded_names.include?(normalise(dep_name))
|
|
86
160
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
next unless (locked_version = locked_details&.fetch("version"))
|
|
90
|
-
|
|
91
|
-
next if source_types.include?(locked_details.dig("source", "type"))
|
|
92
|
-
|
|
93
|
-
if locked_details.dig("source", "type") == "git"
|
|
94
|
-
poetry_object[key][dep_name] = {
|
|
95
|
-
"git" => locked_details.dig("source", "url"),
|
|
96
|
-
"rev" => locked_details.dig("source", "reference")
|
|
97
|
-
}
|
|
98
|
-
subdirectory = locked_details.dig("source", "subdirectory")
|
|
99
|
-
poetry_object[key][dep_name]["subdirectory"] = subdirectory if subdirectory
|
|
100
|
-
elsif poetry_object[key][dep_name].is_a?(Hash)
|
|
101
|
-
poetry_object[key][dep_name]["version"] = locked_version
|
|
102
|
-
elsif poetry_object[key][dep_name].is_a?(Array)
|
|
103
|
-
# if it has multiple-constraints, locking to a single version is
|
|
104
|
-
# going to result in a bad lockfile, ignore
|
|
105
|
-
next
|
|
106
|
-
else
|
|
107
|
-
poetry_object[key][dep_name] = locked_version
|
|
108
|
-
end
|
|
161
|
+
freeze_poetry_dep!(poetry_object[key], dep_name)
|
|
109
162
|
end
|
|
110
163
|
end
|
|
111
164
|
|
|
165
|
+
# Freeze PEP 621 project.dependencies and project.optional-dependencies
|
|
166
|
+
freeze_pep621_top_level_deps!(pyproject_object, excluded_names)
|
|
167
|
+
|
|
112
168
|
TomlRB.dump(pyproject_object)
|
|
113
169
|
end
|
|
114
|
-
# rubocop:enable Metrics/AbcSize
|
|
115
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
116
170
|
|
|
117
171
|
private
|
|
118
172
|
|
|
@@ -122,6 +176,64 @@ module Dependabot
|
|
|
122
176
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
123
177
|
attr_reader :lockfile
|
|
124
178
|
|
|
179
|
+
sig { params(deps_hash: T::Hash[String, T.untyped], dep_name: String).void }
|
|
180
|
+
def freeze_poetry_dep!(deps_hash, dep_name)
|
|
181
|
+
details = locked_details(dep_name)
|
|
182
|
+
return unless (locked_version = details&.fetch("version"))
|
|
183
|
+
|
|
184
|
+
source_type = details.dig("source", "type")
|
|
185
|
+
return if UNSUPPORTED_SOURCE_TYPES.include?(source_type)
|
|
186
|
+
|
|
187
|
+
if source_type == "git"
|
|
188
|
+
freeze_git_dep!(deps_hash, dep_name, details)
|
|
189
|
+
elsif deps_hash[dep_name].is_a?(Hash)
|
|
190
|
+
deps_hash[dep_name]["version"] = locked_version
|
|
191
|
+
elsif !deps_hash[dep_name].is_a?(Array)
|
|
192
|
+
deps_hash[dep_name] = locked_version
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
sig { params(deps_hash: T::Hash[String, T.untyped], dep_name: String, details: T::Hash[String, T.untyped]).void }
|
|
197
|
+
def freeze_git_dep!(deps_hash, dep_name, details)
|
|
198
|
+
deps_hash[dep_name] = {
|
|
199
|
+
"git" => details.dig("source", "url"),
|
|
200
|
+
"rev" => details.dig("source", "reference")
|
|
201
|
+
}
|
|
202
|
+
subdirectory = details.dig("source", "subdirectory")
|
|
203
|
+
deps_hash[dep_name]["subdirectory"] = subdirectory if subdirectory
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
sig { params(pyproject_object: T::Hash[String, T.untyped], excluded_names: T::Array[String]).void }
|
|
207
|
+
def freeze_pep621_top_level_deps!(pyproject_object, excluded_names)
|
|
208
|
+
project_object = pyproject_object["project"]
|
|
209
|
+
return unless project_object
|
|
210
|
+
|
|
211
|
+
freeze_pep621_dep_array!(project_object["dependencies"], excluded_names)
|
|
212
|
+
|
|
213
|
+
project_object["optional-dependencies"]&.each_value do |opt_deps|
|
|
214
|
+
freeze_pep621_dep_array!(opt_deps, excluded_names)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
sig { params(dep_array: T.nilable(T::Array[String]), excluded_names: T::Array[String]).void }
|
|
219
|
+
def freeze_pep621_dep_array!(dep_array, excluded_names)
|
|
220
|
+
return unless dep_array
|
|
221
|
+
|
|
222
|
+
dep_array.each_with_index do |entry, index|
|
|
223
|
+
# Extract dependency name from PEP 508 string
|
|
224
|
+
match = entry.match(/\A([a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?)/i)
|
|
225
|
+
next unless match
|
|
226
|
+
|
|
227
|
+
dep_name = normalise(T.must(match[1]))
|
|
228
|
+
next if excluded_names.include?(dep_name)
|
|
229
|
+
|
|
230
|
+
locked_details = locked_details(dep_name)
|
|
231
|
+
next unless (locked_version = locked_details&.fetch("version"))
|
|
232
|
+
|
|
233
|
+
dep_array[index] = self.class.send(:pin_pep508_entry, entry, locked_version)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
125
237
|
sig { params(dep_name: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
126
238
|
def locked_details(dep_name)
|
|
127
239
|
parsed_lockfile.fetch("package")
|
|
@@ -86,6 +86,22 @@ module Dependabot
|
|
|
86
86
|
def unsupported?
|
|
87
87
|
false
|
|
88
88
|
end
|
|
89
|
+
|
|
90
|
+
# Poetry supports requires-poetry constraints in pyproject.toml;
|
|
91
|
+
# other Python package managers don't have an equivalent mechanism.
|
|
92
|
+
sig { override.void }
|
|
93
|
+
def raise_if_unsupported!
|
|
94
|
+
super
|
|
95
|
+
return unless requirement
|
|
96
|
+
return unless version
|
|
97
|
+
return if T.cast(T.must(requirement).satisfied_by?(T.must(version)), T::Boolean)
|
|
98
|
+
|
|
99
|
+
raise Dependabot::ToolVersionNotSupported.new(
|
|
100
|
+
NAME,
|
|
101
|
+
version.to_s,
|
|
102
|
+
requirement.to_s
|
|
103
|
+
)
|
|
104
|
+
end
|
|
89
105
|
end
|
|
90
106
|
|
|
91
107
|
class PipCompilePackageManager < Dependabot::Ecosystem::VersionManager
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
require "toml-rb"
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/errors"
|
|
9
|
+
require "dependabot/shared_helpers"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Python
|
|
13
|
+
class PoetryPluginInstaller
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
# Only allow valid PyPI package names to prevent command injection
|
|
17
|
+
VALID_PLUGIN_NAME = /\A[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?\z/
|
|
18
|
+
|
|
19
|
+
# Only allow valid version constraint characters to prevent command injection
|
|
20
|
+
VALID_CONSTRAINT = /\A[a-zA-Z0-9.*,!=<>~^ ]+\z/
|
|
21
|
+
|
|
22
|
+
sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).returns(PoetryPluginInstaller) }
|
|
23
|
+
def self.from_dependency_files(dependency_files)
|
|
24
|
+
pyproject_content = dependency_files.find { |f| f.name == "pyproject.toml" }&.content
|
|
25
|
+
new(pyproject_content: pyproject_content)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(pyproject_content: T.nilable(String)).void }
|
|
29
|
+
def initialize(pyproject_content:)
|
|
30
|
+
@pyproject_content = T.let(pyproject_content, T.nilable(String))
|
|
31
|
+
@plugins_installed = T.let(false, T::Boolean)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { void }
|
|
35
|
+
def install_required_plugins
|
|
36
|
+
return if @plugins_installed
|
|
37
|
+
|
|
38
|
+
required_plugins.each do |name, constraint|
|
|
39
|
+
install_plugin(name, constraint)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@plugins_installed = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
sig { returns(T.nilable(String)) }
|
|
48
|
+
attr_reader :pyproject_content
|
|
49
|
+
|
|
50
|
+
sig { returns(T::Hash[String, String]) }
|
|
51
|
+
def required_plugins
|
|
52
|
+
return {} unless pyproject_content
|
|
53
|
+
|
|
54
|
+
parsed = TomlRB.parse(pyproject_content)
|
|
55
|
+
plugins = parsed.dig("tool", "poetry", "requires-plugins")
|
|
56
|
+
return {} unless plugins.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
plugins.each_with_object({}) do |(name, constraint), result|
|
|
59
|
+
next unless name.is_a?(String) && constraint.is_a?(String)
|
|
60
|
+
next unless valid_plugin_name?(name)
|
|
61
|
+
next unless valid_constraint?(constraint)
|
|
62
|
+
|
|
63
|
+
result[name] = constraint
|
|
64
|
+
end
|
|
65
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
66
|
+
{}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
70
|
+
def valid_plugin_name?(name)
|
|
71
|
+
VALID_PLUGIN_NAME.match?(name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { params(constraint: String).returns(T::Boolean) }
|
|
75
|
+
def valid_constraint?(constraint)
|
|
76
|
+
VALID_CONSTRAINT.match?(constraint)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { params(name: String, constraint: String).void }
|
|
80
|
+
def install_plugin(name, constraint)
|
|
81
|
+
Dependabot.logger.info("Installing Poetry plugin: #{name}@#{constraint}")
|
|
82
|
+
|
|
83
|
+
escaped = Shellwords.shellescape("#{name}@#{constraint}")
|
|
84
|
+
SharedHelpers.run_shell_command(
|
|
85
|
+
"pyenv exec poetry self add #{escaped}",
|
|
86
|
+
fingerprint: "pyenv exec poetry self add <plugin_name>@<constraint>"
|
|
87
|
+
)
|
|
88
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
89
|
+
Dependabot.logger.warn(
|
|
90
|
+
"Failed to install Poetry plugin #{name}@#{constraint}: #{e.message}"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -9,6 +9,7 @@ require "sorbet-runtime"
|
|
|
9
9
|
require "dependabot/dependency"
|
|
10
10
|
require "dependabot/git_commit_checker"
|
|
11
11
|
require "dependabot/python/update_checker"
|
|
12
|
+
require "dependabot/update_checkers/cooldown_calculation"
|
|
12
13
|
require "dependabot/update_checkers/version_filters"
|
|
13
14
|
require "dependabot/registry_client"
|
|
14
15
|
require "dependabot/python/authed_url_builder"
|
|
@@ -110,8 +111,9 @@ module Dependabot
|
|
|
110
111
|
new_version = version_class.new(tag_version_str)
|
|
111
112
|
days = cooldown_days_for(current_version, new_version)
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
release_time = Time.at(release_date_to_seconds(tag_with_detail.release_date))
|
|
115
|
+
Dependabot::UpdateCheckers::CooldownCalculation
|
|
116
|
+
.within_cooldown_window?(release_time, days)
|
|
115
117
|
end
|
|
116
118
|
|
|
117
119
|
sig do
|
|
@@ -18,6 +18,7 @@ require "dependabot/python/requirement"
|
|
|
18
18
|
require "dependabot/python/native_helpers"
|
|
19
19
|
require "dependabot/python/authed_url_builder"
|
|
20
20
|
require "dependabot/python/name_normaliser"
|
|
21
|
+
require "dependabot/python/poetry_plugin_installer"
|
|
21
22
|
|
|
22
23
|
module Dependabot
|
|
23
24
|
module Python
|
|
@@ -90,6 +91,7 @@ module Dependabot
|
|
|
90
91
|
@original_reqs_resolvable = T.let(nil, T.nilable(T::Boolean))
|
|
91
92
|
@python_requirement_parser = T.let(nil, T.nilable(FileParser::PythonRequirementParser))
|
|
92
93
|
@language_version_manager = T.let(nil, T.nilable(LanguageVersionManager))
|
|
94
|
+
@poetry_plugin_installer = T.let(nil, T.nilable(PoetryPluginInstaller))
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
sig { params(requirement: T.nilable(String)).returns(T.nilable(Dependabot::Python::Version)) }
|
|
@@ -129,6 +131,9 @@ module Dependabot
|
|
|
129
131
|
|
|
130
132
|
language_version_manager.install_required_python
|
|
131
133
|
|
|
134
|
+
# Install any required Poetry plugins declared in pyproject.toml
|
|
135
|
+
poetry_plugin_installer.install_required_plugins
|
|
136
|
+
|
|
132
137
|
# use system git instead of the pure Python dulwich
|
|
133
138
|
run_poetry_command("pyenv exec poetry config system-git-client true")
|
|
134
139
|
|
|
@@ -385,6 +390,14 @@ module Dependabot
|
|
|
385
390
|
poetry_lock
|
|
386
391
|
end
|
|
387
392
|
|
|
393
|
+
sig { returns(PoetryPluginInstaller) }
|
|
394
|
+
def poetry_plugin_installer
|
|
395
|
+
@poetry_plugin_installer ||= T.let(
|
|
396
|
+
PoetryPluginInstaller.from_dependency_files(dependency_files),
|
|
397
|
+
T.nilable(PoetryPluginInstaller)
|
|
398
|
+
)
|
|
399
|
+
end
|
|
400
|
+
|
|
388
401
|
sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
|
|
389
402
|
def run_poetry_command(command, fingerprint: nil)
|
|
390
403
|
SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
|