Skip to content

Commit

Permalink
Added files with typecheck strict
Browse files Browse the repository at this point in the history
  • Loading branch information
randhircs committed Jan 16, 2025
1 parent 99ac373 commit 5db6a78
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 27 deletions.
51 changes: 40 additions & 11 deletions python/lib/dependabot/python/file_parser/pyproject_files_parser.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "toml-rb"
Expand All @@ -14,15 +14,18 @@ module Dependabot
module Python
class FileParser
class PyprojectFilesParser
extend T::Sig
POETRY_DEPENDENCY_TYPES = %w(dependencies dev-dependencies).freeze

# https://python-poetry.org/docs/dependency-specification/
UNSUPPORTED_DEPENDENCY_TYPES = %w(git path url).freeze

sig { params(dependency_files: T.untyped).void }
def initialize(dependency_files:)
@dependency_files = dependency_files
end

sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def dependency_set
dependency_set = Dependabot::FileParsers::Base::DependencySet.new

Expand All @@ -34,8 +37,10 @@ def dependency_set

private

sig { returns(T.untyped) }
attr_reader :dependency_files

sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def pyproject_dependencies
if using_poetry?
missing_keys = missing_poetry_keys
Expand All @@ -54,10 +59,12 @@ def pyproject_dependencies
end
end

sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def poetry_dependencies
@poetry_dependencies ||= parse_poetry_dependencies
@poetry_dependencies ||= T.let(parse_poetry_dependencies, T.untyped)
end

sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def parse_poetry_dependencies
dependencies = Dependabot::FileParsers::Base::DependencySet.new

Expand All @@ -73,6 +80,7 @@ def parse_poetry_dependencies
dependencies
end

sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def pep621_dependencies
dependencies = Dependabot::FileParsers::Base::DependencySet.new

Expand Down Expand Up @@ -107,6 +115,7 @@ def pep621_dependencies
dependencies
end

sig { params(type: T.untyped, deps_hash: T.untyped).returns(Dependabot::FileParsers::Base::DependencySet) }
def parse_poetry_dependency_group(type, deps_hash)
dependencies = Dependabot::FileParsers::Base::DependencySet.new

Expand All @@ -126,11 +135,13 @@ def parse_poetry_dependency_group(type, deps_hash)
dependencies
end

sig { params(name: String, extras: T::Array[String]).returns(String) }
def normalised_name(name, extras)
NameNormaliser.normalise_including_extras(name, extras)
end

# @param req can be an Array, Hash or String that represents the constraints for a dependency
sig { params(req: T.untyped, type: T.untyped).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
def parse_requirements_from(req, type)
[req].flatten.compact.filter_map do |requirement|
next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
Expand All @@ -155,33 +166,39 @@ def parse_requirements_from(req, type)
end
end

sig { returns(T.nilable(T::Boolean)) }
def using_poetry?
!poetry_root.nil?
end

sig { returns(T::Array[String]) }
def missing_poetry_keys
package_mode = poetry_root.fetch("package-mode", true)
required_keys = package_mode ? %w(name version description authors) : []
required_keys.reject { |key| poetry_root.key?(key) }
end

sig { returns(T.untyped) }
def using_pep621?
!parsed_pyproject.dig("project", "dependencies").nil? ||
!parsed_pyproject.dig("project", "optional-dependencies").nil? ||
!parsed_pyproject.dig("build-system", "requires").nil?
end

sig { returns(T.untyped) }
def poetry_root
parsed_pyproject.dig("tool", "poetry")
end

sig { returns(T.untyped) }
def using_pdm?
using_pep621? && pdm_lock
end

# Create a DependencySet where each element has no requirement. Any
# requirements will be added when combining the DependencySet with
# other DependencySets.
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
def lockfile_dependencies
dependencies = Dependabot::FileParsers::Base::DependencySet.new

Expand All @@ -206,10 +223,13 @@ def lockfile_dependencies
dependencies
end

sig { returns(T::Array[T.nilable(String)]) }
def production_dependency_names
@production_dependency_names ||= parse_production_dependency_names
@production_dependency_names ||= T.let(parse_production_dependency_names,
T.nilable(T::Array[T.nilable(String)]))
end

sig { returns(T::Array[T.nilable(String)]) }
def parse_production_dependency_names
SharedHelpers.in_a_temporary_directory do
File.write(pyproject.name, pyproject.content)
Expand All @@ -232,6 +252,7 @@ def parse_production_dependency_names
end
end

sig { params(dep_name: T.untyped).returns(T.untyped) }
def version_from_lockfile(dep_name)
return unless parsed_lockfile

Expand All @@ -240,38 +261,44 @@ def version_from_lockfile(dep_name)
&.fetch("version", nil)
end

sig { params(req: T.untyped).returns(T::Array[Dependabot::Python::Requirement]) }
def check_requirements(req)
requirement = req.is_a?(String) ? req : req["version"]
Python::Requirement.requirements_array(requirement)
rescue Gem::Requirement::BadRequirementError => e
raise Dependabot::DependencyFileNotEvaluatable, e.message
end

sig { params(name: String).returns(String) }
def normalise(name)
NameNormaliser.normalise(name)
end

sig { returns(T.untyped) }
def parsed_pyproject
@parsed_pyproject ||= TomlRB.parse(pyproject.content)
@parsed_pyproject ||= T.let(TomlRB.parse(pyproject.content), T.untyped)
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
raise Dependabot::DependencyFileNotParseable, pyproject.path
end

sig { returns(T.untyped) }
def parsed_poetry_lock
@parsed_poetry_lock ||= TomlRB.parse(poetry_lock.content)
@parsed_poetry_lock ||= T.let(TomlRB.parse(poetry_lock.content), T.untyped)
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
raise Dependabot::DependencyFileNotParseable, poetry_lock.path
end

sig { returns(T.untyped) }
def pyproject
@pyproject ||=
dependency_files.find { |f| f.name == "pyproject.toml" }
@pyproject ||= T.let(dependency_files.find { |f| f.name == "pyproject.toml" }, T.untyped)
end

sig { returns(T.untyped) }
def lockfile
poetry_lock
end

sig { returns(T.untyped) }
def parsed_pep621_dependencies
SharedHelpers.in_a_temporary_directory do
write_temporary_pyproject
Expand All @@ -284,24 +311,26 @@ def parsed_pep621_dependencies
end
end

sig { returns(Integer) }
def write_temporary_pyproject
path = pyproject.name
FileUtils.mkdir_p(Pathname.new(path).dirname)
File.write(path, pyproject.content)
end

sig { returns(T.untyped) }
def parsed_lockfile
parsed_poetry_lock if poetry_lock
end

sig { returns(T.untyped) }
def poetry_lock
@poetry_lock ||=
dependency_files.find { |f| f.name == "poetry.lock" }
@poetry_lock ||= T.let(dependency_files.find { |f| f.name == "poetry.lock" }, T.untyped)
end

sig { returns(T.untyped) }
def pdm_lock
@pdm_lock ||=
dependency_files.find { |f| f.name == "pdm.lock" }
@pdm_lock ||= T.let(dependency_files.find { |f| f.name == "pdm.lock" }, T.untyped)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions python/lib/dependabot/python/file_parser/setup_file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ def parsed_sanitized_setup_file
[]
end

sig { params(requirements: T.untyped).returns(T.nilable(Python::Requirement)) }
sig { params(requirements: T.untyped).returns(T.untyped) }
def check_requirements(requirements)
requirements&.each do |dep|
next unless dep["requirement"]

T.let(Python::Requirement.new(dep["requirement"].split(",")), Python::Requirement)
Python::Requirement.new(dep["requirement"].split(","))
rescue Gem::Requirement::BadRequirementError => e
raise Dependabot::DependencyFileNotEvaluatable, e.message
end
Expand Down
33 changes: 23 additions & 10 deletions python/lib/dependabot/python/file_updater/setup_file_sanitizer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "dependabot/python/file_updater"
Expand All @@ -12,11 +12,14 @@ class FileUpdater
# setup.py using only the information which will appear in the lockfile.
class SetupFileSanitizer
extend T::Sig

sig { params(setup_file: DependencyFile, setup_cfg: T.untyped).void }
def initialize(setup_file:, setup_cfg:)
@setup_file = setup_file
@setup_cfg = setup_cfg
end

sig { returns(String) }
def sanitized_content
# The part of the setup.py that Pipenv cares about appears to be the
# install_requires. A name and version are required by don't end up
Expand All @@ -33,14 +36,19 @@ def sanitized_content

private

sig { returns(DependencyFile) }
attr_reader :setup_file
sig { returns(String) }
attr_reader :setup_cfg

sig { returns(T::Boolean) }
def include_pbr?
setup_requires_array.any? { |d| d.start_with?("pbr") }
end

sig { returns(T.untyped) }
def install_requires_array
@install_requires_array = T.let(T.untyped, T.untyped)
@install_requires_array ||=
parsed_setup_file.dependencies.filter_map do |dep|
next unless dep.requirements.first[:groups]
Expand All @@ -50,7 +58,9 @@ def install_requires_array
end
end

sig { returns(T::Array[String]) }
def setup_requires_array
@setup_requires_array = T.let(T.untyped, T.untyped)
@setup_requires_array ||=
parsed_setup_file.dependencies.filter_map do |dep|
next unless dep.requirements.first[:groups]
Expand All @@ -60,7 +70,9 @@ def setup_requires_array
end
end

sig { returns(T::Hash[T.untyped, T.untyped]) }
def extras_require_hash
@extras_require_hash = T.let(T.untyped, T.untyped)
@extras_require_hash ||=
begin
hash = {}
Expand All @@ -78,20 +90,21 @@ def extras_require_hash
end
end

sig { returns(T.untyped) }
def parsed_setup_file
@parsed_setup_file ||=
Python::FileParser::SetupFileParser.new(
dependency_files: [
setup_file&.dup&.tap { |f| f.name = "setup.py" },
setup_cfg&.dup&.tap { |f| f.name = "setup.cfg" }
].compact
).dependency_set
@parsed_setup_file ||= T.let(Python::FileParser::SetupFileParser.new(
dependency_files: [
setup_file.dup.tap { |f| f.name = "setup.py" },
setup_cfg.dup.tap { |f| f.name = "setup.cfg" }
].compact
)
.dependency_set, T.untyped)
end

sig { returns(String) }
sig { returns(T.nilable(String)) }
def package_name
content = setup_file.content
match = content.match(/name\s*=\s*['"](?<package_name>[^'"]+)['"]/)
match = T.must(content).match(/name\s*=\s*['"](?<package_name>[^'"]+)['"]/)
match ? match[:package_name] : "default_package_name"
end
end
Expand Down
15 changes: 11 additions & 4 deletions python/lib/dependabot/python/language_version_manager.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "dependabot/logger"
Expand All @@ -19,6 +19,7 @@ class LanguageVersionManager
3.8.20
).freeze

sig { params(python_requirement_parser: T.untyped).void }
def initialize(python_requirement_parser:)
@python_requirement_parser = python_requirement_parser
end
Expand All @@ -33,20 +34,23 @@ def install_required_python
)
end

sig { returns(String) }
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")

version
T.must(version)
end

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

sig { returns(String) }
def python_version
@python_version ||= python_version_from_supported_versions
@python_version ||= T.let(python_version_from_supported_versions, T.nilable(String))
end

sig { returns(String) }
Expand Down Expand Up @@ -81,10 +85,12 @@ def python_version_from_supported_versions
raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
end

sig { returns(T.untyped) }
def user_specified_python_version
@python_requirement_parser.user_specified_requirements.first
end

sig { returns(T.nilable(String)) }
def python_version_matching_imputed_requirements
compiled_file_python_requirement_markers =
@python_requirement_parser.imputed_requirements.map do |r|
Expand All @@ -93,6 +99,7 @@ def python_version_matching_imputed_requirements
python_version_matching(compiled_file_python_requirement_markers)
end

sig { params(requirements: T.untyped).returns(T.nilable(String)) }
def python_version_matching(requirements)
PRE_INSTALLED_PYTHON_VERSIONS.find do |version_string|
version = Python::Version.new(version_string)
Expand Down

0 comments on commit 5db6a78

Please sign in to comment.