lib/dependabot/uv/pipenv_runner.rb



# typed: strict
# frozen_string_literal: true

require "dependabot/shared_helpers"
require "dependabot/uv/file_parser"
require "json"
require "sorbet-runtime"

module Dependabot
  module Uv
    class PipenvRunner
      extend T::Sig

      sig do
        params(
          dependency: Dependabot::Dependency,
          lockfile: T.nilable(Dependabot::DependencyFile),
          language_version_manager: LanguageVersionManager
        )
          .void
      end
      def initialize(dependency:, lockfile:, language_version_manager:)
        @dependency = dependency
        @lockfile = lockfile
        @language_version_manager = language_version_manager
      end

      sig { params(constraint: String).returns(String) }
      def run_upgrade(constraint)
        constraint = "" if constraint == "*"
        command = "pyenv exec pipenv upgrade --verbose #{dependency_name}#{constraint}"
        command << " --dev" if lockfile_section == "develop"

        run(command, fingerprint: "pyenv exec pipenv upgrade --verbose <dependency_name><constraint>")
      end

      sig { params(constraint: String).returns(T.nilable(String)) }
      def run_upgrade_and_fetch_version(constraint)
        run_upgrade(constraint)

        updated_lockfile = JSON.parse(File.read("Pipfile.lock"))

        fetch_version_from_parsed_lockfile(updated_lockfile)
      end

      sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
      def run(command, fingerprint: nil)
        run_command(
          "pyenv local #{language_version_manager.python_major_minor}",
          fingerprint: "pyenv local <python_major_minor>"
        )

        run_command(command, fingerprint: fingerprint)
      end

      private

      sig { returns(Dependabot::Dependency) }
      attr_reader :dependency
      sig { returns(T.nilable(Dependabot::DependencyFile)) }
      attr_reader :lockfile
      sig { returns(LanguageVersionManager) }
      attr_reader :language_version_manager

      sig { params(updated_lockfile: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
      def fetch_version_from_parsed_lockfile(updated_lockfile)
        deps = updated_lockfile[lockfile_section] || {}

        deps.dig(dependency_name, "version")
            &.gsub(/^==/, "")
      end

      sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
      def run_command(command, fingerprint: nil)
        SharedHelpers.run_shell_command(command, env: pipenv_env_variables, fingerprint: fingerprint)
      end

      sig { returns(String) }
      def lockfile_section
        if dependency.requirements.any?
          T.must(dependency.requirements.first)[:groups].first
        else
          Uv::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
            section = keys.fetch(:lockfile)
            return section if JSON.parse(T.must(T.must(lockfile).content))[section].keys.any?(dependency_name)
          end
        end
      end

      sig { returns(String) }
      def dependency_name
        dependency.metadata[:original_name] || dependency.name
      end

      sig { returns(T::Hash[String, String]) }
      def pipenv_env_variables
        {
          "PIPENV_YES" => "true",        # Install new Python ver if needed
          "PIPENV_MAX_RETRIES" => "3",   # Retry timeouts
          "PIPENV_NOSPIN" => "1",        # Don't pollute logs with spinner
          "PIPENV_TIMEOUT" => "600",     # Set install timeout to 10 minutes
          "PIP_DEFAULT_TIMEOUT" => "60", # Set pip timeout to 1 minute
          "COLUMNS" => "250"             # Avoid line wrapping
        }
      end
    end
  end
end