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

Enables python ecosystem metric collection #10986

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
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
sachin-sandhu marked this conversation as resolved.
Show resolved Hide resolved

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
sachin-sandhu marked this conversation as resolved.
Show resolved Hide resolved

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
Loading