dependabot-python 0.234.0 → 0.235.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a898b6d459367fc728deb010aed7f9adbba8064ffe506547b3076d8320024642
4
- data.tar.gz: 9fc76c80711472410d5919bab2ff8cca12ab161b7dfae2d23c83fe5afe3e30a2
3
+ metadata.gz: b267a681a1cb7f4fe9493bab088098d89b943ab77c074ee5dda1837f0a3901a2
4
+ data.tar.gz: f3fe927fe0afbec634e0ca2feb0ed313bfeda7a0c510457d00e864d409d277da
5
5
  SHA512:
6
- metadata.gz: 5c90405c02d5c636ee64d0ecd43b9fbe504b8f6b888d22f81806dddaca8799f590fcf7b46545d06584c3b45f68ffff58f13d0e2b0699441223419eb1e4d35bb9
7
- data.tar.gz: 7d10aa0fd61a0707b00bac29c8bd12ac533a49b37cfd1bb58a0830ad73eb50ad5767771889eb78236c175dc0e9cf400522395093894f715a6a80c90c523298f1
6
+ metadata.gz: a6e4ce8030018586812a2e87db6c7fcfacf8408982b7925a722ff503c8a6d4fd8352b9da3e6c8d527447a4931242a9146fe3a758294bd50c89675e4d77ac6769
7
+ data.tar.gz: 953aa95ad316e5856c55069bcebb1ec886c0482ed5bdc430ca4c616411e052cee928d6a0a9c8c3833a85b6e6ada8a91a0dc4ecccc4fca9458145d8ef67628561
@@ -105,6 +105,11 @@ def parse_requirements(directory):
105
105
 
106
106
  pattern = r"-[cr] (.*) \(line \d+\)"
107
107
  abs_path = re.search(pattern, install_req.comes_from).group(1)
108
+
109
+ # Ignore dependencies from remote constraint files
110
+ if not os.path.isfile(abs_path):
111
+ continue
112
+
108
113
  rel_path = os.path.relpath(abs_path, directory)
109
114
 
110
115
  requirement_packages.append({
@@ -6,6 +6,7 @@ require "toml-rb"
6
6
  require "dependabot/file_fetchers"
7
7
  require "dependabot/file_fetchers/base"
8
8
  require "dependabot/python/language_version_manager"
9
+ require "dependabot/python/pip_compile_file_matcher"
9
10
  require "dependabot/python/requirement_parser"
10
11
  require "dependabot/python/file_parser/pyproject_files_parser"
11
12
  require "dependabot/python/file_parser/python_requirement_parser"
@@ -75,7 +76,7 @@ module Dependabot
75
76
 
76
77
  fetched_files << setup_file if setup_file
77
78
  fetched_files << setup_cfg_file if setup_cfg_file
78
- fetched_files += path_setup_files
79
+ fetched_files += project_files
79
80
  fetched_files << pip_conf if pip_conf
80
81
  fetched_files << python_version_file if python_version_file
81
82
 
@@ -113,8 +114,7 @@ module Dependabot
113
114
  pipfile ||
114
115
  pyproject
115
116
 
116
- path = Pathname.new(File.join(directory, "requirements.txt"))
117
- .cleanpath.to_path
117
+ path = cleanpath(File.join(directory, "requirements.txt"))
118
118
  raise Dependabot::DependencyFileNotFound, path
119
119
  end
120
120
 
@@ -269,7 +269,7 @@ module Dependabot
269
269
 
270
270
  paths.flat_map do |path|
271
271
  path = File.join(current_dir, path) unless current_dir == "."
272
- path = Pathname.new(path).cleanpath.to_path
272
+ path = cleanpath(path)
273
273
 
274
274
  next if previously_fetched_files.map(&:name).include?(path)
275
275
  next if file.name == path
@@ -293,43 +293,47 @@ module Dependabot
293
293
 
294
294
  paths.map do |path|
295
295
  path = File.join(current_dir, path) unless current_dir == "."
296
- Pathname.new(path).cleanpath.to_path
296
+ cleanpath(path)
297
297
  end
298
298
  end.flatten.uniq
299
299
 
300
300
  constraints_paths.map { |path| fetch_file_from_host(path) }
301
301
  end
302
302
 
303
- def path_setup_files
304
- path_setup_files = []
305
- unfetchable_files = []
303
+ def project_files
304
+ project_files = []
305
+ unfetchable_deps = []
306
306
 
307
- path_setup_file_paths.each do |path|
308
- path_setup_files += fetch_path_setup_file(path)
307
+ path_dependencies.each do |dep|
308
+ path = dep[:path]
309
+ project_files += fetch_project_file(path)
309
310
  rescue Dependabot::DependencyFileNotFound => e
310
- unfetchable_files << e.file_path.gsub(%r{^/}, "")
311
+ unfetchable_deps << if sdist_or_wheel?(path)
312
+ e.file_path.gsub(%r{^/}, "")
313
+ else
314
+ "\"#{dep[:name]}\" at #{cleanpath(File.join(directory, dep[:file]))}"
315
+ end
311
316
  end
312
317
 
313
- poetry_path_setup_file_paths.each do |path|
314
- path_setup_files += fetch_path_setup_file(path, allow_pyproject: true)
318
+ poetry_path_dependencies.each do |path|
319
+ project_files += fetch_project_file(path)
315
320
  rescue Dependabot::DependencyFileNotFound => e
316
- unfetchable_files << e.file_path.gsub(%r{^/}, "")
321
+ unfetchable_deps << e.file_path.gsub(%r{^/}, "")
317
322
  end
318
323
 
319
- raise Dependabot::PathDependenciesNotReachable, unfetchable_files if unfetchable_files.any?
324
+ raise Dependabot::PathDependenciesNotReachable, unfetchable_deps if unfetchable_deps.any?
320
325
 
321
- path_setup_files
326
+ project_files
322
327
  end
323
328
 
324
- def fetch_path_setup_file(path, allow_pyproject: false)
325
- path_setup_files = []
329
+ def fetch_project_file(path)
330
+ project_files = []
331
+
332
+ path = cleanpath(File.join(path, "setup.py")) unless sdist_or_wheel?(path)
326
333
 
327
- unless path.end_with?(".tar.gz", ".whl", ".zip")
328
- path = Pathname.new(File.join(path, "setup.py")).cleanpath.to_path
329
- end
330
334
  return [] if path == "setup.py" && setup_file
331
335
 
332
- path_setup_files <<
336
+ project_files <<
333
337
  begin
334
338
  fetch_file_from_host(
335
339
  path,
@@ -337,19 +341,20 @@ module Dependabot
337
341
  ).tap { |f| f.support_file = true }
338
342
  rescue Dependabot::DependencyFileNotFound
339
343
  # For projects with pyproject.toml attempt to fetch a pyproject.toml
340
- # at the given path instead of a setup.py. We do not require a
341
- # setup.py to be present, so if none can be found, simply return
342
- return [] unless allow_pyproject
343
-
344
+ # at the given path instead of a setup.py.
344
345
  fetch_file_from_host(
345
346
  path.gsub("setup.py", "pyproject.toml"),
346
347
  fetch_submodules: true
347
348
  ).tap { |f| f.support_file = true }
348
349
  end
349
350
 
350
- return path_setup_files unless path.end_with?(".py")
351
+ return project_files unless path.end_with?(".py")
351
352
 
352
- path_setup_files + cfg_files_for_setup_py(path)
353
+ project_files + cfg_files_for_setup_py(path)
354
+ end
355
+
356
+ def sdist_or_wheel?(path)
357
+ path.end_with?(".tar.gz", ".whl", ".zip")
353
358
  end
354
359
 
355
360
  def cfg_files_for_setup_py(path)
@@ -378,60 +383,63 @@ module Dependabot
378
383
  end
379
384
  end
380
385
 
381
- def path_setup_file_paths
382
- requirement_txt_path_setup_file_paths +
383
- requirement_in_path_setup_file_paths +
384
- pipfile_path_setup_file_paths
386
+ def path_dependencies
387
+ requirement_txt_path_dependencies +
388
+ requirement_in_path_dependencies +
389
+ pipfile_path_dependencies
385
390
  end
386
391
 
387
- def requirement_txt_path_setup_file_paths
392
+ def requirement_txt_path_dependencies
388
393
  (requirements_txt_files + child_requirement_txt_files)
389
- .map { |req_file| parse_path_setup_paths(req_file) }
390
- .flatten.uniq
394
+ .map { |req_file| parse_requirement_path_dependencies(req_file) }
395
+ .flatten.uniq { |dep| dep[:path] }
391
396
  end
392
397
 
393
- def requirement_in_path_setup_file_paths
398
+ def requirement_in_path_dependencies
394
399
  requirements_in_files
395
- .map { |req_file| parse_path_setup_paths(req_file) }
396
- .flatten.uniq
400
+ .map { |req_file| parse_requirement_path_dependencies(req_file) }
401
+ .flatten.uniq { |dep| dep[:path] }
397
402
  end
398
403
 
399
- def parse_path_setup_paths(req_file)
404
+ def parse_requirement_path_dependencies(req_file)
405
+ # If this is a pip-compile lockfile, rely on whatever path dependencies we found in the main manifest
406
+ return [] if pip_compile_file_matcher.lockfile_for_pip_compile_file?(req_file)
407
+
400
408
  uneditable_reqs =
401
409
  req_file.content
402
- .scan(/^['"]?(?:file:)?(?<path>\..*?)(?=\[|#|'|"|$)/)
403
- .flatten
404
- .map(&:strip)
405
- .reject { |p| p.include?("://") }
410
+ .scan(/(?<name>^['"]?(?:file:)?(?<path>\..*?)(?=\[|#|'|"|$))/)
411
+ .filter_map do |n, p|
412
+ { name: n.strip, path: p.strip, file: req_file.name } unless p.include?("://")
413
+ end
406
414
 
407
415
  editable_reqs =
408
416
  req_file.content
409
- .scan(/^(?:-e)\s+['"]?(?:file:)?(?<path>.*?)(?=\[|#|'|"|$)/)
410
- .flatten
411
- .map(&:strip)
412
- .reject { |p| p.include?("://") || p.include?("git@") }
417
+ .scan(/(?<name>^(?:-e)\s+['"]?(?:file:)?(?<path>.*?)(?=\[|#|'|"|$))/)
418
+ .filter_map do |n, p|
419
+ { name: n.strip, path: p.strip, file: req_file.name } unless p.include?("://") || p.include?("git@")
420
+ end
413
421
 
414
422
  uneditable_reqs + editable_reqs
415
423
  end
416
424
 
417
- def pipfile_path_setup_file_paths
425
+ def pipfile_path_dependencies
418
426
  return [] unless pipfile
419
427
 
420
- paths = []
428
+ deps = []
421
429
  DEPENDENCY_TYPES.each do |dep_type|
422
430
  next unless parsed_pipfile[dep_type]
423
431
 
424
432
  parsed_pipfile[dep_type].each do |_, req|
425
433
  next unless req.is_a?(Hash) && req["path"]
426
434
 
427
- paths << req["path"]
435
+ deps << { name: req["path"], path: req["path"], file: pipfile.name }
428
436
  end
429
437
  end
430
438
 
431
- paths
439
+ deps
432
440
  end
433
441
 
434
- def poetry_path_setup_file_paths
442
+ def poetry_path_dependencies
435
443
  return [] unless pyproject
436
444
 
437
445
  paths = []
@@ -447,6 +455,14 @@ module Dependabot
447
455
 
448
456
  paths
449
457
  end
458
+
459
+ def cleanpath(path)
460
+ Pathname.new(path).cleanpath.to_path
461
+ end
462
+
463
+ def pip_compile_file_matcher
464
+ @pip_compile_file_matcher ||= PipCompileFileMatcher.new(requirements_in_files)
465
+ end
450
466
  end
451
467
  end
452
468
  end
@@ -8,7 +8,6 @@ require "dependabot/file_parsers/base/dependency_set"
8
8
  require "dependabot/python/file_parser"
9
9
  require "dependabot/python/requirement"
10
10
  require "dependabot/errors"
11
- require "dependabot/python/helpers"
12
11
  require "dependabot/python/name_normaliser"
13
12
 
14
13
  module Dependabot
@@ -28,7 +27,7 @@ module Dependabot
28
27
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
29
28
 
30
29
  dependency_set += pyproject_dependencies if using_poetry? || using_pep621?
31
- dependency_set += lockfile_dependencies if lockfile
30
+ dependency_set += lockfile_dependencies if using_poetry? && lockfile
32
31
 
33
32
  dependency_set
34
33
  end
@@ -39,6 +38,16 @@ module Dependabot
39
38
 
40
39
  def pyproject_dependencies
41
40
  if using_poetry?
41
+ missing_keys = missing_poetry_keys
42
+
43
+ if missing_keys.any?
44
+ raise DependencyFileNotParseable.new(
45
+ pyproject.path,
46
+ "#{pyproject.path} is missing the following sections:\n" \
47
+ " * #{missing_keys.map { |key| "tool.poetry.#{key}" }.join("\n * ")}\n"
48
+ )
49
+ end
50
+
42
51
  poetry_dependencies
43
52
  else
44
53
  pep621_dependencies
@@ -53,11 +62,11 @@ module Dependabot
53
62
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
54
63
 
55
64
  POETRY_DEPENDENCY_TYPES.each do |type|
56
- deps_hash = parsed_pyproject.dig("tool", "poetry", type) || {}
65
+ deps_hash = poetry_root[type] || {}
57
66
  dependencies += parse_poetry_dependency_group(type, deps_hash)
58
67
  end
59
68
 
60
- groups = parsed_pyproject.dig("tool", "poetry", "group") || {}
69
+ groups = poetry_root["group"] || {}
61
70
  groups.each do |group, group_spec|
62
71
  dependencies += parse_poetry_dependency_group(group, group_spec["dependencies"])
63
72
  end
@@ -138,7 +147,11 @@ module Dependabot
138
147
  end
139
148
 
140
149
  def using_poetry?
141
- !parsed_pyproject.dig("tool", "poetry").nil?
150
+ !poetry_root.nil?
151
+ end
152
+
153
+ def missing_poetry_keys
154
+ %w(name version description authors).reject { |key| poetry_root.key?(key) }
142
155
  end
143
156
 
144
157
  def using_pep621?
@@ -146,6 +159,10 @@ module Dependabot
146
159
  !parsed_pyproject.dig("project", "optional-dependencies").nil?
147
160
  end
148
161
 
162
+ def poetry_root
163
+ parsed_pyproject.dig("tool", "poetry")
164
+ end
165
+
149
166
  def using_pdm?
150
167
  using_pep621? && pdm_lock
151
168
  end
@@ -187,7 +204,7 @@ module Dependabot
187
204
  File.write(lockfile.name, lockfile.content)
188
205
 
189
206
  begin
190
- output = Helpers.run_poetry_command("pyenv exec poetry show --only main")
207
+ output = SharedHelpers.run_shell_command("pyenv exec poetry show --only main")
191
208
 
192
209
  output.split("\n").map { |line| line.split.first }
193
210
  rescue SharedHelpers::HelperSubprocessFailed
@@ -103,21 +103,7 @@ module Dependabot
103
103
  end
104
104
 
105
105
  def run_command(command, env: {})
106
- start = Time.now
107
- command = SharedHelpers.escape_command(command)
108
- stdout, process = Open3.capture2e(env, command)
109
- time_taken = Time.now - start
110
-
111
- return stdout if process.success?
112
-
113
- raise SharedHelpers::HelperSubprocessFailed.new(
114
- message: stdout,
115
- error_context: {
116
- command: command,
117
- time_taken: time_taken,
118
- process_exit_value: process.to_s
119
- }
120
- )
106
+ SharedHelpers.run_shell_command(command, env: env, stderr_to_stdout: true)
121
107
  end
122
108
 
123
109
  def requirement_class
@@ -10,6 +10,7 @@ require "dependabot/python/requirement"
10
10
  require "dependabot/errors"
11
11
  require "dependabot/python/native_helpers"
12
12
  require "dependabot/python/name_normaliser"
13
+ require "dependabot/python/pip_compile_file_matcher"
13
14
 
14
15
  module Dependabot
15
16
  module Python
@@ -74,21 +75,29 @@ module Dependabot
74
75
  # probably blocked. Ignore it.
75
76
  next if blocking_marker?(dep)
76
77
 
78
+ name = dep["name"]
79
+ file = dep["file"]
80
+ version = dep["version"]
81
+ original_file = get_original_file(file)
82
+
77
83
  requirements =
78
- if lockfile_for_pip_compile_file?(dep["file"]) then []
84
+ if original_file && pip_compile_file_matcher.lockfile_for_pip_compile_file?(original_file) then []
79
85
  else
80
86
  [{
81
87
  requirement: dep["requirement"],
82
- file: Pathname.new(dep["file"]).cleanpath.to_path,
88
+ file: Pathname.new(file).cleanpath.to_path,
83
89
  source: nil,
84
- groups: group_from_filename(dep["file"])
90
+ groups: group_from_filename(file)
85
91
  }]
86
92
  end
87
93
 
94
+ # PyYAML < 6.0 will cause `pip-compile` to fail due to incompatiblity with Cython 3. Workaround it.
95
+ SharedHelpers.run_shell_command("pyenv exec pip install cython<3.0") if old_pyyaml?(name, version)
96
+
88
97
  dependencies <<
89
98
  Dependency.new(
90
- name: normalised_name(dep["name"], dep["extras"]),
91
- version: dep["version"]&.include?("*") ? nil : dep["version"],
99
+ name: normalised_name(name, dep["extras"]),
100
+ version: version&.include?("*") ? nil : version,
92
101
  requirements: requirements,
93
102
  package_manager: "pip"
94
103
  )
@@ -96,6 +105,13 @@ module Dependabot
96
105
  dependencies
97
106
  end
98
107
 
108
+ def old_pyyaml?(name, version)
109
+ major_version = version&.split(".")&.first
110
+ return false unless major_version
111
+
112
+ name == "pyyaml" && major_version < "6"
113
+ end
114
+
99
115
  def group_from_filename(filename)
100
116
  if filename.include?("dev") then ["dev-dependencies"]
101
117
  else
@@ -118,17 +134,6 @@ module Dependabot
118
134
  .dependency_set
119
135
  end
120
136
 
121
- def lockfile_for_pip_compile_file?(filename)
122
- return false unless pip_compile_files.any?
123
- return false unless filename.end_with?(".txt")
124
-
125
- file = dependency_files.find { |f| f.name == filename }
126
- return true if file&.content&.match?(output_file_regex(filename))
127
-
128
- basename = filename.gsub(/\.txt$/, "")
129
- pip_compile_files.any? { |f| f.name == basename + ".in" }
130
- end
131
-
132
137
  def parsed_requirement_files
133
138
  SharedHelpers.in_a_temporary_directory do
134
139
  write_temporary_dependency_files
@@ -201,10 +206,6 @@ module Dependabot
201
206
  @pipfile_lock ||= get_original_file("Pipfile.lock")
202
207
  end
203
208
 
204
- def output_file_regex(filename)
205
- "--output-file[=\s]+#{Regexp.escape(filename)}(?:\s|$)"
206
- end
207
-
208
209
  def pyproject
209
210
  @pyproject ||= get_original_file("pyproject.toml")
210
211
  end
@@ -225,6 +226,10 @@ module Dependabot
225
226
  @pip_compile_files ||=
226
227
  dependency_files.select { |f| f.name.end_with?(".in") }
227
228
  end
229
+
230
+ def pip_compile_file_matcher
231
+ @pip_compile_file_matcher ||= PipCompileFileMatcher.new(pip_compile_files)
232
+ end
228
233
  end
229
234
  end
230
235
  end
@@ -27,6 +27,8 @@ module Dependabot
27
27
  WARNINGS = /\s*# WARNING:.*\Z/m
28
28
  UNSAFE_NOTE = /\s*# The following packages are considered to be unsafe.*\Z/m
29
29
  RESOLVER_REGEX = /(?<=--resolver=)(\w+)/
30
+ NATIVE_COMPILATION_ERROR =
31
+ "pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1"
30
32
 
31
33
  attr_reader :dependencies, :dependency_files, :credentials
32
34
 
@@ -34,6 +36,7 @@ module Dependabot
34
36
  @dependencies = dependencies
35
37
  @dependency_files = dependency_files
36
38
  @credentials = credentials
39
+ @build_isolation = true
37
40
  end
38
41
 
39
42
  def updated_dependency_files
@@ -67,28 +70,7 @@ module Dependabot
67
70
  language_version_manager.install_required_python
68
71
 
69
72
  filenames_to_compile.each do |filename|
70
- # Shell out to pip-compile, generate a new set of requirements.
71
- # This is slow, as pip-compile needs to do installs.
72
- options = pip_compile_options(filename)
73
- options_fingerprint = pip_compile_options_fingerprint(options)
74
-
75
- name_part = "pyenv exec pip-compile " \
76
- "#{options} -P " \
77
- "#{dependency.name}"
78
- fingerprint_name_part = "pyenv exec pip-compile " \
79
- "#{options_fingerprint} -P " \
80
- "<dependency_name>"
81
-
82
- version_part = "#{dependency.version} #{filename}"
83
- fingerprint_version_part = "<dependency_version> <filename>"
84
-
85
- # Don't escape pyenv `dep-name==version` syntax
86
- run_pip_compile_command(
87
- "#{SharedHelpers.escape_command(name_part)}==" \
88
- "#{SharedHelpers.escape_command(version_part)}",
89
- allow_unsafe_shell_command: true,
90
- fingerprint: "#{fingerprint_name_part}==#{fingerprint_version_part}"
91
- )
73
+ compile_file(filename)
92
74
  end
93
75
 
94
76
  # Remove any .python-version file before parsing the reqs
@@ -108,6 +90,44 @@ module Dependabot
108
90
  end
109
91
  end
110
92
 
93
+ def compile_file(filename)
94
+ # Shell out to pip-compile, generate a new set of requirements.
95
+ # This is slow, as pip-compile needs to do installs.
96
+ options = pip_compile_options(filename)
97
+ options_fingerprint = pip_compile_options_fingerprint(options)
98
+
99
+ name_part = "pyenv exec pip-compile " \
100
+ "#{options} -P " \
101
+ "#{dependency.name}"
102
+ fingerprint_name_part = "pyenv exec pip-compile " \
103
+ "#{options_fingerprint} -P " \
104
+ "<dependency_name>"
105
+
106
+ version_part = "#{dependency.version} #{filename}"
107
+ fingerprint_version_part = "<dependency_version> <filename>"
108
+
109
+ # Don't escape pyenv `dep-name==version` syntax
110
+ run_pip_compile_command(
111
+ "#{SharedHelpers.escape_command(name_part)}==" \
112
+ "#{SharedHelpers.escape_command(version_part)}",
113
+ allow_unsafe_shell_command: true,
114
+ fingerprint: "#{fingerprint_name_part}==#{fingerprint_version_part}"
115
+ )
116
+ rescue SharedHelpers::HelperSubprocessFailed => e
117
+ retry_count ||= 0
118
+ retry_count += 1
119
+ if compilation_error?(e) && retry_count <= 1
120
+ @build_isolation = false
121
+ retry
122
+ end
123
+
124
+ raise
125
+ end
126
+
127
+ def compilation_error?(error)
128
+ error.message.include?(NATIVE_COMPILATION_ERROR)
129
+ end
130
+
111
131
  def update_manifest_files
112
132
  dependency_files.filter_map do |file|
113
133
  next unless file.name.end_with?(".in")
@@ -146,30 +166,21 @@ module Dependabot
146
166
  end
147
167
 
148
168
  def run_command(cmd, env: python_env, allow_unsafe_shell_command: false, fingerprint:)
149
- start = Time.now
150
- command = if allow_unsafe_shell_command
151
- cmd
152
- else
153
- SharedHelpers.escape_command(cmd)
154
- end
155
- stdout, process = Open3.capture2e(env, command)
156
- time_taken = Time.now - start
157
-
158
- return stdout if process.success?
169
+ SharedHelpers.run_shell_command(
170
+ cmd,
171
+ env: env,
172
+ allow_unsafe_shell_command: allow_unsafe_shell_command,
173
+ fingerprint: fingerprint,
174
+ stderr_to_stdout: true
175
+ )
176
+ rescue SharedHelpers::HelperSubprocessFailed => e
177
+ stdout = e.message
159
178
 
160
179
  if stdout.match?(INCOMPATIBLE_VERSIONS_REGEX)
161
180
  raise DependencyFileNotResolvable, stdout.match(INCOMPATIBLE_VERSIONS_REGEX)
162
181
  end
163
182
 
164
- raise SharedHelpers::HelperSubprocessFailed.new(
165
- message: stdout,
166
- error_context: {
167
- command: command,
168
- fingerprint: fingerprint,
169
- time_taken: time_taken,
170
- process_exit_value: process.to_s
171
- }
172
- )
183
+ raise
173
184
  end
174
185
 
175
186
  def run_pip_compile_command(command, allow_unsafe_shell_command: false, fingerprint:)
@@ -412,7 +423,7 @@ module Dependabot
412
423
  end
413
424
 
414
425
  def pip_compile_options(filename)
415
- options = ["--build-isolation"]
426
+ options = @build_isolation ? ["--build-isolation"] : ["--no-build-isolation"]
416
427
  options += pip_compile_index_options
417
428
 
418
429
  if (requirements_file = compiled_file_for_filename(filename))
@@ -245,28 +245,16 @@ module Dependabot
245
245
  File.write("dev-req.txt", dev_req_content)
246
246
  end
247
247
 
248
- def run_command(command, env: {})
249
- start = Time.now
250
- command = SharedHelpers.escape_command(command)
251
- stdout, _, process = Open3.capture3(env, command)
252
- time_taken = Time.now - start
253
-
254
- # Raise an error with the output from the shell session if Pipenv
255
- # returns a non-zero status
256
- return stdout if process.success?
257
-
258
- raise SharedHelpers::HelperSubprocessFailed.new(
259
- message: stdout,
260
- error_context: {
261
- command: command,
262
- time_taken: time_taken,
263
- process_exit_value: process.to_s
264
- }
265
- )
248
+ def run_command(command, env: {}, fingerprint: nil)
249
+ SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint)
266
250
  end
267
251
 
268
252
  def run_pipenv_command(command, env: pipenv_env_variables)
269
- run_command("pyenv local #{language_version_manager.python_major_minor}")
253
+ run_command(
254
+ "pyenv local #{language_version_manager.python_major_minor}",
255
+ fingerprint: "pyenv local <python_major_minor>"
256
+ )
257
+
270
258
  run_command(command, env: env)
271
259
  end
272
260
 
@@ -10,7 +10,6 @@ require "dependabot/python/version"
10
10
  require "dependabot/python/requirement"
11
11
  require "dependabot/python/file_parser/python_requirement_parser"
12
12
  require "dependabot/python/file_updater"
13
- require "dependabot/python/helpers"
14
13
  require "dependabot/python/native_helpers"
15
14
  require "dependabot/python/name_normaliser"
16
15
 
@@ -61,34 +60,37 @@ module Dependabot
61
60
  end
62
61
 
63
62
  def updated_pyproject_content
64
- dependencies
65
- .select { |dep| requirement_changed?(pyproject, dep) }
66
- .reduce(pyproject.content.dup) do |content, dep|
67
- updated_requirement =
68
- dep.requirements.find { |r| r[:file] == pyproject.name }
69
- .fetch(:requirement)
70
-
71
- old_req =
72
- dep.previous_requirements
73
- .find { |r| r[:file] == pyproject.name }
74
- .fetch(:requirement)
75
-
76
- declaration_regex = declaration_regex(dep)
77
- updated_content = if content.match?(declaration_regex)
78
- content.gsub(declaration_regex(dep)) do |match|
79
- match.gsub(old_req, updated_requirement)
80
- end
81
- else
82
- content.gsub(table_declaration_regex(dep)) do |match|
83
- match.gsub(/(\s*version\s*=\s*["'])#{Regexp.escape(old_req)}/,
84
- '\1' + updated_requirement)
85
- end
86
- end
87
-
88
- raise "Content did not change!" if content == updated_content
89
-
90
- updated_content
63
+ content = pyproject.content
64
+ return content unless requirement_changed?(pyproject, dependency)
65
+
66
+ updated_content = content.dup
67
+
68
+ dependency.requirements.zip(dependency.previous_requirements).each do |new_r, old_r|
69
+ next unless new_r[:file] == pyproject.name && old_r[:file] == pyproject.name
70
+
71
+ updated_content = replace_dep(dependency, updated_content, new_r, old_r)
72
+ end
73
+
74
+ raise "Content did not change!" if content == updated_content
75
+
76
+ updated_content
77
+ end
78
+
79
+ def replace_dep(dep, content, new_r, old_r)
80
+ new_req = new_r[:requirement]
81
+ old_req = old_r[:requirement]
82
+
83
+ declaration_regex = declaration_regex(dep, old_r)
84
+ if content.match?(declaration_regex)
85
+ content.gsub(declaration_regex) do |match|
86
+ match.gsub(old_req, new_req)
91
87
  end
88
+ else
89
+ content.gsub(table_declaration_regex(dep, new_r)) do |match|
90
+ match.gsub(/(\s*version\s*=\s*["'])#{Regexp.escape(old_req)}/,
91
+ '\1' + new_req)
92
+ end
93
+ end
92
94
  end
93
95
 
94
96
  def updated_lockfile_content
@@ -204,7 +206,7 @@ module Dependabot
204
206
  end
205
207
 
206
208
  def run_poetry_command(command, fingerprint: nil)
207
- Helpers.run_poetry_command(command, fingerprint: fingerprint)
209
+ SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
208
210
  end
209
211
 
210
212
  def write_temporary_dependency_files(pyproject_content)
@@ -241,12 +243,12 @@ module Dependabot
241
243
  end
242
244
  end
243
245
 
244
- def declaration_regex(dep)
245
- /(?:^\s*|["'])#{escape(dep)}["']?\s*=.*$/i
246
+ def declaration_regex(dep, old_req)
247
+ /#{old_req[:groups].first}(?:\.dependencies)?\]\s*\n.*?(?:^\s*|["'])#{escape(dep)}["']?\s*=.*$/mi
246
248
  end
247
249
 
248
- def table_declaration_regex(dep)
249
- /tool\.poetry\.[^\n]+\.#{escape(dep)}\]\n.*?\s*version\s* =.*?\n/m
250
+ def table_declaration_regex(dep, old_req)
251
+ /tool\.poetry\.#{old_req[:groups].first}\.#{escape(dep)}\]\n.*?\s*version\s* =.*?\n/m
250
252
  end
251
253
 
252
254
  def escape(dep)
@@ -48,10 +48,11 @@ module Dependabot
48
48
  end
49
49
 
50
50
  def updated_requirement_or_setup_file_content(new_req, old_req)
51
- content = get_original_file(new_req.fetch(:file)).content
51
+ original_file = get_original_file(new_req.fetch(:file))
52
+ raise "Could not find a dependency file for #{new_req}" unless original_file
52
53
 
53
54
  RequirementReplacer.new(
54
- content: content,
55
+ content: original_file.content,
55
56
  dependency_name: dependency.name,
56
57
  old_requirement: old_req.fetch(:requirement),
57
58
  new_requirement: new_req.fetch(:requirement),
@@ -0,0 +1,32 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module Python
6
+ class PipCompileFileMatcher
7
+ def initialize(requirements_in_files)
8
+ @requirements_in_files = requirements_in_files
9
+ end
10
+
11
+ def lockfile_for_pip_compile_file?(file)
12
+ return false unless requirements_in_files.any?
13
+
14
+ name = file.name
15
+ return false unless name.end_with?(".txt")
16
+
17
+ return true if file.content.match?(output_file_regex(name))
18
+
19
+ basename = name.gsub(/\.txt$/, "")
20
+ requirements_in_files.any? { |f| f.name == basename + ".in" }
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :requirements_in_files
26
+
27
+ def output_file_regex(filename)
28
+ "--output-file[=\s]+#{Regexp.escape(filename)}(?:\s|$)"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -9,7 +9,7 @@ module Dependabot
9
9
  COMPARISON = /===|==|>=|<=|<|>|~=|!=/
10
10
  VERSION = /([1-9][0-9]*!)?[0-9]+[a-zA-Z0-9\-_.*]*(\+[0-9a-zA-Z]+(\.[0-9a-zA-Z]+)*)?/
11
11
 
12
- REQUIREMENT = /(?<comparison>#{COMPARISON})\s*\\?\s*(?<version>#{VERSION})/
12
+ REQUIREMENT = /(?<comparison>#{COMPARISON})\s*\\?\s*v?(?<version>#{VERSION})/
13
13
  HASH = /--hash=(?<algorithm>.*?):(?<hash>.*?)(?=\s|\\|$)/
14
14
  REQUIREMENTS = /#{REQUIREMENT}(\s*,\s*\\?\s*#{REQUIREMENT})*/
15
15
  HASHES = /#{HASH}(\s*\\?\s*#{HASH})*/
@@ -27,7 +27,7 @@ module Dependabot
27
27
  GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none --quiet (?<url>[^\s]+).* /
28
28
  GIT_REFERENCE_NOT_FOUND_REGEX = /Did not find branch or tag '(?<tag>[^\n"]+)'/m
29
29
  NATIVE_COMPILATION_ERROR =
30
- "pip._internal.exceptions.InstallationSubprocessError: Command errored out with exit status 1:"
30
+ "pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1"
31
31
  # See https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata
32
32
  PYTHON_PACKAGE_NAME_REGEX = /[A-Za-z0-9_\-]+/
33
33
  RESOLUTION_IMPOSSIBLE_ERROR = "ResolutionImpossible"
@@ -43,17 +43,21 @@ module Dependabot
43
43
  end
44
44
 
45
45
  def latest_resolvable_version(requirement: nil)
46
+ @latest_resolvable_version_string ||= {}
47
+ return @latest_resolvable_version_string[requirement] if @latest_resolvable_version_string.key?(requirement)
48
+
46
49
  version_string =
47
50
  fetch_latest_resolvable_version_string(requirement: requirement)
48
51
 
49
- version_string.nil? ? nil : Python::Version.new(version_string)
52
+ @latest_resolvable_version_string[requirement] ||=
53
+ version_string.nil? ? nil : Python::Version.new(version_string)
50
54
  end
51
55
 
52
56
  def resolvable?(version:)
53
57
  @resolvable ||= {}
54
58
  return @resolvable[version] if @resolvable.key?(version)
55
59
 
56
- @resolvable[version] = if fetch_latest_resolvable_version_string(requirement: "==#{version}")
60
+ @resolvable[version] = if latest_resolvable_version(requirement: "==#{version}")
57
61
  true
58
62
  else
59
63
  false
@@ -63,57 +67,59 @@ module Dependabot
63
67
  private
64
68
 
65
69
  def fetch_latest_resolvable_version_string(requirement:)
66
- @latest_resolvable_version_string ||= {}
67
- return @latest_resolvable_version_string[requirement] if @latest_resolvable_version_string.key?(requirement)
70
+ SharedHelpers.in_a_temporary_directory do
71
+ SharedHelpers.with_git_configured(credentials: credentials) do
72
+ write_temporary_dependency_files(updated_req: requirement)
73
+ language_version_manager.install_required_python
68
74
 
69
- @latest_resolvable_version_string[requirement] ||=
70
- SharedHelpers.in_a_temporary_directory do
71
- SharedHelpers.with_git_configured(credentials: credentials) do
72
- write_temporary_dependency_files(updated_req: requirement)
73
- language_version_manager.install_required_python
74
-
75
- filenames_to_compile.each do |filename|
76
- # Shell out to pip-compile.
77
- # This is slow, as pip-compile needs to do installs.
78
- options = pip_compile_options(filename)
79
- options_fingerprint = pip_compile_options_fingerprint(options)
80
-
81
- run_pip_compile_command(
82
- "pyenv exec pip-compile -v #{options} -P #{dependency.name} #{filename}",
83
- fingerprint: "pyenv exec pip-compile -v #{options_fingerprint} -P <dependency_name> <filename>"
84
- )
85
-
86
- next if dependency.top_level?
87
-
88
- # Run pip-compile a second time for transient dependencies
89
- # to make sure we do not update dependencies that are
90
- # superfluous. pip-compile does not detect these when
91
- # updating a specific dependency with the -P option.
92
- # Running pip-compile a second time will automatically remove
93
- # superfluous dependencies. Dependabot then marks those with
94
- # update_not_possible.
95
- write_original_manifest_files
96
- run_pip_compile_command(
97
- "pyenv exec pip-compile #{options} #{filename}",
98
- fingerprint: "pyenv exec pip-compile #{options_fingerprint} <filename>"
99
- )
100
- end
101
-
102
- # Remove any .python-version file before parsing the reqs
103
- FileUtils.remove_entry(".python-version", true)
104
-
105
- parse_updated_files
106
- end
107
- rescue SharedHelpers::HelperSubprocessFailed => e
108
- retry_count ||= 0
109
- retry_count += 1
110
- if compilation_error?(e) && retry_count <= 1
111
- @build_isolation = false
112
- retry
75
+ filenames_to_compile.each do |filename|
76
+ return nil unless compile_file(filename)
113
77
  end
114
78
 
115
- handle_pip_compile_errors(e)
79
+ # Remove any .python-version file before parsing the reqs
80
+ FileUtils.remove_entry(".python-version", true)
81
+
82
+ parse_updated_files
116
83
  end
84
+ end
85
+ end
86
+
87
+ def compile_file(filename)
88
+ # Shell out to pip-compile.
89
+ # This is slow, as pip-compile needs to do installs.
90
+ options = pip_compile_options(filename)
91
+ options_fingerprint = pip_compile_options_fingerprint(options)
92
+
93
+ run_pip_compile_command(
94
+ "pyenv exec pip-compile -v #{options} -P #{dependency.name} #{filename}",
95
+ fingerprint: "pyenv exec pip-compile -v #{options_fingerprint} -P <dependency_name> <filename>"
96
+ )
97
+
98
+ return true if dependency.top_level?
99
+
100
+ # Run pip-compile a second time for transient dependencies
101
+ # to make sure we do not update dependencies that are
102
+ # superfluous. pip-compile does not detect these when
103
+ # updating a specific dependency with the -P option.
104
+ # Running pip-compile a second time will automatically remove
105
+ # superfluous dependencies. Dependabot then marks those with
106
+ # update_not_possible.
107
+ write_original_manifest_files
108
+ run_pip_compile_command(
109
+ "pyenv exec pip-compile #{options} #{filename}",
110
+ fingerprint: "pyenv exec pip-compile #{options_fingerprint} <filename>"
111
+ )
112
+
113
+ true
114
+ rescue SharedHelpers::HelperSubprocessFailed => e
115
+ retry_count ||= 0
116
+ retry_count += 1
117
+ if compilation_error?(e) && retry_count <= 1
118
+ @build_isolation = false
119
+ retry
120
+ end
121
+
122
+ handle_pip_compile_errors(e.message)
117
123
  end
118
124
 
119
125
  def compilation_error?(error)
@@ -122,8 +128,8 @@ module Dependabot
122
128
 
123
129
  # rubocop:disable Metrics/AbcSize
124
130
  # rubocop:disable Metrics/PerceivedComplexity
125
- def handle_pip_compile_errors(error)
126
- if error.message.include?(RESOLUTION_IMPOSSIBLE_ERROR)
131
+ def handle_pip_compile_errors(message)
132
+ if message.include?(RESOLUTION_IMPOSSIBLE_ERROR)
127
133
  check_original_requirements_resolvable
128
134
  # If the original requirements are resolvable but we get an
129
135
  # incompatibility error after unlocking then it's likely to be
@@ -131,14 +137,14 @@ module Dependabot
131
137
  return nil
132
138
  end
133
139
 
134
- if error.message.include?("UnsupportedConstraint")
140
+ if message.include?("UnsupportedConstraint")
135
141
  # If there's an unsupported constraint, check if it existed
136
142
  # previously (and raise if it did)
137
143
  check_original_requirements_resolvable
138
144
  end
139
145
 
140
- if (error.message.include?('Command "python setup.py egg_info') ||
141
- error.message.include?(
146
+ if (message.include?('Command "python setup.py egg_info') ||
147
+ message.include?(
142
148
  "exit status 1: python setup.py egg_info"
143
149
  )) &&
144
150
  check_original_requirements_resolvable
@@ -147,16 +153,16 @@ module Dependabot
147
153
  return
148
154
  end
149
155
 
150
- if error.message.include?(RESOLUTION_IMPOSSIBLE_ERROR) &&
151
- !error.message.match?(/#{Regexp.quote(dependency.name)}/i)
156
+ if message.include?(RESOLUTION_IMPOSSIBLE_ERROR) &&
157
+ !message.match?(/#{Regexp.quote(dependency.name)}/i)
152
158
  # Sometimes pip-tools gets confused and can't work around
153
159
  # sub-dependency incompatibilities. Ignore those cases.
154
160
  return nil
155
161
  end
156
162
 
157
- if error.message.match?(GIT_REFERENCE_NOT_FOUND_REGEX)
158
- tag = error.message.match(GIT_REFERENCE_NOT_FOUND_REGEX).named_captures.fetch("tag")
159
- constraints_section = error.message.split("Finding the best candidates:").first
163
+ if message.match?(GIT_REFERENCE_NOT_FOUND_REGEX)
164
+ tag = message.match(GIT_REFERENCE_NOT_FOUND_REGEX).named_captures.fetch("tag")
165
+ constraints_section = message.split("Finding the best candidates:").first
160
166
  egg_regex = /#{Regexp.escape(tag)}#egg=(#{PYTHON_PACKAGE_NAME_REGEX})/
161
167
  name_match = constraints_section.scan(egg_regex)
162
168
 
@@ -166,15 +172,15 @@ module Dependabot
166
172
  raise GitDependencyReferenceNotFound, "(unknown package at #{tag})"
167
173
  end
168
174
 
169
- if error.message.match?(GIT_DEPENDENCY_UNREACHABLE_REGEX)
170
- url = error.message.match(GIT_DEPENDENCY_UNREACHABLE_REGEX)
171
- .named_captures.fetch("url")
175
+ if message.match?(GIT_DEPENDENCY_UNREACHABLE_REGEX)
176
+ url = message.match(GIT_DEPENDENCY_UNREACHABLE_REGEX)
177
+ .named_captures.fetch("url")
172
178
  raise GitDependenciesNotReachable, url
173
179
  end
174
180
 
175
- raise Dependabot::OutOfDisk if error.message.end_with?("[Errno 28] No space left on device")
181
+ raise Dependabot::OutOfDisk if message.end_with?("[Errno 28] No space left on device")
176
182
 
177
- raise Dependabot::OutOfMemory if error.message.end_with?("MemoryError")
183
+ raise Dependabot::OutOfMemory if message.end_with?("MemoryError")
178
184
 
179
185
  raise
180
186
  end
@@ -217,22 +223,7 @@ module Dependabot
217
223
  end
218
224
 
219
225
  def run_command(command, env: python_env, fingerprint:)
220
- start = Time.now
221
- command = SharedHelpers.escape_command(command)
222
- stdout, process = Open3.capture2e(env, command)
223
- time_taken = Time.now - start
224
-
225
- return stdout if process.success?
226
-
227
- raise SharedHelpers::HelperSubprocessFailed.new(
228
- message: stdout,
229
- error_context: {
230
- command: command,
231
- fingerprint: fingerprint,
232
- time_taken: time_taken,
233
- process_exit_value: process.to_s
234
- }
235
- )
226
+ SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint, stderr_to_stdout: true)
236
227
  end
237
228
 
238
229
  def pip_compile_options_fingerprint(options)
@@ -381,26 +381,16 @@ module Dependabot
381
381
  .replace_sources(credentials)
382
382
  end
383
383
 
384
- def run_command(command, env: {})
385
- start = Time.now
386
- command = SharedHelpers.escape_command(command)
387
- stdout, process = Open3.capture2e(env, command)
388
- time_taken = Time.now - start
389
-
390
- return stdout if process.success?
391
-
392
- raise SharedHelpers::HelperSubprocessFailed.new(
393
- message: stdout,
394
- error_context: {
395
- command: command,
396
- time_taken: time_taken,
397
- process_exit_value: process.to_s
398
- }
399
- )
384
+ def run_command(command, env: {}, fingerprint: nil)
385
+ SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint, stderr_to_stdout: true)
400
386
  end
401
387
 
402
388
  def run_pipenv_command(command, env: pipenv_env_variables)
403
- run_command("pyenv local #{language_version_manager.python_major_minor}")
389
+ run_command(
390
+ "pyenv local #{language_version_manager.python_major_minor}",
391
+ fingerprint: "pyenv local <python_major_minor>"
392
+ )
393
+
404
394
  run_command(command, env: env)
405
395
  end
406
396
 
@@ -14,7 +14,6 @@ require "dependabot/python/file_updater/pyproject_preparer"
14
14
  require "dependabot/python/update_checker"
15
15
  require "dependabot/python/version"
16
16
  require "dependabot/python/requirement"
17
- require "dependabot/python/helpers"
18
17
  require "dependabot/python/native_helpers"
19
18
  require "dependabot/python/authed_url_builder"
20
19
  require "dependabot/python/name_normaliser"
@@ -309,7 +308,7 @@ module Dependabot
309
308
  end
310
309
 
311
310
  def run_poetry_command(command, fingerprint: nil)
312
- Helpers.run_poetry_command(command, fingerprint: fingerprint)
311
+ SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
313
312
  end
314
313
 
315
314
  def normalise(name)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-python
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.234.0
4
+ version: 0.235.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-12 00:00:00.000000000 Z
11
+ date: 2023-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.234.0
19
+ version: 0.235.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.234.0
26
+ version: 0.235.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -224,11 +224,11 @@ files:
224
224
  - lib/dependabot/python/file_updater/requirement_file_updater.rb
225
225
  - lib/dependabot/python/file_updater/requirement_replacer.rb
226
226
  - lib/dependabot/python/file_updater/setup_file_sanitizer.rb
227
- - lib/dependabot/python/helpers.rb
228
227
  - lib/dependabot/python/language_version_manager.rb
229
228
  - lib/dependabot/python/metadata_finder.rb
230
229
  - lib/dependabot/python/name_normaliser.rb
231
230
  - lib/dependabot/python/native_helpers.rb
231
+ - lib/dependabot/python/pip_compile_file_matcher.rb
232
232
  - lib/dependabot/python/requirement.rb
233
233
  - lib/dependabot/python/requirement_parser.rb
234
234
  - lib/dependabot/python/update_checker.rb
@@ -245,7 +245,7 @@ licenses:
245
245
  - Nonstandard
246
246
  metadata:
247
247
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
248
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.234.0
248
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.235.0
249
249
  post_install_message:
250
250
  rdoc_options: []
251
251
  require_paths:
@@ -1,35 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- require "time"
5
- require "open3"
6
-
7
- require "dependabot/errors"
8
- require "dependabot/shared_helpers"
9
-
10
- module Dependabot
11
- module Python
12
- module Helpers
13
- def self.run_poetry_command(command, fingerprint: nil)
14
- start = Time.now
15
- command = SharedHelpers.escape_command(command)
16
- stdout, stderr, process = Open3.capture3(command)
17
- time_taken = Time.now - start
18
-
19
- # Raise an error with the output from the shell session if Poetry
20
- # returns a non-zero status
21
- return stdout if process.success?
22
-
23
- raise SharedHelpers::HelperSubprocessFailed.new(
24
- message: stderr,
25
- error_context: {
26
- command: command,
27
- fingerprint: fingerprint,
28
- time_taken: time_taken,
29
- process_exit_value: process.to_s
30
- }
31
- )
32
- end
33
- end
34
- end
35
- end