Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python metric collection #11013

Merged
merged 7 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
pavera marked this conversation as resolved.
Show resolved Hide resolved
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
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
Loading