dependabot-python 0.215.0 → 0.216.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: 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