class Dependabot::Uv::FileParser::PythonRequirementParser
def imputed_requirements
TODO: Add better Python version detection using dependency versions
def imputed_requirements requirement_files.flat_map do |file| file.content.lines .select { |l| l.include?(";") && l.include?("python") } .filter_map { |l| l.match(/python_version(?<req>.*?["'].*?['"])/) } .map { |re| re.named_captures.fetch("req").gsub(/['"]/, "") } .select { |r| valid_requirement?(r) } end end
def initialize(dependency_files:)
def initialize(dependency_files:) @dependency_files = dependency_files end
def pip_compile_files
def pip_compile_files dependency_files.select { |f| f.name.end_with?(".in") } end
def pip_compile_python_requirement
def pip_compile_python_requirement requirement_files.each do |file| next unless requirements_in_file_matcher.compiled_file?(file) marker = /^# This file is autogenerated by pip-compile with [pP]ython (?<version>\d+.\d+)$/m match = marker.match(file.content) next unless match return match[:version] end nil end
def pyenv_versions
def pyenv_versions @pyenv_versions ||= run_command("pyenv install --list") end
def pyproject
def pyproject dependency_files.find { |f| f.name == "pyproject.toml" } end
def pyproject_python_requirement
def pyproject_python_requirement return unless pyproject pyproject_object = TomlRB.parse(pyproject.content) # Check for PEP621 requires-python pep621_python = pyproject_object.dig("project", "requires-python") return pep621_python if pep621_python # Fallback to Poetry configuration poetry_object = pyproject_object.dig("tool", "poetry") poetry_object&.dig("dependencies", "python") || poetry_object&.dig("dev-dependencies", "python") end
def python_version_file
def python_version_file dependency_files.find { |f| f.name == ".python-version" } end
def python_version_file_version
def python_version_file_version return unless python_version_file # read the content, split into lines and remove any lines with '#' content_lines = python_version_file.content.each_line.map do |line| line.sub(/#.*$/, " ").strip end.reject(&:empty?) file_version = content_lines.first return if file_version&.empty? return unless pyenv_versions.include?("#{file_version}\n") file_version end
def requirement_class
def requirement_class Dependabot::Uv::Requirement end
def requirement_files
def requirement_files dependency_files.select { |f| f.name.end_with?(".txt") } end
def requirements_in_file_matcher
def requirements_in_file_matcher @requirements_in_file_matcher ||= RequiremenstFileMatcher.new(pip_compile_files) end
def run_command(command, env: {})
def run_command(command, env: {}) SharedHelpers.run_shell_command(command, env: env, stderr_to_stdout: true) end
def runtime_file
def runtime_file dependency_files.find { |f| f.name.end_with?("runtime.txt") } end
def runtime_file_python_version
def runtime_file_python_version return unless runtime_file file_version = runtime_file.content .match(/(?<=python-).*/)&.to_s&.strip return if file_version&.empty? return unless pyenv_versions.include?("#{file_version}\n") file_version end
def user_specified_requirements
def user_specified_requirements [ pyproject_python_requirement, pip_compile_python_requirement, python_version_file_version, runtime_file_python_version ].compact end
def valid_requirement?(req_string)
def valid_requirement?(req_string) requirement_class.new(req_string) true rescue Gem::Requirement::BadRequirementError false end