dependabot-python 0.212.0 → 0.213.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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +1 -6
  3. data/helpers/lib/parser.py +52 -0
  4. data/helpers/requirements.txt +3 -3
  5. data/helpers/run.py +2 -0
  6. data/lib/dependabot/python/file_fetcher.rb +21 -12
  7. data/lib/dependabot/python/file_parser/{poetry_files_parser.rb → pyproject_files_parser.rb} +83 -2
  8. data/lib/dependabot/python/file_parser/setup_file_parser.rb +4 -4
  9. data/lib/dependabot/python/file_parser.rb +5 -29
  10. data/lib/dependabot/python/file_updater/pip_compile_file_updater.rb +5 -20
  11. data/lib/dependabot/python/file_updater/pipfile_file_updater.rb +1 -5
  12. data/lib/dependabot/python/file_updater/poetry_file_updater.rb +7 -6
  13. data/lib/dependabot/python/file_updater/pyproject_preparer.rb +1 -1
  14. data/lib/dependabot/python/file_updater.rb +14 -1
  15. data/lib/dependabot/python/helpers.rb +20 -0
  16. data/lib/dependabot/python/metadata_finder.rb +2 -0
  17. data/lib/dependabot/python/python_versions.rb +5 -5
  18. data/lib/dependabot/python/requirement.rb +7 -4
  19. data/lib/dependabot/python/requirement_parser.rb +20 -23
  20. data/lib/dependabot/python/update_checker/index_finder.rb +1 -1
  21. data/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb +17 -19
  22. data/lib/dependabot/python/update_checker/pipenv_version_resolver.rb +7 -16
  23. data/lib/dependabot/python/update_checker/poetry_version_resolver.rb +13 -11
  24. data/lib/dependabot/python/update_checker/requirements_updater.rb +17 -4
  25. data/lib/dependabot/python/update_checker.rb +82 -25
  26. data/lib/dependabot/python/version.rb +2 -2
  27. metadata +15 -56
@@ -6,7 +6,7 @@ require "dependabot/python/version"
6
6
  module Dependabot
7
7
  module Python
8
8
  class Requirement < Gem::Requirement
9
- OR_SEPARATOR = /(?<=[a-zA-Z0-9)*])\s*\|+/.freeze
9
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9)*])\s*\|+/
10
10
 
11
11
  # Add equality and arbitrary-equality matchers
12
12
  OPS = OPS.merge(
@@ -19,8 +19,8 @@ module Dependabot
19
19
  version_pattern = Python::Version::VERSION_PATTERN
20
20
 
21
21
  PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
22
- PATTERN = /\A#{PATTERN_RAW}\z/.freeze
23
- PARENS_PATTERN = /\A\(([^)]+)\)\z/.freeze
22
+ PATTERN = /\A#{PATTERN_RAW}\z/
23
+ PARENS_PATTERN = /\A\(([^)]+)\)\z/
24
24
 
25
25
  def self.parse(obj)
26
26
  return ["=", Python::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
@@ -60,6 +60,9 @@ module Dependabot
60
60
  requirements = requirements.flatten.flat_map do |req_string|
61
61
  next if req_string.nil?
62
62
 
63
+ # Standard python doesn't support whitespace in requirements, but Poetry does.
64
+ req_string = req_string.gsub(/(\d +)([<=>])/, '\1,\2')
65
+
63
66
  req_string.split(",").map(&:strip).map do |r|
64
67
  convert_python_constraint_to_ruby_constraint(r)
65
68
  end
@@ -87,7 +90,7 @@ module Dependabot
87
90
  return nil if req_string == "*"
88
91
 
89
92
  req_string = req_string.gsub("~=", "~>")
90
- req_string = req_string.gsub(/(?<=\d)[<=>].*/, "")
93
+ req_string = req_string.gsub(/(?<=\d)[<=>].*\Z/, "")
91
94
 
92
95
  if req_string.match?(/~[^>]/) then convert_tilde_req(req_string)
93
96
  elsif req_string.start_with?("^") then convert_caret_req(req_string)
@@ -3,29 +3,26 @@
3
3
  module Dependabot
4
4
  module Python
5
5
  class RequirementParser
6
- NAME = /[a-zA-Z0-9](?:[a-zA-Z0-9\-_\.]*[a-zA-Z0-9])?/.freeze
7
- EXTRA = /[a-zA-Z0-9\-_\.]+/.freeze
8
- COMPARISON = /===|==|>=|<=|<|>|~=|!=/.freeze
9
- VERSION = /([1-9][0-9]*!)?[0-9]+[a-zA-Z0-9\-_.*]*(\+[0-9a-zA-Z]+(\.[0-9a-zA-Z]+)*)?/.
10
- freeze
11
- REQUIREMENT =
12
- /(?<comparison>#{COMPARISON})\s*\\?\s*(?<version>#{VERSION})/.freeze
13
- HASH = /--hash=(?<algorithm>.*?):(?<hash>.*?)(?=\s|$)/.freeze
14
- REQUIREMENTS = /#{REQUIREMENT}(\s*,\s*\\?\s*#{REQUIREMENT})*/.freeze
15
- HASHES = /#{HASH}(\s*\\?\s*#{HASH})*/.freeze
16
- MARKER_OP = /\s*(#{COMPARISON}|(\s*in)|(\s*not\s*in))/.freeze
17
- PYTHON_STR_C =
18
- %r{[a-zA-Z0-9\s\(\)\.\{\}\-_\*#:;/\?\[\]!~`@\$%\^&=\+\|<>]}.freeze
19
- PYTHON_STR = /('(#{PYTHON_STR_C}|")*'|"(#{PYTHON_STR_C}|')*")/.freeze
6
+ NAME = /[a-zA-Z0-9](?:[a-zA-Z0-9\-_\.]*[a-zA-Z0-9])?/
7
+ EXTRA = /[a-zA-Z0-9\-_\.]+/
8
+ COMPARISON = /===|==|>=|<=|<|>|~=|!=/
9
+ VERSION = /([1-9][0-9]*!)?[0-9]+[a-zA-Z0-9\-_.*]*(\+[0-9a-zA-Z]+(\.[0-9a-zA-Z]+)*)?/
10
+
11
+ REQUIREMENT = /(?<comparison>#{COMPARISON})\s*\\?\s*(?<version>#{VERSION})/
12
+ HASH = /--hash=(?<algorithm>.*?):(?<hash>.*?)(?=\s|$)/
13
+ REQUIREMENTS = /#{REQUIREMENT}(\s*,\s*\\?\s*#{REQUIREMENT})*/
14
+ HASHES = /#{HASH}(\s*\\?\s*#{HASH})*/
15
+ MARKER_OP = /\s*(#{COMPARISON}|(\s*in)|(\s*not\s*in))/
16
+ PYTHON_STR_C = %r{[a-zA-Z0-9\s\(\)\.\{\}\-_\*#:;/\?\[\]!~`@\$%\^&=\+\|<>]}
17
+ PYTHON_STR = /('(#{PYTHON_STR_C}|")*'|"(#{PYTHON_STR_C}|')*")/
20
18
  ENV_VAR =
21
19
  /python_version|python_full_version|os_name|sys_platform|
22
20
  platform_release|platform_system|platform_version|platform_machine|
23
21
  platform_python_implementation|implementation_name|
24
- implementation_version/.freeze
25
- MARKER_VAR = /\s*(#{ENV_VAR}|#{PYTHON_STR})/.freeze
26
- MARKER_EXPR_ONE = /#{MARKER_VAR}#{MARKER_OP}#{MARKER_VAR}/.freeze
27
- MARKER_EXPR =
28
- /(#{MARKER_EXPR_ONE}|\(\s*|\s*\)|\s+and\s+|\s+or\s+)+/.freeze
22
+ implementation_version/
23
+ MARKER_VAR = /\s*(#{ENV_VAR}|#{PYTHON_STR})/
24
+ MARKER_EXPR_ONE = /#{MARKER_VAR}#{MARKER_OP}#{MARKER_VAR}/
25
+ MARKER_EXPR = /(#{MARKER_EXPR_ONE}|\(\s*|\s*\)|\s+and\s+|\s+or\s+)+/
29
26
 
30
27
  INSTALL_REQ_WITH_REQUIREMENT =
31
28
  /\s*\\?\s*(?<name>#{NAME})
@@ -34,7 +31,7 @@ module Dependabot
34
31
  \s*\\?\s*(;\s*(?<markers>#{MARKER_EXPR}))?
35
32
  \s*\\?\s*(?<hashes>#{HASHES})?
36
33
  \s*#*\s*(?<comment>.+)?
37
- /x.freeze
34
+ /x
38
35
 
39
36
  INSTALL_REQ_WITHOUT_REQUIREMENT =
40
37
  /^\s*\\?\s*(?<name>#{NAME})
@@ -42,7 +39,7 @@ module Dependabot
42
39
  \s*\\?\s*(;\s*(?<markers>#{MARKER_EXPR}))?
43
40
  \s*\\?\s*(?<hashes>#{HASHES})?
44
41
  \s*#*\s*(?<comment>.+)?$
45
- /x.freeze
42
+ /x
46
43
 
47
44
  VALID_REQ_TXT_REQUIREMENT =
48
45
  /^\s*\\?\s*(?<name>#{NAME})
@@ -51,12 +48,12 @@ module Dependabot
51
48
  \s*\\?\s*(;\s*(?<markers>#{MARKER_EXPR}))?
52
49
  \s*\\?\s*(?<hashes>#{HASHES})?
53
50
  \s*(\#+\s*(?<comment>.*))?$
54
- /x.freeze
51
+ /x
55
52
 
56
53
  NAME_WITH_EXTRAS =
57
54
  /\s*\\?\s*(?<name>#{NAME})
58
55
  (\s*\\?\s*\[\s*(?<extras>#{EXTRA}(\s*,\s*#{EXTRA})*)\s*\])?
59
- /x.freeze
56
+ /x
60
57
  end
61
58
  end
62
59
  end
@@ -9,7 +9,7 @@ module Dependabot
9
9
  class UpdateChecker
10
10
  class IndexFinder
11
11
  PYPI_BASE_URL = "https://pypi.org/simple/"
12
- ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/.freeze
12
+ ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/
13
13
 
14
14
  def initialize(dependency_files:, credentials:)
15
15
  @dependency_files = dependency_files
@@ -11,6 +11,7 @@ require "dependabot/python/file_updater/requirement_replacer"
11
11
  require "dependabot/python/file_updater/setup_file_sanitizer"
12
12
  require "dependabot/python/version"
13
13
  require "dependabot/shared_helpers"
14
+ require "dependabot/python/helpers"
14
15
  require "dependabot/python/native_helpers"
15
16
  require "dependabot/python/python_versions"
16
17
  require "dependabot/python/name_normaliser"
@@ -24,16 +25,14 @@ module Dependabot
24
25
  # - Run `pip-compile` and see what the result is
25
26
  # rubocop:disable Metrics/ClassLength
26
27
  class PipCompileVersionResolver
27
- GIT_DEPENDENCY_UNREACHABLE_REGEX =
28
- /git clone --filter=blob:none --quiet (?<url>[^\s]+).* /.freeze
29
- GIT_REFERENCE_NOT_FOUND_REGEX =
30
- /Did not find branch or tag '(?<tag>[^\n"]+)'/m.freeze
28
+ GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none --quiet (?<url>[^\s]+).* /
29
+ GIT_REFERENCE_NOT_FOUND_REGEX = /Did not find branch or tag '(?<tag>[^\n"]+)'/m
31
30
  NATIVE_COMPILATION_ERROR =
32
31
  "pip._internal.exceptions.InstallationSubprocessError: Command errored out with exit status 1:"
33
32
  # See https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata
34
- PYTHON_PACKAGE_NAME_REGEX = /[A-Za-z0-9_\-]+/.freeze
33
+ PYTHON_PACKAGE_NAME_REGEX = /[A-Za-z0-9_\-]+/
35
34
  RESOLUTION_IMPOSSIBLE_ERROR = "ResolutionImpossible"
36
- ERROR_REGEX = /(?<=ERROR\:\W).*$/.freeze
35
+ ERROR_REGEX = /(?<=ERROR\:\W).*$/
37
36
 
38
37
  attr_reader :dependency, :dependency_files, :credentials
39
38
 
@@ -72,7 +71,7 @@ module Dependabot
72
71
  SharedHelpers.in_a_temporary_directory do
73
72
  SharedHelpers.with_git_configured(credentials: credentials) do
74
73
  write_temporary_dependency_files(updated_req: requirement)
75
- install_required_python
74
+ Helpers.install_required_python(python_version)
76
75
 
77
76
  filenames_to_compile.each do |filename|
78
77
  # Shell out to pip-compile.
@@ -80,9 +79,17 @@ module Dependabot
80
79
  run_pip_compile_command(
81
80
  "pyenv exec pip-compile -v #{pip_compile_options(filename)} -P #{dependency.name} #{filename}"
82
81
  )
83
- # Run pip-compile a second time, without an update argument,
84
- # to ensure it handles markers correctly
85
- write_original_manifest_files unless dependency.top_level?
82
+
83
+ next if dependency.top_level?
84
+
85
+ # Run pip-compile a second time for transient dependencies
86
+ # to make sure we do not update dependencies that are
87
+ # superfluous. pip-compile does not detect these when
88
+ # updating a specific dependency with the -P option.
89
+ # Running pip-compile a second time will automatically remove
90
+ # superfluous dependencies. Dependabot then marks those with
91
+ # update_not_possible.
92
+ write_original_manifest_files
86
93
  run_pip_compile_command(
87
94
  "pyenv exec pip-compile #{pip_compile_options(filename)} #{filename}"
88
95
  )
@@ -313,15 +320,6 @@ module Dependabot
313
320
  end
314
321
  end
315
322
 
316
- def install_required_python
317
- return if run_command("pyenv versions").include?("#{python_version}\n")
318
-
319
- run_command("pyenv install -s #{python_version}")
320
- run_command("pyenv exec pip install --upgrade pip")
321
- run_command("pyenv exec pip install -r" \
322
- "#{NativeHelpers.python_requirements_path}")
323
- end
324
-
325
323
  def sanitized_setup_file_content(file)
326
324
  @sanitized_setup_file_content ||= {}
327
325
  return @sanitized_setup_file_content[file.name] if @sanitized_setup_file_content[file.name]
@@ -30,21 +30,18 @@ module Dependabot
30
30
  # still better than nothing, though.
31
31
  class PipenvVersionResolver
32
32
  # rubocop:disable Layout/LineLength
33
- GIT_DEPENDENCY_UNREACHABLE_REGEX =
34
- /git clone -q (?<url>[^\s]+).* /.freeze
35
- GIT_REFERENCE_NOT_FOUND_REGEX =
36
- %r{git checkout -q (?<tag>[^\n"]+)\n?[^\n]*/(?<name>.*?)(\\n'\]|$)}m.
37
- freeze
33
+ GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone -q (?<url>[^\s]+).* /
34
+ GIT_REFERENCE_NOT_FOUND_REGEX = %r{git checkout -q (?<tag>[^\n"]+)\n?[^\n]*/(?<name>.*?)(\\n'\]|$)}m
38
35
  PIPENV_INSTALLATION_ERROR = "pipenv.patched.notpip._internal.exceptions.InstallationError: Command errored out" \
39
36
  " with exit status 1: python setup.py egg_info"
40
37
  TRACEBACK = "Traceback (most recent call last):"
41
38
  PIPENV_INSTALLATION_ERROR_REGEX =
42
- /#{Regexp.quote(TRACEBACK)}[\s\S]*^\s+import\s(?<name>.+)[\s\S]*^#{Regexp.quote(PIPENV_INSTALLATION_ERROR)}/.
43
- freeze
39
+ /#{Regexp.quote(TRACEBACK)}[\s\S]*^\s+import\s(?<name>.+)[\s\S]*^#{Regexp.quote(PIPENV_INSTALLATION_ERROR)}/
40
+
44
41
  UNSUPPORTED_DEPS = %w(pyobjc).freeze
45
42
  UNSUPPORTED_DEP_REGEX =
46
- /Could not find a version that satisfies the requirement.*(?:#{UNSUPPORTED_DEPS.join("|")})/.freeze
47
- PIPENV_RANGE_WARNING = /Warning:\sPython\s[<>].* was not found/.freeze
43
+ /Could not find a version that satisfies the requirement.*(?:#{UNSUPPORTED_DEPS.join("|")})/
44
+ PIPENV_RANGE_WARNING = /Warning:\sPython\s[<>].* was not found/
48
45
  # rubocop:enable Layout/LineLength
49
46
 
50
47
  DEPENDENCY_TYPES = %w(packages dev-packages).freeze
@@ -323,13 +320,7 @@ module Dependabot
323
320
  nil
324
321
  end
325
322
 
326
- return if run_command("pyenv versions").include?("#{python_version}\n")
327
-
328
- requirements_path = NativeHelpers.python_requirements_path
329
- run_command("pyenv install -s #{python_version}")
330
- run_command("pyenv exec pip install --upgrade pip")
331
- run_command("pyenv exec pip install -r " \
332
- "#{requirements_path}")
323
+ Helpers.install_required_python(python_version)
333
324
  end
334
325
 
335
326
  def sanitized_setup_file_content(file)
@@ -28,10 +28,14 @@ module Dependabot
28
28
  'checkout',
29
29
  '(?<tag>.+?)'
30
30
  |
31
+ Failed to checkout
32
+ (?<tag>.+?)
33
+ (?<url>.+?).git at '(?<tag>.+?)'
34
+ |
31
35
  ...Failedtoclone
32
36
  (?<url>.+?).gitat'(?<tag>.+?)',
33
37
  verifyrefexistsonremote)
34
- /x.freeze # TODO: remove the first clause and | when py3.6 support is EoL
38
+ /x # TODO: remove the first clause and | when py3.6 support is EoL
35
39
  GIT_DEPENDENCY_UNREACHABLE_REGEX = /
36
40
  (?:'\['git',
37
41
  \s+'clone',
@@ -43,7 +47,7 @@ module Dependabot
43
47
  \s+Failed\sto\sclone
44
48
  \s+(?<url>.+?),
45
49
  \s+check\syour\sgit\sconfiguration)
46
- /mx.freeze # TODO: remove the first clause and | when py3.6 support is EoL
50
+ /mx # TODO: remove the first clause and | when py3.6 support is EoL
47
51
 
48
52
  attr_reader :dependency, :dependency_files, :credentials
49
53
 
@@ -88,13 +92,11 @@ module Dependabot
88
92
  write_temporary_dependency_files(updated_req: requirement)
89
93
  add_auth_env_vars
90
94
 
91
- if python_version && !pre_installed_python?(python_version)
92
- run_poetry_command("pyenv install -s #{python_version}")
93
- run_poetry_command("pyenv exec pip install --upgrade pip")
94
- run_poetry_command(
95
- "pyenv exec pip install -r " \
96
- "#{NativeHelpers.python_requirements_path}"
97
- )
95
+ Helpers.install_required_python(python_version)
96
+
97
+ # use system git instead of the pure Python dulwich
98
+ unless python_version&.start_with?("3.6")
99
+ run_poetry_command("pyenv exec poetry config experimental.system-git-client true")
98
100
  end
99
101
 
100
102
  # Shell out to Poetry, which handles everything for us.
@@ -282,7 +284,7 @@ module Dependabot
282
284
  pyproject_object = TomlRB.parse(pyproject_content)
283
285
  poetry_object = pyproject_object.dig("tool", "poetry")
284
286
 
285
- Dependabot::Python::FileParser::PoetryFilesParser::POETRY_DEPENDENCY_TYPES.each do |type|
287
+ Dependabot::Python::FileParser::PyprojectFilesParser::POETRY_DEPENDENCY_TYPES.each do |type|
286
288
  names = poetry_object[type]&.keys || []
287
289
  pkg_name = names.find { |nm| normalise(nm) == dependency.name }
288
290
  next unless pkg_name
@@ -335,7 +337,7 @@ module Dependabot
335
337
  stdout, process = Open3.capture2e(command)
336
338
  time_taken = Time.now - start
337
339
 
338
- # Raise an error with the output from the shell session if Pipenv
340
+ # Raise an error with the output from the shell session if poetry
339
341
  # returns a non-zero status
340
342
  return if process.success?
341
343
 
@@ -9,8 +9,8 @@ module Dependabot
9
9
  module Python
10
10
  class UpdateChecker
11
11
  class RequirementsUpdater
12
- PYPROJECT_OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/.freeze
13
- PYPROJECT_SEPARATOR = /#{PYPROJECT_OR_SEPARATOR}|,/.freeze
12
+ PYPROJECT_OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/
13
+ PYPROJECT_SEPARATOR = /#{PYPROJECT_OR_SEPARATOR}|,/
14
14
 
15
15
  class UnfixableRequirement < StandardError; end
16
16
 
@@ -175,11 +175,25 @@ module Dependabot
175
175
  end
176
176
  # rubocop:enable Metrics/PerceivedComplexity
177
177
 
178
- # rubocop:disable Metrics/PerceivedComplexity
179
178
  def updated_requirement(req)
180
179
  return req unless latest_resolvable_version
181
180
  return req unless req.fetch(:requirement)
182
181
 
182
+ case update_strategy
183
+ when :bump_versions
184
+ update_requirement(req)
185
+ when :bump_versions_if_necessary
186
+ update_requirement_if_needed(req)
187
+ end
188
+ end
189
+
190
+ def update_requirement_if_needed(req)
191
+ return req if new_version_satisfies?(req)
192
+
193
+ update_requirement(req)
194
+ end
195
+
196
+ def update_requirement(req)
183
197
  requirement_strings = req[:requirement].split(",").map(&:strip)
184
198
 
185
199
  new_requirement =
@@ -197,7 +211,6 @@ module Dependabot
197
211
  rescue UnfixableRequirement
198
212
  req.merge(requirement: :unfixable)
199
213
  end
200
- # rubocop:enable Metrics/PerceivedComplexity
201
214
 
202
215
  def new_version_satisfies?(req)
203
216
  requirement_class.
@@ -26,7 +26,7 @@ module Dependabot
26
26
  https://pypi.python.org/simple/
27
27
  https://pypi.org/simple/
28
28
  ).freeze
29
- VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/.freeze
29
+ VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/
30
30
 
31
31
  def latest_version
32
32
  @latest_version ||= fetch_latest_version
@@ -89,7 +89,7 @@ module Dependabot
89
89
 
90
90
  def updated_requirements
91
91
  RequirementsUpdater.new(
92
- requirements: dependency.requirements,
92
+ requirements: requirements,
93
93
  latest_resolvable_version: preferred_resolvable_version&.to_s,
94
94
  update_strategy: requirements_update_strategy,
95
95
  has_lockfile: !(pipfile_lock || poetry_lock || pyproject_lock).nil?
@@ -100,8 +100,8 @@ module Dependabot
100
100
  # If passed in as an option (in the base class) honour that option
101
101
  return @requirements_update_strategy.to_sym if @requirements_update_strategy
102
102
 
103
- # Otherwise, check if this is a poetry library or not
104
- poetry_library? ? :widen_ranges : :bump_versions
103
+ # Otherwise, check if this is a library or not
104
+ library? ? :widen_ranges : :bump_versions
105
105
  end
106
106
 
107
107
  private
@@ -115,6 +115,17 @@ module Dependabot
115
115
  raise NotImplementedError
116
116
  end
117
117
 
118
+ def preferred_version_resolvable_with_unlock?
119
+ # Our requirements file updater doesn't currently support widening
120
+ # ranges, so avoid updating this dependency if widening ranges has been
121
+ # required and the dependency is present on a requirements file.
122
+ # Otherwise, we will crash later on. TODO: Consider what the correct
123
+ # behavior is in these cases.
124
+ return false if requirements_update_strategy == :widen_ranges && updating_requirements_file?
125
+
126
+ super
127
+ end
128
+
118
129
  def fetch_lowest_resolvable_security_fix_version
119
130
  fix_version = lowest_security_fix_version
120
131
  return latest_resolvable_version if fix_version.nil?
@@ -133,8 +144,7 @@ module Dependabot
133
144
  end
134
145
 
135
146
  def resolver_type
136
- reqs = dependency.requirements
137
- req_files = reqs.map { |r| r.fetch(:file) }
147
+ reqs = requirements
138
148
 
139
149
  # If there are no requirements then this is a sub-dependency. It
140
150
  # must come from one of Pipenv, Poetry or pip-tools, and can't come
@@ -143,9 +153,9 @@ module Dependabot
143
153
 
144
154
  # Otherwise, this is a top-level dependency, and we can figure out
145
155
  # which resolver to use based on the filename of its requirements
146
- return :pipenv if req_files.any?("Pipfile")
147
- return :poetry if req_files.any?("pyproject.toml")
148
- return :pip_compile if req_files.any? { |f| f.end_with?(".in") }
156
+ return :pipenv if updating_pipfile?
157
+ return pyproject_resolver if updating_pyproject?
158
+ return :pip_compile if updating_in_file?
149
159
 
150
160
  if dependency.version && !exact_requirement?(reqs)
151
161
  subdependency_resolver
@@ -162,6 +172,12 @@ module Dependabot
162
172
  raise "Claimed to be a sub-dependency, but no lockfile exists!"
163
173
  end
164
174
 
175
+ def pyproject_resolver
176
+ return :poetry if poetry_based?
177
+
178
+ :requirements
179
+ end
180
+
165
181
  def exact_requirement?(reqs)
166
182
  reqs = reqs.map { |r| r.fetch(:requirement) }
167
183
  reqs = reqs.compact
@@ -202,16 +218,14 @@ module Dependabot
202
218
  end
203
219
 
204
220
  def current_requirement_string
205
- reqs = dependency.requirements
221
+ reqs = requirements
206
222
  return if reqs.none?
207
223
 
208
- requirement =
209
- case resolver_type
210
- when :pipenv then reqs.find { |r| r[:file] == "Pipfile" }
211
- when :poetry then reqs.find { |r| r[:file] == "pyproject.toml" }
212
- when :pip_compile then reqs.find { |r| r[:file].end_with?(".in") }
213
- when :requirements then reqs.find { |r| r[:file].end_with?(".txt") }
214
- end
224
+ requirement = reqs.find do |r|
225
+ file = r[:file]
226
+
227
+ file == "Pipfile" || file == "pyproject.toml" || file.end_with?(".in") || file.end_with?(".txt")
228
+ end
215
229
 
216
230
  requirement&.fetch(:requirement)
217
231
  end
@@ -236,7 +250,7 @@ module Dependabot
236
250
  return ">= #{dependency.version}" if dependency.version
237
251
 
238
252
  version_for_requirement =
239
- dependency.requirements.filter_map { |r| r[:requirement] }.
253
+ requirements.filter_map { |r| r[:requirement] }.
240
254
  reject { |req_string| req_string.start_with?("<") }.
241
255
  select { |req_string| req_string.match?(VERSION_REGEX) }.
242
256
  map { |req_string| req_string.match(VERSION_REGEX) }.
@@ -261,26 +275,53 @@ module Dependabot
261
275
  )
262
276
  end
263
277
 
264
- def poetry_library?
265
- return false unless pyproject
278
+ def poetry_based?
279
+ updating_pyproject? && !poetry_details.nil?
280
+ end
281
+
282
+ def library?
283
+ return unless updating_pyproject?
266
284
 
267
285
  # Hit PyPi and check whether there are details for a library with a
268
286
  # matching name and description
269
- details = TomlRB.parse(pyproject.content).dig("tool", "poetry")
270
- return false unless details
271
-
272
287
  index_response = Dependabot::RegistryClient.get(
273
- url: "https://pypi.org/pypi/#{normalised_name(details['name'])}/json/"
288
+ url: "https://pypi.org/pypi/#{normalised_name(library_details['name'])}/json/"
274
289
  )
275
290
 
276
291
  return false unless index_response.status == 200
277
292
 
278
293
  pypi_info = JSON.parse(index_response.body)["info"] || {}
279
- pypi_info["summary"] == details["description"]
294
+ pypi_info["summary"] == library_details["description"]
295
+ rescue Excon::Error::Timeout
296
+ false
280
297
  rescue URI::InvalidURIError
281
298
  false
282
299
  end
283
300
 
301
+ def updating_pipfile?
302
+ requirement_files.any?("Pipfile")
303
+ end
304
+
305
+ def updating_pyproject?
306
+ requirement_files.any?("pyproject.toml")
307
+ end
308
+
309
+ def updating_in_file?
310
+ requirement_files.any? { |f| f.end_with?(".in") }
311
+ end
312
+
313
+ def updating_requirements_file?
314
+ requirement_files.any? { |f| f =~ /\.txt$|\.in$/ }
315
+ end
316
+
317
+ def requirement_files
318
+ requirements.map { |r| r.fetch(:file) }
319
+ end
320
+
321
+ def requirements
322
+ dependency.requirements
323
+ end
324
+
284
325
  def normalised_name(name)
285
326
  NameNormaliser.normalise(name)
286
327
  end
@@ -305,6 +346,22 @@ module Dependabot
305
346
  dependency_files.find { |f| f.name == "poetry.lock" }
306
347
  end
307
348
 
349
+ def library_details
350
+ @library_details ||= poetry_details || standard_details
351
+ end
352
+
353
+ def poetry_details
354
+ @poetry_details ||= toml_content.dig("tool", "poetry")
355
+ end
356
+
357
+ def standard_details
358
+ @standard_details ||= toml_content["project"]
359
+ end
360
+
361
+ def toml_content
362
+ @toml_content ||= TomlRB.parse(pyproject.content)
363
+ end
364
+
308
365
  def pip_compile_files
309
366
  dependency_files.select { |f| f.name.end_with?(".in") }
310
367
  end
@@ -16,9 +16,9 @@ module Dependabot
16
16
 
17
17
  # See https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
18
18
  VERSION_PATTERN = 'v?([1-9][0-9]*!)?[0-9]+[0-9a-zA-Z]*(?>\.[0-9a-zA-Z]+)*' \
19
- '(-[0-9A-Za-z-]+(\.[0-9a-zA-Z-]+)*)?' \
19
+ '(-[0-9A-Za-z]+(\.[0-9a-zA-Z]+)*)?' \
20
20
  '(\+[0-9a-zA-Z]+(\.[0-9a-zA-Z]+)*)?'
21
- ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/.freeze
21
+ ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/
22
22
 
23
23
  def self.correct?(version)
24
24
  return false if version.nil?