class Dependabot::Python::FileParser

Constants

DEPENDENCY_GROUP_KEYS
POETRY_DEPENDENCY_TYPES
REQUIREMENT_FILE_EVALUATION_ERRORS

Public Instance Methods

parse() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 38
def parse
  # TODO: setup.py from external dependencies is evaluated. Provide guards before removing this.
  raise Dependabot::UnexpectedExternalCode if @reject_external_code

  dependency_set = DependencySet.new

  dependency_set += pipenv_dependencies if pipfile
  dependency_set += poetry_dependencies if using_poetry?
  dependency_set += requirement_dependencies if requirement_files.any?
  dependency_set += setup_file_dependencies if setup_file || setup_cfg_file

  dependency_set.dependencies
end

Private Instance Methods

blocking_marker?(dep) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 125
def blocking_marker?(dep)
  return false if dep["markers"] == "None"
  return true if dep["markers"].include?("<")
  return false if dep["markers"].include?(">")

  dep["requirement"]&.include?("<")
end
check_required_files() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 204
def check_required_files
  filenames = dependency_files.map(&:name)
  return if filenames.any? { |name| name.end_with?(".txt", ".in") }
  return if pipfile
  return if pyproject
  return if setup_file
  return if setup_cfg_file

  raise "Missing required files!"
end
check_requirements(requirements) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 171
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
group_from_filename(filename) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 107
def group_from_filename(filename)
  if filename.include?("dev") then ["dev-dependencies"]
  else ["dependencies"]
  end
end
included_in_pipenv_deps?(dep_name) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 113
def included_in_pipenv_deps?(dep_name)
  return false unless pipfile

  pipenv_dependencies.dependencies.map(&:name).include?(dep_name)
end
included_in_poetry_deps?(dep_name) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 119
def included_in_poetry_deps?(dep_name)
  return false unless using_poetry?

  poetry_dependencies.dependencies.map(&:name).include?(dep_name)
end
lockfile_for_pip_compile_file?(filename) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 140
def lockfile_for_pip_compile_file?(filename)
  return false unless pip_compile_files.any?
  return false unless filename.end_with?(".txt")

  file = dependency_files.find { |f| f.name == filename }
  return true if file&.content&.match?(output_file_regex(filename))

  basename = filename.gsub(/\.txt$/, "")
  pip_compile_files.any? { |f| f.name == basename + ".in" }
end
normalised_name(name, extras = []) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 200
def normalised_name(name, extras = [])
  NameNormaliser.normalise_including_extras(name, extras)
end
output_file_regex(filename) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 232
def output_file_regex(filename)
  "--output-file[=\s]+#{Regexp.escape(filename)}(?:\s|$)"
end
parsed_requirement_files() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 151
def parsed_requirement_files
  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_requirements",
      args: [Dir.pwd]
    )

    check_requirements(requirements)
    requirements
  end
rescue SharedHelpers::HelperSubprocessFailed => e
  evaluation_errors = REQUIREMENT_FILE_EVALUATION_ERRORS
  raise unless e.message.start_with?(*evaluation_errors)

  raise Dependabot::DependencyFileNotEvaluatable, e.message
end
pip_compile_files() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 256
def pip_compile_files
  @pip_compile_files ||=
    dependency_files.select { |f| f.name.end_with?(".in") }
end
pipenv_dependencies() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 58
def pipenv_dependencies
  @pipenv_dependencies ||=
    PipfileFilesParser.
    new(dependency_files: dependency_files).
    dependency_set
end
pipfile() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 215
def pipfile
  @pipfile ||= get_original_file("Pipfile")
end
pipfile_lock() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 219
def pipfile_lock
  @pipfile_lock ||= get_original_file("Pipfile.lock")
end
poetry_dependencies() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 65
def poetry_dependencies
  @poetry_dependencies ||=
    PoetryFilesParser.
    new(dependency_files: dependency_files).
    dependency_set
end
poetry_lock() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 244
def poetry_lock
  @poetry_lock ||= get_original_file("poetry.lock")
end
pyproject() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 236
def pyproject
  @pyproject ||= get_original_file("pyproject.toml")
end
pyproject_lock() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 240
def pyproject_lock
  @pyproject_lock ||= get_original_file("pyproject.lock")
end
remove_imports(file) click to toggle source
# File lib/dependabot/python/file_parser.rb, line 191
def remove_imports(file)
  return file.content if file.path.end_with?(".tar.gz", ".whl", ".zip")

  file.content.lines.
    reject { |l| l.match?(/^['"]?(?<path>\..*?)(?=\[|#|'|"|$)/) }.
    reject { |l| l.match?(/^(?:-e)\s+['"]?(?<path>.*?)(?=\[|#|'|"|$)/) }.
    join
end
requirement_dependencies() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 72
def requirement_dependencies
  dependencies = DependencySet.new
  parsed_requirement_files.each do |dep|
    # This isn't ideal, but currently the FileUpdater won't update
    # deps that appear in a requirements.txt and Pipenv / Poetry
    # and *aren't* a straight lockfile for Pipenv / Poetry
    next if included_in_pipenv_deps?(normalised_name(dep["name"]))
    next if included_in_poetry_deps?(normalised_name(dep["name"]))

    # If a requirement has a `<`, `<=` or '==' marker then updating it is
    # probably blocked. Ignore it.
    next if blocking_marker?(dep)

    requirements =
      if lockfile_for_pip_compile_file?(dep["file"]) then []
      else
        [{
          requirement: dep["requirement"],
          file: Pathname.new(dep["file"]).cleanpath.to_path,
          source: nil,
          groups: group_from_filename(dep["file"])
        }]
      end

    dependencies <<
      Dependency.new(
        name: normalised_name(dep["name"], dep["extras"]),
        version: dep["version"]&.include?("*") ? nil : dep["version"],
        requirements: requirements,
        package_manager: "pip"
      )
  end
  dependencies
end
requirement_files() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 54
def requirement_files
  dependency_files.select { |f| f.name.end_with?(".txt", ".in") }
end
setup_cfg_file() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 252
def setup_cfg_file
  @setup_cfg_file ||= get_original_file("setup.cfg")
end
setup_file() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 248
def setup_file
  @setup_file ||= get_original_file("setup.py")
end
setup_file_dependencies() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 133
def setup_file_dependencies
  @setup_file_dependencies ||=
    SetupFileParser.
    new(dependency_files: dependency_files).
    dependency_set
end
using_poetry?() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 223
def using_poetry?
  return false unless pyproject
  return true if poetry_lock || pyproject_lock

  !TomlRB.parse(pyproject.content).dig("tool", "poetry").nil?
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
  raise Dependabot::DependencyFileNotParseable, pyproject.path
end
write_temporary_dependency_files() click to toggle source
# File lib/dependabot/python/file_parser.rb, line 181
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, remove_imports(file))
    end
end