Skip to content

Commit

Permalink
Python metric collection (#11013)
Browse files Browse the repository at this point in the history
Python metric collection (#11013)
  • Loading branch information
sachin-sandhu authored Nov 27, 2024
1 parent c02fdae commit 5d96f2d
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 18 deletions.
104 changes: 94 additions & 10 deletions python/lib/dependabot/python/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,41 +91,113 @@ def package_manager

sig { returns(Ecosystem::VersionManager) }
def detected_package_manager
return PeotryPackageManager.new(detect_poetry_version) if poetry_lock && detect_poetry_version
setup_python_environment if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)

return PoetryPackageManager.new(T.must(detect_poetry_version)) if detect_poetry_version

return PipCompilePackageManager.new(T.must(detect_pipcompile_version)) if detect_pipcompile_version

return PipenvPackageManager.new(T.must(detect_pipenv_version)) if detect_pipenv_version

PipPackageManager.new(detect_pip_version)
end

# Detects the version of poetry. If the version cannot be detected, it returns nil
sig { returns(T.nilable(String)) }
def detect_poetry_version
if poetry_lock
version = SharedHelpers.run_shell_command("pyenv exec poetry --version")
.to_s.split("version ").last&.split(")")&.first
if poetry_files
package_manager = PoetryPackageManager::NAME

version = package_manager_version(package_manager)
.to_s.split("version ").last&.split(")")&.first

log_if_version_malformed(package_manager, version)

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

# Detects the version of pip-compile. If the version cannot be detected, it returns nil
sig { returns(T.nilable(String)) }
def detect_pipcompile_version
if pip_compile_files
package_manager = PipCompilePackageManager::NAME

log_if_version_malformed(PeotryPackageManager.name, version)
version = package_manager_version(package_manager)
.to_s.split("version ").last&.split(")")&.first

log_if_version_malformed(package_manager, version)

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

# Detects the version of pipenv. If the version cannot be detected, it returns nil
sig { returns(T.nilable(String)) }
def detect_pipenv_version
if pipenv_files
package_manager = PipenvPackageManager::NAME

version = package_manager_version(package_manager)
.to_s.split("versions ").last&.strip

log_if_version_malformed(package_manager, version)

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

# Detects the version of pip. If the version cannot be detected, it returns 0.0
sig { returns(String) }
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
package_manager = PipPackageManager::NAME

version = package_manager_version(package_manager)
.split("from").first&.split("pip")&.last&.strip

log_if_version_malformed(PipPackageManager.name, version)
log_if_version_malformed(package_manager, version)

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

sig { params(package_manager: String).returns(T.any(String, T.untyped)) }
def package_manager_version(package_manager)
version_info = SharedHelpers.run_shell_command("pyenv exec #{package_manager} --version")

Dependabot.logger.info("Package manager #{package_manager}, Info : #{version_info}")

version_info
rescue StandardError => e
Dependabot.logger.error(e.message)
nil
end

# setup python local setup on file parser stage
sig { void }
def setup_python_environment
language_version_manager.install_required_python

SharedHelpers.run_shell_command("pyenv local #{language_version_manager.python_major_minor}")
rescue StandardError => e
Dependabot.logger.error(e.message)
nil
end

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

Dependabot.logger.warn(
"Detected #{package_manager} with malformed version #{version}"
Expand Down Expand Up @@ -256,6 +328,18 @@ def check_requirements(requirements)
end
end

def pipcompile_in_file
requirement_files.any? { |f| f.end_with?(".in") }
end

def pipenv_files
requirement_files.any?(PipenvPackageManager::MANIFEST_FILENAME)
end

def poetry_files
true if get_original_file(PoetryPackageManager::LOCKFILE_NAME)
end

def write_temporary_dependency_files
dependency_files
.reject { |f| f.name == ".python-version" }
Expand Down
83 changes: 79 additions & 4 deletions python/lib/dependabot/python/package_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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])
Expand Down Expand Up @@ -51,11 +50,87 @@ def unsupported?
end
end

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

NAME = "poetry"

LOCKFILE_NAME = "poetry.lock"

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 PipCompilePackageManager < Dependabot::Ecosystem::VersionManager
extend T::Sig

NAME = "pip-compile"

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 PipenvPackageManager < Dependabot::Ecosystem::VersionManager
extend T::Sig

NAME = "pipenv"

MANIFEST_FILENAME = "Pipfile"

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

DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
Expand All @@ -70,8 +145,8 @@ def initialize(raw_version, requirement = nil)
super(
NAME,
Version.new(raw_version),
DEPRECATED_PYTHON_VERSIONS,
SUPPORTED_PYTHON_VERSIONS,
SUPPORTED_VERSIONS,
DEPRECATED_VERSIONS,
requirement,
)
end
Expand Down
33 changes: 33 additions & 0 deletions python/spec/dependabot/python/pip_compile_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::PipCompilePackageManager do
let(:package_manager) { described_class.new("2024.0.1") }

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

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

context "when pip-compile version extracted from pyenv is well formed" do
# If this test starts failing, you need to adjust the "detect_pipenv_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-compile --version")
.to_s.split("version ").last&.split(")")&.first

it "does not raise error" do
expect(version.match(/^\d+(?:\.\d+)*$/)).to be_truthy
end
end
end
end
4 changes: 2 additions & 2 deletions python/spec/dependabot/python/pip_package_manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
end
end

context "when pip version is extracted from pyenv is well formed" do
context "when pip version 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")
version = Dependabot::SharedHelpers.run_shell_command("pyenv exec pip --version")
.split("from").first&.split("pip")&.last&.strip.to_s

it "does not raise error" do
Expand Down
33 changes: 33 additions & 0 deletions python/spec/dependabot/python/pipenv_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::PipenvPackageManager 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("pipenv")
end
end

context "when pipenv version extracted from pyenv is well formed" do
# If this test starts failing, you need to adjust the "detect_pipenv_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 pipenv --version")
.to_s.split("version ").last&.strip

it "does not raise error" do
expect(version.match(/^\d+(?:\.\d+)*$/)).to be_truthy
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require "dependabot/ecosystem"
require "spec_helper"

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

describe "#initialize" do
Expand All @@ -19,7 +19,7 @@
end
end

context "when poetry version is extracted from pyenv is well formed" do
context "when poetry version 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")
Expand Down

0 comments on commit 5d96f2d

Please sign in to comment.