class Dependabot::Python::FileUpdater::PoetryFileUpdater

Attributes

credentials[R]
dependencies[R]
dependency_files[R]

Public Class Methods

new(dependencies:, dependency_files:, credentials:) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 23
def initialize(dependencies:, dependency_files:, credentials:)
  @dependencies = dependencies
  @dependency_files = dependency_files
  @credentials = credentials
end

Public Instance Methods

updated_dependency_files() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 29
def updated_dependency_files
  return @updated_dependency_files if @update_already_attempted

  @update_already_attempted = true
  @updated_dependency_files ||= fetch_updated_dependency_files
end

Private Instance Methods

add_private_sources(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 153
def add_private_sources(pyproject_content)
  PyprojectPreparer.
    new(pyproject_content: pyproject_content).
    replace_sources(credentials)
end
create_declaration_at_new_version!(poetry_object, dep) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 148
def create_declaration_at_new_version!(poetry_object, dep)
  poetry_object[subdep_type] ||= {}
  poetry_object[subdep_type][dependency.name] = dep.version
end
declaration_regex(dep) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 271
def declaration_regex(dep)
  escaped_name = Regexp.escape(dep.name).gsub("\\-", "[-_.]")
  /(?:^\s*|["'])#{escaped_name}["']?\s*=.*$/i
end
dependency() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 38
def dependency
  # For now, we'll only ever be updating a single dependency
  dependencies.first
end
fetch_updated_dependency_files() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 43
def fetch_updated_dependency_files
  updated_files = []

  if file_changed?(pyproject)
    updated_files <<
      updated_file(
        file: pyproject,
        content: updated_pyproject_content
      )
  end

  raise "Expected lockfile to change!" if lockfile && lockfile.content == updated_lockfile_content

  if lockfile
    updated_files <<
      updated_file(file: lockfile, content: updated_lockfile_content)
  end

  updated_files
end
file_changed?(file) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 276
def file_changed?(file)
  dependencies.any? { |dep| requirement_changed?(file, dep) }
end
freeze_dependencies_being_updated(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 119
def freeze_dependencies_being_updated(pyproject_content)
  pyproject_object = TomlRB.parse(pyproject_content)
  poetry_object = pyproject_object.fetch("tool").fetch("poetry")

  dependencies.each do |dep|
    if dep.requirements.find { |r| r[:file] == pyproject.name }
      lock_declaration_to_new_version!(poetry_object, dep)
    else
      create_declaration_at_new_version!(poetry_object, dep)
    end
  end

  TomlRB.dump(pyproject_object)
end
freeze_other_dependencies(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 113
def freeze_other_dependencies(pyproject_content)
  PyprojectPreparer.
    new(pyproject_content: pyproject_content, lockfile: lockfile).
    freeze_top_level_dependencies_except(dependencies)
end
lock_declaration_to_new_version!(poetry_object, dep) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 134
def lock_declaration_to_new_version!(poetry_object, dep)
  Dependabot::Python::FileParser::PoetryFilesParser::POETRY_DEPENDENCY_TYPES.each do |type|
    names = poetry_object[type]&.keys || []
    pkg_name = names.find { |nm| normalise(nm) == dep.name }
    next unless pkg_name

    if poetry_object[type][pkg_name].is_a?(Hash)
      poetry_object[type][pkg_name]["version"] = dep.version
    else
      poetry_object[type][pkg_name] = dep.version
    end
  end
end
lockfile() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 302
def lockfile
  @lockfile ||= pyproject_lock || poetry_lock
end
normalise(name) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 293
def normalise(name)
  NameNormaliser.normalise(name)
end
poetry_lock() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 314
def poetry_lock
  dependency_files.find { |f| f.name == "poetry.lock" }
end
poetry_update_command() click to toggle source

Using `–lock` avoids doing an install. Using `–no-interaction` avoids asking for passwords.

# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 196
def poetry_update_command
  "pyenv exec poetry update #{dependency.name} --lock --no-interaction"
end
pre_installed_python?(version) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 253
def pre_installed_python?(version)
  PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.include?(version)
end
prepared_pyproject() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 101
def prepared_pyproject
  @prepared_pyproject ||=
    begin
      content = updated_pyproject_content
      content = sanitize(content)
      content = freeze_other_dependencies(content)
      content = freeze_dependencies_being_updated(content)
      content = add_private_sources(content)
      content
    end
end
pyproject() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 297
def pyproject
  @pyproject ||=
    dependency_files.find { |f| f.name == "pyproject.toml" }
end
pyproject_hash_for(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 257
def pyproject_hash_for(pyproject_content)
  SharedHelpers.in_a_temporary_directory do |dir|
    SharedHelpers.with_git_configured(credentials: credentials) do
      write_temporary_dependency_files(pyproject_content)

      SharedHelpers.run_helper_subprocess(
        command: "pyenv exec python #{python_helper_path}",
        function: "get_pyproject_hash",
        args: [dir]
      )
    end
  end
end
pyproject_lock() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 310
def pyproject_lock
  dependency_files.find { |f| f.name == "pyproject.lock" }
end
python_helper_path() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 306
def python_helper_path
  NativeHelpers.python_helper_path
end
python_requirement_parser() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 246
def python_requirement_parser
  @python_requirement_parser ||=
    FileParser::PythonRequirementParser.new(
      dependency_files: dependency_files
    )
end
python_version() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 234
def python_version
  requirements = python_requirement_parser.user_specified_requirements
  requirements = requirements.
                 map { |r| Python::Requirement.requirements_array(r) }

  PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.find do |version|
    requirements.all? do |reqs|
      reqs.any? { |r| r.satisfied_by?(Python::Version.new(version)) }
    end
  end
end
requirement_changed?(file, dependency) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 280
def requirement_changed?(file, dependency)
  changed_requirements =
    dependency.requirements - dependency.previous_requirements

  changed_requirements.any? { |f| f[:file] == file.name }
end
run_poetry_command(command) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 200
def run_poetry_command(command)
  start = Time.now
  command = SharedHelpers.escape_command(command)
  stdout, process = Open3.capture2e(command)
  time_taken = Time.now - start

  # Raise an error with the output from the shell session if Pipenv
  # returns a non-zero status
  return if process.success?

  raise SharedHelpers::HelperSubprocessFailed.new(
    message: stdout,
    error_context: {
      command: command,
      time_taken: time_taken,
      process_exit_value: process.to_s
    }
  )
end
sanitize(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 168
def sanitize(pyproject_content)
  PyprojectPreparer.
    new(pyproject_content: pyproject_content).
    sanitize
end
subdep_type() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 159
def subdep_type
  category =
    TomlRB.parse(lockfile.content).fetch("package", []).
    find { |dets| normalise(dets.fetch("name")) == dependency.name }.
    fetch("category")

  category == "dev" ? "dev-dependencies" : "dependencies"
end
updated_file(file:, content:) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 287
def updated_file(file:, content:)
  updated_file = file.dup
  updated_file.content = content
  updated_file
end
updated_lockfile_content() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 88
def updated_lockfile_content
  @updated_lockfile_content ||=
    begin
      new_lockfile = updated_lockfile_content_for(prepared_pyproject)

      tmp_hash =
        TomlRB.parse(new_lockfile)["metadata"]["content-hash"]
      correct_hash = pyproject_hash_for(updated_pyproject_content)

      new_lockfile.gsub(tmp_hash, correct_hash)
    end
end
updated_lockfile_content_for(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 174
def updated_lockfile_content_for(pyproject_content)
  SharedHelpers.in_a_temporary_directory do
    SharedHelpers.with_git_configured(credentials: credentials) do
      write_temporary_dependency_files(pyproject_content)

      if python_version && !pre_installed_python?(python_version)
        run_poetry_command("pyenv install -s #{python_version}")
        run_poetry_command("pyenv exec pip install -r"\
                           "#{NativeHelpers.python_requirements_path}")
      end

      run_poetry_command(poetry_update_command)

      return File.read("poetry.lock") if File.exist?("poetry.lock")

      File.read("pyproject.lock")
    end
  end
end
updated_pyproject_content() click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 64
def updated_pyproject_content
  dependencies.
    select { |dep| requirement_changed?(pyproject, dep) }.
    reduce(pyproject.content.dup) do |content, dep|
      updated_requirement =
        dep.requirements.find { |r| r[:file] == pyproject.name }.
        fetch(:requirement)

      old_req =
        dep.previous_requirements.
        find { |r| r[:file] == pyproject.name }.
        fetch(:requirement)

      updated_content =
        content.gsub(declaration_regex(dep)) do |line|
          line.gsub(old_req, updated_requirement)
        end

      raise "Content did not change!" if content == updated_content

      updated_content
    end
end
write_temporary_dependency_files(pyproject_content) click to toggle source
# File lib/dependabot/python/file_updater/poetry_file_updater.rb, line 220
def write_temporary_dependency_files(pyproject_content)
  dependency_files.each do |file|
    path = file.name
    FileUtils.mkdir_p(Pathname.new(path).dirname)
    File.write(path, file.content)
  end

  # Overwrite the .python-version with updated content
  File.write(".python-version", python_version) if python_version

  # Overwrite the pyproject with updated content
  File.write("pyproject.toml", pyproject_content)
end