dependabot-python 0.212.0 → 0.213.0

Sign up to get free protection for your applications and to get access to all the features.
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?