Class: Dependabot::Python::LanguageVersionManager

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/dependabot/python/language_version_manager.rb

Instance Method Summary collapse

Constructor Details

#initialize(python_requirement_parser:) ⇒ LanguageVersionManager

Returns a new instance of LanguageVersionManager.



14
15
16
# File 'lib/dependabot/python/language_version_manager.rb', line 14

def initialize(python_requirement_parser:)
  @python_requirement_parser = python_requirement_parser
end

Instance Method Details

#install_required_pythonObject



19
20
21
22
23
24
25
26
# File 'lib/dependabot/python/language_version_manager.rb', line 19

def install_required_python
  # The leading space is important in the version check
  return if SharedHelpers.run_shell_command("pyenv versions").include?(" #{python_major_minor}.")

  SharedHelpers.run_shell_command(
    "tar -axf /usr/local/.pyenv/versions/#{python_version}.tar.zst -C /usr/local/.pyenv/versions"
  )
end

#installed_versionObject



29
30
31
32
33
34
35
# File 'lib/dependabot/python/language_version_manager.rb', line 29

def installed_version
  # Use `pyenv exec` to query the active Python version
  output, _status = SharedHelpers.run_shell_command("pyenv exec python --version")
  version = output.strip.split.last # Extract the version number (e.g., "3.13.1")

  T.must(version)
end

#normalize_python_exact_version(requirement_string) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/dependabot/python/language_version_manager.rb', line 62

def normalize_python_exact_version(requirement_string)
  return requirement_string if requirement_string.nil? || requirement_string.strip.empty?

  requirement_string = requirement_string.strip

  # If the requirement already has a wildcard, return nil
  return nil if requirement_string == "*"

  # If the requirement is not an exact version such as not X.Y.Z, =X.Y.Z, ==X.Y.Z, ===X.Y.Z
  # then return the requirement as is
  return requirement_string unless requirement_string.match?(/^=?={0,2}\s*\d+\.\d+(\.\d+)?(-[a-z0-9.-]+)?$/i)

  parts = requirement_string.gsub(/^=+/, "").split(".")

  case parts.length
  when 1 # Only major version (X)
    ">= #{parts[0]}.0.0 < #{parts[0].to_i + 1}.0.0" # Ensure only major version range
  when 2 # Major.Minor (X.Y)
    ">= #{parts[0]}.#{parts[1]}.0 < #{parts[0].to_i}.#{parts[1].to_i + 1}.0" # Ensure only minor version range
  when 3 # Major.Minor.Patch (X.Y.Z)
    ">= #{parts[0]}.#{parts[1]}.0 < #{parts[0].to_i}.#{parts[1].to_i + 1}.0" # Convert to >= X.Y.0
  else
    requirement_string
  end
end

#python_major_minorObject



38
39
40
# File 'lib/dependabot/python/language_version_manager.rb', line 38

def python_major_minor
  @python_major_minor ||= T.let(T.must(Python::Version.new(python_version).segments[0..1]).join("."), T.untyped)
end

#python_requirement_stringObject



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/dependabot/python/language_version_manager.rb', line 48

def python_requirement_string
  if user_specified_python_version
    if user_specified_python_version.start_with?(/\d/)
      parts = user_specified_python_version.split(".")
      parts.fill("*", (parts.length)..2).join(".")
    else
      user_specified_python_version
    end
  else
    python_version_matching_imputed_requirements || Language::PRE_INSTALLED_HIGHEST_VERSION.to_s
  end
end

#python_versionObject



43
44
45
# File 'lib/dependabot/python/language_version_manager.rb', line 43

def python_version
  @python_version ||= T.let(python_version_from_supported_versions, T.nilable(String))
end

#python_version_from_supported_versionsObject

Raises:

  • (ToolVersionNotSupported)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/dependabot/python/language_version_manager.rb', line 89

def python_version_from_supported_versions
  requirement_string = python_requirement_string

  # If the requirement string isn't already a range (eg ">3.10"), coerce it to "major.minor.*".
  # The patch version is ignored because a non-matching patch version is unlikely to affect resolution.
  requirement_string = requirement_string.gsub(/\.\d+$/, ".*") if /^\d/.match?(requirement_string)

  requirement_string = normalize_python_exact_version(requirement_string)

  if requirement_string.nil? || requirement_string.strip.empty?
    return Language::PRE_INSTALLED_HIGHEST_VERSION.to_s
  end

  # Try to match one of our pre-installed Python versions
  requirement = T.must(Python::Requirement.requirements_array(requirement_string).first)

  version = Language::PRE_INSTALLED_PYTHON_VERSIONS.find { |v| requirement.satisfied_by?(v) }
  return version.to_s if version

  # Otherwise we have to raise an error
  supported_versions = Language::SUPPORTED_VERSIONS.map { |v| "#{v}.*" }.join(", ")
  raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
end

#python_version_matching(requirements) ⇒ Object



130
131
132
133
134
135
136
137
138
# File 'lib/dependabot/python/language_version_manager.rb', line 130

def python_version_matching(requirements)
  Language::PRE_INSTALLED_PYTHON_VERSIONS.find do |version|
    requirements.all? do |req|
      next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)

      req.satisfied_by?(version)
    end
  end.to_s
end

#python_version_matching_imputed_requirementsObject



119
120
121
122
123
124
125
126
127
# File 'lib/dependabot/python/language_version_manager.rb', line 119

def python_version_matching_imputed_requirements
  compiled_file_python_requirement_markers =
    @python_requirement_parser.imputed_requirements.map do |r|
      Dependabot::Python::Requirement.new(r)
    end
  return unless compiled_file_python_requirement_markers.any?

  python_version_matching(compiled_file_python_requirement_markers)
end

#user_specified_python_versionObject



114
115
116
# File 'lib/dependabot/python/language_version_manager.rb', line 114

def user_specified_python_version
  @python_requirement_parser.user_specified_requirements.first
end