dependabot-python 0.215.0 → 0.216.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb146c09fb17142425be804da23abdf95e938a9c8e70c8b95697ebdbc55f89c3
4
- data.tar.gz: d0b61ca9973b582448c78edecb798b500806b6eb5805f7236ae87703255ad953
3
+ metadata.gz: 62df822653f41408461f72892b9886776c124a195af5569c6af9e3bdf044e93d
4
+ data.tar.gz: 8cba64f792d1be98900fe5136d549230d5156d487d07150e0356a147402c5ca3
5
5
  SHA512:
6
- metadata.gz: e023894b96f723c3cf3d812a959b35f9a1d9de5b33981c2090af0b0ba259376e83f5dc728cd3e06c5f3aceb39c6ce1181e6ed8eb10d8d1d0a9e0e216698a24fa
7
- data.tar.gz: bfeafe03ba027242f9a1327f1aee036768ea693cffb08e485062879829109f956dcd8b77b7a24c3bde2eb289b50d9b57717216a199e107df221c8a391de8ecea
6
+ metadata.gz: d8c60892487b523ca5879fd08f188dfabcd4d9c4ca17b3317accb3558a22a7731f53129616494230cf62138503e7aece5e9b304fe53bbd525e484197fe74f110
7
+ data.tar.gz: d7cfe8feb5f919ec01521377366c3716954f898361cc666a1df6384afc3389b9e6db9a92dd4c91cd25fa2e043cbd1aa5a71745d7c030a9627f7c434fcfee5731
data/helpers/build CHANGED
@@ -18,8 +18,8 @@ cp -r \
18
18
  "$install_dir"
19
19
 
20
20
  cd "$install_dir"
21
- PYENV_VERSION=3.11.0 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
22
- PYENV_VERSION=3.10.8 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
23
- PYENV_VERSION=3.9.15 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
24
- PYENV_VERSION=3.8.15 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
25
- PYENV_VERSION=3.7.15 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
21
+ PYENV_VERSION=3.11.3 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
22
+ PYENV_VERSION=3.10.11 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
23
+ PYENV_VERSION=3.9.16 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
24
+ PYENV_VERSION=3.8.16 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
25
+ PYENV_VERSION=3.7.16 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ if [ -z "$DEPENDABOT_NATIVE_HELPERS_PATH" ]; then
6
+ echo "Unable to build, DEPENDABOT_NATIVE_HELPERS_PATH is not set"
7
+ exit 1
8
+ fi
9
+
10
+ install_dir="$DEPENDABOT_NATIVE_HELPERS_PATH/python"
11
+ mkdir -p "$install_dir"
12
+
13
+ helpers_dir="$(dirname "${BASH_SOURCE[0]}")"
14
+ cp -r \
15
+ "$helpers_dir/lib" \
16
+ "$helpers_dir/run.py" \
17
+ "$helpers_dir/requirements.txt" \
18
+ "$install_dir"
19
+
20
+ cd "$install_dir"
21
+ PYENV_VERSION=$1 pyenv exec pip --disable-pip-version-check install --use-pep517 -r "requirements.txt"
@@ -1,11 +1,10 @@
1
- pip>=21.3.1,<22.4.0 # Range maintains py36 support TODO: Review python 3.6 support in April 2023 (eol ubuntu 18.04)
2
- pip-tools>=6.4.0,<6.11.1 # Range maintains py36 support TODO: Review python 3.6 support in April 2023 (eol ubuntu 18.04)
3
- flake8==5.0.4
1
+ pip>=21.3.1,<23.1.0 # Range maintains py36 support TODO: Review python 3.6 support in April 2023 (eol ubuntu 18.04)
2
+ pip-tools>=6.4.0,<=6.12.3 # Range maintains py36 support TODO: Review python 3.6 support in April 2023 (eol ubuntu 18.04)
4
3
  hashin==0.17.0
5
4
  pipenv==2022.4.8
6
5
  pipfile==0.0.2
7
- poetry>=1.1.15,<1.3.0
6
+ poetry>=1.1.15,<1.4.0
8
7
  wheel==0.37.1
9
8
 
10
9
  # Some dependencies will only install if Cython is present
11
- Cython==0.29.32
10
+ Cython==0.29.34
@@ -48,22 +48,13 @@ module Dependabot
48
48
 
49
49
  POETRY_DEPENDENCY_TYPES.each do |type|
50
50
  deps_hash = parsed_pyproject.dig("tool", "poetry", type) || {}
51
-
52
- deps_hash.each do |name, req|
53
- next if normalise(name) == "python"
54
-
55
- requirements = parse_requirements_from(req, type)
56
- next if requirements.empty?
57
-
58
- dependencies << Dependency.new(
59
- name: normalise(name),
60
- version: version_from_lockfile(name),
61
- requirements: requirements,
62
- package_manager: "pip"
63
- )
64
- end
51
+ dependencies += parse_poetry_dependencies(type, deps_hash)
65
52
  end
66
53
 
54
+ groups = parsed_pyproject.dig("tool", "poetry", "group") || {}
55
+ groups.each do |group, group_spec|
56
+ dependencies += parse_poetry_dependencies(group, group_spec["dependencies"])
57
+ end
67
58
  dependencies
68
59
  end
69
60
 
@@ -101,6 +92,25 @@ module Dependabot
101
92
  dependencies
102
93
  end
103
94
 
95
+ def parse_poetry_dependencies(type, deps_hash)
96
+ dependencies = Dependabot::FileParsers::Base::DependencySet.new
97
+
98
+ deps_hash.each do |name, req|
99
+ next if normalise(name) == "python"
100
+
101
+ requirements = parse_requirements_from(req, type)
102
+ next if requirements.empty?
103
+
104
+ dependencies << Dependency.new(
105
+ name: normalise(name),
106
+ version: version_from_lockfile(name),
107
+ requirements: requirements,
108
+ package_manager: "pip"
109
+ )
110
+ end
111
+ dependencies
112
+ end
113
+
104
114
  def normalised_name(name, extras)
105
115
  NameNormaliser.normalise_including_extras(name, extras)
106
116
  end
@@ -108,7 +118,7 @@ module Dependabot
108
118
  # @param req can be an Array, Hash or String that represents the constraints for a dependency
109
119
  def parse_requirements_from(req, type)
110
120
  [req].flatten.compact.filter_map do |requirement|
111
- next if requirement.is_a?(Hash) && (UNSUPPORTED_DEPENDENCY_TYPES & requirement.keys).any?
121
+ next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
112
122
 
113
123
  check_requirements(requirement)
114
124
 
@@ -7,7 +7,7 @@ require "dependabot/python/file_fetcher"
7
7
  require "dependabot/python/file_parser/python_requirement_parser"
8
8
  require "dependabot/python/file_updater"
9
9
  require "dependabot/shared_helpers"
10
- require "dependabot/python/helpers"
10
+ require "dependabot/python/language_version_manager"
11
11
  require "dependabot/python/native_helpers"
12
12
  require "dependabot/python/python_versions"
13
13
  require "dependabot/python/name_normaliser"
@@ -26,6 +26,7 @@ module Dependabot
26
26
  INCOMPATIBLE_VERSIONS_REGEX = /There are incompatible versions in the resolved dependencies:.*\z/m
27
27
  WARNINGS = /\s*# WARNING:.*\Z/m
28
28
  UNSAFE_NOTE = /\s*# The following packages are considered to be unsafe.*\Z/m
29
+ RESOLVER_REGEX = /(?<=--resolver=)(\w+)/
29
30
 
30
31
  attr_reader :dependencies, :dependency_files, :credentials
31
32
 
@@ -66,7 +67,7 @@ module Dependabot
66
67
  def compile_new_requirement_files
67
68
  SharedHelpers.in_a_temporary_directory do
68
69
  write_updated_dependency_files
69
- Helpers.install_required_python(python_version)
70
+ language_version_manager.install_required_python
70
71
 
71
72
  filenames_to_compile.each do |filename|
72
73
  # Shell out to pip-compile, generate a new set of requirements.
@@ -176,7 +177,7 @@ module Dependabot
176
177
 
177
178
  def run_pip_compile_command(command, allow_unsafe_shell_command: false, fingerprint:)
178
179
  run_command(
179
- "pyenv local #{Helpers.python_major_minor(python_version)}",
180
+ "pyenv local #{language_version_manager.python_major_minor}",
180
181
  fingerprint: "pyenv local <python_major_minor>"
181
182
  )
182
183
 
@@ -210,7 +211,7 @@ module Dependabot
210
211
  end
211
212
 
212
213
  # Overwrite the .python-version with updated content
213
- File.write(".python-version", Helpers.python_major_minor(python_version))
214
+ File.write(".python-version", language_version_manager.python_major_minor)
214
215
 
215
216
  setup_files.each do |file|
216
217
  path = file.name
@@ -441,6 +442,10 @@ module Dependabot
441
442
 
442
443
  options << "--strip-extras" if requirements_file.content.include?("--strip-extras")
443
444
 
445
+ if (resolver = RESOLVER_REGEX.match(requirements_file.content))
446
+ options << "--resolver=#{resolver}"
447
+ end
448
+
444
449
  options
445
450
  end
446
451
 
@@ -517,9 +522,9 @@ module Dependabot
517
522
  while (remaining_filenames = filenames - ordered_filenames).any?
518
523
  ordered_filenames +=
519
524
  remaining_filenames.
520
- select do |fn|
525
+ reject do |fn|
521
526
  unupdated_reqs = requirement_map[fn] - ordered_filenames
522
- (unupdated_reqs & filenames).empty?
527
+ unupdated_reqs.intersect?(filenames)
523
528
  end
524
529
  end
525
530
 
@@ -545,41 +550,6 @@ module Dependabot
545
550
  end
546
551
  end
547
552
 
548
- def python_version
549
- @python_version ||=
550
- user_specified_python_version ||
551
- python_version_matching_imputed_requirements ||
552
- PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.first
553
- end
554
-
555
- def user_specified_python_version
556
- return unless python_requirement_parser.user_specified_requirements.any?
557
-
558
- user_specified_requirements =
559
- python_requirement_parser.user_specified_requirements.
560
- map { |r| Python::Requirement.requirements_array(r) }
561
- python_version_matching(user_specified_requirements)
562
- end
563
-
564
- def python_version_matching_imputed_requirements
565
- compiled_file_python_requirement_markers =
566
- python_requirement_parser.imputed_requirements.map do |r|
567
- Dependabot::Python::Requirement.new(r)
568
- end
569
- python_version_matching(compiled_file_python_requirement_markers)
570
- end
571
-
572
- def python_version_matching(requirements)
573
- PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.find do |version_string|
574
- version = Python::Version.new(version_string)
575
- requirements.all? do |req|
576
- next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)
577
-
578
- req.satisfied_by?(version)
579
- end
580
- end
581
- end
582
-
583
553
  def python_requirement_parser
584
554
  @python_requirement_parser ||=
585
555
  FileParser::PythonRequirementParser.new(
@@ -587,8 +557,11 @@ module Dependabot
587
557
  )
588
558
  end
589
559
 
590
- def pre_installed_python?(version)
591
- PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.include?(version)
560
+ def language_version_manager
561
+ @language_version_manager ||=
562
+ LanguageVersionManager.new(
563
+ python_requirement_parser: python_requirement_parser
564
+ )
592
565
  end
593
566
 
594
567
  def setup_files
@@ -6,6 +6,7 @@ require "dependabot/dependency"
6
6
  require "dependabot/python/requirement_parser"
7
7
  require "dependabot/python/file_parser/python_requirement_parser"
8
8
  require "dependabot/python/file_updater"
9
+ require "dependabot/python/language_version_manager"
9
10
  require "dependabot/shared_helpers"
10
11
  require "dependabot/python/native_helpers"
11
12
  require "dependabot/python/name_normaliser"
@@ -146,7 +147,7 @@ module Dependabot
146
147
  def update_python_requirement(pipfile_content)
147
148
  PipfilePreparer.
148
149
  new(pipfile_content: pipfile_content).
149
- update_python_requirement(Helpers.python_major_minor(python_version))
150
+ update_python_requirement(language_version_manager.python_major_minor)
150
151
  end
151
152
 
152
153
  # rubocop:disable Metrics/PerceivedComplexity
@@ -198,10 +199,6 @@ module Dependabot
198
199
  write_temporary_dependency_files(prepared_pipfile_content)
199
200
  install_required_python
200
201
 
201
- # Initialize a git repo to appease pip-tools
202
- command = SharedHelpers.escape_command("git init")
203
- IO.popen(command, err: %i(child out)) if setup_files.any?
204
-
205
202
  run_pipenv_command(
206
203
  "pyenv exec pipenv lock"
207
204
  )
@@ -271,7 +268,7 @@ module Dependabot
271
268
  end
272
269
 
273
270
  def run_pipenv_command(command, env: pipenv_env_variables)
274
- run_command("pyenv local #{Helpers.python_major_minor(python_version)}")
271
+ run_command("pyenv local #{language_version_manager.python_major_minor}")
275
272
  run_command(command, env: env)
276
273
  end
277
274
 
@@ -283,7 +280,7 @@ module Dependabot
283
280
  end
284
281
 
285
282
  # Overwrite the .python-version with updated content
286
- File.write(".python-version", Helpers.python_major_minor(python_version))
283
+ File.write(".python-version", language_version_manager.python_major_minor)
287
284
 
288
285
  setup_files.each do |file|
289
286
  path = file.name
@@ -309,7 +306,7 @@ module Dependabot
309
306
  nil
310
307
  end
311
308
 
312
- Helpers.install_required_python(python_version)
309
+ language_version_manager.install_required_python
313
310
  end
314
311
 
315
312
  def sanitized_setup_file_content(file)
@@ -322,57 +319,6 @@ module Dependabot
322
319
  sanitized_content
323
320
  end
324
321
 
325
- def python_version
326
- @python_version ||= python_version_from_supported_versions
327
- end
328
-
329
- def python_version_from_supported_versions
330
- requirement_string =
331
- if @using_python_two then "2.7.*"
332
- elsif user_specified_python_requirement
333
- parts = user_specified_python_requirement.split(".")
334
- parts.fill("*", (parts.length)..2).join(".")
335
- else
336
- PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.first
337
- end
338
-
339
- # Ideally, the requirement is satisfied by a Python version we support
340
- requirement =
341
- Python::Requirement.requirements_array(requirement_string).first
342
- version =
343
- PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.
344
- find { |v| requirement.satisfied_by?(Python::Version.new(v)) }
345
- return version if version
346
-
347
- # If not, and changing the patch version would fix things, we do that
348
- # as the patch version is unlikely to affect resolution
349
- requirement =
350
- Python::Requirement.new(requirement_string.gsub(/\.\d+$/, ".*"))
351
- version =
352
- PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.
353
- find { |v| requirement.satisfied_by?(Python::Version.new(v)) }
354
- return version if version
355
-
356
- # Otherwise we have to raise, giving details of the Python versions
357
- # that Dependabot supports
358
- msg = "Dependabot detected the following Python requirement " \
359
- "for your project: '#{requirement_string}'.\n\nCurrently, the " \
360
- "following Python versions are supported in Dependabot: " \
361
- "#{PythonVersions::SUPPORTED_VERSIONS.join(', ')}."
362
- raise DependencyFileNotResolvable, msg
363
- end
364
-
365
- def user_specified_python_requirement
366
- python_requirement_parser.user_specified_requirements.first
367
- end
368
-
369
- def python_requirement_parser
370
- @python_requirement_parser ||=
371
- FileParser::PythonRequirementParser.new(
372
- dependency_files: dependency_files
373
- )
374
- end
375
-
376
322
  def setup_cfg(file)
377
323
  dependency_files.find do |f|
378
324
  f.name == file.name.sub(/\.py$/, ".cfg")
@@ -400,6 +346,20 @@ module Dependabot
400
346
  NameNormaliser.normalise(name)
401
347
  end
402
348
 
349
+ def python_requirement_parser
350
+ @python_requirement_parser ||=
351
+ FileParser::PythonRequirementParser.new(
352
+ dependency_files: dependency_files
353
+ )
354
+ end
355
+
356
+ def language_version_manager
357
+ @language_version_manager ||=
358
+ LanguageVersionManager.new(
359
+ python_requirement_parser: python_requirement_parser
360
+ )
361
+ end
362
+
403
363
  def parsed_lockfile
404
364
  @parsed_lockfile ||= JSON.parse(lockfile.content)
405
365
  end
@@ -21,7 +21,7 @@ module Dependabot
21
21
  pipfile_object = TomlRB.parse(pipfile_content)
22
22
 
23
23
  pipfile_object["source"] =
24
- pipfile_sources.reject { |h| h["url"].include?("${") } +
24
+ pipfile_sources.filter_map { |h| sub_auth_url(h, credentials) } +
25
25
  config_variable_sources(credentials)
26
26
 
27
27
  TomlRB.dump(pipfile_object)
@@ -114,6 +114,22 @@ module Dependabot
114
114
  map { |h| h.dup.merge("url" => h["url"].gsub(%r{/*$}, "") + "/") }
115
115
  end
116
116
 
117
+ def sub_auth_url(source, credentials)
118
+ if source["url"].include?("${")
119
+ base_url = source["url"].sub(/\${.*}@/, "")
120
+
121
+ source_cred = credentials.
122
+ select { |cred| cred["type"] == "python_index" }.
123
+ find { |c| c["index-url"].sub(/\${.*}@/, "") == base_url }
124
+
125
+ return nil if source_cred.nil?
126
+
127
+ source["url"] = AuthedUrlBuilder.authed_url(credential: source_cred)
128
+ end
129
+
130
+ source
131
+ end
132
+
117
133
  def config_variable_sources(credentials)
118
134
  @config_variable_sources ||=
119
135
  credentials.
@@ -4,7 +4,7 @@ require "toml-rb"
4
4
  require "open3"
5
5
  require "dependabot/dependency"
6
6
  require "dependabot/shared_helpers"
7
- require "dependabot/python/helpers"
7
+ require "dependabot/python/language_version_manager"
8
8
  require "dependabot/python/version"
9
9
  require "dependabot/python/requirement"
10
10
  require "dependabot/python/python_versions"
@@ -75,10 +75,17 @@ module Dependabot
75
75
  find { |r| r[:file] == pyproject.name }.
76
76
  fetch(:requirement)
77
77
 
78
- updated_content =
79
- content.gsub(declaration_regex(dep)) do |line|
80
- line.gsub(old_req, updated_requirement)
81
- end
78
+ declaration_regex = declaration_regex(dep)
79
+ updated_content = if content.match?(declaration_regex)
80
+ content.gsub(declaration_regex(dep)) do |match|
81
+ match.gsub(old_req, updated_requirement)
82
+ end
83
+ else
84
+ content.gsub(table_declaration_regex(dep)) do |match|
85
+ match.gsub(/(\s*version\s*=\s*["'])#{Regexp.escape(old_req)}/,
86
+ '\1' + updated_requirement)
87
+ end
88
+ end
82
89
 
83
90
  raise "Content did not change!" if content == updated_content
84
91
 
@@ -135,7 +142,7 @@ module Dependabot
135
142
  def update_python_requirement(pyproject_content)
136
143
  PyprojectPreparer.
137
144
  new(pyproject_content: pyproject_content).
138
- update_python_requirement(Helpers.python_major_minor(python_version))
145
+ update_python_requirement(language_version_manager.python_major_minor)
139
146
  end
140
147
 
141
148
  def lock_declaration_to_new_version!(poetry_object, dep)
@@ -178,10 +185,10 @@ module Dependabot
178
185
  write_temporary_dependency_files(pyproject_content)
179
186
  add_auth_env_vars
180
187
 
181
- Helpers.install_required_python(python_version)
188
+ language_version_manager.install_required_python
182
189
 
183
190
  # use system git instead of the pure Python dulwich
184
- unless python_version&.start_with?("3.6")
191
+ unless language_version_manager.python_version&.start_with?("3.6")
185
192
  run_poetry_command("pyenv exec poetry config experimental.system-git-client true")
186
193
  end
187
194
 
@@ -232,7 +239,7 @@ module Dependabot
232
239
  end
233
240
 
234
241
  # Overwrite the .python-version with updated content
235
- File.write(".python-version", Helpers.python_major_minor(python_version)) if python_version
242
+ File.write(".python-version", language_version_manager.python_major_minor)
236
243
 
237
244
  # Overwrite the pyproject with updated content
238
245
  File.write("pyproject.toml", pyproject_content)
@@ -244,29 +251,6 @@ module Dependabot
244
251
  add_auth_env_vars(credentials)
245
252
  end
246
253
 
247
- def python_version
248
- requirements = python_requirement_parser.user_specified_requirements
249
- requirements = requirements.
250
- map { |r| Python::Requirement.requirements_array(r) }
251
-
252
- PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.find do |version|
253
- requirements.all? do |reqs|
254
- reqs.any? { |r| r.satisfied_by?(Python::Version.new(version)) }
255
- end
256
- end
257
- end
258
-
259
- def python_requirement_parser
260
- @python_requirement_parser ||=
261
- FileParser::PythonRequirementParser.new(
262
- dependency_files: dependency_files
263
- )
264
- end
265
-
266
- def pre_installed_python?(version)
267
- PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.include?(version)
268
- end
269
-
270
254
  def pyproject_hash_for(pyproject_content)
271
255
  SharedHelpers.in_a_temporary_directory do |dir|
272
256
  SharedHelpers.with_git_configured(credentials: credentials) do
@@ -282,8 +266,15 @@ module Dependabot
282
266
  end
283
267
 
284
268
  def declaration_regex(dep)
285
- escaped_name = Regexp.escape(dep.name).gsub("\\-", "[-_.]")
286
- /(?:^\s*|["'])#{escaped_name}["']?\s*=.*$/i
269
+ /(?:^\s*|["'])#{escape(dep)}["']?\s*=.*$/i
270
+ end
271
+
272
+ def table_declaration_regex(dep)
273
+ /tool\.poetry\.[^\n]+\.#{escape(dep)}\]\n.*?\s*version\s* =.*?\n/m
274
+ end
275
+
276
+ def escape(dep)
277
+ Regexp.escape(dep.name).gsub("\\-", "[-_.]")
287
278
  end
288
279
 
289
280
  def file_changed?(file)
@@ -307,6 +298,20 @@ module Dependabot
307
298
  NameNormaliser.normalise(name)
308
299
  end
309
300
 
301
+ def python_requirement_parser
302
+ @python_requirement_parser ||=
303
+ FileParser::PythonRequirementParser.new(
304
+ dependency_files: dependency_files
305
+ )
306
+ end
307
+
308
+ def language_version_manager
309
+ @language_version_manager ||=
310
+ LanguageVersionManager.new(
311
+ python_requirement_parser: python_requirement_parser
312
+ )
313
+ end
314
+
310
315
  def pyproject
311
316
  @pyproject ||=
312
317
  dependency_files.find { |f| f.name == "pyproject.toml" }
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/logger"
4
+ require "dependabot/python/version"
5
+
6
+ module Dependabot
7
+ module Python
8
+ class LanguageVersionManager
9
+ def initialize(python_requirement_parser:)
10
+ @python_requirement_parser = python_requirement_parser
11
+ end
12
+
13
+ def install_required_python
14
+ # The leading space is important in the version check
15
+ return if SharedHelpers.run_shell_command("pyenv versions").include?(" #{python_major_minor}.")
16
+
17
+ if File.exist?("/usr/local/.pyenv/#{python_major_minor}.tar.gz")
18
+ SharedHelpers.run_shell_command(
19
+ "tar xzf /usr/local/.pyenv/#{python_major_minor}.tar.gz -C /usr/local/.pyenv/"
20
+ )
21
+ return if SharedHelpers.run_shell_command("pyenv versions").
22
+ include?(" #{python_major_minor}.")
23
+ end
24
+
25
+ Dependabot.logger.info("Installing required Python #{python_version}.")
26
+ start = Time.now
27
+ SharedHelpers.run_shell_command("pyenv install -s #{python_version}")
28
+ SharedHelpers.run_shell_command("pyenv exec pip install --upgrade pip")
29
+ SharedHelpers.run_shell_command("pyenv exec pip install -r" \
30
+ "#{NativeHelpers.python_requirements_path}")
31
+ time_taken = Time.now - start
32
+ Dependabot.logger.info("Installing Python #{python_version} took #{time_taken}s.")
33
+ end
34
+
35
+ def python_major_minor
36
+ @python ||= Python::Version.new(python_version)
37
+ "#{@python.segments[0]}.#{@python.segments[1]}"
38
+ end
39
+
40
+ def python_version
41
+ @python_version ||= python_version_from_supported_versions
42
+ end
43
+
44
+ def python_requirement_string
45
+ if user_specified_python_version
46
+ if user_specified_python_version.start_with?(/\d/)
47
+ parts = user_specified_python_version.split(".")
48
+ parts.fill("*", (parts.length)..2).join(".")
49
+ else
50
+ user_specified_python_version
51
+ end
52
+ elsif python_version_matching_imputed_requirements
53
+ python_version_matching_imputed_requirements
54
+ else
55
+ PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.first
56
+ end
57
+ end
58
+
59
+ def python_version_from_supported_versions
60
+ requirement_string = python_requirement_string
61
+
62
+ # Ideally, the requirement is satisfied by a Python version we support
63
+ requirement =
64
+ Python::Requirement.requirements_array(requirement_string).first
65
+ version =
66
+ PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.
67
+ find { |v| requirement.satisfied_by?(Python::Version.new(v)) }
68
+ return version if version
69
+
70
+ # If not, and we're dealing with a simple version string
71
+ # and changing the patch version would fix things, we do that
72
+ # as the patch version is unlikely to affect resolution
73
+ if requirement_string.start_with?(/\d/)
74
+ requirement =
75
+ Python::Requirement.new(requirement_string.gsub(/\.\d+$/, ".*"))
76
+ version =
77
+ PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.
78
+ find { |v| requirement.satisfied_by?(Python::Version.new(v)) }
79
+ return version if version
80
+ end
81
+
82
+ # Otherwise we have to raise, giving details of the Python versions
83
+ # that Dependabot supports
84
+ msg = "Dependabot detected the following Python requirement " \
85
+ "for your project: '#{requirement_string}'.\n\nCurrently, the " \
86
+ "following Python versions are supported in Dependabot: " \
87
+ "#{PythonVersions::SUPPORTED_VERSIONS.join(', ')}."
88
+ raise DependencyFileNotResolvable, msg
89
+ end
90
+
91
+ def user_specified_python_version
92
+ @python_requirement_parser.user_specified_requirements.first
93
+ end
94
+
95
+ def python_version_matching_imputed_requirements
96
+ compiled_file_python_requirement_markers =
97
+ @python_requirement_parser.imputed_requirements.map do |r|
98
+ Dependabot::Python::Requirement.new(r)
99
+ end
100
+ python_version_matching(compiled_file_python_requirement_markers)
101
+ end
102
+
103
+ def python_version_matching(requirements)
104
+ PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.find do |version_string|
105
+ version = Python::Version.new(version_string)
106
+ requirements.all? do |req|
107
+ next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)
108
+
109
+ req.satisfied_by?(version)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end