class Dependabot::Python::UpdateChecker::RequirementsUpdater
Constants
- PYPROJECT_OR_SEPARATOR
- PYPROJECT_SEPARATOR
Attributes
has_lockfile[R]
latest_resolvable_version[R]
requirements[R]
update_strategy[R]
Public Class Methods
new(requirements:, update_strategy:, has_lockfile:, latest_resolvable_version:)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 20 def initialize(requirements:, update_strategy:, has_lockfile:, latest_resolvable_version:) @requirements = requirements @update_strategy = update_strategy @has_lockfile = has_lockfile return unless latest_resolvable_version @latest_resolvable_version = Python::Version.new(latest_resolvable_version) end
Public Instance Methods
updated_requirements()
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 32 def updated_requirements requirements.map do |req| case req[:file] when /setup\.(?:py|cfg)$/ then updated_setup_requirement(req) when "pyproject.toml" then updated_pyproject_requirement(req) when "Pipfile" then updated_pipfile_requirement(req) when /\.txt$|\.in$/ then updated_requirement(req) else raise "Unexpected filename: #{req[:file]}" end end end
Private Instance Methods
add_new_requirement_option(req_string)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 135 def add_new_requirement_option(req_string) option_to_copy = req_string.split(PYPROJECT_OR_SEPARATOR).last. split(PYPROJECT_SEPARATOR).first.strip operator = option_to_copy.gsub(/\d.*/, "").strip new_option = case operator when "", "==", "===" find_and_update_equality_match([option_to_copy]) when "~=", "~", "^" bump_version(option_to_copy, latest_resolvable_version.to_s) else # We don't expect to see OR conditions used with range # operators. If / when we see it, we should handle it. raise "Unexpected operator: #{operator}" end # TODO: Match source spacing "#{req_string.strip} || #{new_option.strip}" end
at_same_precision(new_version, old_version)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 225 def at_same_precision(new_version, old_version) # return new_version unless old_version.include?("*") count = old_version.split(".").count precision = old_version.split(".").index("*") || count new_version. split("."). first(count). map.with_index { |s, i| i < precision ? s : "*" }. join(".") end
bump_version(req_string, version_to_be_permitted)
click to toggle source
Updates the version in a constraint to be the given version
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 261 def bump_version(req_string, version_to_be_permitted) old_version = req_string. match(/(#{RequirementParser::VERSION})/). captures.first req_string.sub( old_version, at_same_precision(version_to_be_permitted, old_version) ) end
convert_to_range(req_string, version_to_be_permitted)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 272 def convert_to_range(req_string, version_to_be_permitted) # Construct an upper bound at the same precision that the original # requirement was at (taking into account ~ dynamics) index_to_update = index_to_update_for(req_string) ub_segments = version_to_be_permitted.segments ub_segments << 0 while ub_segments.count <= index_to_update ub_segments = ub_segments[0..index_to_update] ub_segments[index_to_update] += 1 lb_segments = lower_bound_segments_for_req(req_string) # Ensure versions have the same length as each other (cosmetic) length = [lb_segments.count, ub_segments.count].max lb_segments.fill(0, lb_segments.count...length) ub_segments.fill(0, ub_segments.count...length) ">=#{lb_segments.join('.')},<#{ub_segments.join('.')}" end
find_and_update_equality_match(requirement_strings)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 208 def find_and_update_equality_match(requirement_strings) if requirement_strings.any? { |r| requirement_class.new(r).exact? } # True equality match requirement_strings.find { |r| requirement_class.new(r).exact? }. sub( RequirementParser::VERSION, latest_resolvable_version.to_s ) else # Prefix match requirement_strings.find { |r| r.match?(/^(=+|\d)/) }. sub(RequirementParser::VERSION) do |v| at_same_precision(latest_resolvable_version.to_s, v) end end end
index_to_update_for(req_string)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 302 def index_to_update_for(req_string) req = requirement_class.new(req_string.split(/[.\-]\*/).first) version = req.requirements.first.last.release if req_string.strip.start_with?("^") version.segments.index { |i| i != 0 } elsif req_string.include?("*") version.segments.count - 1 elsif req_string.strip.start_with?("~=", "==") version.segments.count - 2 elsif req_string.strip.start_with?("~") req_string.split(".").count == 1 ? 0 : 1 else raise "Don't know how to convert #{req_string} to range" end end
lower_bound_segments_for_req(req_string)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 291 def lower_bound_segments_for_req(req_string) requirement = requirement_class.new(req_string) version = requirement.requirements.first.last version = version.release if version.prerelease? lb_segments = version.segments lb_segments.pop while lb_segments.last.zero? lb_segments end
new_version_satisfies?(req)
click to toggle source
rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 202 def new_version_satisfies?(req) requirement_class. requirements_array(req.fetch(:requirement)). any? { |r| r.satisfied_by?(latest_resolvable_version) } end
requirement_class()
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 345 def requirement_class Python::Requirement end
update_greatest_version(req_string, version_to_be_permitted)
click to toggle source
Updates the version in a “<” or “<=” constraint to allow the given version
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 320 def update_greatest_version(req_string, version_to_be_permitted) if version_to_be_permitted.is_a?(String) version_to_be_permitted = Python::Version.new(version_to_be_permitted) end version = Python::Version.new(req_string.gsub(/<=?/, "")) version = version.release if version.prerelease? index_to_update = [ version.segments.map.with_index { |n, i| n.zero? ? 0 : i }.max, version_to_be_permitted.segments.count - 1 ].min new_segments = version.segments.map.with_index do |_, index| if index < index_to_update version_to_be_permitted.segments[index] elsif index == index_to_update version_to_be_permitted.segments[index] + 1 else 0 end end new_segments.join(".") end
update_pyproject_version(req)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 97 def update_pyproject_version(req) requirement_strings = req[:requirement].split(",").map(&:strip) new_requirement = if requirement_strings.any? { |r| r.match?(/^=|^\d/) } # If there is an equality operator, just update that. It must # be binding and any other requirements will be being ignored find_and_update_equality_match(requirement_strings) elsif requirement_strings.any? { |r| r.start_with?("~", "^") } # If a compatibility operator is being used, just bump its # version (and remove any other requirements) v_req = requirement_strings.find { |r| r.start_with?("~", "^") } bump_version(v_req, latest_resolvable_version.to_s) elsif new_version_satisfies?(req) # Otherwise we're looking at a range operator. No change # required if it's already satisfied req.fetch(:requirement) else # But if it's not, update it update_requirements_range(requirement_strings) end req.merge(requirement: new_requirement) end
update_requirements_range(requirement_strings)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 238 def update_requirements_range(requirement_strings) ruby_requirements = requirement_strings.map { |r| requirement_class.new(r) } updated_requirement_strings = ruby_requirements.flat_map do |r| next r.to_s if r.satisfied_by?(latest_resolvable_version) case op = r.requirements.first.first when "<", "<=" "<" + update_greatest_version(r.to_s, latest_resolvable_version) when "!=", ">", ">=" raise UnfixableRequirement else raise "Unexpected op for unsatisfied requirement: #{op}" end end.compact updated_requirement_strings. sort_by { |r| requirement_class.new(r).requirements.first.last }. map(&:to_s).join(",").delete(" ") end
updated_pipfile_requirement(req)
click to toggle source
rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 70 def updated_pipfile_requirement(req) # For now, we just proxy to updated_requirement. In future this # method may treat Pipfile requirements differently. updated_requirement(req) end
updated_pyproject_requirement(req)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 76 def updated_pyproject_requirement(req) return req unless latest_resolvable_version return req unless req.fetch(:requirement) return req if new_version_satisfies?(req) && !has_lockfile # If the requirement uses || syntax then we always want to widen it return widen_pyproject_requirement(req) if req.fetch(:requirement).match?(PYPROJECT_OR_SEPARATOR) # If the requirement is a development dependency we always want to # bump it return update_pyproject_version(req) if req.fetch(:groups).include?("dev-dependencies") case update_strategy when :widen_ranges then widen_pyproject_requirement(req) when :bump_versions then update_pyproject_version(req) else raise "Unexpected update strategy: #{update_strategy}" end rescue UnfixableRequirement req.merge(requirement: :unfixable) end
updated_requirement(req)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 179 def updated_requirement(req) return req unless latest_resolvable_version return req unless req.fetch(:requirement) requirement_strings = req[:requirement].split(",").map(&:strip) new_requirement = if requirement_strings.any? { |r| r.match?(/^[=\d]/) } find_and_update_equality_match(requirement_strings) elsif requirement_strings.any? { |r| r.start_with?("~=") } tw_req = requirement_strings.find { |r| r.start_with?("~=") } bump_version(tw_req, latest_resolvable_version.to_s) elsif new_version_satisfies?(req) req.fetch(:requirement) else update_requirements_range(requirement_strings) end req.merge(requirement: new_requirement) rescue UnfixableRequirement req.merge(requirement: :unfixable) end
updated_setup_requirement(req)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 47 def updated_setup_requirement(req) return req unless latest_resolvable_version return req unless req.fetch(:requirement) return req if new_version_satisfies?(req) req_strings = req[:requirement].split(",").map(&:strip) new_requirement = if req_strings.any? { |r| requirement_class.new(r).exact? } find_and_update_equality_match(req_strings) elsif req_strings.any? { |r| r.start_with?("~=", "==") } tw_req = req_strings.find { |r| r.start_with?("~=", "==") } convert_to_range(tw_req, latest_resolvable_version) else update_requirements_range(req_strings) end req.merge(requirement: new_requirement) rescue UnfixableRequirement req.merge(requirement: :unfixable) end
widen_pyproject_requirement(req)
click to toggle source
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 122 def widen_pyproject_requirement(req) return req if new_version_satisfies?(req) new_requirement = if req[:requirement].match?(PYPROJECT_OR_SEPARATOR) add_new_requirement_option(req[:requirement]) else widen_requirement_range(req[:requirement]) end req.merge(requirement: new_requirement) end
widen_requirement_range(req_string)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/requirements_updater.rb, line 157 def widen_requirement_range(req_string) requirement_strings = req_string.split(",").map(&:strip) if requirement_strings.any? { |r| r.match?(/(^=|^\d)[^*]*$/) } # If there is an equality operator, just update that. # (i.e., assume it's being used deliberately) find_and_update_equality_match(requirement_strings) elsif requirement_strings.any? { |r| r.start_with?("~", "^") } || requirement_strings.any? { |r| r.include?("*") } # If a compatibility operator is being used, widen its # range to include the new version v_req = requirement_strings. find { |r| r.start_with?("~", "^") || r.include?("*") } convert_to_range(v_req, latest_resolvable_version) else # Otherwise we have a range, and need to update the upper bound update_requirements_range(requirement_strings) end end