class Dependabot::Python::FileParser::SetupFileParser

Constants

CLOSING_BRACKET
EXTRAS_REQUIRE_REGEX
INSTALL_REQUIRES_REGEX
SETUP_REQUIRES_REGEX
TESTS_REQUIRE_REGEX

Attributes

dependency_files[R]

Public Class Methods

new(dependency_files:) click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 22
def initialize(dependency_files:)
  @dependency_files = dependency_files
end

Public Instance Methods

dependency_set() click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 26
def dependency_set
  dependencies = Dependabot::FileParsers::Base::DependencySet.new

  parsed_setup_file.each do |dep|
    # If a requirement has a `<` or `<=` marker then updating it is
    # probably blocked. Ignore it.
    next if dep["markers"].include?("<")

    # If the requirement is our inserted version, ignore it
    # (we wouldn't be able to update it)
    next if dep["version"] == "0.0.1+dependabot"

    dependencies <<
      Dependency.new(
        name: normalised_name(dep["name"], dep["extras"]),
        version: dep["version"]&.include?("*") ? nil : dep["version"],
        requirements: [{
          requirement: dep["requirement"],
          file: Pathname.new(dep["file"]).cleanpath.to_path,
          source: nil,
          groups: [dep["requirement_type"]]
        }],
        package_manager: "pip"
      )
  end
  dependencies
end

Private Instance Methods

check_requirements(requirements) click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 100
def check_requirements(requirements)
  requirements.each do |dep|
    next unless dep["requirement"]

    Python::Requirement.new(dep["requirement"].split(","))
  rescue Gem::Requirement::BadRequirementError => e
    raise Dependabot::DependencyFileNotEvaluatable, e.message
  end
end
closing_bracket_index(string, bracket) click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 155
def closing_bracket_index(string, bracket)
  closes_required = 1

  string.chars.each_with_index do |char, index|
    closes_required += 1 if char == bracket
    closes_required -= 1 if char == CLOSING_BRACKET.fetch(bracket)
    return index if closes_required.zero?
  end

  0
end
get_regexed_req_array(regex) click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 143
def get_regexed_req_array(regex)
  return unless (mch = setup_file.content.match(regex))

  "[#{mch.post_match[0..closing_bracket_index(mch.post_match, '[')]}"
end
get_regexed_req_dict(regex) click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 149
def get_regexed_req_dict(regex)
  return unless (mch = setup_file.content.match(regex))

  "{#{mch.post_match[0..closing_bracket_index(mch.post_match, '{')]}"
end
normalised_name(name, extras) click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 167
def normalised_name(name, extras)
  NameNormaliser.normalise_including_extras(name, extras)
end
parsed_sanitized_setup_file() click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 79
def parsed_sanitized_setup_file
  SharedHelpers.in_a_temporary_directory do
    write_sanitized_setup_file

    requirements = SharedHelpers.run_helper_subprocess(
      command: "pyenv exec python #{NativeHelpers.python_helper_path}",
      function: "parse_setup",
      args: [Dir.pwd]
    )

    check_requirements(requirements)
    requirements
  end
rescue SharedHelpers::HelperSubprocessFailed
  # Assume there are no dependencies in setup.py files that fail to
  # parse. This isn't ideal, and we should continue to improve
  # parsing, but there are a *lot* of things that can go wrong at
  # the moment!
  []
end
parsed_setup_file() click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 58
def parsed_setup_file
  SharedHelpers.in_a_temporary_directory do
    write_temporary_dependency_files

    requirements = SharedHelpers.run_helper_subprocess(
      command: "pyenv exec python #{NativeHelpers.python_helper_path}",
      function: "parse_setup",
      args: [Dir.pwd]
    )

    check_requirements(requirements)
    requirements
  end
rescue SharedHelpers::HelperSubprocessFailed => e
  raise Dependabot::DependencyFileNotEvaluatable, e.message if e.message.start_with?("InstallationError")

  return [] unless setup_file

  parsed_sanitized_setup_file
end
setup_file() click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 171
def setup_file
  dependency_files.find { |f| f.name == "setup.py" }
end
write_sanitized_setup_file() click to toggle source

Write a setup.py with only entries for the requires fields.

This sanitization is far from perfect (it will fail if any of the entries are dynamic), but it is an alternative approach to the one used in parser.py which sometimes succeeds when that has failed.

# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 125
def write_sanitized_setup_file
  install_requires = get_regexed_req_array(INSTALL_REQUIRES_REGEX)
  setup_requires = get_regexed_req_array(SETUP_REQUIRES_REGEX)
  tests_require = get_regexed_req_array(TESTS_REQUIRE_REGEX)
  extras_require = get_regexed_req_dict(EXTRAS_REQUIRE_REGEX)

  tmp = "from setuptools import setup\n\n"\
        "setup(name=\"sanitized-package\",version=\"0.0.1\","

  tmp += "install_requires=#{install_requires}," if install_requires
  tmp += "setup_requires=#{setup_requires}," if setup_requires
  tmp += "tests_require=#{tests_require}," if tests_require
  tmp += "extras_require=#{extras_require}," if extras_require
  tmp += ")"

  File.write("setup.py", tmp)
end
write_temporary_dependency_files() click to toggle source
# File lib/dependabot/python/file_parser/setup_file_parser.rb, line 110
def write_temporary_dependency_files
  dependency_files.
    reject { |f| f.name == ".python-version" }.
    each do |file|
      path = file.name
      FileUtils.mkdir_p(Pathname.new(path).dirname)
      File.write(path, file.content)
    end
end