class Dependabot::Python::UpdateChecker

Constants

MAIN_PYPI_INDEXES
VERSION_REGEX

Public Instance Methods

latest_resolvable_version() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 35
def latest_resolvable_version
  @latest_resolvable_version ||=
    case resolver_type
    when :pipenv
      pipenv_version_resolver.latest_resolvable_version(
        requirement: unlocked_requirement_string
      )
    when :poetry
      poetry_version_resolver.latest_resolvable_version(
        requirement: unlocked_requirement_string
      )
    when :pip_compile
      pip_compile_version_resolver.latest_resolvable_version(
        requirement: unlocked_requirement_string
      )
    when :requirements
      pip_version_resolver.latest_resolvable_version
    else raise "Unexpected resolver type #{resolver_type}"
    end
end
latest_resolvable_version_with_no_unlock() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 56
def latest_resolvable_version_with_no_unlock
  @latest_resolvable_version_with_no_unlock ||=
    case resolver_type
    when :pipenv
      pipenv_version_resolver.latest_resolvable_version(
        requirement: current_requirement_string
      )
    when :poetry
      poetry_version_resolver.latest_resolvable_version(
        requirement: current_requirement_string
      )
    when :pip_compile
      pip_compile_version_resolver.latest_resolvable_version(
        requirement: current_requirement_string
      )
    when :requirements
      pip_version_resolver.latest_resolvable_version_with_no_unlock
    else raise "Unexpected resolver type #{resolver_type}"
    end
end
latest_version() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 31
def latest_version
  @latest_version ||= fetch_latest_version
end
lowest_resolvable_security_fix_version() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 81
def lowest_resolvable_security_fix_version
  raise "Dependency not vulnerable!" unless vulnerable?

  return @lowest_resolvable_security_fix_version if defined?(@lowest_resolvable_security_fix_version)

  @lowest_resolvable_security_fix_version =
    fetch_lowest_resolvable_security_fix_version
end
lowest_security_fix_version() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 77
def lowest_security_fix_version
  latest_version_finder.lowest_security_fix_version
end
requirements_update_strategy() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 99
def requirements_update_strategy
  # If passed in as an option (in the base class) honour that option
  return @requirements_update_strategy.to_sym if @requirements_update_strategy

  # Otherwise, check if this is a poetry library or not
  poetry_library? ? :widen_ranges : :bump_versions
end
updated_requirements() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 90
def updated_requirements
  RequirementsUpdater.new(
    requirements: dependency.requirements,
    latest_resolvable_version: preferred_resolvable_version&.to_s,
    update_strategy: requirements_update_strategy,
    has_lockfile: !(pipfile_lock || poetry_lock || pyproject_lock).nil?
  ).updated_requirements
end

Private Instance Methods

current_requirement_string() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 209
def current_requirement_string
  reqs = dependency.requirements
  return if reqs.none?

  requirement =
    case resolver_type
    when :pipenv then reqs.find { |r| r[:file] == "Pipfile" }
    when :poetry then reqs.find { |r| r[:file] == "pyproject.toml" }
    when :pip_compile then reqs.find { |r| r[:file].end_with?(".in") }
    when :requirements then reqs.find { |r| r[:file].end_with?(".txt") }
    end

  requirement&.fetch(:requirement)
end
exact_requirement?(reqs) click to toggle source
# File lib/dependabot/python/update_checker.rb, line 170
def exact_requirement?(reqs)
  reqs = reqs.map { |r| r.fetch(:requirement) }
  reqs = reqs.compact
  reqs = reqs.flat_map { |r| r.split(",").map(&:strip) }
  reqs.any? { |r| Python::Requirement.new(r).exact? }
end
fetch_latest_version() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 254
def fetch_latest_version
  latest_version_finder.latest_version
end
fetch_lowest_resolvable_security_fix_version() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 121
def fetch_lowest_resolvable_security_fix_version
  fix_version = lowest_security_fix_version
  return latest_resolvable_version if fix_version.nil?

  return pip_version_resolver.lowest_resolvable_security_fix_version if resolver_type == :requirements

  resolver =
    case resolver_type
    when :pip_compile then pip_compile_version_resolver
    when :pipenv then pipenv_version_resolver
    when :poetry then poetry_version_resolver
    else raise "Unexpected resolver type #{resolver_type}"
    end

  resolver.resolvable?(version: fix_version) ? fix_version : nil
end
latest_version_finder() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 258
def latest_version_finder
  @latest_version_finder ||= LatestVersionFinder.new(
    dependency: dependency,
    dependency_files: dependency_files,
    credentials: credentials,
    ignored_versions: ignored_versions,
    raise_on_ignored: @raise_on_ignored,
    security_advisories: security_advisories
  )
end
latest_version_resolvable_with_full_unlock?() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 109
def latest_version_resolvable_with_full_unlock?
  # Full unlock checks aren't implemented for pip because they're not
  # relevant (pip doesn't have a resolver). This method always returns
  # false to ensure `updated_dependencies_after_full_unlock` is never
  # called.
  false
end
normalised_name(name) click to toggle source
# File lib/dependabot/python/update_checker.rb, line 291
def normalised_name(name)
  NameNormaliser.normalise(name)
end
pip_compile_files() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 315
def pip_compile_files
  dependency_files.select { |f| f.name.end_with?(".in") }
end
pip_compile_version_resolver() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 181
def pip_compile_version_resolver
  @pip_compile_version_resolver ||=
    PipCompileVersionResolver.new(**resolver_args)
end
pip_version_resolver() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 190
def pip_version_resolver
  @pip_version_resolver ||= PipVersionResolver.new(
    dependency: dependency,
    dependency_files: dependency_files,
    credentials: credentials,
    ignored_versions: ignored_versions,
    raise_on_ignored: @raise_on_ignored,
    security_advisories: security_advisories
  )
end
pipenv_version_resolver() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 177
def pipenv_version_resolver
  @pipenv_version_resolver ||= PipenvVersionResolver.new(**resolver_args)
end
pipfile() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 295
def pipfile
  dependency_files.find { |f| f.name == "Pipfile" }
end
pipfile_lock() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 299
def pipfile_lock
  dependency_files.find { |f| f.name == "Pipfile.lock" }
end
poetry_library?() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 269
def poetry_library?
  return false unless pyproject

  # Hit PyPi and check whether there are details for a library with a
  # matching name and description
  details = TomlRB.parse(pyproject.content).dig("tool", "poetry")
  return false unless details

  index_response = Excon.get(
    "https://pypi.org/pypi/#{normalised_name(details['name'])}/json/",
    idempotent: true,
    **SharedHelpers.excon_defaults
  )

  return false unless index_response.status == 200

  pypi_info = JSON.parse(index_response.body)["info"] || {}
  pypi_info["summary"] == details["description"]
rescue URI::InvalidURIError
  false
end
poetry_lock() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 311
def poetry_lock
  dependency_files.find { |f| f.name == "poetry.lock" }
end
poetry_version_resolver() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 186
def poetry_version_resolver
  @poetry_version_resolver ||= PoetryVersionResolver.new(**resolver_args)
end
pyproject() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 303
def pyproject
  dependency_files.find { |f| f.name == "pyproject.toml" }
end
pyproject_lock() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 307
def pyproject_lock
  dependency_files.find { |f| f.name == "pyproject.lock" }
end
resolver_args() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 201
def resolver_args
  {
    dependency: dependency,
    dependency_files: dependency_files,
    credentials: credentials
  }
end
resolver_type() click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/python/update_checker.rb, line 139
def resolver_type
  reqs = dependency.requirements
  req_files = reqs.map { |r| r.fetch(:file) }

  # If there are no requirements then this is a sub-dependency. It
  # must come from one of Pipenv, Poetry or pip-tools, and can't come
  # from the first two unless they have a lockfile.
  return subdependency_resolver if reqs.none?

  # Otherwise, this is a top-level dependency, and we can figure out
  # which resolver to use based on the filename of its requirements
  return :pipenv if req_files.any? { |f| f == "Pipfile" }
  return :poetry if req_files.any? { |f| f == "pyproject.toml" }
  return :pip_compile if req_files.any? { |f| f.end_with?(".in") }

  if dependency.version && !exact_requirement?(reqs)
    subdependency_resolver
  else
    :requirements
  end
end
subdependency_resolver() click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/python/update_checker.rb, line 162
def subdependency_resolver
  return :pipenv if pipfile_lock
  return :poetry if poetry_lock || pyproject_lock
  return :pip_compile if pip_compile_files.any?

  raise "Claimed to be a sub-dependency, but no lockfile exists!"
end
unlocked_requirement_string() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 224
def unlocked_requirement_string
  lower_bound_req = updated_version_req_lower_bound

  # Add the latest_version as an upper bound. This means
  # ignore conditions are considered when checking for the latest
  # resolvable version.
  #
  # NOTE: This isn't perfect. If v2.x is ignored and v3 is out but
  # unresolvable then the `latest_version` will be v3, and
  # we won't be ignoring v2.x releases like we should be.
  return lower_bound_req if latest_version.nil?
  return lower_bound_req unless Python::Version.correct?(latest_version)

  lower_bound_req + ", <= #{latest_version}"
end
updated_dependencies_after_full_unlock() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 117
def updated_dependencies_after_full_unlock
  raise NotImplementedError
end
updated_version_req_lower_bound() click to toggle source
# File lib/dependabot/python/update_checker.rb, line 240
def updated_version_req_lower_bound
  return ">= #{dependency.version}" if dependency.version

  version_for_requirement =
    dependency.requirements.map { |r| r[:requirement] }.compact.
    reject { |req_string| req_string.start_with?("<") }.
    select { |req_string| req_string.match?(VERSION_REGEX) }.
    map { |req_string| req_string.match(VERSION_REGEX) }.
    select { |version| Gem::Version.correct?(version) }.
    max_by { |version| Gem::Version.new(version) }

  ">= #{version_for_requirement || 0}"
end