Skip to content

Commit

Permalink
Enables python ecosystem metric collection (dependabot#10986)
Browse files Browse the repository at this point in the history
Enables python ecosystem metric collection (dependabot#10986)
  • Loading branch information
sachin-sandhu authored Nov 25, 2024
1 parent 50b93dc commit 2f0fb29
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 0 deletions.
92 changes: 92 additions & 0 deletions python/lib/dependabot/python/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
require "dependabot/shared_helpers"
require "dependabot/python/requirement"
require "dependabot/errors"
require "dependabot/python/language"
require "dependabot/python/native_helpers"
require "dependabot/python/name_normaliser"
require "dependabot/python/pip_compile_file_matcher"
require "dependabot/python/language_version_manager"
require "dependabot/python/package_manager"

module Dependabot
module Python
Expand All @@ -34,6 +37,11 @@ class FileParser < Dependabot::FileParsers::Base
InvalidRequirement ValueError RecursionError
).freeze

# we use this placeholder version in case we are not able to detect any
# PIP version from shell, we are ensuring that the actual update is not blocked
# in any way if any metric collection exception start happening
UNDETECTED_PACKAGE_MANAGER_VERSION = "0.0"

def parse
# TODO: setup.py from external dependencies is evaluated. Provide guards before removing this.
raise Dependabot::UnexpectedExternalCode if @reject_external_code
Expand All @@ -48,8 +56,92 @@ def parse
dependency_set.dependencies
end

sig { returns(Ecosystem) }
def ecosystem
@ecosystem ||= T.let(
Ecosystem.new(
name: ECOSYSTEM,
package_manager: package_manager,
language: language
),
T.nilable(Ecosystem)
)
end

private

def language_version_manager
@language_version_manager ||=
LanguageVersionManager.new(
python_requirement_parser: python_requirement_parser
)
end

def python_requirement_parser
@python_requirement_parser ||=
FileParser::PythonRequirementParser.new(
dependency_files: dependency_files
)
end

sig { returns(Ecosystem::VersionManager) }
def package_manager
@package_manager ||= detected_package_manager
end

sig { returns(Ecosystem::VersionManager) }
def detected_package_manager
return PeotryPackageManager.new(detect_poetry_version) if poetry_lock && detect_poetry_version

PipPackageManager.new(detect_pip_version)
end

def detect_poetry_version
if poetry_lock
version = SharedHelpers.run_shell_command("pyenv exec poetry --version")
.to_s.split("version ").last&.split(")")&.first

log_if_version_malformed(PeotryPackageManager.name, version)

# makes sure we have correct version format returned
version if version&.match?(/^\d+(?:\.\d+)*$/)

end
rescue StandardError
nil
end

def detect_pip_version
# extracts pip version from current python via executing shell command
version = SharedHelpers.run_shell_command("pyenv exec pip -V")
.split("from").first&.split("pip")&.last&.strip

log_if_version_malformed(PipPackageManager.name, version)

version&.match?(/^\d+(?:\.\d+)*$/) ? version : UNDETECTED_PACKAGE_MANAGER_VERSION
rescue StandardError
nil
end

def log_if_version_malformed(package_manager, version)
# logs warning if malformed version is found
return true if version&.match?(/^\d+(?:\.\d+)*$/)

Dependabot.logger.warn(
"Detected #{package_manager} with malformed version #{version}"
)
end

sig { returns(String) }
def python_raw_version
language_version_manager.python_version
end

sig { returns(T.nilable(Ecosystem::VersionManager)) }
def language
Language.new(python_raw_version)
end

def requirement_files
dependency_files.select { |f| f.name.end_with?(".txt", ".in") }
end
Expand Down
21 changes: 21 additions & 0 deletions python/lib/dependabot/python/language.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"
require "dependabot/python/version"
require "dependabot/ecosystem"

module Dependabot
module Python
LANGUAGE = "python"

class Language < Dependabot::Ecosystem::VersionManager
extend T::Sig

sig { params(raw_version: String, requirement: T.nilable(Requirement)).void }
def initialize(raw_version, requirement = nil)
super(LANGUAGE, Version.new(raw_version), [], [], requirement)
end
end
end
end
90 changes: 90 additions & 0 deletions python/lib/dependabot/python/package_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"
require "dependabot/python/version"
require "dependabot/ecosystem"
require "dependabot/python/requirement"

module Dependabot
module Python
ECOSYSTEM = "Python"

# Keep versions in ascending order
SUPPORTED_PYTHON_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])

DEPRECATED_PYTHON_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])

class PipPackageManager < Dependabot::Ecosystem::VersionManager
extend T::Sig

NAME = "pip"

SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])

DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])

sig do
params(
raw_version: String,
requirement: T.nilable(Requirement)
).void
end
def initialize(raw_version, requirement = nil)
super(
NAME,
Version.new(raw_version),
SUPPORTED_VERSIONS,
DEPRECATED_VERSIONS,
requirement,
)
end

sig { override.returns(T::Boolean) }
def deprecated?
false
end

sig { override.returns(T::Boolean) }
def unsupported?
false
end
end

class PeotryPackageManager < Dependabot::Ecosystem::VersionManager
extend T::Sig

NAME = "poetry"

SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])

DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])

sig do
params(
raw_version: String,
requirement: T.nilable(Requirement)
).void
end
def initialize(raw_version, requirement = nil)
super(
NAME,
Version.new(raw_version),
DEPRECATED_PYTHON_VERSIONS,
SUPPORTED_PYTHON_VERSIONS,
requirement,
)
end

sig { override.returns(T::Boolean) }
def deprecated?
false
end

sig { override.returns(T::Boolean) }
def unsupported?
false
end
end
end
end
33 changes: 33 additions & 0 deletions python/spec/dependabot/python/peotry_package_manager_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# typed: false
# frozen_string_literal: true

require "dependabot/python/package_manager"
require "dependabot/ecosystem"
require "spec_helper"

RSpec.describe Dependabot::Python::PeotryPackageManager do
let(:package_manager) { described_class.new("1.8.3") }

describe "#initialize" do
context "when version is a String" do
it "sets the version correctly" do
expect(package_manager.version).to eq("1.8.3")
end

it "sets the name correctly" do
expect(package_manager.name).to eq("poetry")
end
end

context "when poetry version is extracted from pyenv is well formed" do
# If this test starts failing, you need to adjust the "detect_poetry_version" function
# to return a valid version in format x.x, x.x.x etc. examples: 3.12.5, 3.12
version = Dependabot::SharedHelpers.run_shell_command("pyenv exec poetry --version")
.split("version ").last&.split(")")&.first

it "does not raise error" do
expect(version.match(/^\d+(?:\.\d+)*$/)).to be_truthy
end
end
end
end
33 changes: 33 additions & 0 deletions python/spec/dependabot/python/pip_package_manager_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# typed: false
# frozen_string_literal: true

require "dependabot/python/package_manager"
require "dependabot/ecosystem"
require "spec_helper"

RSpec.describe Dependabot::Python::PipPackageManager do
let(:package_manager) { described_class.new("24.0") }

describe "#initialize" do
context "when version is a String" do
it "sets the version correctly" do
expect(package_manager.version).to eq("24.0")
end

it "sets the name correctly" do
expect(package_manager.name).to eq("pip")
end
end

context "when pip version is extracted from pyenv is well formed" do
# If this test starts failing, you need to adjust the "detect_pip_version" function
# to return a valid version in format x.x, x.x.x etc. examples: 3.12.5, 3.12
version = Dependabot::SharedHelpers.run_shell_command("pyenv exec pip -V")
.split("from").first&.split("pip")&.last&.strip.to_s

it "does not raise error" do
expect(version.match(/^\d+(?:\.\d+)*$/)).to be_truthy
end
end
end
end

0 comments on commit 2f0fb29

Please sign in to comment.