diff --git a/python/lib/dependabot/python/file_parser/pyproject_files_parser.rb b/python/lib/dependabot/python/file_parser/pyproject_files_parser.rb index 744d3457dc2..65d4b43704f 100644 --- a/python/lib/dependabot/python/file_parser/pyproject_files_parser.rb +++ b/python/lib/dependabot/python/file_parser/pyproject_files_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "toml-rb" @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -155,26 +166,31 @@ 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 @@ -182,6 +198,7 @@ def using_pdm? # 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 @@ -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) @@ -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 @@ -240,6 +261,7 @@ 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) @@ -247,31 +269,36 @@ def check_requirements(req) 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 @@ -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 diff --git a/python/lib/dependabot/python/file_parser/setup_file_parser.rb b/python/lib/dependabot/python/file_parser/setup_file_parser.rb index e14661241fa..3c27a8d4566 100644 --- a/python/lib/dependabot/python/file_parser/setup_file_parser.rb +++ b/python/lib/dependabot/python/file_parser/setup_file_parser.rb @@ -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 diff --git a/python/lib/dependabot/python/file_updater/setup_file_sanitizer.rb b/python/lib/dependabot/python/file_updater/setup_file_sanitizer.rb index d89dc160207..2018c14b6ff 100644 --- a/python/lib/dependabot/python/file_updater/setup_file_sanitizer.rb +++ b/python/lib/dependabot/python/file_updater/setup_file_sanitizer.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "dependabot/python/file_updater" @@ -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 @@ -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] @@ -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] @@ -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 = {} @@ -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*['"](?[^'"]+)['"]/) + match = T.must(content).match(/name\s*=\s*['"](?[^'"]+)['"]/) match ? match[:package_name] : "default_package_name" end end diff --git a/python/lib/dependabot/python/language_version_manager.rb b/python/lib/dependabot/python/language_version_manager.rb index 3ec07661ea6..d921b268ba5 100644 --- a/python/lib/dependabot/python/language_version_manager.rb +++ b/python/lib/dependabot/python/language_version_manager.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "dependabot/logger" @@ -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 @@ -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) } @@ -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| @@ -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)