class Dependabot::Python::UpdateChecker::PoetryVersionResolver
This class does version resolution for pyproject.toml files.
Constants
- GIT_DEPENDENCY_UNREACHABLE_REGEX
- GIT_REFERENCE_NOT_FOUND_REGEX
Attributes
credentials[R]
dependency[R]
dependency_files[R]
Public Class Methods
new(dependency:, dependency_files:, credentials:)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 40 def initialize(dependency:, dependency_files:, credentials:) @dependency = dependency @dependency_files = dependency_files @credentials = credentials end
Public Instance Methods
latest_resolvable_version(requirement: nil)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 46 def latest_resolvable_version(requirement: nil) version_string = fetch_latest_resolvable_version_string(requirement: requirement) version_string.nil? ? nil : Python::Version.new(version_string) end
resolvable?(version:)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 53 def resolvable?(version:) @resolvable ||= {} return @resolvable[version] if @resolvable.key?(version) @resolvable[version] = if fetch_latest_resolvable_version_string(requirement: "==#{version}") true else false end rescue SharedHelpers::HelperSubprocessFailed => e raise unless e.message.include?("SolverProblemError") @resolvable[version] = false end
Private Instance Methods
add_private_sources(pyproject_content)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 247 def add_private_sources(pyproject_content) Python::FileUpdater::PyprojectPreparer. new(pyproject_content: pyproject_content). replace_sources(credentials) end
check_original_requirements_resolvable()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 148 def check_original_requirements_resolvable return @original_reqs_resolvable if @original_reqs_resolvable SharedHelpers.in_a_temporary_directory do SharedHelpers.with_git_configured(credentials: credentials) do write_temporary_dependency_files(update_pyproject: false) run_poetry_command(poetry_update_command) @original_reqs_resolvable = true rescue SharedHelpers::HelperSubprocessFailed => e raise unless e.message.include?("SolverProblemError") || e.message.include?("PackageNotFound") msg = clean_error_message(e.message) raise DependencyFileNotResolvable, msg end end end
clean_error_message(message)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 168 def clean_error_message(message) # Redact any URLs, as they may include credentials message.gsub(/http.*?(?=\s)/, "<redacted>") end
fetch_latest_resolvable_version_string(requirement:)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 70 def fetch_latest_resolvable_version_string(requirement:) @latest_resolvable_version_string ||= {} return @latest_resolvable_version_string[requirement] if @latest_resolvable_version_string.key?(requirement) @latest_resolvable_version_string[requirement] ||= SharedHelpers.in_a_temporary_directory do SharedHelpers.with_git_configured(credentials: credentials) do write_temporary_dependency_files(updated_req: requirement) if python_version && !pre_installed_python?(python_version) run_poetry_command("pyenv install -s #{python_version}") run_poetry_command( "pyenv exec pip install -r "\ "#{NativeHelpers.python_requirements_path}" ) end # Shell out to Poetry, which handles everything for us. run_poetry_command(poetry_update_command) updated_lockfile = if File.exist?("poetry.lock") then File.read("poetry.lock") else File.read("pyproject.lock") end updated_lockfile = TomlRB.parse(updated_lockfile) fetch_version_from_parsed_lockfile(updated_lockfile) rescue SharedHelpers::HelperSubprocessFailed => e handle_poetry_errors(e) end end end
fetch_version_from_parsed_lockfile(updated_lockfile)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 103 def fetch_version_from_parsed_lockfile(updated_lockfile) version = updated_lockfile.fetch("package", []). find { |d| d["name"] && normalise(d["name"]) == dependency.name }&. fetch("version") return version unless version.nil? && dependency.top_level? raise "No version in lockfile!" end
freeze_other_dependencies(pyproject_content)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 253 def freeze_other_dependencies(pyproject_content) Python::FileUpdater::PyprojectPreparer. new(pyproject_content: pyproject_content, lockfile: lockfile). freeze_top_level_dependencies_except([dependency]) end
handle_poetry_errors(error)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 114 def handle_poetry_errors(error) if error.message.gsub(/\s/, "").match?(GIT_REFERENCE_NOT_FOUND_REGEX) message = error.message.gsub(/\s/, "") name = message.match(GIT_REFERENCE_NOT_FOUND_REGEX). named_captures.fetch("name") raise GitDependencyReferenceNotFound, name end if error.message.match?(GIT_DEPENDENCY_UNREACHABLE_REGEX) url = error.message.match(GIT_DEPENDENCY_UNREACHABLE_REGEX). named_captures.fetch("url") raise GitDependenciesNotReachable, url end raise unless error.message.include?("SolverProblemError") || error.message.include?("PackageNotFound") check_original_requirements_resolvable # If the original requirements are resolvable but the new version # would break Python version compatibility the update is blocked return if error.message.include?("support the following Python") # If any kind of other error is now occurring as a result of our # change then we want to hear about it raise end
lockfile()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 309 def lockfile poetry_lock || pyproject_lock end
normalise(name)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 333 def normalise(name) NameNormaliser.normalise(name) end
poetry_lock()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 305 def poetry_lock dependency_files.find { |f| f.name == "poetry.lock" } end
poetry_update_command()
click to toggle source
Using `–lock` avoids doing an install. Using `–no-interaction` avoids asking for passwords.
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 144 def poetry_update_command "pyenv exec poetry update #{dependency.name} --lock --no-interaction" end
pre_installed_python?(version)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 221 def pre_installed_python?(version) PythonVersions::PRE_INSTALLED_PYTHON_VERSIONS.include?(version) end
pyproject()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 297 def pyproject dependency_files.find { |f| f.name == "pyproject.toml" } end
pyproject_lock()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 301 def pyproject_lock dependency_files.find { |f| f.name == "pyproject.lock" } end
python_requirement_parser()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 214 def python_requirement_parser @python_requirement_parser ||= FileParser::PythonRequirementParser.new( dependency_files: dependency_files ) end
python_version()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 195 def python_version requirements = python_requirement_parser.user_specified_requirements requirements = requirements. map { |r| Python::Requirement.requirements_array(r) } version = PythonVersions::SUPPORTED_VERSIONS_TO_ITERATE.find do |v| requirements.all? do |reqs| reqs.any? { |r| r.satisfied_by?(Python::Version.new(v)) } end end return version if version msg = "Dependabot detected the following Python requirements "\ "for your project: '#{requirements}'.\n\nCurrently, the "\ "following Python versions are supported in Dependabot: "\ "#{PythonVersions::SUPPORTED_VERSIONS.join(', ')}." raise DependencyFileNotResolvable, msg end
run_poetry_command(command)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 313 def run_poetry_command(command) start = Time.now command = SharedHelpers.escape_command(command) stdout, process = Open3.capture2e(command) time_taken = Time.now - start # Raise an error with the output from the shell session if Pipenv # returns a non-zero status return if process.success? raise SharedHelpers::HelperSubprocessFailed.new( message: stdout, error_context: { command: command, time_taken: time_taken, process_exit_value: process.to_s } ) end
sanitize_pyproject_content(pyproject_content)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 241 def sanitize_pyproject_content(pyproject_content) Python::FileUpdater::PyprojectPreparer. new(pyproject_content: pyproject_content). sanitize end
sanitized_pyproject_content()
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 234 def sanitized_pyproject_content content = pyproject.content content = sanitize_pyproject_content(content) content = add_private_sources(content) content end
set_target_dependency_req(pyproject_content, updated_requirement)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 260 def set_target_dependency_req(pyproject_content, updated_requirement) return pyproject_content unless updated_requirement pyproject_object = TomlRB.parse(pyproject_content) poetry_object = pyproject_object.dig("tool", "poetry") Dependabot::Python::FileParser::PoetryFilesParser::POETRY_DEPENDENCY_TYPES.each do |type| names = poetry_object[type]&.keys || [] pkg_name = names.find { |nm| normalise(nm) == dependency.name } next unless pkg_name if poetry_object.dig(type, pkg_name).is_a?(Hash) poetry_object[type][pkg_name]["version"] = updated_requirement else poetry_object[type][pkg_name] = updated_requirement end end # If this is a sub-dependency, add the new requirement unless dependency.requirements.find { |r| r[:file] == pyproject.name } poetry_object[subdep_type] ||= {} poetry_object[subdep_type][dependency.name] = updated_requirement end TomlRB.dump(pyproject_object) end
subdep_type()
click to toggle source
rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 288 def subdep_type category = TomlRB.parse(lockfile.content).fetch("package", []). find { |dets| normalise(dets.fetch("name")) == dependency.name }. fetch("category") category == "dev" ? "dev-dependencies" : "dependencies" end
updated_pyproject_content(updated_requirement:)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 225 def updated_pyproject_content(updated_requirement:) content = pyproject.content content = sanitize_pyproject_content(content) content = add_private_sources(content) content = freeze_other_dependencies(content) content = set_target_dependency_req(content, updated_requirement) content end
write_temporary_dependency_files(updated_req: nil, update_pyproject: true)
click to toggle source
# File lib/dependabot/python/update_checker/poetry_version_resolver.rb, line 173 def write_temporary_dependency_files(updated_req: nil, update_pyproject: true) dependency_files.each do |file| path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) File.write(path, file.content) end # Overwrite the .python-version with updated content File.write(".python-version", python_version) if python_version # Overwrite the pyproject with updated content if update_pyproject File.write( "pyproject.toml", updated_pyproject_content(updated_requirement: updated_req) ) else File.write("pyproject.toml", sanitized_pyproject_content) end end