From 1972be3f75013e363bafdfe35afb6412ea7196d8 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Thu, 27 Feb 2025 14:27:17 +0000 Subject: [PATCH] Update uv to remove other python package managers --- uv/lib/dependabot/uv/file_fetcher.rb | 167 +--- uv/lib/dependabot/uv/file_parser.rb | 81 +- .../uv/file_parser/pipfile_files_parser.rb | 4 +- .../uv/file_parser/pyproject_files_parser.rb | 6 +- .../uv/file_parser/setup_file_parser.rb | 2 +- uv/lib/dependabot/uv/file_updater.rb | 66 +- ...ile_updater.rb => compile_file_updater.rb} | 90 +- .../uv/file_updater/pipfile_file_updater.rb | 339 ------- .../file_updater/pipfile_manifest_updater.rb | 107 --- .../uv/file_updater/pipfile_preparer.rb | 110 --- .../uv/file_updater/poetry_file_updater.rb | 318 ------- .../uv/file_updater/setup_file_sanitizer.rb | 97 -- uv/lib/dependabot/uv/package_manager.rb | 119 +-- uv/lib/dependabot/uv/update_checker.rb | 55 +- .../pip_compile_version_resolver.rb | 62 +- .../update_checker/pipenv_version_resolver.rb | 355 ------- .../update_checker/poetry_version_resolver.rb | 479 ---------- uv/spec/dependabot/uv/file_fetcher_spec.rb | 436 +-------- .../pyproject_files_parser_spec.rb | 20 +- uv/spec/dependabot/uv/file_parser_spec.rb | 612 +------------ ...2_spec.rb => compile_file_updater_spec.rb} | 6 +- .../pip_compile_file_updater_spec.rb | 636 ------------- .../file_updater/pipfile_file_updater_spec.rb | 684 -------------- .../pipfile_manifest_updater_spec.rb | 287 ------ .../uv/file_updater/pipfile_preparer_spec.rb | 92 -- .../file_updater/poetry_file_updater_spec.rb | 863 ------------------ .../file_updater/pyproject_preparer_spec.rb | 2 +- .../requirement_file_updater_spec.rb | 36 +- .../file_updater/setup_file_sanitizer_spec.rb | 69 -- uv/spec/dependabot/uv/file_updater_spec.rb | 285 +----- uv/spec/dependabot/uv/metadata_finder_spec.rb | 2 +- ...anager_spec.rb => package_manager_spec.rb} | 8 +- .../dependabot/uv/pip_package_manager_spec.rb | 33 - .../uv/pipenv_package_manager_spec.rb | 33 - .../uv/poetry_package_manager_spec.rb | 33 - .../uv/update_checker/index_finder_spec.rb | 4 +- .../latest_version_finder_spec.rb | 6 +- .../pip_compile_version_resolver_spec.rb | 496 ---------- .../pip_version_resolver_spec.rb | 6 +- .../pipenv_version_resolver_spec.rb | 512 ----------- .../poetry_version_resolver_spec.rb | 654 ------------- uv/spec/dependabot/uv/update_checker_spec.rb | 264 +----- .../dependabot/{python_spec.rb => uv_spec.rb} | 2 +- 43 files changed, 188 insertions(+), 8350 deletions(-) rename uv/lib/dependabot/uv/file_updater/{pip_compile_file_updater.rb => compile_file_updater.rb} (86%) delete mode 100644 uv/lib/dependabot/uv/file_updater/pipfile_file_updater.rb delete mode 100644 uv/lib/dependabot/uv/file_updater/pipfile_manifest_updater.rb delete mode 100644 uv/lib/dependabot/uv/file_updater/pipfile_preparer.rb delete mode 100644 uv/lib/dependabot/uv/file_updater/poetry_file_updater.rb delete mode 100644 uv/lib/dependabot/uv/file_updater/setup_file_sanitizer.rb delete mode 100644 uv/lib/dependabot/uv/update_checker/pipenv_version_resolver.rb delete mode 100644 uv/lib/dependabot/uv/update_checker/poetry_version_resolver.rb rename uv/spec/dependabot/uv/file_updater/{pip_compile_file_updater_v2_spec.rb => compile_file_updater_spec.rb} (99%) delete mode 100644 uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_spec.rb delete mode 100644 uv/spec/dependabot/uv/file_updater/pipfile_file_updater_spec.rb delete mode 100644 uv/spec/dependabot/uv/file_updater/pipfile_manifest_updater_spec.rb delete mode 100644 uv/spec/dependabot/uv/file_updater/pipfile_preparer_spec.rb delete mode 100644 uv/spec/dependabot/uv/file_updater/poetry_file_updater_spec.rb delete mode 100644 uv/spec/dependabot/uv/file_updater/setup_file_sanitizer_spec.rb rename uv/spec/dependabot/uv/{pip_compile_package_manager_spec.rb => package_manager_spec.rb} (79%) delete mode 100644 uv/spec/dependabot/uv/pip_package_manager_spec.rb delete mode 100644 uv/spec/dependabot/uv/pipenv_package_manager_spec.rb delete mode 100644 uv/spec/dependabot/uv/poetry_package_manager_spec.rb delete mode 100644 uv/spec/dependabot/uv/update_checker/pip_compile_version_resolver_spec.rb delete mode 100644 uv/spec/dependabot/uv/update_checker/pipenv_version_resolver_spec.rb delete mode 100644 uv/spec/dependabot/uv/update_checker/poetry_version_resolver_spec.rb rename uv/spec/dependabot/{python_spec.rb => uv_spec.rb} (75%) diff --git a/uv/lib/dependabot/uv/file_fetcher.rb b/uv/lib/dependabot/uv/file_fetcher.rb index 0eb0bbb587..1d92a26e75 100644 --- a/uv/lib/dependabot/uv/file_fetcher.rb +++ b/uv/lib/dependabot/uv/file_fetcher.rb @@ -29,20 +29,12 @@ def self.required_files_in?(filenames) # If there is a directory of requirements return true return true if filenames.include?("requirements") - # If this repo is using a Pipfile return true - return true if filenames.include?("Pipfile") - # If this repo is using pyproject.toml return true - return true if filenames.include?("pyproject.toml") - - return true if filenames.include?("setup.py") - - filenames.include?("setup.cfg") + filenames.include?("pyproject.toml") end def self.required_files_message - "Repo must contain a requirements.txt, setup.py, setup.cfg, pyproject.toml, " \ - "or a Pipfile." + "Repo must contain a requirements.txt, requirements.in, or pyproject.toml" \ end def ecosystem_versions @@ -72,16 +64,12 @@ def ecosystem_versions def fetch_files fetched_files = [] - fetched_files += pipenv_files fetched_files += pyproject_files fetched_files += requirements_in_files fetched_files += requirement_files if requirements_txt_files.any? - fetched_files << setup_file if setup_file - fetched_files << setup_cfg_file if setup_cfg_file fetched_files += project_files - fetched_files << pip_conf if pip_conf fetched_files << python_version_file if python_version_file uniq_files(fetched_files) @@ -95,12 +83,8 @@ def uniq_files(fetched_files) .reject { |f| uniq_files.map(&:name).include?(f.name) } end - def pipenv_files - [pipfile, pipfile_lock].compact - end - def pyproject_files - [pyproject, poetry_lock, pdm_lock].compact + [pyproject].compact end def requirement_files @@ -111,24 +95,6 @@ def requirement_files ] end - def setup_file - return @setup_file if defined?(@setup_file) - - @setup_file = fetch_file_if_present("setup.py") - end - - def setup_cfg_file - return @setup_cfg_file if defined?(@setup_cfg_file) - - @setup_cfg_file = fetch_file_if_present("setup.cfg") - end - - def pip_conf - return @pip_conf if defined?(@pip_conf) - - @pip_conf = fetch_support_file("pip.conf") - end - def python_version_file return @python_version_file if defined?(@python_version_file) @@ -144,36 +110,12 @@ def python_version_file &.tap { |f| f.name = ".python-version" } end - def pipfile - return @pipfile if defined?(@pipfile) - - @pipfile = fetch_file_if_present("Pipfile") - end - - def pipfile_lock - return @pipfile_lock if defined?(@pipfile_lock) - - @pipfile_lock = fetch_file_if_present("Pipfile.lock") - end - def pyproject return @pyproject if defined?(@pyproject) @pyproject = fetch_file_if_present("pyproject.toml") end - def poetry_lock - return @poetry_lock if defined?(@poetry_lock) - - @poetry_lock = fetch_file_if_present("poetry.lock") - end - - def pdm_lock - return @pdm_lock if defined?(@pdm_lock) - - @pdm_lock = fetch_file_if_present("pdm.lock") - end - def requirements_txt_files req_txt_and_in_files.select { |f| f.name.end_with?(".txt") } end @@ -183,14 +125,6 @@ def requirements_in_files child_requirement_in_files end - def parsed_pipfile - raise "No Pipfile" unless pipfile - - @parsed_pipfile ||= TomlRB.parse(pipfile.content) - rescue TomlRB::ParseError, TomlRB::ValueOverwriteError - raise Dependabot::DependencyFileNotParseable, pipfile.path - end - def parsed_pyproject raise "No pyproject.toml" unless pyproject @@ -300,18 +234,8 @@ def project_files path_dependencies.each do |dep| path = dep[:path] project_files += fetch_project_file(path) - rescue Dependabot::DependencyFileNotFound => e - unfetchable_deps << if sdist_or_wheel?(path) - e.file_path&.gsub(%r{^/}, "") - else - "\"#{dep[:name]}\" at #{cleanpath(File.join(directory, dep[:file]))}" - end - end - - poetry_path_dependencies.each do |path| - project_files += fetch_project_file(path) - rescue Dependabot::DependencyFileNotFound => e - unfetchable_deps << e.file_path&.gsub(%r{^/}, "") + rescue Dependabot::DependencyFileNotFound + unfetchable_deps << "\"#{dep[:name]}\" at #{cleanpath(File.join(directory, dep[:file]))}" end raise Dependabot::PathDependenciesNotReachable, unfetchable_deps if unfetchable_deps.any? @@ -322,48 +246,22 @@ def project_files def fetch_project_file(path) project_files = [] - path = cleanpath(File.join(path, "setup.py")) unless sdist_or_wheel?(path) - - return [] if path == "setup.py" && setup_file + path = cleanpath(File.join(path, "pyproject.toml")) unless sdist_or_wheel?(path) - project_files << - begin - fetch_file_from_host( - path, - fetch_submodules: true - ).tap { |f| f.support_file = true } - rescue Dependabot::DependencyFileNotFound - # For projects with pyproject.toml attempt to fetch a pyproject.toml - # at the given path instead of a setup.py. - fetch_file_from_host( - path.gsub("setup.py", "pyproject.toml"), - fetch_submodules: true - ).tap { |f| f.support_file = true } - end + return [] if path == "pyproject.toml" && pyproject - return project_files unless path.end_with?(".py") + project_files << fetch_file_from_host( + path, + fetch_submodules: true + ).tap { |f| f.support_file = true } - project_files + cfg_files_for_setup_py(path) + project_files end def sdist_or_wheel?(path) path.end_with?(".tar.gz", ".whl", ".zip") end - def cfg_files_for_setup_py(path) - cfg_path = path.gsub(/\.py$/, ".cfg") - - begin - [ - fetch_file_from_host(cfg_path, fetch_submodules: true) - .tap { |f| f.support_file = true } - ] - rescue Dependabot::DependencyFileNotFound - # Ignore lack of a setup.cfg - [] - end - end - def requirements_file?(file) return false unless file.content.valid_encoding? return true if file.name.match?(/requirements/x) @@ -377,9 +275,10 @@ def requirements_file?(file) end def path_dependencies - requirement_txt_path_dependencies + - requirement_in_path_dependencies + - pipfile_path_dependencies + [ + *requirement_txt_path_dependencies, + *requirement_in_path_dependencies + ] end def requirement_txt_path_dependencies @@ -415,40 +314,6 @@ def parse_requirement_path_dependencies(req_file) uneditable_reqs + editable_reqs end - def pipfile_path_dependencies - return [] unless pipfile - - deps = [] - DEPENDENCY_TYPES.each do |dep_type| - next unless parsed_pipfile[dep_type] - - parsed_pipfile[dep_type].each do |_, req| - next unless req.is_a?(Hash) && req["path"] - - deps << { name: req["path"], path: req["path"], file: pipfile.name } - end - end - - deps - end - - def poetry_path_dependencies - return [] unless pyproject - - paths = [] - Dependabot::Uv::FileParser::PyprojectFilesParser::POETRY_DEPENDENCY_TYPES.each do |dep_type| - next unless parsed_pyproject.dig("tool", "poetry", dep_type) - - parsed_pyproject.dig("tool", "poetry", dep_type).each do |_, req| - next unless req.is_a?(Hash) && req["path"] - - paths << req["path"] - end - end - - paths - end - def cleanpath(path) Pathname.new(path).cleanpath.to_path end diff --git a/uv/lib/dependabot/uv/file_parser.rb b/uv/lib/dependabot/uv/file_parser.rb index 73170402ee..d44adfebf9 100644 --- a/uv/lib/dependabot/uv/file_parser.rb +++ b/uv/lib/dependabot/uv/file_parser.rb @@ -17,7 +17,7 @@ module Dependabot module Uv - class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/ClassLength + class FileParser < Dependabot::FileParsers::Base extend T::Sig require_relative "file_parser/pipfile_files_parser" require_relative "file_parser/pyproject_files_parser" @@ -51,10 +51,8 @@ def parse dependency_set = DependencySet.new - dependency_set += pipenv_dependencies if pipfile dependency_set += pyproject_file_dependencies if pyproject dependency_set += requirement_dependencies if requirement_files.any? - dependency_set += setup_file_dependencies if setup_file || setup_cfg_file dependency_set.dependencies end @@ -98,38 +96,14 @@ def package_manager def detected_package_manager setup_python_environment if Dependabot::Experiments.enabled?(:enable_file_parser_python_local) - return PipenvPackageManager.new(T.must(detect_pipenv_version)) if detect_pipenv_version - - return PoetryPackageManager.new(T.must(detect_poetry_version)) if detect_poetry_version - - return PipCompilePackageManager.new(T.must(detect_pipcompile_version)) if detect_pipcompile_version - - PipPackageManager.new(detect_pip_version) - end - - # Detects the version of poetry. If the version cannot be detected, it returns nil - sig { returns(T.nilable(String)) } - def detect_poetry_version - if poetry_files - package_manager = PoetryPackageManager::NAME - - version = package_manager_version(package_manager) - .to_s.split("version ").last&.split(")")&.first - - log_if_version_malformed(package_manager, version) - - # makes sure we have correct version format returned - version if version&.match?(/^\d+(?:\.\d+)*$/) - end - rescue StandardError - nil + PackageManager.new(T.must(detect_pipcompile_version)) end # Detects the version of pip-compile. If the version cannot be detected, it returns nil sig { returns(T.nilable(String)) } def detect_pipcompile_version if pipcompile_in_file - package_manager = PipCompilePackageManager::NAME + package_manager = PackageManager::NAME version = package_manager_version(package_manager) .to_s.split("version ").last&.split(")")&.first @@ -143,45 +117,12 @@ def detect_pipcompile_version nil end - # Detects the version of pipenv. If the version cannot be detected, it returns nil - sig { returns(T.nilable(String)) } - def detect_pipenv_version - if pipenv_files - package_manager = PipenvPackageManager::NAME - - version = package_manager_version(package_manager) - .to_s.split("version ").last&.strip - - log_if_version_malformed(package_manager, version) - - # makes sure we have correct version format returned - version if version&.match?(/^\d+(?:\.\d+)*$/) - end - rescue StandardError - nil - end - - # Detects the version of pip. If the version cannot be detected, it returns 0.0 - sig { returns(String) } - def detect_pip_version - package_manager = PipPackageManager::NAME - - version = package_manager_version(package_manager) - .split("from").first&.split("pip")&.last&.strip - - log_if_version_malformed(package_manager, version) - - version&.match?(/^\d+(?:\.\d+)*$/) ? version : UNDETECTED_PACKAGE_MANAGER_VERSION - rescue StandardError - nil - end - sig { params(package_manager: String).returns(T.any(String, T.untyped)) } def package_manager_version(package_manager) version_info = SharedHelpers.run_shell_command("pyenv exec #{package_manager} --version") Dependabot.logger.info("Package manager #{package_manager}, Info : #{version_info}") - version_info + version_info.match(/\d+(?:\.\d+)*/)&.to_s rescue StandardError => e Dependabot.logger.error(e.message) nil @@ -281,7 +222,7 @@ def requirement_dependencies name: normalised_name(name, dep["extras"]), version: version&.include?("*") ? nil : version, requirements: requirements, - package_manager: "pip" + package_manager: "uv" ) end dependencies @@ -408,17 +349,7 @@ def check_requirements(requirements) sig { returns(T::Boolean) } def pipcompile_in_file - requirement_files.any? { |f| f.name.end_with?(PipCompilePackageManager::MANIFEST_FILENAME) } - end - - sig { returns(T::Boolean) } - def pipenv_files - dependency_files.any? { |f| f.name == PipenvPackageManager::LOCKFILE_FILENAME } - end - - sig { returns(T.nilable(TrueClass)) } - def poetry_files - true if get_original_file(PoetryPackageManager::LOCKFILE_NAME) + requirement_files.any? { |f| f.name.end_with?(PackageManager::MANIFEST_FILENAME) } end sig { returns(T::Array[Dependabot::DependencyFile]) } diff --git a/uv/lib/dependabot/uv/file_parser/pipfile_files_parser.rb b/uv/lib/dependabot/uv/file_parser/pipfile_files_parser.rb index 9e9999ba0d..74767f473d 100644 --- a/uv/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +++ b/uv/lib/dependabot/uv/file_parser/pipfile_files_parser.rb @@ -72,7 +72,7 @@ def pipfile_dependencies source: nil, groups: [group] }], - package_manager: "pip", + package_manager: "uv", metadata: { original_name: dep_name } ) end @@ -105,7 +105,7 @@ def pipfile_lock_dependencies name: dep_name, version: version&.gsub(/^===?/, ""), requirements: [], - package_manager: "pip", + package_manager: "uv", subdependency_metadata: [{ production: key != "develop" }] ) end diff --git a/uv/lib/dependabot/uv/file_parser/pyproject_files_parser.rb b/uv/lib/dependabot/uv/file_parser/pyproject_files_parser.rb index 5f7d8e33ac..5af0bf994d 100644 --- a/uv/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +++ b/uv/lib/dependabot/uv/file_parser/pyproject_files_parser.rb @@ -108,7 +108,7 @@ def pep621_dependencies source: nil, groups: [dep["requirement_type"]].compact }], - package_manager: "pip" + package_manager: "uv" ) end @@ -133,7 +133,7 @@ def parse_poetry_dependency_group(type, deps_hash) name: normalise(name), version: version_from_lockfile(name), requirements: requirements, - package_manager: "pip" + package_manager: "uv" ) end dependencies @@ -217,7 +217,7 @@ def lockfile_dependencies name: name, version: details.fetch("version"), requirements: [], - package_manager: "pip", + package_manager: "uv", subdependency_metadata: [{ production: production_dependency_names.include?(name) }] diff --git a/uv/lib/dependabot/uv/file_parser/setup_file_parser.rb b/uv/lib/dependabot/uv/file_parser/setup_file_parser.rb index 6a20980d7b..ae6b6cac05 100644 --- a/uv/lib/dependabot/uv/file_parser/setup_file_parser.rb +++ b/uv/lib/dependabot/uv/file_parser/setup_file_parser.rb @@ -50,7 +50,7 @@ def dependency_set source: nil, groups: [dep["requirement_type"]] }], - package_manager: "pip" + package_manager: "uv" ) end dependencies diff --git a/uv/lib/dependabot/uv/file_updater.rb b/uv/lib/dependabot/uv/file_updater.rb index bb3ba1a4a4..15c8433aee 100644 --- a/uv/lib/dependabot/uv/file_updater.rb +++ b/uv/lib/dependabot/uv/file_updater.rb @@ -12,23 +12,15 @@ module Uv class FileUpdater < Dependabot::FileUpdaters::Base extend T::Sig - require_relative "file_updater/pipfile_file_updater" - require_relative "file_updater/pip_compile_file_updater" - require_relative "file_updater/poetry_file_updater" + require_relative "file_updater/compile_file_updater" require_relative "file_updater/requirement_file_updater" sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ - /^.*Pipfile$/, # Match Pipfile at any level - /^.*Pipfile\.lock$/, # Match Pipfile.lock at any level /^.*\.txt$/, # Match any .txt files (e.g., requirements.txt) at any level /^.*\.in$/, # Match any .in files at any level - /^.*setup\.py$/, # Match setup.py at any level - /^.*setup\.cfg$/, # Match setup.cfg at any level - /^.*pyproject\.toml$/, # Match pyproject.toml at any level - /^.*pyproject\.lock$/, # Match pyproject.lock at any level - /^.*poetry\.lock$/ # Match poetry.lock at any level + /^.*pyproject\.toml$/ # Match pyproject.toml at any level ] end @@ -46,37 +38,16 @@ def updated_dependency_files private - sig { returns(Symbol) } + sig { returns(T.nilable(Symbol)) } def subdependency_resolver - return :pipfile if pipfile_lock - return :poetry if poetry_lock - return :pip_compile if pip_compile_files.any? + raise "Claimed to be a sub-dependency, but no lockfile exists!" if pip_compile_files.empty? - raise "Claimed to be a sub-dependency, but no lockfile exists!" - end - - sig { returns(T::Array[DependencyFile]) } - def updated_pipfile_based_files - PipfileFileUpdater.new( - dependencies: dependencies, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ).updated_dependency_files - end - - sig { returns(T::Array[DependencyFile]) } - def updated_poetry_based_files - PoetryFileUpdater.new( - dependencies: dependencies, - dependency_files: dependency_files, - credentials: credentials - ).updated_dependency_files + :pip_compile if pip_compile_files.any? end sig { returns(T::Array[DependencyFile]) } def updated_pip_compile_based_files - PipCompileFileUpdater.new( + CompileFileUpdater.new( dependencies: dependencies, dependency_files: dependency_files, credentials: credentials, @@ -110,41 +81,16 @@ def pip_compile_index_urls def check_required_files filenames = dependency_files.map(&:name) return if filenames.any? { |name| name.end_with?(".txt", ".in") } - return if pipfile return if pyproject - return if get_original_file("setup.py") - return if get_original_file("setup.cfg") raise "Missing required files!" end - sig { returns(T::Boolean) } - def poetry_based? - return false unless pyproject - - !TomlRB.parse(pyproject&.content).dig("tool", "poetry").nil? - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def pipfile - @pipfile ||= T.let(get_original_file("Pipfile"), T.nilable(Dependabot::DependencyFile)) - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def pipfile_lock - @pipfile_lock ||= T.let(get_original_file("Pipfile.lock"), T.nilable(Dependabot::DependencyFile)) - end - sig { returns(T.nilable(Dependabot::DependencyFile)) } def pyproject @pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile)) end - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def poetry_lock - @poetry_lock ||= T.let(get_original_file("poetry.lock"), T.nilable(Dependabot::DependencyFile)) - end - sig { returns(T::Array[DependencyFile]) } def pip_compile_files @pip_compile_files ||= T.let( diff --git a/uv/lib/dependabot/uv/file_updater/pip_compile_file_updater.rb b/uv/lib/dependabot/uv/file_updater/compile_file_updater.rb similarity index 86% rename from uv/lib/dependabot/uv/file_updater/pip_compile_file_updater.rb rename to uv/lib/dependabot/uv/file_updater/compile_file_updater.rb index 63f1dcbb76..734261415e 100644 --- a/uv/lib/dependabot/uv/file_updater/pip_compile_file_updater.rb +++ b/uv/lib/dependabot/uv/file_updater/compile_file_updater.rb @@ -17,10 +17,9 @@ module Dependabot module Uv class FileUpdater # rubocop:disable Metrics/ClassLength - class PipCompileFileUpdater + class CompileFileUpdater require_relative "requirement_replacer" require_relative "requirement_file_updater" - require_relative "setup_file_sanitizer" UNSAFE_PACKAGES = %w(setuptools distribute pip).freeze INCOMPATIBLE_VERSIONS_REGEX = /There are incompatible versions in the resolved dependencies:.*\z/m @@ -96,8 +95,8 @@ def compile_new_requirement_files def compile_file(filename) # Shell out to pip-compile, generate a new set of requirements. # This is slow, as pip-compile needs to do installs. - options = pip_compile_options(filename) - options_fingerprint = pip_compile_options_fingerprint(options) + options = compile_options(filename) + options_fingerprint = compile_options_fingerprint(options) name_part = "pyenv exec uv pip compile " \ "#{options} -P " \ @@ -110,7 +109,7 @@ def compile_file(filename) fingerprint_version_part = " " # Don't escape pyenv `dep-name==version` syntax - run_pip_compile_command( + run_uv_compile_command( "#{SharedHelpers.escape_command(name_part)}==" \ "#{SharedHelpers.escape_command(version_part)}", allow_unsafe_shell_command: true, @@ -186,7 +185,7 @@ def run_command(cmd, env: python_env, allow_unsafe_shell_command: false, fingerp raise end - def run_pip_compile_command(command, allow_unsafe_shell_command: false, fingerprint:) + def run_uv_compile_command(command, allow_unsafe_shell_command: false, fingerprint:) run_command( "pyenv local #{language_version_manager.python_major_minor}", fingerprint: "pyenv local " @@ -223,34 +222,6 @@ def write_updated_dependency_files # Overwrite the .python-version with updated content File.write(".python-version", language_version_manager.python_major_minor) - - setup_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, sanitized_setup_file_content(file)) - end - - setup_cfg_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, "[metadata]\nname = sanitized-package\n") - end - end - - def sanitized_setup_file_content(file) - @sanitized_setup_file_content ||= {} - return @sanitized_setup_file_content[file.name] if @sanitized_setup_file_content[file.name] - - @sanitized_setup_file_content[file.name] = - SetupFileSanitizer - .new(setup_file: file, setup_cfg: setup_cfg(file)) - .sanitized_content - end - - def setup_cfg(file) - dependency_files.find do |f| - f.name == file.name.sub(/\.py$/, ".cfg") - end end def freeze_dependency_requirement(file) @@ -438,7 +409,7 @@ def hash_separator(requirement_string) current_separator || default_separator end - def pip_compile_options_fingerprint(options) + def compile_options_fingerprint(options) options.sub( /--output-file=\S+/, "--output-file=" ).sub( @@ -448,42 +419,18 @@ def pip_compile_options_fingerprint(options) ) end - def pip_compile_options(filename) + def compile_options(filename) options = @build_isolation ? ["--build-isolation"] : ["--no-build-isolation"] - options += pip_compile_index_options + options += compile_index_options if (requirements_file = compiled_file_for_filename(filename)) - options += uv_pip_compile_options_from_compiled_file(requirements_file) + options += uv_compile_options_from_compiled_file(requirements_file) end options.join(" ") end - def pip_compile_options_from_compiled_file(requirements_file) # TODO: remove this method - options = ["--output-file=#{requirements_file.name}"] - - options << "--no-emit-index-url" unless requirements_file.content.include?("index-url http") - - options << "--generate-hashes" if requirements_file.content.include?("--hash=sha") - - options << "--allow-unsafe" if includes_unsafe_packages?(requirements_file.content) - - options << "--no-annotate" unless requirements_file.content.include?("# via ") - - options << "--no-header" unless requirements_file.content.include?("autogenerated by pip-c") - - options << "--pre" if requirements_file.content.include?("--pre") - - options << "--strip-extras" if requirements_file.content.include?("--strip-extras") - - if (resolver = RESOLVER_REGEX.match(requirements_file.content)) - options << "--resolver=#{resolver}" - end - - options - end - - def uv_pip_compile_options_from_compiled_file(requirements_file) + def uv_compile_options_from_compiled_file(requirements_file) options = ["--output-file=#{requirements_file.name}"] options << "--emit-index-url" if requirements_file.content.include?("index-url http") options << "--generate-hashes" if requirements_file.content.include?("--hash=sha") @@ -499,7 +446,8 @@ def uv_pip_compile_options_from_compiled_file(requirements_file) options end - def pip_compile_index_options + + def compile_index_options credentials .select { |cred| cred["type"] == "python_index" } .map do |cred| @@ -524,7 +472,7 @@ def filenames_to_compile .select { |fn| fn.end_with?(".in") } files_from_compiled_files = - pip_compile_files.map(&:name).select do |fn| + compile_files.map(&:name).select do |fn| compiled_file = compiled_file_for_filename(fn) compiled_file_includes_dependency?(compiled_file) end @@ -584,7 +532,7 @@ def order_filenames_for_compilation(filenames) def requirement_map child_req_regex = Uv::FileFetcher::CHILD_REQUIREMENT_REGEX @requirement_map ||= - pip_compile_files.each_with_object({}) do |file, req_map| + compile_files.each_with_object({}) do |file, req_map| paths = file.content.scan(child_req_regex).flatten current_dir = File.dirname(file.name) @@ -614,21 +562,13 @@ def language_version_manager ) end - def setup_files - dependency_files.select { |f| f.name.end_with?("setup.py") } - end - - def pip_compile_files + def compile_files dependency_files.select { |f| f.name.end_with?(".in") } end def compiled_files dependency_files.select { |f| f.name.end_with?(".txt") } end - - def setup_cfg_files - dependency_files.select { |f| f.name.end_with?("setup.cfg") } - end end # rubocop:enable Metrics/ClassLength end diff --git a/uv/lib/dependabot/uv/file_updater/pipfile_file_updater.rb b/uv/lib/dependabot/uv/file_updater/pipfile_file_updater.rb deleted file mode 100644 index 0e0bcf1cce..0000000000 --- a/uv/lib/dependabot/uv/file_updater/pipfile_file_updater.rb +++ /dev/null @@ -1,339 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "open3" -require "dependabot/dependency" -require "dependabot/uv/requirement_parser" -require "dependabot/uv/file_parser/python_requirement_parser" -require "dependabot/uv/file_updater" -require "dependabot/uv/language_version_manager" -require "dependabot/shared_helpers" -require "dependabot/uv/native_helpers" -require "dependabot/uv/pipenv_runner" - -module Dependabot - module Uv - class FileUpdater - class PipfileFileUpdater - require_relative "pipfile_preparer" - require_relative "pipfile_manifest_updater" - require_relative "setup_file_sanitizer" - - DEPENDENCY_TYPES = %w(packages dev-packages).freeze - - attr_reader :dependencies - attr_reader :dependency_files - attr_reader :credentials - attr_reader :repo_contents_path - - def initialize(dependencies:, dependency_files:, credentials:, repo_contents_path:) - @dependencies = dependencies - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - end - - def updated_dependency_files - @updated_dependency_files ||= fetch_updated_dependency_files - end - - private - - def dependency - # For now, we'll only ever be updating a single dependency - dependencies.first - end - - def fetch_updated_dependency_files - updated_files = [] - - if pipfile.content != updated_pipfile_content - updated_files << - updated_file(file: pipfile, content: updated_pipfile_content) - end - - if lockfile - raise "Expected Pipfile.lock to change!" if lockfile.content == updated_lockfile_content - - updated_files << - updated_file(file: lockfile, content: updated_lockfile_content) - end - - updated_files += updated_generated_requirements_files - updated_files - end - - def updated_pipfile_content - @updated_pipfile_content ||= - PipfileManifestUpdater.new( - dependencies: dependencies, - manifest: pipfile - ).updated_manifest_content - end - - def updated_lockfile_content - @updated_lockfile_content ||= - updated_generated_files.fetch(:lockfile) - end - - def generate_updated_requirements_files? - return true if generated_requirements_files("default").any? - - generated_requirements_files("develop").any? - end - - def generated_requirements_files(type) - return [] unless lockfile - - pipfile_lock_deps = parsed_lockfile[type]&.keys&.sort || [] - return [] unless pipfile_lock_deps.any? - - regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT - - # Find any requirement files that list the same dependencies as - # the (old) Pipfile.lock. Any such files were almost certainly - # generated using `pipenv requirements` - requirements_files.select do |req_file| - deps = [] - req_file.content.scan(regex) { deps << Regexp.last_match } - deps = deps.map { |m| m[:name] } - deps.sort == pipfile_lock_deps - end - end - - def updated_generated_requirements_files - updated_files = [] - - generated_requirements_files("default").each do |file| - next if file.content == updated_req_content - - updated_files << - updated_file(file: file, content: updated_req_content) - end - - generated_requirements_files("develop").each do |file| - next if file.content == updated_dev_req_content - - updated_files << - updated_file(file: file, content: updated_dev_req_content) - end - - updated_files - end - - def updated_req_content - updated_generated_files.fetch(:requirements_txt) - end - - def updated_dev_req_content - updated_generated_files.fetch(:dev_requirements_txt) - end - - def prepared_pipfile_content - content = updated_pipfile_content - content = add_private_sources(content) - content = update_python_requirement(content) - content = update_ssl_requirement(content, updated_pipfile_content) - - content - end - - def update_python_requirement(pipfile_content) - PipfilePreparer - .new(pipfile_content: pipfile_content) - .update_python_requirement(language_version_manager.python_major_minor) - end - - def update_ssl_requirement(pipfile_content, parsed_file) - Uv::FileUpdater::PipfilePreparer - .new(pipfile_content: pipfile_content) - .update_ssl_requirement(parsed_file) - end - - def add_private_sources(pipfile_content) - PipfilePreparer - .new(pipfile_content: pipfile_content) - .replace_sources(credentials) - end - - def updated_generated_files - @updated_generated_files ||= - SharedHelpers.in_a_temporary_repo_directory(dependency_files.first.directory, repo_contents_path) do - SharedHelpers.with_git_configured(credentials: credentials) do - write_temporary_dependency_files(prepared_pipfile_content) - install_required_python - - pipenv_runner.run_upgrade("==#{dependency.version}") - - result = { lockfile: File.read("Pipfile.lock") } - result[:lockfile] = post_process_lockfile(result[:lockfile]) - - # Generate updated requirement.txt entries, if needed. - if generate_updated_requirements_files? - generate_updated_requirements_files - - result[:requirements_txt] = File.read("req.txt") - result[:dev_requirements_txt] = File.read("dev-req.txt") - end - - result - end - end - end - - def post_process_lockfile(updated_lockfile_content) - pipfile_hash = pipfile_hash_for(updated_pipfile_content) - original_reqs = parsed_lockfile["_meta"]["requires"] - original_source = parsed_lockfile["_meta"]["sources"] - - new_lockfile = updated_lockfile_content.dup - new_lockfile_json = JSON.parse(new_lockfile) - new_lockfile_json["_meta"]["hash"]["sha256"] = pipfile_hash - new_lockfile_json["_meta"]["requires"] = original_reqs - new_lockfile_json["_meta"]["sources"] = original_source - - JSON.pretty_generate(new_lockfile_json, indent: " ") - .gsub(/\{\n\s*\}/, "{}") - .gsub(/\}\z/, "}\n") - end - - def generate_updated_requirements_files - req_content = run_pipenv_command( - "pyenv exec pipenv requirements" - ) - File.write("req.txt", req_content) - - dev_req_content = run_pipenv_command( - "pyenv exec pipenv requirements --dev" - ) - File.write("dev-req.txt", dev_req_content) - end - - def run_command(command) - SharedHelpers.run_shell_command(command) - end - - def run_pipenv_command(command) - pipenv_runner.run(command) - end - - def write_temporary_dependency_files(pipfile_content) - 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", language_version_manager.python_major_minor) - - setup_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, sanitized_setup_file_content(file)) - end - - setup_cfg_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, "[metadata]\nname = sanitized-package\n") - end - - # Overwrite the pipfile with updated content - File.write("Pipfile", pipfile_content) - end - - def install_required_python - # Initialize a git repo to appease pip-tools - begin - run_command("git init") if setup_files.any? - rescue Dependabot::SharedHelpers::HelperSubprocessFailed - nil - end - - language_version_manager.install_required_python - end - - def sanitized_setup_file_content(file) - @sanitized_setup_file_content ||= {} - return @sanitized_setup_file_content[file.name] if @sanitized_setup_file_content[file.name] - - @sanitized_setup_file_content[file.name] = - SetupFileSanitizer - .new(setup_file: file, setup_cfg: setup_cfg(file)) - .sanitized_content - end - - def setup_cfg(file) - dependency_files.find do |f| - f.name == file.name.sub(/\.py$/, ".cfg") - end - end - - def pipfile_hash_for(pipfile_content) - SharedHelpers.in_a_temporary_directory do |dir| - File.write(File.join(dir, "Pipfile"), pipfile_content) - SharedHelpers.run_helper_subprocess( - command: "pyenv exec python3 #{NativeHelpers.python_helper_path}", - function: "get_pipfile_hash", - args: [T.cast(dir, Pathname).to_s] - ) - end - end - - def updated_file(file:, content:) - updated_file = file.dup - updated_file.content = content - updated_file - end - - def python_requirement_parser - @python_requirement_parser ||= - FileParser::PythonRequirementParser.new( - dependency_files: dependency_files - ) - end - - def language_version_manager - @language_version_manager ||= - LanguageVersionManager.new( - python_requirement_parser: python_requirement_parser - ) - end - - def pipenv_runner - @pipenv_runner ||= - PipenvRunner.new( - dependency: dependency, - lockfile: lockfile, - language_version_manager: language_version_manager - ) - end - - def parsed_lockfile - @parsed_lockfile ||= JSON.parse(lockfile.content) - end - - def pipfile - @pipfile ||= dependency_files.find { |f| f.name == "Pipfile" } - end - - def lockfile - @lockfile ||= dependency_files.find { |f| f.name == "Pipfile.lock" } - end - - def setup_files - dependency_files.select { |f| f.name.end_with?("setup.py") } - end - - def setup_cfg_files - dependency_files.select { |f| f.name.end_with?("setup.cfg") } - end - - def requirements_files - dependency_files.select { |f| f.name.end_with?(".txt") } - end - end - end - end -end diff --git a/uv/lib/dependabot/uv/file_updater/pipfile_manifest_updater.rb b/uv/lib/dependabot/uv/file_updater/pipfile_manifest_updater.rb deleted file mode 100644 index 8094691644..0000000000 --- a/uv/lib/dependabot/uv/file_updater/pipfile_manifest_updater.rb +++ /dev/null @@ -1,107 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "dependabot/uv/file_updater" - -module Dependabot - module Uv - class FileUpdater - class PipfileManifestUpdater - def initialize(dependencies:, manifest:) - @dependencies = dependencies - @manifest = manifest - end - - def updated_manifest_content - dependencies - .select { |dep| requirement_changed?(dep) } - .reduce(manifest.content.dup) do |content, dep| - updated_content = content - - updated_content = update_requirements( - content: updated_content, - dependency: dep - ) - - raise "Content did not change!" if content == updated_content - - updated_content - end - end - - private - - attr_reader :dependencies - attr_reader :manifest - - def update_requirements(content:, dependency:) - updated_content = content.dup - - # The UpdateChecker ensures the order of requirements is preserved - # when updating, so we can zip them together in new/old pairs. - reqs = dependency.requirements - .zip(dependency.previous_requirements) - .reject { |new_req, old_req| new_req == old_req } - - # Loop through each changed requirement - reqs.each do |new_req, old_req| - raise "Bad req match" unless new_req[:file] == old_req[:file] - next if new_req[:requirement] == old_req[:requirement] - next unless new_req[:file] == manifest.name - - updated_content = update_manifest_req( - content: updated_content, - dep: dependency, - old_req: old_req.fetch(:requirement), - new_req: new_req.fetch(:requirement) - ) - end - - updated_content - end - - def update_manifest_req(content:, dep:, old_req:, new_req:) - simple_declaration = content.scan(declaration_regex(dep)) - .find { |m| m.include?(old_req) } - - if simple_declaration - simple_declaration_regex = - /(?:^|["'])#{Regexp.escape(simple_declaration)}/ - content.gsub(simple_declaration_regex) do |line| - line.gsub(old_req, new_req) - end - elsif content.match?(table_declaration_version_regex(dep)) - content.gsub(table_declaration_version_regex(dep)) do |part| - line = content.match(table_declaration_version_regex(dep)) - .named_captures.fetch("version_declaration") - new_line = line.gsub(old_req, new_req) - part.gsub(line, new_line) - end - else - content - end - end - - def declaration_regex(dep) - escaped_name = Regexp.escape(dep.name).gsub("\\-", "[-_.]") - /(?:^|["'])#{escaped_name}["']?\s*=.*$/i - end - - def table_declaration_version_regex(dep) - / - packages\.#{Regexp.quote(dep.name)}\] - (?:(?!^\[).)+ - (?version\s*=[^\[]*)$ - /mx - end - - def requirement_changed?(dependency) - changed_requirements = - dependency.requirements - dependency.previous_requirements - - changed_requirements.any? { |f| f[:file] == manifest.name } - end - end - end - end -end diff --git a/uv/lib/dependabot/uv/file_updater/pipfile_preparer.rb b/uv/lib/dependabot/uv/file_updater/pipfile_preparer.rb deleted file mode 100644 index 1c8387af7f..0000000000 --- a/uv/lib/dependabot/uv/file_updater/pipfile_preparer.rb +++ /dev/null @@ -1,110 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "toml-rb" - -require "dependabot/dependency" -require "dependabot/uv/file_parser" -require "dependabot/uv/file_updater" -require "dependabot/uv/authed_url_builder" - -module Dependabot - module Uv - class FileUpdater - class PipfilePreparer - extend T::Sig - - sig { params(pipfile_content: String).void } - def initialize(pipfile_content:) - @pipfile_content = pipfile_content - end - - sig { params(credentials: T::Array[T::Hash[String, T.untyped]]).returns(String) } - def replace_sources(credentials) - pipfile_object = TomlRB.parse(pipfile_content) - - pipfile_object["source"] = - pipfile_sources.filter_map { |h| sub_auth_url(h, credentials) } + - config_variable_sources(credentials) - - TomlRB.dump(pipfile_object) - end - - sig { params(requirement: String).returns(String) } - def update_python_requirement(requirement) - pipfile_object = TomlRB.parse(pipfile_content) - - pipfile_object["requires"] ||= {} - if pipfile_object.dig("requires", "python_full_version") && pipfile_object.dig("requires", "python_version") - pipfile_object["requires"].delete("python_full_version") - elsif pipfile_object.dig("requires", "python_full_version") - pipfile_object["requires"].delete("python_full_version") - pipfile_object["requires"]["python_version"] = requirement - end - TomlRB.dump(pipfile_object) - end - - sig { params(parsed_file: String).returns(String) } - def update_ssl_requirement(parsed_file) - pipfile_object = TomlRB.parse(pipfile_content) - parsed_object = TomlRB.parse(parsed_file) - - raise DependencyFileNotResolvable, "Unable to resolve pipfile." unless parsed_object["source"] - - # we parse the verify_ssl value from manifest if it exists - verify_ssl = parsed_object["source"].map { |x| x["verify_ssl"] }.first - - # provide a default "true" value to file generator in case no value is provided in manifest file - pipfile_object["source"].each do |key| - key["verify_ssl"] = verify_ssl.nil? ? true : verify_ssl - end - - TomlRB.dump(pipfile_object) - end - - private - - sig { returns(String) } - attr_reader :pipfile_content - - sig { returns(T::Array[T::Hash[String, T.untyped]]) } - def pipfile_sources - @pipfile_sources ||= T.let(TomlRB.parse(pipfile_content).fetch("source", []), - T.nilable(T::Array[T::Hash[String, T.untyped]])) - end - - sig do - params(source: T::Hash[String, T.untyped], - credentials: T::Array[T::Hash[String, T.untyped]]).returns(T.nilable(T::Hash[String, T.untyped])) - end - def sub_auth_url(source, credentials) - if source["url"].include?("${") - base_url = source["url"].sub(/\${.*}@/, "") - - source_cred = credentials - .select { |cred| cred["type"] == "python_index" && cred["index-url"] } - .find { |c| c["index-url"].sub(/\${.*}@/, "") == base_url } - - return nil if source_cred.nil? - - source["url"] = AuthedUrlBuilder.authed_url(credential: source_cred) - end - - source - end - - sig { params(credentials: T::Array[T::Hash[String, T.untyped]]).returns(T::Array[T::Hash[String, T.untyped]]) } - def config_variable_sources(credentials) - @config_variable_sources = T.let([], T.nilable(T::Array[T::Hash[String, T.untyped]])) - @config_variable_sources = - credentials.select { |cred| cred["type"] == "python_index" }.map.with_index do |c, i| - { - "name" => "dependabot-inserted-index-#{i}", - "url" => AuthedUrlBuilder.authed_url(credential: c) - } - end - end - end - end - end -end diff --git a/uv/lib/dependabot/uv/file_updater/poetry_file_updater.rb b/uv/lib/dependabot/uv/file_updater/poetry_file_updater.rb deleted file mode 100644 index 6da3b2d723..0000000000 --- a/uv/lib/dependabot/uv/file_updater/poetry_file_updater.rb +++ /dev/null @@ -1,318 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "toml-rb" -require "open3" -require "dependabot/dependency" -require "dependabot/shared_helpers" -require "dependabot/uv/language_version_manager" -require "dependabot/uv/version" -require "dependabot/uv/requirement" -require "dependabot/uv/file_parser/python_requirement_parser" -require "dependabot/uv/file_updater" -require "dependabot/uv/native_helpers" -require "dependabot/uv/name_normaliser" - -module Dependabot - module Uv - class FileUpdater - class PoetryFileUpdater - require_relative "pyproject_preparer" - - attr_reader :dependencies - attr_reader :dependency_files - attr_reader :credentials - - def initialize(dependencies:, dependency_files:, credentials:) - @dependencies = dependencies - @dependency_files = dependency_files - @credentials = credentials - end - - def updated_dependency_files - @updated_dependency_files ||= fetch_updated_dependency_files - end - - private - - def dependency - # For now, we'll only ever be updating a single dependency - dependencies.first - end - - def fetch_updated_dependency_files - updated_files = [] - - if file_changed?(pyproject) - updated_files << - updated_file( - file: pyproject, - content: updated_pyproject_content - ) - end - - raise "Expected lockfile to change!" if lockfile && lockfile.content == updated_lockfile_content - - if lockfile - updated_files << - updated_file(file: lockfile, content: updated_lockfile_content) - end - - updated_files - end - - def updated_pyproject_content - content = pyproject.content - return content unless requirement_changed?(pyproject, dependency) - - updated_content = content.dup - - dependency.requirements.zip(dependency.previous_requirements).each do |new_r, old_r| - next unless new_r[:file] == pyproject.name && old_r[:file] == pyproject.name - - updated_content = replace_dep(dependency, updated_content, new_r, old_r) - end - - raise DependencyFileContentNotChanged, "Content did not change!" if content == updated_content - - updated_content - end - - def replace_dep(dep, content, new_r, old_r) - new_req = new_r[:requirement] - old_req = old_r[:requirement] - - declaration_regex = declaration_regex(dep, old_r) - declaration_match = content.match(declaration_regex) - if declaration_match - declaration = declaration_match[:declaration] - new_declaration = declaration.sub(old_req, new_req) - content.sub(declaration, new_declaration) - else - content.gsub(table_declaration_regex(dep, new_r)) do |match| - match.gsub(/(\s*version\s*=\s*["'])#{Regexp.escape(old_req)}/, - '\1' + new_req) - end - end - end - - def updated_lockfile_content - @updated_lockfile_content ||= - begin - new_lockfile = updated_lockfile_content_for(prepared_pyproject) - - original_locked_python = TomlRB.parse(lockfile.content)["metadata"]["python-versions"] - - new_lockfile.gsub!(/\[metadata\]\n.*python-versions[^\n]+\n/m) do |match| - match.gsub(/(["']).*(['"])\n\Z/, '\1' + original_locked_python + '\1' + "\n") - end - - tmp_hash = - TomlRB.parse(new_lockfile)["metadata"]["content-hash"] - correct_hash = pyproject_hash_for(updated_pyproject_content) - - new_lockfile.gsub(tmp_hash, correct_hash) - end - end - - def prepared_pyproject - @prepared_pyproject ||= - begin - content = updated_pyproject_content - content = sanitize(content) - content = freeze_other_dependencies(content) - content = freeze_dependencies_being_updated(content) - content = update_python_requirement(content) - content - end - end - - def freeze_other_dependencies(pyproject_content) - PyprojectPreparer - .new(pyproject_content: pyproject_content, lockfile: lockfile) - .freeze_top_level_dependencies_except(dependencies) - end - - def freeze_dependencies_being_updated(pyproject_content) - pyproject_object = TomlRB.parse(pyproject_content) - poetry_object = pyproject_object.fetch("tool").fetch("poetry") - - dependencies.each do |dep| - if dep.requirements.find { |r| r[:file] == pyproject.name } - lock_declaration_to_new_version!(poetry_object, dep) - else - create_declaration_at_new_version!(poetry_object, dep) - end - end - - TomlRB.dump(pyproject_object) - end - - def update_python_requirement(pyproject_content) - PyprojectPreparer - .new(pyproject_content: pyproject_content) - .update_python_requirement(language_version_manager.python_version) - end - - def lock_declaration_to_new_version!(poetry_object, dep) - Dependabot::Uv::FileParser::PyprojectFilesParser::POETRY_DEPENDENCY_TYPES.each do |type| - names = poetry_object[type]&.keys || [] - pkg_name = names.find { |nm| normalise(nm) == dep.name } - next unless pkg_name - - if poetry_object[type][pkg_name].is_a?(Hash) - poetry_object[type][pkg_name]["version"] = dep.version - else - poetry_object[type][pkg_name] = dep.version - end - end - end - - def create_declaration_at_new_version!(poetry_object, dep) - subdep_type = dep.production? ? "dependencies" : "dev-dependencies" - - poetry_object[subdep_type] ||= {} - poetry_object[subdep_type][dep.name] = dep.version - end - - def sanitize(pyproject_content) - PyprojectPreparer - .new(pyproject_content: pyproject_content) - .sanitize - end - - def updated_lockfile_content_for(pyproject_content) - SharedHelpers.in_a_temporary_directory do - SharedHelpers.with_git_configured(credentials: credentials) do - write_temporary_dependency_files(pyproject_content) - add_auth_env_vars - - language_version_manager.install_required_python - - # use system git instead of the pure Python dulwich - run_poetry_command("pyenv exec poetry config experimental.system-git-client true") - - run_poetry_update_command - - File.read("poetry.lock") - end - end - end - - # Using `--lock` avoids doing an install. - # Using `--no-interaction` avoids asking for passwords. - def run_poetry_update_command - run_poetry_command( - "pyenv exec poetry update #{dependency.name} --lock --no-interaction", - fingerprint: "pyenv exec poetry update --lock --no-interaction" - ) - end - - def run_poetry_command(command, fingerprint: nil) - SharedHelpers.run_shell_command(command, fingerprint: fingerprint) - end - - def write_temporary_dependency_files(pyproject_content) - 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", language_version_manager.python_major_minor) - - # Overwrite the pyproject with updated content - File.write("pyproject.toml", pyproject_content) - end - - def add_auth_env_vars - Uv::FileUpdater::PyprojectPreparer - .new(pyproject_content: pyproject.content) - .add_auth_env_vars(credentials) - end - - def pyproject_hash_for(pyproject_content) - SharedHelpers.in_a_temporary_directory do |dir| - SharedHelpers.with_git_configured(credentials: credentials) do - write_temporary_dependency_files(pyproject_content) - - SharedHelpers.run_helper_subprocess( - command: "pyenv exec python3 #{python_helper_path}", - function: "get_pyproject_hash", - args: [T.cast(dir, Pathname).to_s] - ) - end - end - end - - def declaration_regex(dep, old_req) - group = old_req[:groups].first - - header_regex = "#{group}(?:\\.dependencies)?\\]\s*(?:\s*#.*?)*?" - /#{header_regex}\n.*?(?(?:^\s*|["'])#{escape(dep)}["']?\s*=[^\n]*)$/mi - end - - def table_declaration_regex(dep, old_req) - /tool\.poetry\.#{old_req[:groups].first}\.#{escape(dep)}\]\n.*?\s*version\s* =.*?\n/m - end - - def escape(dep) - Regexp.escape(dep.name).gsub("\\-", "[-_.]") - end - - def file_changed?(file) - dependencies.any? { |dep| requirement_changed?(file, dep) } - end - - def requirement_changed?(file, dependency) - changed_requirements = - dependency.requirements - dependency.previous_requirements - - changed_requirements.any? { |f| f[:file] == file.name } - end - - def updated_file(file:, content:) - updated_file = file.dup - updated_file.content = content - updated_file - end - - def normalise(name) - NameNormaliser.normalise(name) - end - - def python_requirement_parser - @python_requirement_parser ||= - FileParser::PythonRequirementParser.new( - dependency_files: dependency_files - ) - end - - def language_version_manager - @language_version_manager ||= - LanguageVersionManager.new( - python_requirement_parser: python_requirement_parser - ) - end - - def pyproject - @pyproject ||= - dependency_files.find { |f| f.name == "pyproject.toml" } - end - - def lockfile - @lockfile ||= poetry_lock - end - - def python_helper_path - NativeHelpers.python_helper_path - end - - def poetry_lock - dependency_files.find { |f| f.name == "poetry.lock" } - end - end - end - end -end diff --git a/uv/lib/dependabot/uv/file_updater/setup_file_sanitizer.rb b/uv/lib/dependabot/uv/file_updater/setup_file_sanitizer.rb deleted file mode 100644 index 7566332247..0000000000 --- a/uv/lib/dependabot/uv/file_updater/setup_file_sanitizer.rb +++ /dev/null @@ -1,97 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "dependabot/uv/file_updater" -require "dependabot/uv/file_parser/setup_file_parser" - -module Dependabot - module Uv - class FileUpdater - # Take a setup.py, parses it (carefully!) and then create a new, clean - # setup.py using only the information which will appear in the lockfile. - class SetupFileSanitizer - def initialize(setup_file:, setup_cfg:) - @setup_file = setup_file - @setup_cfg = setup_cfg - end - - 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 - # in the lockfile. - content = - "from setuptools import setup\n\n" \ - "setup(name=\"#{package_name}\",version=\"0.0.1\"," \ - "install_requires=#{install_requires_array.to_json}," \ - "extras_require=#{extras_require_hash.to_json}" - - content += ',setup_requires=["pbr"],pbr=True' if include_pbr? - content + ")" - end - - private - - attr_reader :setup_file - attr_reader :setup_cfg - - def include_pbr? - setup_requires_array.any? { |d| d.start_with?("pbr") } - end - - def install_requires_array - @install_requires_array ||= - parsed_setup_file.dependencies.filter_map do |dep| - next unless dep.requirements.first[:groups] - .include?("install_requires") - - dep.name + dep.requirements.first[:requirement].to_s - end - end - - def setup_requires_array - @setup_requires_array ||= - parsed_setup_file.dependencies.filter_map do |dep| - next unless dep.requirements.first[:groups] - .include?("setup_requires") - - dep.name + dep.requirements.first[:requirement].to_s - end - end - - def extras_require_hash - @extras_require_hash ||= - begin - hash = {} - parsed_setup_file.dependencies.each do |dep| - dep.requirements.first[:groups].each do |group| - next unless group.start_with?("extras_require:") - - hash[group.split(":").last] ||= [] - hash[group.split(":").last] << - (dep.name + dep.requirements.first[:requirement].to_s) - end - end - - hash - end - end - - def parsed_setup_file - @parsed_setup_file ||= - Uv::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 - end - - def package_name - content = setup_file.content - match = content.match(/name\s*=\s*['"](?[^'"]+)['"]/) - match ? match[:package_name] : "default_package_name" - end - end - end - end -end diff --git a/uv/lib/dependabot/uv/package_manager.rb b/uv/lib/dependabot/uv/package_manager.rb index db39f4a6c1..35ef376f0e 100644 --- a/uv/lib/dependabot/uv/package_manager.rb +++ b/uv/lib/dependabot/uv/package_manager.rb @@ -8,90 +8,16 @@ module Dependabot module Uv - ECOSYSTEM = "Python" + ECOSYSTEM = "uv" 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 + class PackageManager < 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: NAME, - version: Version.new(raw_version), - deprecated_versions: DEPRECATED_VERSIONS, - supported_versions: SUPPORTED_VERSIONS, - requirement: requirement, - ) - end - - sig { override.returns(T::Boolean) } - def deprecated? - false - end - - sig { override.returns(T::Boolean) } - def unsupported? - false - end - end - - class PoetryPackageManager < Dependabot::Ecosystem::VersionManager - extend T::Sig - - NAME = "poetry" - - LOCKFILE_NAME = "poetry.lock" - - 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: NAME, - version: Version.new(raw_version), - deprecated_versions: DEPRECATED_VERSIONS, - supported_versions: SUPPORTED_VERSIONS, - requirement: requirement, - ) - end - - sig { override.returns(T::Boolean) } - def deprecated? - false - end - - sig { override.returns(T::Boolean) } - def unsupported? - false - end - end - - class PipCompilePackageManager < Dependabot::Ecosystem::VersionManager - extend T::Sig - - NAME = "pip-compile" + NAME = "uv" MANIFEST_FILENAME = ".in" SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version]) @@ -124,44 +50,5 @@ def unsupported? false end end - - class PipenvPackageManager < Dependabot::Ecosystem::VersionManager - extend T::Sig - - NAME = "pipenv" - - MANIFEST_FILENAME = "Pipfile" - LOCKFILE_FILENAME = "Pipfile.lock" - - 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: NAME, - version: Version.new(raw_version), - deprecated_versions: DEPRECATED_VERSIONS, - supported_versions: SUPPORTED_VERSIONS, - requirement: requirement, - ) - end - - sig { override.returns(T::Boolean) } - def deprecated? - false - end - - sig { override.returns(T::Boolean) } - def unsupported? - false - end - end end end diff --git a/uv/lib/dependabot/uv/update_checker.rb b/uv/lib/dependabot/uv/update_checker.rb index a78074ddef..a2ed3524b0 100644 --- a/uv/lib/dependabot/uv/update_checker.rb +++ b/uv/lib/dependabot/uv/update_checker.rb @@ -17,8 +17,6 @@ module Dependabot module Uv class UpdateChecker < Dependabot::UpdateCheckers::Base - require_relative "update_checker/poetry_version_resolver" - require_relative "update_checker/pipenv_version_resolver" require_relative "update_checker/pip_compile_version_resolver" require_relative "update_checker/pip_version_resolver" require_relative "update_checker/requirements_updater" @@ -76,7 +74,7 @@ def updated_requirements requirements: requirements, latest_resolvable_version: preferred_resolvable_version&.to_s, update_strategy: requirements_update_strategy, - has_lockfile: !(pipfile_lock || poetry_lock).nil? + has_lockfile: requirements_text_file? ).updated_requirements end @@ -119,8 +117,6 @@ def resolver case resolver_type when :pip_compile then pip_compile_version_resolver - when :pipenv then pipenv_version_resolver - when :poetry then poetry_version_resolver when :requirements then pip_version_resolver else raise "Unexpected resolver type #{resolver_type}" end @@ -136,8 +132,7 @@ def resolver_type # Otherwise, this is a top-level dependency, and we can figure out # which resolver to use based on the filename of its requirements - return :pipenv if updating_pipfile? - return pyproject_resolver if updating_pyproject? + return :requirements if updating_pyproject? return :pip_compile if updating_in_file? if dependency.version && !exact_requirement?(reqs) @@ -148,19 +143,11 @@ def resolver_type end def subdependency_resolver - return :pipenv if pipfile_lock - return :poetry if poetry_lock return :pip_compile if pip_compile_files.any? raise "Claimed to be a sub-dependency, but no lockfile exists!" end - def pyproject_resolver - return :poetry if poetry_based? - - :requirements - end - def exact_requirement?(reqs) reqs = reqs.map { |r| r.fetch(:requirement) } reqs = reqs.compact @@ -168,19 +155,11 @@ def exact_requirement?(reqs) reqs.any? { |r| Uv::Requirement.new(r).exact? } end - def pipenv_version_resolver - @pipenv_version_resolver ||= PipenvVersionResolver.new(**resolver_args) - end - def pip_compile_version_resolver @pip_compile_version_resolver ||= PipCompileVersionResolver.new(**resolver_args) end - def poetry_version_resolver - @poetry_version_resolver ||= PoetryVersionResolver.new(**resolver_args) - end - def pip_version_resolver @pip_version_resolver ||= PipVersionResolver.new( dependency: dependency, @@ -259,10 +238,6 @@ def latest_version_finder ) end - def poetry_based? - updating_pyproject? && !poetry_details.nil? - end - def library? return false unless updating_pyproject? @@ -284,10 +259,6 @@ def library? false end - def updating_pipfile? - requirement_files.any?("Pipfile") - end - def updating_pyproject? requirement_files.any?("pyproject.toml") end @@ -296,6 +267,10 @@ def updating_in_file? requirement_files.any? { |f| f.end_with?(".in") } end + def requirements_text_file? + requirement_files.any? { |f| f.end_with?("requirements.txt") } + end + def updating_requirements_file? requirement_files.any? { |f| f =~ /\.txt$|\.in$/ } end @@ -312,28 +287,12 @@ def normalised_name(name) NameNormaliser.normalise(name) end - def pipfile - dependency_files.find { |f| f.name == "Pipfile" } - end - - def pipfile_lock - dependency_files.find { |f| f.name == "Pipfile.lock" } - end - def pyproject dependency_files.find { |f| f.name == "pyproject.toml" } end - def poetry_lock - dependency_files.find { |f| f.name == "poetry.lock" } - end - def library_details - @library_details ||= poetry_details || standard_details || build_system_details - end - - def poetry_details - @poetry_details ||= toml_content.dig("tool", "poetry") + @library_details ||= standard_details || build_system_details end def standard_details diff --git a/uv/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb b/uv/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb index b825c44b0f..2149a0bb97 100644 --- a/uv/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +++ b/uv/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb @@ -9,7 +9,6 @@ require "dependabot/uv/file_parser/python_requirement_parser" require "dependabot/uv/update_checker" require "dependabot/uv/file_updater/requirement_replacer" -require "dependabot/uv/file_updater/setup_file_sanitizer" require "dependabot/uv/version" require "dependabot/shared_helpers" require "dependabot/uv/language_version_manager" @@ -32,6 +31,7 @@ class PipCompileVersionResolver PYTHON_PACKAGE_NAME_REGEX = /[A-Za-z0-9_\-]+/ RESOLUTION_IMPOSSIBLE_ERROR = "ResolutionImpossible" ERROR_REGEX = /(?<=ERROR\:\W).*$/ + UV_UNRESOLVABLE_REGEX = / × No solution found when resolving dependencies:[\s\S]*$/ attr_reader :dependency attr_reader :dependency_files @@ -135,29 +135,15 @@ def compilation_error?(error) # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/PerceivedComplexity def handle_pip_compile_errors(message) - if message.include?(RESOLUTION_IMPOSSIBLE_ERROR) - check_original_requirements_resolvable - # If the original requirements are resolvable but we get an - # incompatibility error after unlocking then it's likely to be - # due to problems with pip-compile's cascading resolution - return nil + if message.include?("No solution found when resolving dependencies") + raise DependencyFileNotResolvable, message.scan(UV_UNRESOLVABLE_REGEX).last end - if message.include?("UnsupportedConstraint") - # If there's an unsupported constraint, check if it existed - # previously (and raise if it did) - check_original_requirements_resolvable - end + check_original_requirements_resolvable if message.include?(RESOLUTION_IMPOSSIBLE_ERROR) - if (message.include?('Command "python setup.py egg_info') || - message.include?( - "exit status 1: python setup.py egg_info" - )) && - check_original_requirements_resolvable - # The latest version of the dependency we're updating is borked - # (because it has an unevaluatable setup.py). Skip the update. - return - end + # If there's an unsupported constraint, check if it existed + # previously (and raise if it did) + check_original_requirements_resolvable if message.include?("UnsupportedConstraint") if message.include?(RESOLUTION_IMPOSSIBLE_ERROR) && !message.match?(/#{Regexp.quote(dependency.name)}/i) @@ -232,6 +218,8 @@ def check_original_requirements_resolvable def run_command(command, env: python_env, fingerprint:) SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint, stderr_to_stdout: true) + rescue SharedHelpers::HelperSubprocessFailed => e + handle_pip_compile_errors(e.message) end def pip_compile_options_fingerprint(options) @@ -282,7 +270,7 @@ def run_pip_compile_command(command, fingerprint:) run_command(command, fingerprint: fingerprint) end - def uv_pip_compile_options_from_compiled_file(requirements_file) # TODO: Consolidate with PipCompileFileUpdater + def uv_pip_compile_options_from_compiled_file(requirements_file) options = [] options << "--no-emit-index-url" unless requirements_file.content.include?("index-url http") @@ -299,7 +287,7 @@ def uv_pip_compile_options_from_compiled_file(requirements_file) # TODO: Consoli options << "--emit-build-options" end - if (resolver = FileUpdater::PipCompileFileUpdater::RESOLVER_REGEX.match(requirements_file.content)) + if (resolver = FileUpdater::CompileFileUpdater::RESOLVER_REGEX.match(requirements_file.content)) options << "--resolver=#{resolver}" end @@ -338,18 +326,6 @@ def write_temporary_dependency_files(updated_req: nil, # Overwrite the .python-version with updated content File.write(".python-version", language_version_manager.python_major_minor) - - setup_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, sanitized_setup_file_content(file)) - end - - setup_cfg_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, "[metadata]\nname = sanitized-package\n") - end end def write_original_manifest_files @@ -359,22 +335,6 @@ def write_original_manifest_files end end - def sanitized_setup_file_content(file) - @sanitized_setup_file_content ||= {} - return @sanitized_setup_file_content[file.name] if @sanitized_setup_file_content[file.name] - - @sanitized_setup_file_content[file.name] = - Uv::FileUpdater::SetupFileSanitizer - .new(setup_file: file, setup_cfg: setup_cfg(file)) - .sanitized_content - end - - def setup_cfg(file) - dependency_files.find do |f| - f.name == file.name.sub(/\.py$/, ".cfg") - end - end - def update_req_file(file, updated_req) return file.content unless file.name.end_with?(".in") diff --git a/uv/lib/dependabot/uv/update_checker/pipenv_version_resolver.rb b/uv/lib/dependabot/uv/update_checker/pipenv_version_resolver.rb deleted file mode 100644 index 835fc1d610..0000000000 --- a/uv/lib/dependabot/uv/update_checker/pipenv_version_resolver.rb +++ /dev/null @@ -1,355 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "excon" -require "open3" -require "dependabot/dependency" -require "dependabot/errors" -require "dependabot/shared_helpers" -require "dependabot/uv/file_parser" -require "dependabot/uv/file_parser/python_requirement_parser" -require "dependabot/uv/file_updater/pipfile_preparer" -require "dependabot/uv/file_updater/setup_file_sanitizer" -require "dependabot/uv/update_checker" -require "dependabot/uv/native_helpers" -require "dependabot/uv/pipenv_runner" -require "dependabot/uv/version" - -module Dependabot - module Uv - class UpdateChecker - class PipenvVersionResolver - GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none --quiet (?[^\s]+).*/ - GIT_REFERENCE_NOT_FOUND_REGEX = /git checkout -q (?[^\s]+).*/ - PIPENV_INSTALLATION_ERROR_NEW = "Getting requirements to build wheel exited with 1" - - # Can be removed when Python 3.11 support is dropped - PIPENV_INSTALLATION_ERROR_OLD = Regexp.quote("python setup.py egg_info exited with 1") - - PIPENV_INSTALLATION_ERROR = /#{PIPENV_INSTALLATION_ERROR_NEW}|#{PIPENV_INSTALLATION_ERROR_OLD}/ - PIPENV_INSTALLATION_ERROR_REGEX = - /[\s\S]*Collecting\s(?.+)\s\(from\s-r.+\)[\s\S]*(#{PIPENV_INSTALLATION_ERROR})/ - - PIPENV_RANGE_WARNING = /Python version range specifier '(?.*)' is not supported/ - - attr_reader :dependency - attr_reader :dependency_files - attr_reader :credentials - attr_reader :repo_contents_path - - def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - end - - def latest_resolvable_version(requirement: nil) - version_string = - fetch_latest_resolvable_version_string(requirement: requirement) - - version_string.nil? ? nil : Uv::Version.new(version_string) - end - - def resolvable?(version:) - @resolvable ||= {} - return @resolvable[version] if @resolvable.key?(version) - - @resolvable[version] = !!fetch_latest_resolvable_version_string(requirement: "==#{version}") - end - - private - - 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_repo_directory(base_directory, repo_contents_path) do - SharedHelpers.with_git_configured(credentials: credentials) do - write_temporary_dependency_files - install_required_python - - pipenv_runner.run_upgrade_and_fetch_version(requirement) - end - rescue SharedHelpers::HelperSubprocessFailed => e - handle_pipenv_errors(e) - end - end - - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength - def handle_pipenv_errors(error) - if error.message.include?("no version found at all") || - error.message.include?("Invalid specifier:") || - error.message.include?("Max retries exceeded") - msg = clean_error_message(error.message) - raise if msg.empty? - - raise DependencyFileNotResolvable, msg - end - - if error.message.match?(PIPENV_RANGE_WARNING) - msg = "Pipenv does not support specifying Python ranges " \ - "(see https://github.com/pypa/pipenv/issues/1050 for more " \ - "details)." - raise DependencyFileNotResolvable, msg - end - - if error.message.match?(GIT_REFERENCE_NOT_FOUND_REGEX) - tag = error.message.match(GIT_REFERENCE_NOT_FOUND_REGEX).named_captures.fetch("tag") - # Unfortunately the error message doesn't include the package name. - # TODO: Talk with pipenv maintainers about exposing the package name, it used to be part of the error output - raise GitDependencyReferenceNotFound, "(unknown package at #{tag})" - 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 - - if error.message.include?("Could not find a version") || error.message.include?("ResolutionFailure") - check_original_requirements_resolvable - end - - if error.message.include?("SyntaxError: invalid syntax") - raise DependencyFileNotResolvable, - "SyntaxError while installing dependencies. Is one of the dependencies not Python 3 compatible? " \ - "Pip v21 no longer supports Python 2." - end - - if (error.message.include?('Command "python setup.py egg_info"') || - error.message.include?( - "exit status 1: python setup.py egg_info" - )) && - check_original_requirements_resolvable - # The latest version of the dependency we're updating is borked - # (because it has an unevaluatable setup.py). Skip the update. - return - end - - if error.message.include?("UnsupportedPythonVersion") && - language_version_manager.user_specified_python_version - check_original_requirements_resolvable - - # The latest version of the dependency we're updating to needs a - # different Python version. Skip the update. - return if error.message.match?(/#{Regexp.quote(dependency.name)}/i) - end - - raise unless error.message.include?("ResolutionFailure") - end - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - - # Needed because Pipenv's resolver isn't perfect. - # Note: We raise errors from this method, rather than returning a - # boolean, so that all deps for this repo will raise identical - # errors when failing to update - def check_original_requirements_resolvable - SharedHelpers.in_a_temporary_repo_directory(base_directory, repo_contents_path) do - write_temporary_dependency_files(update_pipfile: false) - - pipenv_runner.run_upgrade("==#{dependency.version}") - - true - rescue SharedHelpers::HelperSubprocessFailed => e - handle_pipenv_errors_resolving_original_reqs(e) - end - end - - def base_directory - dependency_files.first.directory - end - - def handle_pipenv_errors_resolving_original_reqs(error) - if error.message.include?("Could not find a version") || - error.message.include?("package versions have conflicting dependencies") - msg = clean_error_message(error.message) - msg.gsub!(/\s+\(from .*$/, "") - raise if msg.empty? - - raise DependencyFileNotResolvable, msg - end - - if error.message.include?("UnsupportedPythonVersion") && - language_version_manager.user_specified_python_version - msg = clean_error_message(error.message) - .lines.take_while { |l| !l.start_with?("File") }.join.strip - raise if msg.empty? - - raise DependencyFileNotResolvable, msg - end - - handle_pipenv_installation_error(error.message) if error.message.match?(PIPENV_INSTALLATION_ERROR_REGEX) - - # Raise an unhandled error, as this could be a problem with - # Dependabot's infrastructure, rather than the Pipfile - raise - end - - def clean_error_message(message) - # Pipenv outputs a lot of things to STDERR, so we need to clean - # up the error message - msg_lines = message.lines - msg = msg_lines - .take_while { |l| !l.start_with?("During handling of") } - .drop_while do |l| - next false if l.start_with?("CRITICAL:") - next false if l.start_with?("ERROR:") - next false if l.start_with?("packaging.specifiers") - next false if l.start_with?("pipenv.patched.pip._internal") - next false if l.include?("Max retries exceeded") - - true - end.join.strip - - # We also need to redact any URLs, as they may include credentials - msg.gsub(/http.*?(?=\s)/, "") - end - - def handle_pipenv_installation_error(error_message) - # Find the dependency that's causing resolution to fail - dependency_name = error_message.match(PIPENV_INSTALLATION_ERROR_REGEX).named_captures["name"] - raise unless dependency_name - - msg = "Pipenv failed to install \"#{dependency_name}\". This could be caused by missing system " \ - "dependencies that can't be installed by Dependabot or required installation flags.\n\n" \ - "Error output from running \"pipenv lock\":\n" \ - "#{clean_error_message(error_message)}" - - raise DependencyFileNotResolvable, msg - end - - def write_temporary_dependency_files(update_pipfile: 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", language_version_manager.python_major_minor) - - setup_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, sanitized_setup_file_content(file)) - end - - setup_cfg_files.each do |file| - path = file.name - FileUtils.mkdir_p(Pathname.new(path).dirname) - File.write(path, "[metadata]\nname = sanitized-package\n") - end - return unless update_pipfile - - # Overwrite the pipfile with updated content - File.write( - "Pipfile", - pipfile_content - ) - end - - def install_required_python - # Initialize a git repo to appease pip-tools - begin - run_command("git init") if setup_files.any? - rescue Dependabot::SharedHelpers::HelperSubprocessFailed - nil - end - - language_version_manager.install_required_python - end - - def sanitized_setup_file_content(file) - @sanitized_setup_file_content ||= {} - @sanitized_setup_file_content[file.name] ||= - Uv::FileUpdater::SetupFileSanitizer - .new(setup_file: file, setup_cfg: setup_cfg(file)) - .sanitized_content - end - - def setup_cfg(file) - config_name = file.name.sub(/\.py$/, ".cfg") - dependency_files.find { |f| f.name == config_name } - end - - def pipfile_content - content = pipfile.content - content = add_private_sources(content) - content = update_python_requirement(content) - content = update_ssl_requirement(content, pipfile.content) - - content - end - - def update_python_requirement(pipfile_content) - Uv::FileUpdater::PipfilePreparer - .new(pipfile_content: pipfile_content) - .update_python_requirement(language_version_manager.python_major_minor) - end - - def update_ssl_requirement(pipfile_content, parsed_file) - Uv::FileUpdater::PipfilePreparer - .new(pipfile_content: pipfile_content) - .update_ssl_requirement(parsed_file) - end - - def add_private_sources(pipfile_content) - Uv::FileUpdater::PipfilePreparer - .new(pipfile_content: pipfile_content) - .replace_sources(credentials) - end - - def run_command(command) - SharedHelpers.run_shell_command(command, stderr_to_stdout: true) - end - - def python_requirement_parser - @python_requirement_parser ||= - FileParser::PythonRequirementParser.new( - dependency_files: dependency_files - ) - end - - def language_version_manager - @language_version_manager ||= - LanguageVersionManager.new( - python_requirement_parser: python_requirement_parser - ) - end - - def pipenv_runner - @pipenv_runner ||= - PipenvRunner.new( - dependency: dependency, - lockfile: lockfile, - language_version_manager: language_version_manager - ) - end - - def pipfile - dependency_files.find { |f| f.name == "Pipfile" } - end - - def lockfile - dependency_files.find { |f| f.name == "Pipfile.lock" } - end - - def setup_files - dependency_files.select { |f| f.name.end_with?("setup.py") } - end - - def setup_cfg_files - dependency_files.select { |f| f.name.end_with?("setup.cfg") } - end - end - end - end -end diff --git a/uv/lib/dependabot/uv/update_checker/poetry_version_resolver.rb b/uv/lib/dependabot/uv/update_checker/poetry_version_resolver.rb deleted file mode 100644 index 8fb3ad9626..0000000000 --- a/uv/lib/dependabot/uv/update_checker/poetry_version_resolver.rb +++ /dev/null @@ -1,479 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "excon" -require "toml-rb" -require "open3" -require "uri" -require "dependabot/dependency" -require "dependabot/errors" -require "dependabot/shared_helpers" -require "dependabot/uv/file_parser" -require "dependabot/uv/file_parser/python_requirement_parser" -require "dependabot/uv/file_updater/pyproject_preparer" -require "dependabot/uv/update_checker" -require "dependabot/uv/version" -require "dependabot/uv/requirement" -require "dependabot/uv/native_helpers" -require "dependabot/uv/authed_url_builder" -require "dependabot/uv/name_normaliser" - -module Dependabot - module Uv - class UpdateChecker - # This class does version resolution for pyproject.toml files. - class PoetryVersionResolver - extend T::Sig - extend T::Helpers - - GIT_REFERENCE_NOT_FOUND_REGEX = / - (Failed to checkout - (?.+?) - (?.+?).git at '(?.+?)' - | - ...Failedtoclone - (?.+?).gitat'(?.+?)', - verifyrefexistsonremote) - /x - GIT_DEPENDENCY_UNREACHABLE_REGEX = / - \s+Failed\sto\sclone - \s+(?.+?), - \s+check\syour\sgit\sconfiguration - /mx - - INCOMPATIBLE_CONSTRAINTS = /Incompatible constraints in requirements of (?.+?) ((?.+?)):/ - - attr_reader :dependency - attr_reader :dependency_files - attr_reader :credentials - attr_reader :repo_contents_path - - sig { returns(Dependabot::Uv::PoetryErrorHandler) } - attr_reader :error_handler - - def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - @error_handler = PoetryErrorHandler.new(dependencies: dependency, - dependency_files: dependency_files) - end - - def latest_resolvable_version(requirement: nil) - version_string = - fetch_latest_resolvable_version_string(requirement: requirement) - - version_string.nil? ? nil : Uv::Version.new(version_string) - end - - 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?("version solving failed.") - - @resolvable[version] = false - end - - private - - 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) - add_auth_env_vars - - language_version_manager.install_required_python - - # use system git instead of the pure Python dulwich - run_poetry_command("pyenv exec poetry config experimental.system-git-client true") - - # Shell out to Poetry, which handles everything for us. - run_poetry_update_command - - updated_lockfile = File.read("poetry.lock") - updated_lockfile = TomlRB.parse(updated_lockfile) - - fetch_version_from_parsed_lockfile(updated_lockfile) - rescue SharedHelpers::HelperSubprocessFailed => e - handle_poetry_errors(e) - end - end - end - - 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 - - # rubocop:disable Metrics/AbcSize - def handle_poetry_errors(error) - error_handler.handle_poetry_error(error) - - if error.message.gsub(/\s/, "").match?(GIT_REFERENCE_NOT_FOUND_REGEX) - message = error.message.gsub(/\s/, "") - match = message.match(GIT_REFERENCE_NOT_FOUND_REGEX) - name = if (url = match.named_captures.fetch("url")) - File.basename(T.must(URI.parse(url).path)) - else - message.match(GIT_REFERENCE_NOT_FOUND_REGEX) - .named_captures.fetch("name") - end - 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?("not found") || - error.message.include?("version solving failed.") - - 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 - # rubocop:enable Metrics/AbcSize - - # Using `--lock` avoids doing an install. - # Using `--no-interaction` avoids asking for passwords. - def run_poetry_update_command - run_poetry_command( - "pyenv exec poetry update #{dependency.name} --lock --no-interaction", - fingerprint: "pyenv exec poetry update --lock --no-interaction" - ) - end - - def check_original_requirements_resolvable - return @original_reqs_resolvable if @original_reqs_resolvable - - SharedHelpers.in_a_temporary_directory do - write_temporary_dependency_files(update_pyproject: false) - - run_poetry_update_command - - @original_reqs_resolvable = true - rescue SharedHelpers::HelperSubprocessFailed => e - raise unless e.message.include?("SolverProblemError") || - e.message.include?("not found") || - e.message.include?("version solving failed.") - - msg = clean_error_message(e.message) - raise DependencyFileNotResolvable, msg - end - end - - def clean_error_message(message) - # Redact any URLs, as they may include credentials - message.gsub(/http.*?(?=\s)/, "") - end - - 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", language_version_manager.python_major_minor) - - # 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 - - def add_auth_env_vars - Uv::FileUpdater::PyprojectPreparer - .new(pyproject_content: pyproject.content) - .add_auth_env_vars(credentials) - end - - def updated_pyproject_content(updated_requirement:) - content = pyproject.content - content = sanitize_pyproject_content(content) - content = update_python_requirement(content) - content = freeze_other_dependencies(content) - content = set_target_dependency_req(content, updated_requirement) - content - end - - def sanitized_pyproject_content - content = pyproject.content - content = sanitize_pyproject_content(content) - content = update_python_requirement(content) - content - end - - def sanitize_pyproject_content(pyproject_content) - Uv::FileUpdater::PyprojectPreparer - .new(pyproject_content: pyproject_content) - .sanitize - end - - def update_python_requirement(pyproject_content) - Uv::FileUpdater::PyprojectPreparer - .new(pyproject_content: pyproject_content) - .update_python_requirement(language_version_manager.python_version) - end - - def freeze_other_dependencies(pyproject_content) - Uv::FileUpdater::PyprojectPreparer - .new(pyproject_content: pyproject_content, lockfile: lockfile) - .freeze_top_level_dependencies_except([dependency]) - end - - 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::Uv::FileParser::PyprojectFilesParser::POETRY_DEPENDENCY_TYPES.each do |type| - dependencies = poetry_object[type] - next unless dependencies - - update_dependency_requirement(dependencies, updated_requirement) - end - - groups = poetry_object["group"]&.values || [] - groups.each do |group_spec| - update_dependency_requirement(group_spec["dependencies"], updated_requirement) - 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 - - def update_dependency_requirement(toml_node, requirement) - names = toml_node.keys - pkg_name = names.find { |nm| normalise(nm) == dependency.name } - return unless pkg_name - - if toml_node[pkg_name].is_a?(Hash) - toml_node[pkg_name]["version"] = requirement - else - toml_node[pkg_name] = requirement - end - end - - def subdep_type - dependency.production? ? "dependencies" : "dev-dependencies" - end - - def python_requirement_parser - @python_requirement_parser ||= - FileParser::PythonRequirementParser.new( - dependency_files: dependency_files - ) - end - - def language_version_manager - @language_version_manager ||= - LanguageVersionManager.new( - python_requirement_parser: python_requirement_parser - ) - end - - def pyproject - dependency_files.find { |f| f.name == "pyproject.toml" } - end - - def poetry_lock - dependency_files.find { |f| f.name == "poetry.lock" } - end - - def lockfile - poetry_lock - end - - def run_poetry_command(command, fingerprint: nil) - SharedHelpers.run_shell_command(command, fingerprint: fingerprint) - end - - def normalise(name) - NameNormaliser.normalise(name) - end - end - end - - class PoetryErrorHandler < UpdateChecker - extend T::Sig - - # if a valid config value is not found in project.toml file - INVALID_CONFIGURATION = /The Poetry configuration is invalid:(?.*)/ - - # if .toml has incorrect version specification i.e. <0.2.0app - INVALID_VERSION = /Could not parse version constraint: (?.*)/ - - # dependency source link not accessible - INVALID_LINK = /No valid distribution links found for package: "(?.*)" version: "(?.*)"/ - - # Python version range mentioned in .toml [tool.poetry.dependencies] python = "x.x" is not satisfied by dependency - PYTHON_RANGE_NOT_SATISFIED = /(?.*) requires Python (?.*), so it will not be satisfied for Python (?.*)/ # rubocop:disable Layout/LineLength - - # package version mentioned in .toml not found in package index - PACKAGE_NOT_FOUND = /Package (?.*) ((?.*)) not found./ - - # client access error codes while accessing package index - CLIENT_ERROR_CODES = T.let({ - error401: /401 Client Error/, - error403: /403 Client Error/, - error404: /404 Client Error/, - http403: /HTTP error 403/, - http404: /HTTP error 404/ - }.freeze, T::Hash[T.nilable(String), Regexp]) - - # server response error codes while accessing package index - SERVER_ERROR_CODES = T.let({ - server500: /500 Server Error/, - server502: /502 Server Error/, - server503: /503 Server Error/, - server504: /504 Server Error/ - }.freeze, T::Hash[T.nilable(String), Regexp]) - - # invalid configuration in pyproject.toml - POETRY_VIRTUAL_ENV_CONFIG = %r{pypoetry/virtualenvs(.|\n)*list index out of range} - - # error related to local project as dependency in pyproject.toml - ERR_LOCAL_PROJECT_PATH = /Path (?.*) for (?.*) does not exist/ - - TIME_OUT_ERRORS = T.let({ - time_out_max_retries: /Max retries exceeded/, - time_out_read_timed_out: /Read timed out/, - time_out_inactivity: /Timed out due to inactivity/ - }.freeze, T::Hash[T.nilable(String), Regexp]) - - PACKAGE_RESOLVER_ERRORS = T.let({ - package_info_error: /Unable to determine package info/, - self_dep_error: /Package '(?.*)' is listed as a dependency of itself./, - incompatible_constraints: /Incompatible constraints in requirements/ - }.freeze, T::Hash[T.nilable(String), Regexp]) - - sig do - params( - dependencies: Dependabot::Dependency, - dependency_files: T::Array[Dependabot::DependencyFile] - ).void - end - def initialize(dependencies:, dependency_files:) - @dependencies = dependencies - @dependency_files = dependency_files - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependencies - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig do - params( - url: T.nilable(String) - ).returns(String) - end - def sanitize_url(url) - T.must(url&.match(%r{^(?:https?://)?(?:[^@\n])?([^:/\n?]+)})).to_s - end - - public - - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/PerceivedComplexity - # rubocop:disable Metrics/CyclomaticComplexity - sig { params(error: Exception).void } - def handle_poetry_error(error) - Dependabot.logger.warn(error.message) - - if (msg = error.message.match(PoetryVersionResolver::INCOMPATIBLE_CONSTRAINTS) || - error.message.match(INVALID_CONFIGURATION) || error.message.match(INVALID_VERSION) || - error.message.match(INVALID_LINK)) - - raise DependencyFileNotResolvable, msg - end - - if (msg = error.message.match(PACKAGE_NOT_FOUND)) - raise DependencyFileNotResolvable, msg - end - - raise DependencyFileNotResolvable, error.message if error.message.match(PYTHON_RANGE_NOT_SATISFIED) - - if error.message.match(POETRY_VIRTUAL_ENV_CONFIG) || error.message.match(ERR_LOCAL_PROJECT_PATH) - msg = "Error while resolving pyproject.toml file" - - raise DependencyFileNotResolvable, msg - end - - SERVER_ERROR_CODES.each do |(_error_codes, error_regex)| - next unless error.message.match?(error_regex) - - index_url = URI.extract(error.message.to_s).last .then { sanitize_url(_1) } - raise InconsistentRegistryResponse, index_url - end - - TIME_OUT_ERRORS.each do |(_error_codes, error_regex)| - next unless error.message.match?(error_regex) - - raise InconsistentRegistryResponse, "Inconsistent registry response" - end - - CLIENT_ERROR_CODES.each do |(_error_codes, error_regex)| - next unless error.message.match?(error_regex) - - index_url = URI.extract(error.message.to_s).last .then { sanitize_url(_1) } - raise PrivateSourceAuthenticationFailure, index_url - end - - PACKAGE_RESOLVER_ERRORS.each do |(_error_codes, error_regex)| - next unless error.message.match?(error_regex) - - message = "Package solving failed while resolving manifest file" - raise DependencyFileNotResolvable, message - end - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/CyclomaticComplexity - end - end -end diff --git a/uv/spec/dependabot/uv/file_fetcher_spec.rb b/uv/spec/dependabot/uv/file_fetcher_spec.rb index 0ca04a7c3d..ed56c4da39 100644 --- a/uv/spec/dependabot/uv/file_fetcher_spec.rb +++ b/uv/spec/dependabot/uv/file_fetcher_spec.rb @@ -231,78 +231,9 @@ expect(file_fetcher_instance.files.map(&:name)) .to match_array(%w(pyproject.toml)) end - - context "when importing a path dependency" do - before do - stub_request(:get, url + "pyproject.toml?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_pyproject_with_path.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "path_dep/setup.py?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return(status: 404) - stub_request(:get, url + "path_dep/setup.cfg?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return(status: 404) - stub_request(:get, url + "path_dep?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_python_only_pyproject.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "path_dep/pyproject.toml?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_python_pyproject.json"), - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the path dependency" do # TODO: WIP Mon 24th Feb 2025 - debugger - expect(file_fetcher_instance.files.count).to eq(2) - expect(file_fetcher_instance.files.map(&:name)) - .to match_array(%w(pyproject.toml path_dep/pyproject.toml)) - end - end end - context "with a pyproject.toml and pdm.lock files" do - let(:repo_contents) do - fixture("github", "contents_python_pyproject_and_pdm_lock.json") - end - - before do - stub_request(:get, url + "pyproject.toml?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_python_pyproject.json"), - headers: { "content-type" => "application/json" } - ) - - stub_request(:get, url + "pdm.lock?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_python_pdm_lock.json"), - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the pyproject.toml and pdm.lock files" do - expect(file_fetcher_instance.files.count).to eq(2) - expect(file_fetcher_instance.files.map(&:name)) - .to match_array(%w(pyproject.toml pdm.lock)) - end - end - - context "with no setup.py, requirements.txt or Pipfile" do + context "with requirements.txt, requirements.in, or pyproject.toml" do let(:repo_contents) { "[]" } it "raises a Dependabot::DependencyFileNotFound error" do @@ -311,212 +242,6 @@ end end - context "with a requirements.txt and a setup.py" do - let(:repo_contents) do - fixture("github", "contents_python.json") - end - - before do - stub_request(:get, url + "requirements.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "setup.py?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "app%20?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return(status: 200, body: "[]", headers: json_header) - end - - it "fetches the requirements.txt and the setup.py file" do - expect(file_fetcher_instance.files.count).to eq(2) - expect(file_fetcher_instance.files.map(&:name)).to include("setup.py") - end - end - - context "with a requirements.txt and a pip.conf" do - let(:repo_contents) do - fixture("github", "contents_python_with_conf.json") - end - - before do - stub_request(:get, url + "requirements.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "pip.conf?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + ".python-version?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the requirements.txt, pip.conf and .python-version files" do - expect(file_fetcher_instance.files.count).to eq(3) - expect(file_fetcher_instance.files.map(&:name)).to include("pip.conf") - expect(file_fetcher_instance.files.map(&:name)) - .to include(".python-version") - end - end - - context "with a setup.py and a setup.cfg" do - let(:repo_contents) do - fixture("github", "contents_python_with_setup_cfg.json") - end - - before do - stub_request(:get, url + "setup.py?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "setup.cfg?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the requirements.txt and the setup.cfg file" do - expect(file_fetcher_instance.files.count).to eq(2) - expect(file_fetcher_instance.files.map(&:name)).to include("setup.cfg") - end - - it "exposes the expected ecosystem_versions metric" do - expect(file_fetcher_instance.ecosystem_versions).to eq({ - languages: { python: { "max" => "3.13", "raw" => "unknown" } } - }) - end - end - - context "with a requirements.txt, a setup.py and a requirements folder" do - let(:repo_contents) do - fixture("github", "contents_python_repo.json") - end - - before do - stub_request(:get, url + "requirements.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "setup.py?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_python_requirements_folder.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/coverage.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/test.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/tools.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/typing.txt?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/coverage.in?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/test.in?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/tools.in?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "requirements/typing.in?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "requirements_content.json"), - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the right files file" do - expect(file_fetcher_instance.files.map(&:name)) - .to match_array( - %w( - requirements.txt - setup.py - requirements/coverage.txt - requirements/test.txt - requirements/tools.txt - requirements/typing.txt - requirements/coverage.in - requirements/test.in - requirements/tools.in - requirements/typing.in - ) - ) - end - end - context "with a cascading requirement" do let(:repo_contents) do fixture("github", "contents_python_only_requirements.json") @@ -743,7 +468,7 @@ end end - context "with a path-based dependency that it's fetchable" do + context "with a path-based dependency that is fetchable" do let(:repo_contents) do fixture("github", "contents_python_only_requirements.json") end @@ -770,11 +495,19 @@ body: fixture("github", "setup_content.json"), headers: { "content-type" => "application/json" } ) + stub_request(:get, url + "pyproject.toml?ref=sha") + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "contents_python_pyproject.json"), + headers: { "content-type" => "application/json" } + ) end it "fetches the setup.py" do expect(file_fetcher_instance.files.count).to eq(2) - expect(file_fetcher_instance.files.map(&:name)).to include("setup.py") + expect(file_fetcher_instance.files.map(&:name)).to include("requirements.txt") + expect(file_fetcher_instance.files.map(&:name)).to include("pyproject.toml") end context "when using a variety of quote styles" do @@ -845,36 +578,35 @@ stub_request(:get, url + "file:./setup.py?ref=sha") .with(headers: { "Authorization" => "token token" }) .to_return(status: 404) - end - - it "fetches the path dependencies" do - expect(file_fetcher_instance.files.map(&:name)) - .to match_array( - %w(requirements.txt setup.py my/setup.py my-single/setup.py - my-other/setup.py my-other/setup.cfg some/zip-file.tar.gz) + stub_request(:get, url + "my/pyproject.toml?ref=sha") + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "contents_python_pyproject.json"), + headers: { "content-type" => "application/json" } ) - end - end - - context "when referencing extras" do - let(:requirements_txt) do - fixture("github", "requirements_with_self_reference_extras.json") - end - - before do - stub_request(:get, url + "requirements.txt?ref=sha") + stub_request(:get, url + "my-single/pyproject.toml?ref=sha") .with(headers: { "Authorization" => "token token" }) .to_return( status: 200, - body: requirements_txt, + body: fixture("github", "contents_python_pyproject.json"), + headers: { "content-type" => "application/json" } + ) + stub_request(:get, url + "my-other/pyproject.toml?ref=sha") + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "contents_python_pyproject.json"), headers: { "content-type" => "application/json" } ) end - it "fetches the setup.py" do - expect(file_fetcher_instance.files.count).to eq(2) + it "fetches the path dependencies" do expect(file_fetcher_instance.files.map(&:name)) - .to include("setup.py") + .to match_array( + %w(requirements.txt pyproject.toml my/pyproject.toml my-single/pyproject.toml + my-other/pyproject.toml some/zip-file.tar.gz) + ) end end @@ -923,107 +655,10 @@ ) end - it "fetches the setup.py (does not look in the nested directory)" do + it "fetches the pyproject.toml (does not look in the nested directory)" do expect(file_fetcher_instance.files.count).to eq(5) expect(file_fetcher_instance.files.map(&:name)) - .to include("setup.py") - end - end - - context "when in a Pipfile" do - let(:repo_contents) do - fixture("github", "contents_python_only_pipfile_and_lockfile.json") - end - let(:directory) { "/docs" } - - before do - stub_request(:get, url + "docs?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return(status: 200, body: repo_contents, headers: json_header) - stub_request(:get, url + "docs/Pipfile?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", - "contents_python_pipfile_with_path_dep.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "docs/Pipfile.lock?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "flowmachine/setup.py?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "flowmachine/setup.cfg?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return(status: 404) - stub_request(:get, url + "flowmachine?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: "[]", - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "flowclient/setup.py?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + "flowclient/setup.cfg?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return(status: 404) - stub_request(:get, url + "flowclient?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: "[]", - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the setup.py" do - expect(file_fetcher_instance.files.map(&:name)) - .to match_array( - %w(Pipfile Pipfile.lock - ../flowmachine/setup.py ../flowclient/setup.py) - ) - end - - context "with a .python-version file at the top level" do - before do - stub_request(:get, url + "?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "contents_python_with_conf.json"), - headers: { "content-type" => "application/json" } - ) - stub_request(:get, url + ".python-version?ref=sha") - .with(headers: { "Authorization" => "token token" }) - .to_return( - status: 200, - body: fixture("github", "setup_content.json"), - headers: { "content-type" => "application/json" } - ) - end - - it "fetches the .python-version" do - expect(file_fetcher_instance.files.map(&:name)) - .to match_array( - %w(Pipfile Pipfile.lock .python-version - ../flowmachine/setup.py ../flowclient/setup.py) - ) - end + .to include("pyproject.toml") end end end @@ -1061,9 +696,8 @@ end it "doesn't raise a path dependency error" do - expect(file_fetcher_instance.files.count).to eq(3) - expect(file_fetcher_instance.files.map(&:name)).to contain_exactly("requirements-test.txt", "pyproject.toml", - "setup.cfg") + expect(file_fetcher_instance.files.count).to eq(2) + expect(file_fetcher_instance.files.map(&:name)).to contain_exactly("requirements-test.txt", "pyproject.toml") end end diff --git a/uv/spec/dependabot/uv/file_parser/pyproject_files_parser_spec.rb b/uv/spec/dependabot/uv/file_parser/pyproject_files_parser_spec.rb index a4ecd5a036..2ea613186a 100644 --- a/uv/spec/dependabot/uv/file_parser/pyproject_files_parser_spec.rb +++ b/uv/spec/dependabot/uv/file_parser/pyproject_files_parser_spec.rb @@ -32,14 +32,16 @@ .to raise_error do |error| expect(error.class) .to eq(Dependabot::DependencyFileNotParseable) + # rubocop:disable Style/RedundantStringEscape expect(error.message) .to eq <<~ERROR.strip - /pyproject.toml is missing the following sections: + \/pyproject.toml is missing the following sections: * tool.poetry.name * tool.poetry.version * tool.poetry.description * tool.poetry.authors ERROR + # rubocop:enable Style/RedundantStringEscape end end end @@ -369,14 +371,6 @@ its(:length) { is_expected.to eq(5) } end - context "with optional dependencies only" do - subject(:dependencies) { parser.dependency_set.dependencies } - - let(:pyproject_fixture_name) { "optional_dependencies_only.toml" } - - its(:length) { is_expected.to be > 0 } - end - describe "parse standard python files" do subject(:dependencies) { parser.dependency_set.dependencies } @@ -406,14 +400,6 @@ its(:length) { is_expected.to eq(0) } end - - context "with optional dependencies only" do - subject(:dependencies) { parser.dependency_set.dependencies } - - let(:pyproject_fixture_name) { "pyproject_1_0_0_optional_deps.toml" } - - its(:length) { is_expected.to be > 0 } - end end end end diff --git a/uv/spec/dependabot/uv/file_parser_spec.rb b/uv/spec/dependabot/uv/file_parser_spec.rb index 9c6395ef59..f10041fb5f 100644 --- a/uv/spec/dependabot/uv/file_parser_spec.rb +++ b/uv/spec/dependabot/uv/file_parser_spec.rb @@ -405,93 +405,6 @@ its(:length) { is_expected.to eq(1) } end - context "with a constraints file" do - let(:files) { [requirements, constraints] } - let(:requirements_fixture_name) { "with_constraints.txt" } - - context "when not specific" do - let(:constraints) do - Dependabot::DependencyFile.new( - name: "constraints.txt", - content: fixture("constraints", "less_than.txt") - ) - end - - its(:length) { is_expected.to eq(1) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to be_nil - expect(dependency.requirements.map { |r| r[:requirement] }) - .to contain_exactly("<2.0.0", nil) - end - end - end - - context "when specific" do - let(:constraints) do - Dependabot::DependencyFile.new( - name: "constraints.txt", - content: fixture("constraints", "specific.txt") - ) - end - - its(:length) { is_expected.to eq(1) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to eq("2.0.0") - expect(dependency.requirements).to contain_exactly({ - requirement: nil, - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }, { - requirement: "==2.0.0", - file: "constraints.txt", - groups: ["dependencies"], - source: nil - }) - end - end - - context "when the requirements file is specific, too" do - let(:requirements_fixture_name) { "specific_with_constraints.txt" } - - its(:length) { is_expected.to eq(1) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to eq("2.0.0") - expect(dependency.requirements).to contain_exactly({ - requirement: "==2.0.0", - file: "constraints.txt", - groups: ["dependencies"], - source: nil - }, { - requirement: "==2.4.1", - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }) - end - end - end - end - end - context "with requirements-dev.txt" do let(:file) { [requirements] } let(:requirements) do @@ -582,186 +495,6 @@ end end - context "with reference to its setup.py" do - let(:files) { [requirements, setup_file] } - let(:requirements) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture("requirements", "with_setup_path.txt") - ) - end - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "setup.py") - ) - end - - # setup.py dependencies get imported - its(:length) { is_expected.to eq(15) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to eq("2.1.0") - expect(dependency.requirements).to eq( - [{ - requirement: "==2.1.0", - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }, { - requirement: "==2.12.*", - file: "setup.py", - groups: ["install_requires"], - source: nil - }] - ) - end - end - - describe "the last dependency" do - subject(:dependency) { dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("flask") - expect(dependency.version).to eq("0.12.2") - expect(dependency.requirements).to eq( - [{ - requirement: "==0.12.2", - file: "setup.py", - groups: ["extras_require:API"], - source: nil - }] - ) - end - end - - context "when in a nested requirements file" do - let(:files) { [requirements, child_requirements, setup_file] } - let(:requirements) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture("requirements", "cascading_nested.txt") - ) - end - let(:child_requirements) do - Dependabot::DependencyFile.new( - name: "nested/more_requirements.txt", - content: fixture("requirements", "with_setup_path.txt") - ) - end - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "nested/setup.py", - content: fixture("setup_files", "small_needs_sanitizing.py") - ) - end - - # Note that the path dependency *isn't* parsed (because it's a manifest - # for a path dependency, not for *this* project) - its(:length) { is_expected.to eq(2) } - end - - context "with a parse_requirements statement" do - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "with_parse_reqs.py") - ) - end - - its(:length) { is_expected.to eq(5) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to eq("2.1.0") - expect(dependency.requirements).to eq( - [{ - requirement: "==2.1.0", - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }] - ) - end - end - end - - context "with a file that must be executed as main" do - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "requires_main.py") - ) - end - - its(:length) { is_expected.to eq(6) } - - describe "the last dependency" do - subject(:dependency) { dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("flask") - expect(dependency.version).to eq("0.12.2") - expect(dependency.requirements).to eq( - [{ - requirement: "==0.12.2", - file: "setup.py", - groups: ["extras_require:API"], - source: nil - }] - ) - end - end - end - - context "with a setup.cfg" do - let(:files) { [requirements, setup_file, setup_cfg] } - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "with_pbr.py") - ) - end - let(:setup_cfg) do - Dependabot::DependencyFile.new( - name: "setup.cfg", - content: fixture("setup_files", "setup.cfg") - ) - end - - its(:length) { is_expected.to eq(3) } - - describe "the last dependency" do - subject(:dependency) { dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("raven") - expect(dependency.version).to be_nil - expect(dependency.requirements).to eq( - [{ - requirement: nil, - file: "setup.py", - groups: ["install_requires"], - source: nil - }] - ) - end - end - end - end - context "with child requirement files" do let(:files) { [requirements, child_requirements] } let(:requirements_fixture_name) { "cascading.txt" } @@ -784,7 +517,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" ), Dependabot::Dependency.new( name: "attrs", version: "18.0.0", @@ -794,7 +527,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" ), Dependabot::Dependency.new( name: "aiocache[redis]", version: "0.10.0", @@ -804,7 +537,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" ), Dependabot::Dependency.new( name: "luigi", version: "2.2.0", @@ -814,7 +547,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" ), Dependabot::Dependency.new( name: "psycopg2", version: "2.6.1", @@ -824,7 +557,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" ), Dependabot::Dependency.new( name: "pytest", version: "3.4.0", @@ -834,7 +567,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" )) end end @@ -954,8 +687,9 @@ it "returns the correct ecosystem and package manager set" do ecosystem = parser.ecosystem - expect(ecosystem.name).to eq("Python") - expect(ecosystem.package_manager.name).to eq("pip-compile") + expect(ecosystem.name).to eq("uv") + expect(ecosystem.package_manager.name).to eq("uv") + expect(ecosystem.package_manager.version.to_s).to eq("0.6.2") expect(ecosystem.language.name).to eq("python") end end @@ -978,325 +712,6 @@ end end - context "with a setup.py" do - let(:files) { [setup_file] } - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "setup.py") - ) - end - - its(:length) { is_expected.to eq(15) } - - describe "an install_requires dependencies" do - subject(:dependency) { dependencies.find { |d| d.name == "boto3" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("boto3") - expect(dependency.version).to eq("1.3.1") - expect(dependency.requirements).to eq( - [{ - requirement: "==1.3.1", - file: "setup.py", - groups: ["install_requires"], - source: nil - }] - ) - end - end - - context "with markers" do - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "markers.py") - ) - end - - describe "a dependency with markers" do - subject(:dependency) { dependencies.find { |d| d.name == "boto3" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("boto3") - expect(dependency.version).to eq("1.3.1") - expect(dependency.requirements).to eq( - [{ - requirement: "==1.3.1", - file: "setup.py", - groups: ["install_requires"], - source: nil - }] - ) - end - end - end - - context "with extras" do - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", "extras.py") - ) - end - - describe "a dependency with extras" do - subject(:dependency) do - dependencies.find { |d| d.name == "requests[security]" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests[security]") - expect(dependency.version).to be_nil - expect(dependency.requirements).to eq( - [{ - requirement: "==2.12.*", - file: "setup.py", - groups: ["install_requires"], - source: nil - }] - ) - end - end - end - end - - context "with a setup.cfg" do - let(:files) { [setup_cfg_file] } - let(:setup_cfg_file) do - Dependabot::DependencyFile.new( - name: "setup.cfg", - content: fixture("setup_files", "setup_with_requires.cfg") - ) - end - - its(:length) { is_expected.to eq(15) } - - describe "an install_requires dependencies" do - subject(:dependency) { dependencies.find { |d| d.name == "boto3" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("boto3") - expect(dependency.version).to eq("1.3.1") - expect(dependency.requirements).to eq( - [{ - requirement: "==1.3.1", - file: "setup.cfg", - groups: ["install_requires"], - source: nil - }] - ) - end - end - - context "with markers" do - let(:setup_cfg_file) do - Dependabot::DependencyFile.new( - name: "setup.cfg", - content: fixture("setup_files", "markers.cfg") - ) - end - - describe "a dependency with markers" do - subject(:dependency) { dependencies.find { |d| d.name == "boto3" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("boto3") - expect(dependency.version).to eq("1.3.1") - expect(dependency.requirements).to eq( - [{ - requirement: "==1.3.1", - file: "setup.cfg", - groups: ["install_requires"], - source: nil - }] - ) - end - end - end - - context "with extras" do - let(:setup_cfg_file) do - Dependabot::DependencyFile.new( - name: "setup.cfg", - content: fixture("setup_files", "extras.cfg") - ) - end - - describe "a dependency with extras" do - subject(:dependency) do - dependencies.find { |d| d.name == "requests[security]" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests[security]") - expect(dependency.version).to be_nil - expect(dependency.requirements).to eq( - [{ - requirement: "==2.12.*", - file: "setup.cfg", - groups: ["install_requires"], - source: nil - }] - ) - end - end - end - end - - context "with a Pipfile and Pipfile.lock" do - let(:files) { [pipfile, lockfile] } - let(:pipfile) do - Dependabot::DependencyFile.new(name: "Pipfile", content: pipfile_body) - end - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "Pipfile.lock", - content: lockfile_body - ) - end - - let(:pipfile_body) { fixture("pipfile_files", pipfile_fixture_name) } - let(:lockfile_body) do - fixture("pipfile_files", lockfile_fixture_name) - end - let(:pipfile_fixture_name) { "version_not_specified" } - let(:lockfile_fixture_name) { "version_not_specified.lock" } - - its(:length) { is_expected.to eq(7) } - - describe "top level dependencies" do - subject(:dependencies) { parser.parse.select(&:top_level?) } - - its(:length) { is_expected.to eq(2) } - - describe "the first dependency" do - subject { dependencies.first } - - let(:expected_requirements) do - [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - end - - it { is_expected.to be_a(Dependabot::Dependency) } - its(:name) { is_expected.to eq("requests") } - its(:version) { is_expected.to eq("2.18.0") } - its(:requirements) { is_expected.to eq(expected_requirements) } - end - end - - context "when importing a path dependency" do - let(:files) { [pipfile, lockfile, setup_file] } - let(:pipfile_fixture_name) { "path_dependency_not_self" } - let(:lockfile_fixture_name) { "path_dependency_not_self.lock" } - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "mydep/setup.py", - content: fixture("setup_files", "small.py"), - support_file: true - ) - end - - describe "top level dependencies" do - subject(:dependencies) { parser.parse.select(&:top_level?) } - - its(:length) { is_expected.to eq(2) } - - it "excludes the path dependency" do - expect(dependencies.map(&:name)).to match_array(%w(requests pytest)) - end - end - end - end - - context "with a Pipfile but no Pipfile.lock" do - let(:files) { [pipfile] } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", "version_not_specified") - ) - end - - its(:length) { is_expected.to eq(2) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to be_nil - expect(dependency.requirements).to eq( - [{ - requirement: "*", - file: "Pipfile", - groups: ["default"], - source: nil - }] - ) - end - end - - context "when dealing with a requirements.txt" do - let(:files) { [pipfile, requirements] } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", "version_not_specified") - ) - end - - its(:length) { is_expected.to eq(6) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("requests") - expect(dependency.version).to be_nil - expect(dependency.requirements).to eq( - [{ - requirement: "*", - file: "Pipfile", - groups: ["default"], - source: nil - }] - ) - end - end - - describe "the third dependency" do - subject(:dependency) { dependencies[2] } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("psycopg2") - expect(dependency.version).to eq("2.6.1") - expect(dependency.requirements).to eq( - [{ - requirement: "==2.6.1", - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }] - ) - end - end - end - end - context "with a pyproject.toml in poetry format and a lock file" do let(:files) { [pyproject, poetry_lock] } let(:pyproject) do @@ -1440,14 +855,5 @@ expect { dependencies }.to raise_error(Dependabot::UnexpectedExternalCode) end end - - context "with multiple requirements" do - let(:files) { project_dependency_files("poetry/multiple_requirements") } - - it "returns the dependencies with multiple requirements" do - expect { dependencies }.not_to raise_error - expect(dependencies.map(&:name)).to contain_exactly("numpy", "scipy") - end - end end end diff --git a/uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_v2_spec.rb b/uv/spec/dependabot/uv/file_updater/compile_file_updater_spec.rb similarity index 99% rename from uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_v2_spec.rb rename to uv/spec/dependabot/uv/file_updater/compile_file_updater_spec.rb index b6770f8ae9..a6838240af 100644 --- a/uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_v2_spec.rb +++ b/uv/spec/dependabot/uv/file_updater/compile_file_updater_spec.rb @@ -4,10 +4,10 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" -require "dependabot/uv/file_updater/pip_compile_file_updater" +require "dependabot/uv/file_updater/compile_file_updater" require "dependabot/shared_helpers" -RSpec.describe Dependabot::Uv::FileUpdater::PipCompileFileUpdater do +RSpec.describe Dependabot::Uv::FileUpdater::CompileFileUpdater do let(:updater) do described_class.new( dependency_files: dependency_files, @@ -37,7 +37,7 @@ previous_version: dependency_previous_version, requirements: dependency_requirements, previous_requirements: dependency_previous_requirements, - package_manager: "pip" + package_manager: "uv" ) end let(:dependency_name) { "attrs" } diff --git a/uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_spec.rb b/uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_spec.rb deleted file mode 100644 index a5730e35bf..0000000000 --- a/uv/spec/dependabot/uv/file_updater/pip_compile_file_updater_spec.rb +++ /dev/null @@ -1,636 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/file_updater/pip_compile_file_updater" -require "dependabot/shared_helpers" - -RSpec.describe Dependabot::Uv::FileUpdater::PipCompileFileUpdater do - let(:updater) do - described_class.new( - dependency_files: dependency_files, - dependencies: [dependency], - credentials: credentials - ) - end - let(:dependency_files) { [manifest_file, generated_file] } - let(:manifest_file) do - Dependabot::DependencyFile.new( - name: "requirements/test.in", - content: fixture("pip_compile_files", manifest_fixture_name) - ) - end - let(:generated_file) do - Dependabot::DependencyFile.new( - name: "requirements/test.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - let(:manifest_fixture_name) { "unpinned.in" } - let(:generated_fixture_name) { "pip_compile_unpinned.txt" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - previous_version: dependency_previous_version, - requirements: dependency_requirements, - previous_requirements: dependency_previous_requirements, - package_manager: "pip" - ) - end - let(:dependency_name) { "attrs" } - let(:dependency_version) { "18.1.0" } - let(:dependency_previous_version) { "17.3.0" } - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: nil, - groups: [], - source: nil - }] - end - let(:dependency_previous_requirements) do - [{ - file: "requirements/test.in", - requirement: nil, - groups: [], - source: nil - }] - end - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - })] - end - let(:tmp_path) { Dependabot::Utils::BUMP_TMP_DIR_PATH } - - before { FileUtils.mkdir_p(tmp_path) } - - describe "#updated_dependency_files" do - subject(:updated_files) { updater.updated_dependency_files } - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content) - .to include("pbr==4.0.2\n # via mock") - expect(updated_files.first.content).to include("# This file is autogen") - expect(updated_files.first.content).not_to include("--hash=sha") - end - - context "with a mismatch in filename" do - let(:generated_fixture_name) { "pip_compile_unpinned_renamed.txt" } - let(:generated_file) do - Dependabot::DependencyFile.new( - name: "requirements/test-funky.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content) - .to include("pbr==4.0.2\n # via mock") - expect(updated_files.first.content).to include("# This file is autogen") - expect(updated_files.first.content).not_to include("--hash=sha") - end - end - - context "with a custom header" do - let(:generated_fixture_name) { "pip_compile_custom_header.txt" } - - it "preserves the header" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content).to include("make upgrade") - end - end - - context "with a no-binary flag" do - let(:manifest_fixture_name) { "no_binary.in" } - let(:generated_fixture_name) { "pip_compile_no_binary.txt" } - let(:dependency_name) { "psycopg2" } - let(:dependency_version) { "2.7.6" } - let(:dependency_previous_version) { "2.7.4" } - - it "updates the requirements.txt correctly" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("psycopg2==2.7.6") - expect(updated_files.first.content).to include("--no-binary psycopg2") - expect(updated_files.first.content) - .not_to include("--no-binary psycopg2==") - end - end - - context "with hashes" do - let(:generated_fixture_name) { "pip_compile_hashes.txt" } - - it "updates the requirements.txt, keeping the hashes" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content).to include("4b90b09eeeb9b88c35bc64") - expect(updated_files.first.content) - .not_to include("# This file is autogen") - end - - context "when needing an augmented hashin" do - let(:manifest_fixture_name) { "extra_hashes.in" } - let(:generated_fixture_name) { "pip_compile_extra_hashes.txt" } - let(:dependency_name) { "pyasn1-modules" } - let(:dependency_version) { "0.1.5" } - let(:dependency_previous_version) { "0.1.4" } - - it "updates the requirements.txt, keeping all the hashes" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content) - .to include("# This file is autogen") - expect(updated_files.first.content) - .to include("pyasn1-modules==0.1.5 \\\n --hash=sha256:01") - expect(updated_files.first.content) - .to include("--hash=sha256:b437be576bdf440fc0e930") - expect(updated_files.first.content) - .to include("pyasn1==0.3.7 \\\n --hash=sha256:16") - expect(updated_files.first.content) - .to include("--hash=sha256:bb6f5d5507621e0298794b") - expect(updated_files.first.content) - .to include("# via pyasn1-modules") - expect(updated_files.first.content).not_to include("WARNING") - end - end - end - - context "with another dependency with an unmet marker" do - let(:manifest_fixture_name) { "unmet_marker.in" } - let(:generated_fixture_name) { "pip_compile_unmet_marker.txt" } - - it "updates the requirements.txt, keeping the unmet dep out of it" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content).not_to include("flaky") - end - end - - context "with an unsafe dependency" do - let(:manifest_fixture_name) { "unsafe.in" } - let(:dependency_name) { "flake8" } - let(:dependency_version) { "3.6.0" } - let(:dependency_previous_version) { "3.5.0" } - - context "when not including in the lockfile" do - let(:generated_fixture_name) { "pip_compile_safe.txt" } - - it "does not include the unsafe dependency" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("flake8==3.6.0") - expect(updated_files.first.content).not_to include("setuptools") - expect(updated_files.first.content).to end_with("via flake8\n") - end - end - - context "when including in the lockfile" do - let(:generated_fixture_name) { "pip_compile_unsafe.txt" } - - it "includes the unsafe dependency" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("flake8==3.6.0") - expect(updated_files.first.content).to include("setuptools") - end - end - end - - context "with an import of the setup.py" do - let(:dependency_files) do - [manifest_file, generated_file, setup_file, pyproject] - end - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", setup_fixture_name) - ) - end - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: fixture("pyproject_files", "black_configuration.toml") - ) - end - let(:manifest_fixture_name) { "imports_setup.in" } - let(:generated_fixture_name) { "pip_compile_imports_setup.txt" } - let(:setup_fixture_name) { "small.py" } - - it "updates the requirements.txt", :slow do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content) - .to include("-e file:///Users/greysteil/code/python-test") - expect(updated_files.first.content).not_to include("tmp/dependabot") - expect(updated_files.first.content) - .to include("pbr==4.0.2\n # via mock") - expect(updated_files.first.content).to include("# This file is autogen") - expect(updated_files.first.content).not_to include("--hash=sha") - end - - context "when needing sanitization", :slow do - let(:setup_fixture_name) { "small_needs_sanitizing.py" } - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - end - end - end - - context "with editable dependencies (that are misordered in the .txt)" do - let(:manifest_fixture_name) { "editable.in" } - let(:generated_fixture_name) { "pip_compile_editable.txt" } - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==18.1.0") - expect(updated_files.first.content) - .to include("-e git+https://github.com/testing-cabal/mock.git@2.0.0") - expect(updated_files.first.content) - .to include("-e git+https://github.com/box/flaky.git@v3.5.3#egg=flaky") - end - end - - context "with a subdependency" do - let(:dependency_name) { "pbr" } - let(:dependency_version) { "4.2.0" } - let(:dependency_previous_version) { "4.0.2" } - let(:dependency_requirements) { [] } - let(:dependency_previous_requirements) { [] } - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content) - .to include("pbr==4.2.0\n # via mock") - end - - context "with an uncompiled requirement file, too" do - let(:dependency_files) do - [manifest_file, generated_file, requirement_file] - end - let(:requirement_file) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture("requirements", "pbr.txt") - ) - end - let(:dependency_requirements) do - [{ - file: "requirements.txt", - requirement: "==4.2.0", - groups: [], - source: nil - }] - end - let(:dependency_previous_requirements) do - [{ - file: "requirements.txt", - requirement: "==4.0.2", - groups: [], - source: nil - }] - end - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(2) - expect(updated_files.first.content) - .to include("pbr==4.2.0\n # via mock") - expect(updated_files.last.content).to include("pbr==4.2.0") - end - end - end - - context "when targeting a non-latest version" do - let(:dependency_version) { "17.4.0" } - - it "updates the requirements.txt" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("attrs==17.4.0") - expect(updated_files.first.content) - .to include("pbr==4.0.2\n # via mock") - expect(updated_files.first.content).to include("# This file is autogen") - expect(updated_files.first.content).not_to include("--hash=sha") - end - end - - context "when the requirement.in file needs to be updated" do - let(:manifest_fixture_name) { "bounded.in" } - let(:generated_fixture_name) { "pip_compile_bounded.txt" } - - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=18.1.0", - groups: [], - source: nil - }] - end - let(:dependency_previous_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }] - end - - it "updates the requirements.txt and the requirements.in" do - expect(updated_files.count).to eq(2) - expect(updated_files.first.content).to include("Attrs<=18.1.0") - expect(updated_files.last.content).to include("attrs==18.1.0") - expect(updated_files.last.content).not_to include("# via mock") - end - - context "with an additional requirements.txt" do - let(:dependency_files) { [manifest_file, generated_file, other_txt] } - let(:other_txt) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: - fixture("requirements", "pip_compile_unpinned.txt") - ) - end - - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=18.1.0", - groups: [], - source: nil - }, { - file: "requirements.txt", - requirement: "==18.1.0", - groups: [], - source: nil - }] - end - let(:dependency_previous_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }, { - file: "requirements.txt", - requirement: "==17.3.0", - groups: [], - source: nil - }] - end - - it "updates the other requirements.txt, too" do - expect(updated_files.count).to eq(3) - expect(updated_files.first.content).to include("Attrs<=18.1.0") - expect(updated_files[1].content).to include("attrs==18.1.0") - expect(updated_files.last.content).to include("attrs==18.1.0") - end - end - - context "with multiple requirement.in files" do - let(:dependency_files) do - [ - manifest_file, manifest_file2, manifest_file3, manifest_file4, - generated_file, generated_file2, generated_file3, generated_file4 - ] - end - - let(:manifest_file2) do - Dependabot::DependencyFile.new( - name: "requirements/dev.in", - content: - fixture("pip_compile_files", manifest_fixture_name) - ) - end - let(:generated_file2) do - Dependabot::DependencyFile.new( - name: "requirements/dev.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - - let(:manifest_file3) do - Dependabot::DependencyFile.new( - name: "requirements/mirror2.in", - content: - fixture("pip_compile_files", "imports_mirror.in") - ) - end - let(:generated_file3) do - Dependabot::DependencyFile.new( - name: "requirements/mirror2.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - - let(:manifest_file4) do - Dependabot::DependencyFile.new( - name: "requirements/mirror.in", - content: - fixture("pip_compile_files", "imports_dev.in") - ) - end - let(:generated_file4) do - Dependabot::DependencyFile.new( - name: "requirements/mirror.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=18.1.0", - groups: [], - source: nil - }, { - file: "requirements/dev.in", - requirement: "<=18.1.0", - groups: [], - source: nil - }] - end - let(:dependency_previous_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }, { - file: "requirements/dev.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }] - end - - it "updates the other manifest file, too" do - expect(updated_files.count).to eq(6) - expect(updated_files[0].name).to eq("requirements/test.in") - expect(updated_files[1].name).to eq("requirements/dev.in") - expect(updated_files[2].name).to eq("requirements/test.txt") - expect(updated_files[3].name).to eq("requirements/dev.txt") - expect(updated_files[4].name).to eq("requirements/mirror2.txt") - expect(updated_files[5].name).to eq("requirements/mirror.txt") - expect(updated_files[0].content).to include("Attrs<=18.1.0") - expect(updated_files[1].content).to include("Attrs<=18.1.0") - expect(updated_files[2].content).to include("attrs==18.1.0") - expect(updated_files[3].content).to include("attrs==18.1.0") - expect(updated_files[4].content).to include("attrs==18.1.0") - expect(updated_files[5].content).to include("attrs==18.1.0") - end - end - end - - context "with incompatible versions" do - let(:manifest_fixture_name) { "incompatible_versions.in" } - let(:generated_fixture_name) { "incompatible_versions.txt" } - let(:dependency_name) { "pyyaml" } - let(:dependency_version) { "6.0.1" } - let(:dependency_previous_version) { "5.3.1" } - let(:dependency_requirements) { [] } - let(:dependency_previous_requirements) { [] } - - it "raises an error indicating the dependencies are not resolvable", :slow do - expect { updated_files }.to raise_error(Dependabot::DependencyFileNotResolvable) do |err| - expect(err.message).to include( - "There are incompatible versions in the resolved dependencies:\n pyyaml==6.0.1" - ) - end - end - end - - context "with stripped extras" do - let(:manifest_fixture_name) { "strip_extras.in" } - let(:generated_fixture_name) { "pip_compile_strip_extras.txt" } - let(:dependency_name) { "cachecontrol" } - let(:dependency_version) { "0.12.10" } - let(:dependency_previous_version) { "0.12.9" } - - it "doesn't add an extras annotation on cachecontrol" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("--strip-extras") - expect(updated_files.first.content).to include("cachecontrol==0.12.10") - expect(updated_files.first.content) - .not_to include("cachecontrol[filecache]==") - end - end - - context "with resolver backtracking header" do - let(:manifest_fixture_name) { "celery_extra_sqs.in" } - let(:generated_fixture_name) { "pip_compile_resolver_backtracking.txt" } - let(:dependency_name) { "celery" } - let(:dependency_version) { "5.2.7" } - let(:dependency_previous_version) { "5.2.6" } - - it "adds pycurl as dependency" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("--resolver=backtracking") - expect(updated_files.first.content).to include("pycurl") - end - end - - context "with resolver legacy header" do - let(:manifest_fixture_name) { "celery_extra_sqs.in" } - let(:generated_fixture_name) { "pip_compile_resolver_legacy.txt" } - let(:dependency_name) { "celery" } - let(:dependency_version) { "5.2.7" } - let(:dependency_previous_version) { "5.2.6" } - - it "do not include pycurl" do - expect(updated_files.count).to eq(1) - expect(updated_files.first.content).to include("--resolver=legacy") - expect(updated_files.first.content).not_to include("pycurl") - end - end - end - - describe "#package_hashes_for" do - let(:name) { "package_name" } - let(:version) { "1.0.0" } - let(:algorithm) { "sha256" } - - context "when index_urls is not set" do - let(:updater) do - described_class.new( - dependencies: [], - dependency_files: [], - credentials: [] - ) - end - - before do - allow(Dependabot::SharedHelpers).to receive(:run_helper_subprocess).and_return([{ "hash" => "123abc" }]) - end - - it "returns hash" do - result = updater.send(:package_hashes_for, name: name, version: version, algorithm: algorithm) - expect(result).to eq(["--hash=sha256:123abc"]) - end - end - - context "when multiple index_urls are set" do - let(:updater) do - described_class.new( - dependencies: [], - dependency_files: [], - credentials: [], - index_urls: [nil, "http://example.com"] - ) - end - - before do - allow(Dependabot::SharedHelpers).to receive(:run_helper_subprocess) - .and_return([{ "hash" => "123abc" }], [{ "hash" => "312cba" }]) - end - - it "returns returns two hashes" do - result = updater.send(:package_hashes_for, name: name, version: version, algorithm: algorithm) - expect(result).to eq(%w(--hash=sha256:123abc --hash=sha256:312cba)) - end - end - - context "when multiple index_urls are set but package does not exist in PyPI" do - let(:updater) do - described_class.new( - dependencies: [], - dependency_files: [], - credentials: [], - index_urls: [nil, "http://example.com"] - ) - end - - before do - allow(Dependabot::SharedHelpers).to receive(:run_helper_subprocess).with({ - args: %w(package_name 1.0.0 sha256), - command: "pyenv exec python3 /opt/python/run.py", - function: "get_dependency_hash" - }).and_raise( - Dependabot::SharedHelpers::HelperSubprocessFailed.new( - message: "Error message", error_context: {}, error_class: "PackageNotFoundError" - ) - ) - - allow(Dependabot::SharedHelpers).to receive(:run_helper_subprocess) - .with({ - args: %w(package_name 1.0.0 sha256 http://example.com), - command: "pyenv exec python3 /opt/python/run.py", - function: "get_dependency_hash" - }).and_return([{ "hash" => "123abc" }]) - end - - it "returns returns two hashes" do - result = updater.send(:package_hashes_for, name: name, version: version, algorithm: algorithm) - expect(result).to eq(["--hash=sha256:123abc"]) - end - end - end -end diff --git a/uv/spec/dependabot/uv/file_updater/pipfile_file_updater_spec.rb b/uv/spec/dependabot/uv/file_updater/pipfile_file_updater_spec.rb deleted file mode 100644 index d211aeacb7..0000000000 --- a/uv/spec/dependabot/uv/file_updater/pipfile_file_updater_spec.rb +++ /dev/null @@ -1,684 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/file_updater/pipfile_file_updater" -require "dependabot/shared_helpers" - -RSpec.describe Dependabot::Uv::FileUpdater::PipfileFileUpdater do - let(:updater) do - described_class.new( - dependency_files: dependency_files, - dependencies: [dependency], - credentials: credentials, - repo_contents_path: repo_contents_path - ) - end - let(:dependency_files) { [pipfile, lockfile] } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", pipfile_fixture_name) - ) - end - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "Pipfile.lock", - content: fixture("pipfile_files", lockfile_fixture_name) - ) - end - let(:pipfile_fixture_name) { "version_not_specified" } - let(:lockfile_fixture_name) { "version_not_specified.lock" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - let(:dependency_name) { "requests" } - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - })] - end - let(:repo_contents_path) { nil } - - describe "#updated_dependency_files" do - subject(:updated_files) { updater.updated_dependency_files } - - context "with a capital letter" do - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - let(:pipfile_fixture_name) { "hard_names" } - let(:lockfile_fixture_name) { "hard_names.lock" } - - it "updates the lockfile successfully (and doesn't affect other deps)" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "Pipfile.lock" } - json_lockfile = JSON.parse(updated_lockfile.content) - - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - expect(json_lockfile["develop"]["pytest"]["version"]) - .to eq("==3.4.0") - end - end - - context "when the Pipfile hasn't changed" do - let(:pipfile_fixture_name) { "version_not_specified" } - let(:lockfile_fixture_name) { "version_not_specified.lock" } - - it "only returns the lockfile" do - expect(updated_files.map(&:name)).to eq(["Pipfile.lock"]) - end - end - - context "when the Pipfile specified a Python version" do - let(:pipfile_fixture_name) { "required_python" } - let(:lockfile_fixture_name) { "required_python.lock" } - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it "updates both files correctly" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "Pipfile.lock" } - updated_pipfile = updated_files.find { |f| f.name == "Pipfile" } - json_lockfile = JSON.parse(updated_lockfile.content) - - expect(updated_pipfile.content) - .to include('python_full_version = "3.9.4"') - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - expect(json_lockfile["develop"]["pytest"]["version"]).to eq("==3.4.0") - expect(json_lockfile["_meta"]["requires"]) - .to eq(JSON.parse(lockfile.content)["_meta"]["requires"]) - end - - context "when from a Poetry file and including || logic" do - let(:pipfile_fixture_name) { "exact_version" } - let(:dependency_files) { [pipfile, lockfile, pyproject] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: fixture("pyproject_files", "basic_poetry_dependencies.toml") - ) - end - - it "updates both files correctly" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - end - end - - context "when including a .python-version file" do - let(:dependency_files) { [pipfile, lockfile, python_version_file] } - let(:python_version_file) do - Dependabot::DependencyFile.new( - name: ".python-version", - content: "3.9.4\n" - ) - end - - it "updates both files correctly" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - end - end - end - - context "with a source not included in the original Pipfile" do - let(:credentials) do - [ - Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), - Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.posrip.com/pypi/" - }) - ] - end - - it "the source is not included in the final updated files" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile.lock)) # because Pipfile shouldn't have changed - - updated_lockfile = updated_files.find { |f| f.name == "Pipfile.lock" } - expect(updated_lockfile.content).not_to include("dependabot-inserted-index") - expect(updated_lockfile.content).not_to include("https://pypi.posrip.com/pypi/") - - json_lockfile = JSON.parse(updated_lockfile.content) - expect(json_lockfile["_meta"]["sources"]).to eq(JSON.parse(lockfile.content)["_meta"]["sources"]) - end - end - - context "when the Pipfile included an environment variable source" do - let(:pipfile_fixture_name) { "environment_variable_source" } - let(:lockfile_fixture_name) { "environment_variable_source.lock" } - let(:credentials) do - [ - Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), - Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.org/simple" - }) - ] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it "updates both files correctly" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "Pipfile.lock" } - updated_pipfile = updated_files.find { |f| f.name == "Pipfile" } - json_lockfile = JSON.parse(updated_lockfile.content) - - expect(updated_pipfile.content) - .to include("pypi.org/${ENV_VAR}") - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - expect(json_lockfile["_meta"]["sources"]) - .to eq([{ "url" => "https://pypi.org/${ENV_VAR}", - "verify_ssl" => true }]) - expect(updated_lockfile.content) - .not_to include("pypi.org/simple") - expect(json_lockfile["develop"]["pytest"]["version"]).to eq("==3.4.0") - end - end - - describe "the updated Pipfile.lock" do - let(:updated_lockfile) do - updated_files.find { |f| f.name == "Pipfile.lock" } - end - - let(:json_lockfile) { JSON.parse(updated_lockfile.content) } - - it "updates only what it needs to" do - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - expect(json_lockfile["develop"]["pytest"]["version"]).to eq("==3.2.3") - expect(json_lockfile["_meta"]["hash"]) - .to eq(JSON.parse(lockfile.content)["_meta"]["hash"]) - end - - describe "when updating a subdependency" do - let(:dependency) do - Dependabot::Dependency.new( - name: "py", - version: "1.7.0", - previous_version: "1.5.3", - package_manager: "pip", - requirements: [], - previous_requirements: [] - ) - end - - it "updates only what it needs to" do - expect(json_lockfile["default"].key?("py")).to be(false) - expect(json_lockfile["develop"]["py"]["version"]).to eq("==1.7.0") - expect(json_lockfile["_meta"]["hash"]) - .to eq(JSON.parse(lockfile.content)["_meta"]["hash"]) - end - end - - describe "with a subdependency from an extra" do - let(:dependency) do - Dependabot::Dependency.new( - name: "raven", - version: "6.7.0", - previous_version: "5.27.1", - package_manager: "pip", - requirements: [{ - requirement: "==6.7.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==5.27.1", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - let(:pipfile_fixture_name) { "extra_subdependency" } - let(:lockfile_fixture_name) { "extra_subdependency.lock" } - - it "doesn't remove the subdependency" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - - expect(json_lockfile["default"]["raven"]["version"]).to eq("==6.7.0") - expect(json_lockfile["default"]["blinker"]).to have_key("version") - end - end - - context "with a git dependency" do - let(:pipfile_fixture_name) { "git_source_no_ref" } - let(:lockfile_fixture_name) { "git_source_no_ref.lock" } - - context "when updating the non-git dependency" do - it "doesn't update the git dependency" do - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - expect(json_lockfile["default"]["pythonfinder"]) - .to eq(JSON.parse(lockfile.content)["default"]["pythonfinder"]) - end - end - end - - context "with a path dependency" do - let(:dependency_files) { [pipfile, lockfile, setupfile] } - let(:setupfile) do - Dependabot::DependencyFile.new( - name: "mydep/setup.py", - content: fixture("setup_files", setupfile_fixture_name) - ) - end - let(:setupfile_fixture_name) { "small.py" } - let(:pipfile_fixture_name) { "path_dependency_not_self" } - let(:lockfile_fixture_name) { "path_dependency_not_self.lock" } - - it "updates the dependency" do - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - end - - context "when needing to be sanitized" do - let(:setupfile_fixture_name) { "small_needs_sanitizing.py" } - - it "updates the dependency" do - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - end - end - - context "when importing a setup.cfg" do - let(:dependency_files) do - [pipfile, lockfile, setupfile, setup_cfg, requirements_file] - end - let(:setupfile_fixture_name) { "with_pbr.py" } - let(:setup_cfg) do - Dependabot::DependencyFile.new( - name: "mydep/setup.cfg", - content: fixture("setup_files", "setup.cfg") - ) - end - let(:requirements_file) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture("requirements", "pbr.txt") - ) - end - - it "updates the dependency" do - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - end - end - - context "when importing its own setup.py" do - let(:dependency_files) do - [pipfile, lockfile, setupfile, setup_cfg, requirements_file] - end - let(:pipfile_fixture_name) { "path_dependency" } - let(:lockfile_fixture_name) { "path_dependency.lock" } - let(:setupfile) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", setupfile_fixture_name) - ) - end - let(:setupfile_fixture_name) { "with_pbr.py" } - let(:setup_cfg) do - Dependabot::DependencyFile.new( - name: "setup.cfg", - content: fixture("setup_files", "setup.cfg") - ) - end - let(:requirements_file) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture("requirements", "pbr.txt") - ) - end - - it "updates the dependency" do - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - end - end - end - - context "with a python library setup as an editable dependency that needs extra files" do - let(:project_name) { "pipenv/editable-package" } - let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } - let(:dependency_files) do - %w(Pipfile Pipfile.lock pyproject.toml).map do |name| - Dependabot::DependencyFile.new( - name: name, - content: fixture("projects", project_name, name) - ) - end - end - let(:dependency) do - Dependabot::Dependency.new( - name: "cryptography", - version: "41.0.5", - previous_version: "40.0.1", - package_manager: "pip", - requirements: [{ - requirement: "==41.0.5", - file: "Pipfile", - source: nil, - groups: ["develop"] - }], - previous_requirements: [{ - requirement: "==40.0.1", - file: "Pipfile", - source: nil, - groups: ["develop"] - }] - ) - end - - it "updates the dependency" do - expect(json_lockfile["develop"]["cryptography"]["version"]) - .to eq("==41.0.5") - end - end - end - - context "when the Pipfile included an environment variable source" do - let(:pipfile_fixture_name) { "environment_variable_verify_ssl_false" } - let(:lockfile_fixture_name) { "environment_variable_verify_ssl_false.lock" } - let(:credentials) do - [ - Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), - Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.org/simple" - }) - ] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it "updates both files correctly" do - expect(updated_files.map(&:name)).to eq(%w(Pipfile Pipfile.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "Pipfile.lock" } - updated_pipfile = updated_files.find { |f| f.name == "Pipfile" } - json_lockfile = JSON.parse(updated_lockfile.content) - - expect(updated_pipfile.content) - .to include("pypi.org/${ENV_VAR}") - expect(json_lockfile["default"]["requests"]["version"]) - .to eq("==2.18.4") - expect(json_lockfile["_meta"]["sources"]) - .to eq([{ "url" => "https://pypi.org/${ENV_VAR}", - "verify_ssl" => true }]) - expect(updated_lockfile.content) - .not_to include("pypi.org/simple") - expect(json_lockfile["develop"]["pytest"]["version"]).to eq("==3.4.0") - end - end - - context "when the Pipfile is unresolvable" do - let(:pipfile_fixture_name) { "malformed_pipfile_source_missing" } - let(:lockfile_fixture_name) { "malformed_pipfile_source_missing.lock" } - let(:credentials) do - [ - Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), - Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.org/simple" - }) - ] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it "raise DependencyFileNotResolvable error" do - expect { updated_files }.to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - - context "with a requirements.txt" do - let(:dependency_files) { [pipfile, lockfile, requirements_file] } - - context "when the output looks like `pipenv requirements`" do - let(:pipfile_fixture_name) { "hard_names" } - let(:lockfile_fixture_name) { "hard_names.lock" } - let(:requirements_file) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture( - "requirements", - "hard_names_runtime.txt" - ) - ) - end - - it "updates the lockfile and the requirements.txt" do - expect(updated_files.map(&:name)) - .to match_array(%w(Pipfile.lock requirements.txt)) - - updated_lock = updated_files.find { |f| f.name == "Pipfile.lock" } - updated_txt = updated_files.find { |f| f.name == "requirements.txt" } - - JSON.parse(updated_lock.content).fetch("default").each do |nm, hash| - expect(updated_txt.content).to include("#{nm}#{hash['version']}") - end - end - - context "when there are no runtime dependencies" do - let(:pipfile_fixture_name) { "only_dev" } - let(:lockfile_fixture_name) { "only_dev.lock" } - let(:requirements_file) do - Dependabot::DependencyFile.new( - name: "runtime.txt", - content: fixture( - "requirements", - "version_not_specified_runtime.txt" - ) - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "pytest", - version: "3.3.1", - previous_version: "3.2.3", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["develop"] - }], - previous_requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["develop"] - }] - ) - end - - it "does not update the requirements.txt" do - expect(updated_files.map(&:name)).to eq(["Pipfile.lock"]) - end - end - end - - context "when the output looks like `pipenv requirements --dev`" do - let(:requirements_file) do - Dependabot::DependencyFile.new( - name: "req-dev.txt", - content: fixture( - "requirements", - "version_not_specified_dev.txt" - ) - ) - end - - it "updates the lockfile and the requirements.txt" do - expect(updated_files.map(&:name)) - .to match_array(%w(Pipfile.lock req-dev.txt)) - - updated_lock = updated_files.find { |f| f.name == "Pipfile.lock" } - updated_txt = updated_files.find { |f| f.name == "req-dev.txt" } - - JSON.parse(updated_lock.content).fetch("develop").each do |nm, hash| - expect(updated_txt.content).to include("#{nm}#{hash['version']}") - end - end - end - - context "when unrelated" do - let(:requirements_file) do - Dependabot::DependencyFile.new( - name: "requirements.txt", - content: fixture("requirements", "pbr.txt") - ) - end - - it "updates the lockfile only" do - expect(updated_files.map(&:name)).to match_array(%w(Pipfile.lock)) - end - end - end - end -end diff --git a/uv/spec/dependabot/uv/file_updater/pipfile_manifest_updater_spec.rb b/uv/spec/dependabot/uv/file_updater/pipfile_manifest_updater_spec.rb deleted file mode 100644 index 34b9e1e5dc..0000000000 --- a/uv/spec/dependabot/uv/file_updater/pipfile_manifest_updater_spec.rb +++ /dev/null @@ -1,287 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/file_updater/pipfile_manifest_updater" -require "dependabot/shared_helpers" - -RSpec.describe Dependabot::Uv::FileUpdater::PipfileManifestUpdater do - let(:updater) do - described_class.new( - manifest: manifest, - dependencies: [dependency] - ) - end - let(:manifest) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", pipfile_fixture_name) - ) - end - let(:pipfile_fixture_name) { "version_not_specified" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - let(:dependency_name) { "requests" } - - describe "#updated_manifest_content" do - subject(:updated_manifest_content) { updater.updated_manifest_content } - - context "when the Pipfile hasn't changed" do - let(:pipfile_fixture_name) { "version_not_specified" } - - it { is_expected.to eq(manifest.content) } - end - - context "with single quotes" do - let(:pipfile_fixture_name) { "with_quotes" } - let(:dependency) do - Dependabot::Dependency.new( - name: "python-decouple", - version: "3.2", - previous_version: "3.1", - package_manager: "pip", - requirements: [{ - requirement: "==3.2", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==3.1", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it { is_expected.to include(%q('python_decouple' = "==3.2")) } - end - - context "with double quotes" do - let(:pipfile_fixture_name) { "with_quotes" } - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it { is_expected.to include('"requests" = "==2.18.4"') } - end - - context "without quotes" do - let(:pipfile_fixture_name) { "with_quotes" } - let(:dependency) do - Dependabot::Dependency.new( - name: "pytest", - version: "3.3.1", - previous_version: "3.2.3", - package_manager: "pip", - requirements: [{ - requirement: "==3.3.1", - file: "Pipfile", - source: nil, - groups: ["develop"] - }], - previous_requirements: [{ - requirement: "==3.2.3", - file: "Pipfile", - source: nil, - groups: ["develop"] - }] - ) - end - - it { is_expected.to include(%(\npytest = "==3.3.1"\n)) } - it { is_expected.to include(%(\npytest-extension = "==3.2.3"\n)) } - it { is_expected.to include(%(\nextension-pytest = "==3.2.3"\n)) } - end - - context "with a version in a hash" do - let(:pipfile_fixture_name) { "version_hash" } - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it { is_expected.to include('requests = { version = "==2.18.4" }') } - end - - context "with a declaration table" do - let(:pipfile_fixture_name) { "version_table" } - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it { is_expected.to include(%(kages.requests]\n\nversion = "==2.18.4")) } - end - - context "with a capital letter" do - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - let(:pipfile_fixture_name) { "hard_names" } - - it { is_expected.to include('Requests = "==2.18.4"') } - end - - context "with multiple requirements" do - let(:pipfile_fixture_name) { "prod_and_dev" } - let(:dependency) do - Dependabot::Dependency.new( - name: "pytest", - version: "3.4.1", - previous_version: "3.4.0", - package_manager: "pip", - requirements: [{ - requirement: "==3.4.1", - file: "Pipfile", - source: nil, - groups: ["default"] - }, { - requirement: "==3.4.1", - file: "Pipfile", - source: nil, - groups: ["develop"] - }], - previous_requirements: [{ - requirement: "==3.4.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }, { - requirement: "==3.4.0", - file: "Pipfile", - source: nil, - groups: ["develop"] - }] - ) - end - - it { is_expected.to include('Pytest = "==3.4.1"') } - it { is_expected.not_to include('Pytest = "==3.4.0"') } - - context "when different" do - let(:pipfile_fixture_name) { "prod_and_dev_different" } - let(:dependency) do - Dependabot::Dependency.new( - name: "pytest", - version: "3.4.1", - previous_version: "3.4.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["develop"] - }, { - requirement: "==3.4.1", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["develop"] - }, { - requirement: "==3.4.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it { is_expected.to include('Pytest = "==3.4.1"') } - it { is_expected.to include('Pytest = "*"') } - end - end - end -end diff --git a/uv/spec/dependabot/uv/file_updater/pipfile_preparer_spec.rb b/uv/spec/dependabot/uv/file_updater/pipfile_preparer_spec.rb deleted file mode 100644 index b88040ce70..0000000000 --- a/uv/spec/dependabot/uv/file_updater/pipfile_preparer_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/file_updater/pipfile_preparer" - -RSpec.describe Dependabot::Uv::FileUpdater::PipfilePreparer do - let(:preparer) do - described_class.new(pipfile_content: pipfile_content) - end - - let(:pipfile_content) do - fixture("pipfile_files", pipfile_fixture_name) - end - let(:pipfile_fixture_name) { "version_not_specified" } - - describe "#replace_sources" do - subject(:updated_content) { preparer.replace_sources(credentials) } - - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://username:password@pypi.posrip.com/pypi/" - })] - end - let(:pipfile_fixture_name) { "version_not_specified" } - - it "adds the source" do - expect(updated_content).to include( - "[[source]]\n" \ - "name = \"dependabot-inserted-index-0\"\n" \ - "url = \"https://username:password@pypi.posrip.com/pypi/\"\n" - ) - end - - context "with auth details provided as a token" do - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.posrip.com/pypi/", - "token" => "username:password" - })] - end - - it "adds the source" do - expect(updated_content).to include( - "[[source]]\n" \ - "name = \"dependabot-inserted-index-0\"\n" \ - "url = \"https://username:password@pypi.posrip.com/pypi/\"\n" - ) - end - end - - context "with auth details provided in Pipfile" do - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.posrip.com/pypi/", - "token" => "username:password" - })] - end - - let(:pipfile_fixture_name) { "private_source_auth" } - - it "keeps source config" do - expect(updated_content).to include( - "[[source]]\n" \ - "name = \"internal-pypi\"\n" \ - "url = \"https://username:password@pypi.posrip.com/pypi/\"\n" \ - "verify_ssl = true\n" - ) - end - end - end -end diff --git a/uv/spec/dependabot/uv/file_updater/poetry_file_updater_spec.rb b/uv/spec/dependabot/uv/file_updater/poetry_file_updater_spec.rb deleted file mode 100644 index 270d9e5bdd..0000000000 --- a/uv/spec/dependabot/uv/file_updater/poetry_file_updater_spec.rb +++ /dev/null @@ -1,863 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "toml-rb" - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/file_updater/poetry_file_updater" -require "dependabot/shared_helpers" - -RSpec.describe Dependabot::Uv::FileUpdater::PoetryFileUpdater do - let(:updater) do - described_class.new( - dependency_files: dependency_files, - dependencies: [dependency], - credentials: credentials - ) - end - let(:dependency_files) { [pyproject, lockfile] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: fixture("pyproject_files", pyproject_fixture_name) - ) - end - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "poetry.lock", - content: fixture("poetry_locks", lockfile_fixture_name) - ) - end - let(:pyproject_fixture_name) { "version_not_specified.toml" } - let(:lockfile_fixture_name) { "version_not_specified.lock" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.19.1", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "*", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - let(:dependency_name) { "requests" } - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - })] - end - - describe "#updated_dependency_files" do - subject(:updated_files) { updater.updated_dependency_files } - - it "updates the lockfile successfully (and doesn't affect other deps)" do - expect(updated_files.map(&:name)).to eq(%w(poetry.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "poetry.lock" } - - lockfile_obj = TomlRB.parse(updated_lockfile.content) - requests = lockfile_obj["package"].find { |d| d["name"] == "requests" } - pytest = lockfile_obj["package"].find { |d| d["name"] == "pytest" } - - expect(requests["version"]).to eq("2.19.1") - expect(pytest["version"]).to eq("3.5.0") - - expect(lockfile_obj["metadata"]["content-hash"]) - .to start_with("8cea4ecb5b2230fbd4a33a67a4da004f1ccabad48352aaf040") - end - - context "with a specified Python version" do - let(:pyproject_fixture_name) { "python_310.toml" } - let(:lockfile_fixture_name) { "python_310.lock" } - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.19.1", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "2.19.1", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "2.18.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - - it "updates the lockfile successfully" do - updated_lockfile = updated_files.find { |f| f.name == "poetry.lock" } - - lockfile_obj = TomlRB.parse(updated_lockfile.content) - requests = lockfile_obj["package"].find { |d| d["name"] == "requests" } - expect(requests["version"]).to eq("2.19.1") - end - - it "does not change python version" do - updated_pyproj = updated_files.find { |f| f.name == "pyproject.toml" } - pyproj_obj = TomlRB.parse(updated_pyproj.content) - expect(pyproj_obj["tool"]["poetry"]["dependencies"]["python"]).to eq("3.10.7") - - updated_lockfile = updated_files.find { |f| f.name == "poetry.lock" } - lockfile_obj = TomlRB.parse(updated_lockfile.content) - expect(lockfile_obj["metadata"]["python-versions"]).to eq("3.10.7") - end - end - - context "with the oldest python version currently supported by Dependabot" do - let(:python_version) { "3.9.21" } - let(:pyproject_fixture_name) { "python_39.toml" } - let(:lockfile_fixture_name) { "python_39.lock" } - let(:dependency) do - Dependabot::Dependency.new( - name: "django", - version: "3.1", - previous_version: "3.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "*", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - - it "updates the lockfile" do - updated_lockfile = updated_files.find { |f| f.name == "poetry.lock" } - - lockfile_obj = TomlRB.parse(updated_lockfile.content) - requests = lockfile_obj["package"].find { |d| d["name"] == "django" } - expect(requests["version"]).to eq("3.1") - end - end - - context "with a pyproject.toml file" do - let(:dependency_files) { [pyproject] } - - context "without a lockfile" do - let(:pyproject_fixture_name) { "caret_version.toml" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.19.1", - previous_version: nil, - package_manager: "pip", - requirements: [{ - requirement: "^2.19.1", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "^1.0.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - - it "updates the pyproject.toml" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - expect(updated_lockfile.content).to include('requests = "^2.19.1"') - end - end - - context "when dealing with indented" do - let(:pyproject_fixture_name) { "indented.toml" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.19.1", - previous_version: nil, - package_manager: "pip", - requirements: [{ - requirement: "^2.19.1", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "^1.0.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - - it "updates the pyproject.toml" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - expect(updated_lockfile.content).to include(' requests = "^2.19.1"') - end - end - - context "when specifying table style dependencies" do - let(:pyproject_fixture_name) { "table.toml" } - let(:dependency_name) { "isort" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "5.7.0", - previous_version: nil, - package_manager: "pip", - requirements: [{ - requirement: "^5.7", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }], - previous_requirements: [{ - requirement: "^5.4", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }] - ) - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dev-dependencies.isort] - version = "^5.7" - TOML - end - end - - context "when specifying table style dependencies with version as the last field" do - let(:pyproject_fixture_name) { "table_version_last.toml" } - let(:dependency_name) { "isort" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "5.7.0", - previous_version: nil, - package_manager: "pip", - requirements: [{ - requirement: "^5.7", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }], - previous_requirements: [{ - requirement: "^5.4", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }] - ) - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dev-dependencies.isort] - extras = [ "pyproject",] - version = "^5.7" - TOML - end - end - - context "when specifying table style dependencies with version conflicting with other deps" do - let(:pyproject_fixture_name) { "table_version_conflicts.toml" } - let(:dependency_name) { "isort" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "5.7.0", - previous_version: nil, - package_manager: "pip", - requirements: [{ - requirement: "^5.7", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }], - previous_requirements: [{ - requirement: "^5.4", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }] - ) - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dev-dependencies.isort] - extras = [ "pyproject",] - version = "^5.7" - - [tool.poetry.dev-dependencies.pytest] - extras = [ "pyproject",] - version = "^5.4" - TOML - end - end - - context "with same dep specified twice in different groups (legacy syntax)" do - let(:pyproject_fixture_name) { "different_requirements_legacy.toml" } - let(:dependency_name) { "streamlit" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "1.27.2", - previous_version: "1.18.1", - package_manager: "pip", - requirements: [{ - requirement: ">=0.65.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }, { - requirement: "^1.27.2", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }], - previous_requirements: [{ - requirement: ">=0.65.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }, { - requirement: "^1.12.2", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }] - ) - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dependencies] - streamlit = ">=0.65.0" - packaging = ">=20.0" - - [tool.poetry.dev-dependencies] - black = "^20.8b1" - isort = "^5.12.0" - flake8 = "^4.0.1" - mypy = "^1.6" - pytest = "^7.4.2" - streamlit = "^1.27.2" - TOML - end - end - - context "with same dep specified twice in different groups (updated is in main)" do - let(:pyproject_fixture_name) { "different_requirements_main.toml" } - let(:dependency_name) { "streamlit" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "1.27.2", - previous_version: "1.18.1", - package_manager: "pip", - requirements: [{ - requirement: "^1.27.2", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }, { - requirement: ">=0.65.0", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }], - previous_requirements: [{ - requirement: "^1.12.2", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }, { - requirement: ">=0.65.0", - file: "pyproject.toml", - source: nil, - groups: ["dev-dependencies"] - }] - ) - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dependencies] - packaging = ">=20.0" - streamlit = "^1.27.2" - - [tool.poetry.dev-dependencies] - black = "^20.8b1" - isort = "^5.12.0" - flake8 = "^4.0.1" - mypy = "^1.6" - pytest = "^7.4.2" - streamlit = ">=0.65.0" - TOML - end - end - - context "with same dep specified twice in different groups" do - let(:pyproject_fixture_name) { "different_requirements.toml" } - let(:dependency_name) { "streamlit" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "1.27.2", - previous_version: "1.18.1", - package_manager: "pip", - requirements: [{ - requirement: ">=0.65.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }, { - requirement: "^1.27.2", - file: "pyproject.toml", - source: nil, - groups: ["dev"] - }], - previous_requirements: [{ - requirement: ">=0.65.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }, { - requirement: "^1.12.2", - file: "pyproject.toml", - source: nil, - groups: ["dev"] - }] - ) - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dependencies] - streamlit = ">=0.65.0" - packaging = ">=20.0" - - [tool.poetry.group.dev.dependencies] - black = "^20.8b1" - isort = "^5.12.0" - flake8 = "^4.0.1" - mypy = "^1.6" - pytest = "^7.4.2" - streamlit = "^1.27.2" - TOML - end - end - - context "with inline comments in the dependencies groups" do - let(:pyproject_fixture_name) { "inline_comments.toml" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "1.27.2", - previous_version: "1.18.1", - package_manager: "pip", - requirements: requirements, - previous_requirements: previous_requirements - ) - end - - context "when dealing with the dependency in the main dependencies group" do - let(:dependency_name) { "jsonschema" } - let(:requirements) do - [{ - requirement: "^4.19.1", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - end - let(:previous_requirements) do - [{ - requirement: "^4.18.5", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dependencies] # Main (runtime) dependencies - python = "~3.10" - jsonschema = "^4.19.1" # jsonschema library - packaging = ">=20.0" - - [tool.poetry.group.dev.dependencies] # Development (local) dependencies - black = "^20.8b1" - flake8 = "^4.0.1" # flake8 - flake8-implicit-str-concat = "^0.4.0" - isort = "^5.9.3" - mypy = "^1.6" - - [tool.poetry.group.test.dependencies]# Test dependencies - coverage = {extras = ["toml"], version = "^7.3.2"} - pytest = "^7.4.0"#pytest - pytest-mock = ">=3.8.2" - TOML - end - end - - context "when dealing with the dependency in the dev dependencies group with multiple spaces" do - let(:dependency_name) { "isort" } - let(:requirements) do - [{ - requirement: "^5.12.0", - file: "pyproject.toml", - source: nil, - groups: ["dev"] - }] - end - let(:previous_requirements) do - [{ - requirement: "^5.9.3", - file: "pyproject.toml", - source: nil, - groups: ["dev"] - }] - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dependencies] # Main (runtime) dependencies - python = "~3.10" - jsonschema = "^4.18.5" # jsonschema library - packaging = ">=20.0" - - [tool.poetry.group.dev.dependencies] # Development (local) dependencies - black = "^20.8b1" - flake8 = "^4.0.1" # flake8 - flake8-implicit-str-concat = "^0.4.0" - isort = "^5.12.0" - mypy = "^1.6" - - [tool.poetry.group.test.dependencies]# Test dependencies - coverage = {extras = ["toml"], version = "^7.3.2"} - pytest = "^7.4.0"#pytest - pytest-mock = ">=3.8.2" - TOML - end - end - - context "when dealing with the dependency in the test dependencies group without spaces" do - let(:dependency_name) { "pytest-mock" } - let(:requirements) do - [{ - requirement: ">=3.12.0", - file: "pyproject.toml", - source: nil, - groups: ["test"] - }] - end - let(:previous_requirements) do - [{ - requirement: ">=3.8.2", - file: "pyproject.toml", - source: nil, - groups: ["test"] - }] - end - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry.dependencies] # Main (runtime) dependencies - python = "~3.10" - jsonschema = "^4.18.5" # jsonschema library - packaging = ">=20.0" - - [tool.poetry.group.dev.dependencies] # Development (local) dependencies - black = "^20.8b1" - flake8 = "^4.0.1" # flake8 - flake8-implicit-str-concat = "^0.4.0" - isort = "^5.9.3" - mypy = "^1.6" - - [tool.poetry.group.test.dependencies]# Test dependencies - coverage = {extras = ["toml"], version = "^7.3.2"} - pytest = "^7.4.0"#pytest - pytest-mock = ">=3.12.0" - TOML - end - end - end - - context "with the same requirement specified in two dependencies" do - let(:pyproject_fixture_name) { "same_requirements.toml" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "1.15.0", - previous_version: "1.13.0", - package_manager: "pip", - requirements: [{ - requirement: "^1.15.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "^1.13.0", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - - context "when dealing with the first dependency" do - let(:dependency_name) { "rq" } - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry] - name = "dependabot-poetry-bug" - version = "0.1.0" - description = "" - authors = [] - - [tool.poetry.dependencies] - python = "^3.9" - rq = "^1.15.0" - dramatiq = "^1.13.0" - - [build-system] - requires = ["poetry-core"] - build-backend = "poetry.core.masonry.api" - TOML - end - end - - context "when dealing with the second dependency" do - let(:dependency_name) { "dramatiq" } - - it "updates the pyproject.toml correctly" do - expect(updated_files.map(&:name)).to eq(%w(pyproject.toml)) - - updated_lockfile = updated_files.find { |f| f.name == "pyproject.toml" } - - expect(updated_lockfile.content).to include <<~TOML - [tool.poetry] - name = "dependabot-poetry-bug" - version = "0.1.0" - description = "" - authors = [] - - [tool.poetry.dependencies] - python = "^3.9" - rq = "^1.13.0" - dramatiq = "^1.15.0" - - [build-system] - requires = ["poetry-core"] - build-backend = "poetry.core.masonry.api" - TOML - end - end - end - - context "when the requirement has not changed" do - let(:pyproject_fixture_name) { "caret_version.toml" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.19.1", - previous_version: nil, - package_manager: "pip", - requirements: [{ - requirement: "^2.19.1", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: ">=2.19.1", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - - it "raises the correct error" do - expect do - updated_files.map(&:name) - end.to raise_error(Dependabot::DependencyFileContentNotChanged, "Content did not change!") - end - end - end - - context "with a poetry.lock" do - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "poetry.lock", - content: fixture("poetry_locks", lockfile_fixture_name) - ) - end - - it "updates the lockfile successfully" do - expect(updated_files.map(&:name)).to eq(%w(poetry.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "poetry.lock" } - - lockfile_obj = TomlRB.parse(updated_lockfile.content) - requests = lockfile_obj["package"].find { |d| d["name"] == "requests" } - pytest = lockfile_obj["package"].find { |d| d["name"] == "pytest" } - - expect(requests["version"]).to eq("2.19.1") - expect(pytest["version"]).to eq("3.5.0") - - expect(lockfile_obj["metadata"]["content-hash"]) - .to start_with("8cea4ecb5b2230fbd4a33a67a4da004f1ccabad48352aaf040a1d") - end - - context "with a sub-dependency" do - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2018.11.29", - previous_version: "2018.4.16", - package_manager: "pip", - requirements: [], - previous_requirements: [] - ) - end - let(:dependency_name) { "certifi" } - - it "updates the lockfile successfully" do - expect(updated_files.map(&:name)).to eq(%w(poetry.lock)) - - updated_lockfile = updated_files.find { |f| f.name == "poetry.lock" } - - lockfile_obj = TomlRB.parse(updated_lockfile.content) - certifi = lockfile_obj["package"].find { |d| d["name"] == "certifi" } - - expect(certifi["version"]).to eq("2018.11.29") - - expect(lockfile_obj["metadata"]["content-hash"]) - .to start_with("8cea4ecb5b2230fbd4a33a67a4da004f1ccabad48352aaf040a") - end - end - end - end - - describe "#prepared_project_file" do - subject(:prepared_project) { updater.send(:prepared_pyproject) } - - context "with a python_index with auth details" do - let(:pyproject_fixture_name) { "private_secondary_source.toml" } - let(:lockfile_fixture_name) { "private_secondary_source.lock" } - let(:dependency_name) { "luigi" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: "2.8.9", - previous_version: "2.8.8", - package_manager: "pip", - requirements: [{ - requirement: "2.8.9", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }], - previous_requirements: [{ - requirement: "2.8.8", - file: "pyproject.toml", - source: nil, - groups: ["dependencies"] - }] - ) - end - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://some.internal.registry.com/pypi/", - "username" => "test", - "password" => "test" - })] - end - - it "prepares a pyproject file without credentials in" do - repo_obj = TomlRB.parse(prepared_project, symbolize_keys: true)[:tool][:poetry][:source] - expect(repo_obj[0][:url]).to eq(credentials[0]["index-url"]) - - user_pass = "#{credentials[0]['user']}:#{credentials[0]['password']}@" - expect(repo_obj[0][:url]).not_to include(user_pass) - end - end - end -end diff --git a/uv/spec/dependabot/uv/file_updater/pyproject_preparer_spec.rb b/uv/spec/dependabot/uv/file_updater/pyproject_preparer_spec.rb index 95146645c2..fe2418cacf 100644 --- a/uv/spec/dependabot/uv/file_updater/pyproject_preparer_spec.rb +++ b/uv/spec/dependabot/uv/file_updater/pyproject_preparer_spec.rb @@ -129,7 +129,7 @@ Dependabot::Dependency.new( name: "geopy", version: "1.14.0", - package_manager: "pip", + package_manager: "uv", requirements: [] ) ] diff --git a/uv/spec/dependabot/uv/file_updater/requirement_file_updater_spec.rb b/uv/spec/dependabot/uv/file_updater/requirement_file_updater_spec.rb index 11b9cf2652..0678d03ca6 100644 --- a/uv/spec/dependabot/uv/file_updater/requirement_file_updater_spec.rb +++ b/uv/spec/dependabot/uv/file_updater/requirement_file_updater_spec.rb @@ -4,7 +4,7 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" -require "dependabot/uv/file_updater/pipfile_file_updater" +require "dependabot/uv/file_updater" require "dependabot/shared_helpers" RSpec.describe Dependabot::Uv::FileUpdater::RequirementFileUpdater do @@ -39,7 +39,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end let(:previous_requirement_string) { "==2.6.1" } @@ -57,7 +57,7 @@ subject(:updated_files) { updater.updated_dependency_files } it "returns DependencyFile objects" do - updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } + expect(updated_files).to all(be_a(Dependabot::DependencyFile)) end its(:length) { is_expected.to eq(1) } @@ -110,7 +110,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -179,7 +179,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -206,7 +206,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -297,7 +297,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -328,7 +328,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -373,7 +373,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -419,7 +419,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -443,7 +443,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -467,7 +467,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -491,7 +491,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -527,7 +527,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -551,7 +551,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -575,7 +575,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -599,7 +599,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -642,7 +642,7 @@ source: nil } ], - package_manager: "pip" + package_manager: "uv" ) end diff --git a/uv/spec/dependabot/uv/file_updater/setup_file_sanitizer_spec.rb b/uv/spec/dependabot/uv/file_updater/setup_file_sanitizer_spec.rb deleted file mode 100644 index a26941f435..0000000000 --- a/uv/spec/dependabot/uv/file_updater/setup_file_sanitizer_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/uv/file_updater/setup_file_sanitizer" - -RSpec.describe Dependabot::Uv::FileUpdater::SetupFileSanitizer do - let(:sanitizer) do - described_class.new(setup_file: setup_file, setup_cfg: setup_cfg) - end - - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", setup_file_fixture_name) - ) - end - let(:setup_cfg) { nil } - let(:setup_file_fixture_name) { "setup.py" } - - describe "#sanitized_content" do - subject(:sanitized_content) { sanitizer.sanitized_content } - - it "extracts the install_requires" do - expect(sanitized_content).to eq( - "from setuptools import setup\n\n" \ - 'setup(name="python-package",version="0.0.1",' \ - 'install_requires=["boto3==1.3.1","flake8<3.0.0,>2.5.4",' \ - '"gocardless-pro","pandas==0.19.2","pep8==1.7.0","psycopg2==2.6.1",' \ - '"raven==5.32.0","requests==2.12.*","scipy==0.18.1",' \ - '"scikit-learn==0.18.1"],extras_require={"API":["flask==0.12.2"]})' - ) - end - - context "when dealing with a setup.py including a dependency with extras" do - let(:setup_file_fixture_name) { "extras.py" } - - it "extracts the install_requires and conserves extras" do - expect(sanitized_content).to eq( - "from setuptools import setup\n\n" \ - 'setup(name="default_package_name",version="0.0.1",' \ - 'install_requires=["requests[security]==2.12.*",' \ - '"scipy==0.18.1","scikit-learn==0.18.1"],' \ - "extras_require={})" - ) - end - end - - context "when dealing with a setup.py using pbr" do - let(:setup_file_fixture_name) { "with_pbr.py" } - let(:setup_cfg) do - Dependabot::DependencyFile.new( - name: "setup.cfg", - content: fixture("setup_files", "setup.cfg") - ) - end - - it "includes pbr" do - expect(sanitized_content).to eq( - "from setuptools import setup\n\n" \ - 'setup(name="default_package_name",version="0.0.1",' \ - 'install_requires=["raven"],extras_require={},' \ - 'setup_requires=["pbr"],pbr=True)' - ) - end - end - end -end diff --git a/uv/spec/dependabot/uv/file_updater_spec.rb b/uv/spec/dependabot/uv/file_updater_spec.rb index 43d85b51a2..aeea2051ea 100644 --- a/uv/spec/dependabot/uv/file_updater_spec.rb +++ b/uv/spec/dependabot/uv/file_updater_spec.rb @@ -34,7 +34,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end let(:requirements_fixture_name) { "version_specified.txt" } @@ -67,17 +67,10 @@ context "when files match the regex patterns" do it "returns true for files that should be updated" do matching_files = [ - "Pipfile", - "Pipfile.lock", "requirements.txt", "constraints.txt", "some_dependency.in", - "setup.py", - "setup.cfg", "pyproject.toml", - "pyproject.lock", - "poetry.lock", - "subdirectory/Pipfile", "subdirectory/requirements.txt", "requirements/test.in", "requirements/test.txt" @@ -96,7 +89,14 @@ "package-lock.json", "package.json", "Gemfile", - "Gemfile.lock" + "Gemfile.lock", + "setup.py", + "setup.cfg", + "pyproject.lock", + "poetry.lock", + "subdirectory/Pipfile", + "Pipfile", + "Pipfile.lock" ] non_matching_files.each do |file_name| @@ -109,249 +109,6 @@ describe "#updated_dependency_files" do subject(:updated_files) { updater.updated_dependency_files } - context "with a relative project path" do - let(:dependency_files) { project_dependency_files("poetry/relative_path") } - - let(:dependency) do - Dependabot::Dependency.new( - name: "mypy", - version: "0.910", - previous_version: "0.812", - requirements: [{ - file: "pyproject.toml", - requirement: "^0.910", - groups: ["dev-dependencies"], - source: nil - }], - previous_requirements: [{ - file: "pyproject.toml", - requirement: "^0.812", - groups: ["dev-dependencies"], - source: nil - }], - package_manager: "pip" - ) - end - - specify { expect(updated_files.count).to eq(2) } - end - - context "with a Pipfile and Pipfile.lock" do - let(:dependency_files) { [pipfile, lockfile] } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", "version_not_specified") - ) - end - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "Pipfile.lock", - content: fixture("pipfile_files", "version_not_specified.lock") - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "*", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it "delegates to PipfileFileUpdater" do - expect(described_class::PipfileFileUpdater) - .to receive(:new).and_call_original - expect { updated_files }.not_to(change { Dir.entries(tmp_path) }) - updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } - end - end - - context "with just a Pipfile" do - let(:dependency_files) { [pipfile, requirements] } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", "exact_version") - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "==2.18.4", - file: "Pipfile", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==2.18.0", - file: "Pipfile", - source: nil, - groups: ["default"] - }] - ) - end - - it "delegates to PipfileFileUpdater" do - expect(described_class::PipfileFileUpdater) - .to receive(:new).and_call_original - expect { updated_files }.not_to(change { Dir.entries(tmp_path) }) - updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } - end - end - - context "with multiple manifests declaring the same dependency" do - let(:dependency_files) { [pyproject, requirements] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: fixture("pyproject_files", "pytest.toml") - ) - end - let(:requirements_fixture_name) { "version_specified.txt" } - - let(:dependency) do - Dependabot::Dependency.new( - name: "pytest", - version: "3.5.0", - previous_version: "3.4.0", - package_manager: "pip", - requirements: [{ - requirement: "3.5.0", - file: "pyproject.toml", - groups: ["dependencies"], - source: nil - }, { - requirement: "==3.5.0", - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }], - previous_requirements: [{ - requirement: "3.4.0", - file: "pyproject.toml", - groups: ["dependencies"], - source: nil - }, { - requirement: "==3.4.0", - file: "requirements.txt", - groups: ["dependencies"], - source: nil - }] - ) - end - - # Perhaps ideally we'd replace both, but this is where we're at right now. - # See https://github.com/dependabot/dependabot-core/pull/4969 - it "replaces one of the outdated dependencies" do - expect(updated_files.length).to eq(1) - expect(updated_files[0].content).to include('pytest = "3.5.0"') - end - end - - context "with a pyproject.toml with pep621 dependencies" do - let(:dependency_files) { [pyproject] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: - fixture("pyproject_files", "standard_python.toml") - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "ansys-templates", - version: "0.5.0", - previous_version: "0.3.0", - package_manager: "pip", - requirements: [{ - requirement: "==0.5.0", - file: "pyproject.toml", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "==0.3.0", - file: "pyproject.toml", - source: nil, - groups: ["default"] - }] - ) - end - - it "delegates to RequirementFileUpdater" do - expect(described_class::RequirementFileUpdater) - .to receive(:new).and_call_original - expect { updated_files }.not_to(change { Dir.entries(tmp_path) }) - updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } - end - end - - context "with a pyproject.toml and poetry.lock" do - let(:dependency_files) { [pyproject, lockfile] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: - fixture("pyproject_files", "version_not_specified.toml") - ) - end - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "poetry.lock", - content: - fixture("poetry_locks", "version_not_specified.lock") - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "2.18.4", - previous_version: "2.18.0", - package_manager: "pip", - requirements: [{ - requirement: "*", - file: "pyproject.toml", - source: nil, - groups: ["default"] - }], - previous_requirements: [{ - requirement: "*", - file: "pyproject.toml", - source: nil, - groups: ["default"] - }] - ) - end - - it "delegates to PoetryFileUpdater" do - expect(described_class::PoetryFileUpdater) - .to receive(:new).and_call_original - expect { updated_files }.not_to(change { Dir.entries(tmp_path) }) - updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } - end - end - context "with a pip-compile file" do let(:dependency_files) { [manifest_file, generated_file] } let(:manifest_file) do @@ -382,16 +139,16 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end - it "delegates to PipCompileFileUpdater" do + it "delegates to CompileFileUpdater" do dummy_updater = - instance_double(described_class::PipCompileFileUpdater) - allow(described_class::PipCompileFileUpdater).to receive(:new) + instance_double(described_class::CompileFileUpdater) + allow(described_class::CompileFileUpdater).to receive(:new) .and_return(dummy_updater) - expect(dummy_updater) + allow(dummy_updater) .to receive(:updated_dependency_files) .and_return([OpenStruct.new(name: "updated files")]) expect(updater.updated_dependency_files) @@ -416,12 +173,12 @@ }] end - it "delegates to PipCompileFileUpdater" do + it "delegates to CompileFileUpdater" do dummy_updater = - instance_double(described_class::PipCompileFileUpdater) - allow(described_class::PipCompileFileUpdater).to receive(:new) + instance_double(described_class::CompileFileUpdater) + allow(described_class::CompileFileUpdater).to receive(:new) .and_return(dummy_updater) - expect(dummy_updater) + allow(dummy_updater) .to receive(:updated_dependency_files) .and_return([OpenStruct.new(name: "updated files")]) expect(updater.updated_dependency_files) @@ -437,7 +194,7 @@ expect(described_class::RequirementFileUpdater) .to receive(:new).and_call_original expect { updated_files }.not_to(change { Dir.entries(tmp_path) }) - updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } + expect(updated_files).to all(be_a(Dependabot::DependencyFile)) end end @@ -450,11 +207,11 @@ ) end - let(:credentials) { [double(replaces_base?: replaces_base)] } + let(:credentials) { [instance_double(Dependabot::Credential, replaces_base?: replaces_base)] } let(:replaces_base) { false } before do - allow_any_instance_of(described_class).to receive(:check_required_files).and_return(true) + allow_any_instance_of(described_class).to receive(:check_required_files).and_return(true) # rubocop:disable RSpec/AnyInstance allow(Dependabot::Uv::AuthedUrlBuilder).to receive(:authed_url).and_return("authed_url") end diff --git a/uv/spec/dependabot/uv/metadata_finder_spec.rb b/uv/spec/dependabot/uv/metadata_finder_spec.rb index 4860944cbd..0141071cc3 100644 --- a/uv/spec/dependabot/uv/metadata_finder_spec.rb +++ b/uv/spec/dependabot/uv/metadata_finder_spec.rb @@ -32,7 +32,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end diff --git a/uv/spec/dependabot/uv/pip_compile_package_manager_spec.rb b/uv/spec/dependabot/uv/package_manager_spec.rb similarity index 79% rename from uv/spec/dependabot/uv/pip_compile_package_manager_spec.rb rename to uv/spec/dependabot/uv/package_manager_spec.rb index 14cd557bc0..d3de474e1b 100644 --- a/uv/spec/dependabot/uv/pip_compile_package_manager_spec.rb +++ b/uv/spec/dependabot/uv/package_manager_spec.rb @@ -5,17 +5,17 @@ require "dependabot/ecosystem" require "spec_helper" -RSpec.describe Dependabot::Uv::PipCompilePackageManager do - let(:package_manager) { described_class.new("2024.0.1") } +RSpec.describe Dependabot::Uv::PackageManager do + let(:package_manager) { described_class.new("0.6.2") } describe "#initialize" do context "when version is a String" do it "sets the version correctly" do - expect(package_manager.version).to eq("2024.0.1") + expect(package_manager.version).to eq("0.6.2") end it "sets the name correctly" do - expect(package_manager.name).to eq("pip-compile") + expect(package_manager.name).to eq("uv") end end diff --git a/uv/spec/dependabot/uv/pip_package_manager_spec.rb b/uv/spec/dependabot/uv/pip_package_manager_spec.rb deleted file mode 100644 index c6a75fb324..0000000000 --- a/uv/spec/dependabot/uv/pip_package_manager_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "dependabot/uv/package_manager" -require "dependabot/ecosystem" -require "spec_helper" - -RSpec.describe Dependabot::Uv::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 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 --version") - .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 diff --git a/uv/spec/dependabot/uv/pipenv_package_manager_spec.rb b/uv/spec/dependabot/uv/pipenv_package_manager_spec.rb deleted file mode 100644 index 4a9a20e2de..0000000000 --- a/uv/spec/dependabot/uv/pipenv_package_manager_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "dependabot/uv/package_manager" -require "dependabot/ecosystem" -require "spec_helper" - -RSpec.describe Dependabot::Uv::PipenvPackageManager 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("pipenv") - end - end - - context "when pipenv version extracted from pyenv is well formed" do - # If this test starts failing, you need to adjust the "detect_pipenv_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 pipenv --version") - .to_s.split("version ").last&.strip - - it "does not raise error" do - expect(version.match(/^\d+(?:\.\d+)*$/)).to be_truthy - end - end - end -end diff --git a/uv/spec/dependabot/uv/poetry_package_manager_spec.rb b/uv/spec/dependabot/uv/poetry_package_manager_spec.rb deleted file mode 100644 index c9d876be0b..0000000000 --- a/uv/spec/dependabot/uv/poetry_package_manager_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "dependabot/uv/package_manager" -require "dependabot/ecosystem" -require "spec_helper" - -RSpec.describe Dependabot::Uv::PoetryPackageManager 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 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 diff --git a/uv/spec/dependabot/uv/update_checker/index_finder_spec.rb b/uv/spec/dependabot/uv/update_checker/index_finder_spec.rb index 786128d320..77924b5cf6 100644 --- a/uv/spec/dependabot/uv/update_checker/index_finder_spec.rb +++ b/uv/spec/dependabot/uv/update_checker/index_finder_spec.rb @@ -63,7 +63,7 @@ groups: ["dependencies"], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -307,7 +307,7 @@ groups: ["dependencies"], source: "custom" }], - package_manager: "pip" + package_manager: "uv" ) end diff --git a/uv/spec/dependabot/uv/update_checker/latest_version_finder_spec.rb b/uv/spec/dependabot/uv/update_checker/latest_version_finder_spec.rb index c655b185b6..d0dd6aa116 100644 --- a/uv/spec/dependabot/uv/update_checker/latest_version_finder_spec.rb +++ b/uv/spec/dependabot/uv/update_checker/latest_version_finder_spec.rb @@ -64,7 +64,7 @@ name: dependency_name, version: dependency_version, requirements: dependency_requirements, - package_manager: "pip" + package_manager: "uv" ) end let(:dependency_name) { "luigi" } @@ -590,7 +590,7 @@ name: dependency_name, version: version, requirements: requirements, - package_manager: "pip" + package_manager: "uv" ) end let(:requirements) do @@ -698,7 +698,7 @@ [ Dependabot::SecurityAdvisory.new( dependency_name: dependency_name, - package_manager: "pip", + package_manager: "uv", vulnerable_versions: ["<= 2.1.0"] ) ] diff --git a/uv/spec/dependabot/uv/update_checker/pip_compile_version_resolver_spec.rb b/uv/spec/dependabot/uv/update_checker/pip_compile_version_resolver_spec.rb deleted file mode 100644 index 22b083f4a9..0000000000 --- a/uv/spec/dependabot/uv/update_checker/pip_compile_version_resolver_spec.rb +++ /dev/null @@ -1,496 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/update_checker/pip_compile_version_resolver" - -namespace = Dependabot::Uv::UpdateChecker -RSpec.describe namespace::PipCompileVersionResolver do - let(:resolver) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: nil - ) - end - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - })] - end - let(:dependency_files) { [manifest_file, generated_file] } - let(:manifest_file) do - Dependabot::DependencyFile.new( - name: "requirements/test.in", - content: fixture("pip_compile_files", manifest_fixture_name) - ) - end - let(:generated_file) do - Dependabot::DependencyFile.new( - name: "requirements/test.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - let(:manifest_fixture_name) { "unpinned.in" } - let(:generated_fixture_name) { "pip_compile_unpinned.txt" } - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "pip" - ) - end - let(:dependency_name) { "attrs" } - let(:dependency_version) { "17.3.0" } - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: nil, - groups: [], - source: nil - }] - end - - describe "#latest_resolvable_version" do - subject(:latest_resolvable_version) do - resolver.latest_resolvable_version(requirement: updated_requirement) - end - - let(:updated_requirement) { ">=17.3.0,<=18.1.0" } - - it { is_expected.to eq(Gem::Version.new("18.1.0")) } - - context "with a mismatch in filename" do - let(:generated_fixture_name) { "pip_compile_unpinned_renamed.txt" } - let(:generated_file) do - Dependabot::DependencyFile.new( - name: "requirements/test-funky.txt", - content: fixture("requirements", generated_fixture_name) - ) - end - - it { is_expected.to eq(Gem::Version.new("18.1.0")) } - end - - context "with an upper bound" do - let(:manifest_fixture_name) { "bounded.in" } - let(:generated_fixture_name) { "pip_compile_bounded.txt" } - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }] - end - - context "when originally unpinned" do - let(:updated_requirement) { "<=18.1.0" } - - it { is_expected.to eq(Gem::Version.new("18.1.0")) } - end - - context "when not unlocking requirements" do - let(:updated_requirement) { "<=17.4.0" } - - it { is_expected.to eq(Gem::Version.new("17.4.0")) } - end - - context "when the latest version isn't allowed (doesn't exist)" do - let(:updated_requirement) { "<=18.0.0" } - - it { is_expected.to eq(Gem::Version.new("17.4.0")) } - end - - context "when the latest version is nil" do - let(:updated_requirement) { ">=0" } - - it { is_expected.to be >= Gem::Version.new("18.1.0") } - end - - context "when updating is blocked" do - let(:dependency_name) { "python-dateutil" } - let(:dependency_version) { "2.6.1" } - let(:dependency_requirements) do - [{ - file: "requirements/shared.in", - requirement: "==2.6.0", - groups: [], - source: nil - }] - end - let(:updated_requirement) { ">=2.6.1,<= 2.7.5" } - - context "when only in an imported file" do - let(:dependency_files) do - [shared_file, manifest_file, generated_file] - end - let(:shared_file) do - Dependabot::DependencyFile.new( - name: "requirements/shared.in", - content: - fixture("pip_compile_files", "python_dateutil.in") - ) - end - let(:manifest_fixture_name) { "imports_shared.in" } - let(:generated_fixture_name) { "pip_compile_imports_shared.txt" } - - it { is_expected.to be >= Gem::Version.new("2.6.1") } - end - end - - context "with multiple requirement.in files" do - let(:dependency_files) do - [manifest_file, manifest_file2, generated_file, generated_file2] - end - - let(:manifest_file2) do - Dependabot::DependencyFile.new( - name: "requirements/dev.in", - content: - fixture("pip_compile_files", manifest_fixture_name2) - ) - end - let(:generated_file2) do - Dependabot::DependencyFile.new( - name: "requirements/dev.txt", - content: fixture("requirements", generated_fixture_name2) - ) - end - let(:manifest_fixture_name2) { manifest_fixture_name } - let(:generated_fixture_name2) { generated_fixture_name } - - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }, { - file: "requirements/dev.in", - requirement: "<=17.4.0", - groups: [], - source: nil - }] - end - - it { is_expected.to be >= Gem::Version.new("18.1.0") } - - context "when a requirement is not resolvable" do - let(:manifest_fixture_name2) { "unresolvable.in" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Cannot install -r requirements/dev.in (line 1) and botocore==1.10.84 because these " \ - "package versions have conflicting dependencies.") - end - end - end - end - end - - context "with an unresolvable project" do - let(:dependency_files) { project_dependency_files("unresolvable") } - let(:dependency) do - Dependabot::Dependency.new( - name: "jupyter-server", - version: "0.1.1", - requirements: dependency_requirements, - package_manager: "pip" - ) - end - let(:dependency_requirements) do - [{ - file: "requirements.in", - requirement: nil, - groups: [], - source: nil - }] - end - - it "raises a helpful error", :slow do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Cannot install jupyter-server<=18.1.0 and >=17.3.0 because these package versions have " \ - "conflicting dependencies.") - end - end - end - - context "with a git source" do - context "when dealing with a dependency that can't be reached" do - let(:manifest_fixture_name) { "git_source_unreachable.in" } - let(:dependency_files) { [manifest_file] } - let(:dependency_version) { nil } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::GitDependenciesNotReachable) do |error| - expect(error.dependency_urls) - .to eq(["https://github.com/greysteil/unreachable"]) - end - end - end - - context "when dealing with a dependency that has a bad ref" do - let(:manifest_fixture_name) { "git_source_bad_ref.in" } - let(:dependency_files) { [manifest_file] } - let(:dependency_version) { nil } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::GitDependencyReferenceNotFound) do |err| - expect(err.dependency).to eq("pythonfinder") - end - end - end - end - - context "with a subdependency" do - let(:dependency_name) { "pbr" } - let(:dependency_version) { "4.0.2" } - let(:dependency_requirements) { [] } - let(:updated_requirement) { ">=4.0.2,<=4.3.0" } - - it { is_expected.to eq(Gem::Version.new("4.3.0")) } - - context "when the requirement is superfluous" do - let(:dependency_name) { "requests" } - let(:dependency_version) { "2.18.0" } - let(:dependency_requirements) { [] } - let(:updated_requirement) { ">=2.18.0,<=2.18.4" } - let(:generated_fixture_name) { "pip_compile_unpinned_rogue.txt" } - - it { is_expected.to be_nil } - end - end - - context "with a dependency that is 'unsafe' to lock" do - let(:manifest_fixture_name) { "setuptools.in" } - let(:generated_fixture_name) { "pip_compile_setuptools.txt" } - let(:dependency_name) { "setuptools" } - let(:dependency_version) { "40.4.1" } - let(:dependency_requirements) { [] } - let(:updated_requirement) { ">=40.4.1" } - - it { is_expected.to be >= Gem::Version.new("40.6.2") } - end - - context "with an import of the setup.py", :slow do - let(:dependency_files) do - [manifest_file, generated_file, setup_file, pyproject] - end - let(:setup_file) do - Dependabot::DependencyFile.new( - name: "setup.py", - content: fixture("setup_files", setup_fixture_name) - ) - end - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: fixture("pyproject_files", "black_configuration.toml") - ) - end - let(:manifest_fixture_name) { "imports_setup.in" } - let(:generated_fixture_name) { "pip_compile_imports_setup.txt" } - let(:setup_fixture_name) { "small.py" } - let(:dependency_name) { "attrs" } - let(:dependency_version) { nil } - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: nil, - groups: [], - source: nil - }] - end - - it { is_expected.to be >= Gem::Version.new("18.1.0") } - - context "when dependency needs sanitizing" do - let(:setup_fixture_name) { "small_needs_sanitizing.py" } - - it { is_expected.to be >= Gem::Version.new("18.1.0") } - end - end - - context "with native dependencies that are not pre-built", :slow do - let(:manifest_fixture_name) { "native_dependencies.in" } - let(:generated_fixture_name) { "pip_compile_native_dependencies.txt" } - let(:dependency_name) { "cryptography" } - let(:dependency_version) { "2.2.2" } - let(:updated_requirement) { ">3.0.0,<3.3" } - - it { is_expected.to eq(Gem::Version.new("3.2.1")) } - end - end - - describe "#resolvable?" do - subject(:resolvable) { resolver.resolvable?(version: version) } - - let(:version) { Gem::Version.new("18.1.0") } - - context "when the version is resolvable" do - let(:version) { Gem::Version.new("18.1.0") } - - it { is_expected.to be(true) } - - context "with a subdependency" do - let(:dependency_name) { "pbr" } - let(:dependency_version) { "4.0.2" } - let(:dependency_requirements) { [] } - let(:version) { Gem::Version.new("5.1.3") } - - it { is_expected.to be(true) } - end - end - - context "when the version is not resolvable" do - let(:version) { Gem::Version.new("99.18.4") } - - it { is_expected.to be(false) } - - context "with a subdependency" do - let(:manifest_fixture_name) { "requests.in" } - let(:generated_fixture_name) { "pip_compile_requests.txt" } - let(:dependency_name) { "urllib3" } - let(:dependency_version) { "1.22" } - let(:dependency_requirements) { [] } - let(:version) { Gem::Version.new("1.23") } - - it { is_expected.to be(false) } - end - - context "when the original manifest isn't resolvable" do - let(:manifest_fixture_name) { "unresolvable.in" } - let(:dependency_files) { [manifest_file] } - let(:dependency_name) { "boto3" } - let(:dependency_version) { nil } - let(:version) { "1.9.28" } - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: "==1.9.27", - groups: [], - source: nil - }] - end - - it "raises a helpful error" do - expect { resolvable } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Cannot install -r requirements/test.in (line 1) and botocore==1.10.84 because these " \ - "package versions have conflicting dependencies.") - end - end - end - end - - context "when failing to resolve due to resource limits" do - context "when dealing with running out of disk space" do - before do - allow(Dependabot::SharedHelpers) - .to receive(:run_shell_command) - allow(Dependabot::SharedHelpers) - .to receive(:run_shell_command).with("pyenv versions").and_return("3.11.5") - allow(Dependabot::SharedHelpers) - .to receive(:run_shell_command).with(a_string_matching(/pyenv exec pip-compile/), *any_args) - .and_raise( - Dependabot::SharedHelpers::HelperSubprocessFailed.new( - message: "OSError: [Errno 28] No space left on device", - error_context: {} - ) - ) - end - - it "raises a helpful error" do - expect { resolvable } - .to raise_error(Dependabot::OutOfDisk) - end - end - - context "when dealing with running out of memory" do - before do - allow(Dependabot::SharedHelpers) - .to receive(:run_shell_command) - allow(Dependabot::SharedHelpers) - .to receive(:run_shell_command).with("pyenv versions").and_return("3.11.5") - allow(Dependabot::SharedHelpers) - .to receive(:run_shell_command).with(a_string_matching(/pyenv exec pip-compile/), *any_args) - .and_raise( - Dependabot::SharedHelpers::HelperSubprocessFailed.new( - message: "MemoryError", - error_context: {} - ) - ) - end - - it "raises a helpful error" do - expect { resolvable } - .to raise_error(Dependabot::OutOfMemory) - end - end - - context "when HelperSubprocessFailed exception is raised" do - let(:error_handler) { Dependabot::Uv::PipCompileErrorHandler.new } - - let(:exception_message) { "HelperSubprocessFailed" } - - context "when dealing with subprocess-exited-with-error error" do - let(:exception_message) do - "Preparing metadata (setup.py): finished with status 'error' - error: subprocess-exited-with-error - - × python setup.py egg_info did not run successfully. - │ exit code: 1 - ╰─> [18 lines of output] - Traceback (most recent call last): - File \"\", line 2, in - exec(compile(''' - ~~~~^^^^^^^^^^^^ - # This is -- a caller that pip uses to run setup.py - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - ...<31 lines>... - exec(compile(setup_py_code, filename, \"exec\")) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - ''' % ('/tmp/pip-resolve-84tqp2g1/pillow_f28c34dffdb342a49c27519580a49fd3/setup.py',)" - end - - it "raises a helpful error" do - expect { error_handler.handle_pipcompile_error(exception_message) } - .to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - - context "when dealing with an installation error" do - let(:exception_message) do - "pip._internal.exceptions.InstallationError: Could not install requirement" \ - " rugby-[FILTERED_REPO]@ https://****@github.com/compute-cloud/a300cfb3a4070c923246dd4.zip " \ - "from https://****@github.com/compute-cloud/[FILTERED_REPO]/archive/s.zip" \ - " (from -r requirements.in (line 23)) because of HTTP error 404 Client Error: Not" \ - " Found for url: https://github.com/compute-cloud/[FILTERED_REPO]/archive/a3246dd4.zip for" \ - " URL https://****@github.com/compute-cloud/[FILTERED_REPO]/archive/a3dd4.zip" - end - - it "raises a helpful error" do - expect { error_handler.handle_pipcompile_error(exception_message) } - .to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - end - end - end -end diff --git a/uv/spec/dependabot/uv/update_checker/pip_version_resolver_spec.rb b/uv/spec/dependabot/uv/update_checker/pip_version_resolver_spec.rb index 347dfb0ed5..9209a458ff 100644 --- a/uv/spec/dependabot/uv/update_checker/pip_version_resolver_spec.rb +++ b/uv/spec/dependabot/uv/update_checker/pip_version_resolver_spec.rb @@ -11,8 +11,6 @@ stub_request(:get, pypi_url).to_return(status: 200, body: pypi_response) end - let(:pypi_url) { "https://pypi.org/simple/luigi/" } - let(:pypi_response) { fixture("pypi", "pypi_simple_response.html") } let(:resolver) do described_class.new( dependency: dependency, @@ -54,7 +52,7 @@ name: dependency_name, version: dependency_version, requirements: dependency_requirements, - package_manager: "pip" + package_manager: "uv" ) end let(:dependency_name) { "django" } @@ -116,7 +114,7 @@ [ Dependabot::SecurityAdvisory.new( dependency_name: dependency_name, - package_manager: "pip", + package_manager: "uv", vulnerable_versions: ["<= 2.1.0"] ) ] diff --git a/uv/spec/dependabot/uv/update_checker/pipenv_version_resolver_spec.rb b/uv/spec/dependabot/uv/update_checker/pipenv_version_resolver_spec.rb deleted file mode 100644 index e3d2f793c5..0000000000 --- a/uv/spec/dependabot/uv/update_checker/pipenv_version_resolver_spec.rb +++ /dev/null @@ -1,512 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/update_checker/pipenv_version_resolver" - -RSpec.describe Dependabot::Uv::UpdateChecker::PipenvVersionResolver do - let(:resolver) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ) - end - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - })] - end - let(:dependency_files) { [pipfile, lockfile] } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", pipfile_fixture_name) - ) - end - let(:pipfile_fixture_name) { "exact_version" } - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "Pipfile.lock", - content: fixture("pipfile_files", lockfile_fixture_name) - ) - end - let(:lockfile_fixture_name) { "exact_version.lock" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "pip", - metadata: dependency_metadata - ) - end - let(:dependency_name) { "requests" } - let(:dependency_version) { "2.18.0" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==2.18.0", - groups: ["default"], - source: nil - }] - end - let(:dependency_metadata) { {} } - let(:repo_contents_path) { nil } - - describe "#latest_resolvable_version" do - subject(:latest_resolvable_version) { resolver.latest_resolvable_version(requirement: updated_requirement) } - - let(:updated_requirement) { ">=2.18.0,<=2.18.4" } - - context "with a lockfile" do - let(:dependency_files) { [pipfile, lockfile] } - let(:dependency_version) { "2.18.0" } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - - context "when not unlocking the requirement" do - let(:updated_requirement) { "==2.18.0" } - - it { is_expected.to be >= Gem::Version.new("2.18.0") } - end - end - - context "with a star requirement" do - let(:pipfile_fixture_name) { "star" } - let(:lockfile_fixture_name) { "star.lock" } - let(:dependency_name) { "boto3" } - let(:dependency_version) { "1.28.50" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "*", - groups: ["default"], - source: nil - }] - end - let(:updated_requirement) { "*" } - - it { is_expected.to be >= Gem::Version.new("1.29.6") } - end - - context "without a lockfile (but with a latest version)" do - let(:dependency_files) { [pipfile] } - let(:dependency_version) { nil } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "when the latest version isn't allowed" do - let(:updated_requirement) { ">=2.18.0,<=2.18.3" } - - it { is_expected.to eq(Gem::Version.new("2.18.3")) } - end - - context "when the latest version is nil" do - let(:updated_requirement) { ">=2.18.0" } - - it { is_expected.to be >= Gem::Version.new("2.19.0") } - end - - context "with a dependency with a hard name" do - let(:pipfile_fixture_name) { "hard_names" } - let(:lockfile_fixture_name) { "hard_names.lock" } - let(:dependency_name) { "discord-py" } - let(:dependency_metadata) { { original_name: "discord.py" } } - let(:dependency_version) { "0.16.1" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==0.16.1", - groups: ["default"], - source: nil - }] - end - let(:updated_requirement) { ">=0.16.1,<=1.0.0" } - - it { is_expected.to be >= Gem::Version.new("0.16.12") } - end - - context "when another dependency has been yanked" do - let(:pipfile_fixture_name) { "yanked" } - let(:lockfile_fixture_name) { "yanked.lock" } - - it "assumes the lockfile resolve is valid and upgrades the dependency just fine" do - expect(latest_resolvable_version).to eq(Gem::Version.new("2.18.4")) - end - end - - context "with a subdependency" do - let(:dependency_name) { "py" } - let(:dependency_version) { "1.5.3" } - let(:dependency_requirements) { [] } - let(:updated_requirement) { ">=1.5.3,<=1.7.0" } - - it { is_expected.to eq(Gem::Version.new("1.7.0")) } - end - - context "with a path dependency" do - let(:dependency_files) { [pipfile, lockfile, setupfile] } - let(:setupfile) do - Dependabot::DependencyFile.new( - name: "mydep/setup.py", - content: fixture("setup_files", setupfile_fixture_name) - ) - end - let(:setupfile_fixture_name) { "small.py" } - let(:pipfile_fixture_name) { "path_dependency_not_self" } - let(:lockfile_fixture_name) { "path_dependency_not_self.lock" } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - - context "when dependency needs to be sanitized" do - let(:setupfile_fixture_name) { "small_needs_sanitizing.py" } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "when dependency imports a setup.cfg" do - let(:dependency_files) { [pipfile, lockfile, setupfile, setup_cfg] } - let(:setupfile_fixture_name) { "with_pbr.py" } - let(:setup_cfg) do - Dependabot::DependencyFile.new( - name: "mydep/setup.cfg", - content: fixture("setup_files", "setup.cfg") - ) - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - end - - context "with a required python version" do - let(:pipfile_fixture_name) { "required_python" } - let(:lockfile_fixture_name) { "required_python.lock" } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - - context "when version comes from a Poetry file and includes || logic" do - let(:pipfile_fixture_name) { "exact_version" } - let(:dependency_files) { [pipfile, pyproject] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: fixture("pyproject_files", "basic_poetry_dependencies.toml") - ) - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "when version is invalid" do - let(:pipfile_fixture_name) { "required_python_invalid" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message).to start_with( - "Pipenv does not support specifying Python ranges" - ) - end - end - end - - context "when version is set to a python version no longer supported by Dependabot" do - let(:pipfile_fixture_name) { "required_python_unsupported" } - - it "raises a helpful error" do - expect { latest_resolvable_version }.to raise_error(Dependabot::ToolVersionNotSupported) do |err| - expect(err.message).to start_with( - "Dependabot detected the following Python requirement for your project: '3.4.*'." - ) - end - end - end - - context "when implicit and occurring on another dependency" do - let(:pipfile_fixture_name) { "required_python_implicit" } - let(:lockfile_fixture_name) { "required_python_implicit.lock" } - let(:dependency_name) { "pytest" } - let(:dependency_version) { "3.4.0" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==3.4.0", - groups: ["develop"], - source: nil - }] - end - let(:updated_requirement) { ">=3.4.0,<=3.8.2" } - - it "assumes the lockfile resolve is valid and upgrades the dependency just fine" do - expect(latest_resolvable_version).to eq(Gem::Version.new("3.8.2")) - end - end - - context "when dealing with a resolution that has caused trouble in the past" do - let(:dependency_files) { [pipfile] } - let(:pipfile_fixture_name) { "problematic_resolution" } - let(:dependency_name) { "twilio" } - let(:dependency_version) { nil } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "*", - groups: ["default"], - source: nil - }] - end - let(:updated_requirement) { ">=3.4.0,<=6.14.6" } - - it { is_expected.to eq(Gem::Version.new("6.14.6")) } - end - end - - context "with extra requirements" do - let(:dependency_name) { "raven" } - let(:dependency_version) { "5.27.1" } - let(:updated_requirement) { ">=5.27.1,<=7.0.0" } - let(:pipfile_fixture_name) { "extra_subdependency" } - let(:lockfile_fixture_name) { "extra_subdependency.lock" } - - it { is_expected.to be >= Gem::Version.new("6.7.0") } - end - - context "with a git source" do - context "when dealing with a dependency reference, that can't be reached" do - let(:pipfile_fixture_name) { "git_source_unreachable" } - let(:lockfile_fixture_name) { "git_source_unreachable.lock" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::GitDependenciesNotReachable) do |error| - expect(error.dependency_urls) - .to eq(["https://github.com/user/django.git"]) - end - end - end - - context "when dealing with a dependency has a bad ref" do - let(:pipfile_fixture_name) { "git_source_bad_ref" } - let(:lockfile_fixture_name) { "git_source_bad_ref.lock" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::GitDependencyReferenceNotFound) do |err| - expect(err.message).to eq( - "The branch or reference specified for (unknown package at v15.1.2) could not be retrieved" - ) - end - end - end - end - - context "with an environment variable source" do - let(:pipfile_fixture_name) { "environment_variable_source" } - let(:lockfile_fixture_name) { "environment_variable_source.lock" } - - context "with a matching credential" do - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }), Dependabot::Credential.new({ - "type" => "python_index", - "index-url" => "https://pypi.org/simple" - })] - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - end - - context "with a `nil` requirement" do - let(:dependency_files) { [pipfile] } - let(:dependency_version) { nil } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==2.18.0", - groups: ["default"], - source: nil - }, { - file: "requirements.txt", - requirement: nil, - groups: ["default"], - source: nil - }] - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "with a conflict at the latest version" do - let(:pipfile_fixture_name) { "conflict_at_latest" } - let(:lockfile_fixture_name) { "conflict_at_latest.lock" } - let(:dependency_version) { "2.6.0" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==2.6.0", - groups: ["default"], - source: nil - }] - end - - it { is_expected.to be_nil } - end - - context "with a conflict at the current version" do - let(:pipfile_fixture_name) { "conflict_at_current" } - let(:lockfile_fixture_name) { "conflict_at_current.lock" } - let(:dependency_version) { "2.18.0" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==2.18.0", - groups: ["default"], - source: nil - }] - end - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message).to match( - "Cannot install -r .* because these package versions have conflicting dependencies" - ) - end - end - end - - context "with a missing system library" do - # NOTE: Attempt to update an unrelated dependency (requests) to cause - # resolution to fail for rtree which has a system dependency on - # libspatialindex which isn't installed in dependabot-core's Dockerfile. - let(:dependency_files) do - project_dependency_files("pipenv/missing-system-library") - end - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message).to include( - "ERROR:pip.subprocessor:Getting requirements to build wheel exited with 1" - ) - end - end - end - - context "with a missing system library, and when running python older than 3.12" do - # NOTE: Attempt to update an unrelated dependency (requests) to cause - # resolution to fail for rtree which has a system dependency on - # libspatialindex which isn't installed in dependabot-core's Dockerfile. - let(:dependency_files) do - project_dependency_files("pipenv/missing-system-library-old-python") - end - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message).to include( - "ERROR:pip.subprocessor:python setup.py egg_info exited with 1" - ) - end - end - end - - context "with a python library setup as an editable dependency that needs extra files" do - let(:project_name) { "pipenv/editable-package" } - let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } - let(:dependency_name) { "cryptography" } - let(:dependency_version) { "40.0.1" } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==40.0.1", - groups: ["develop"], - source: nil - }] - end - let(:updated_requirement) { ">=40.0.1,<=41.0.5" } - - let(:dependency_files) do - %w(Pipfile Pipfile.lock pyproject.toml).map do |name| - Dependabot::DependencyFile.new( - name: name, - content: fixture("projects", project_name, name) - ) - end - end - - it { is_expected.to eq(Gem::Version.new("41.0.5")) } - end - end - - describe "#resolvable?" do - subject(:resolvable) { resolver.resolvable?(version: version) } - - let(:version) { Gem::Version.new("2.18.4") } - - context "when version is resolvable" do - let(:version) { Gem::Version.new("2.18.4") } - - it { is_expected.to be(true) } - - context "with a subdependency" do - let(:dependency_name) { "py" } - let(:dependency_version) { "1.5.3" } - let(:dependency_requirements) { [] } - let(:version) { Gem::Version.new("1.7.0") } - - it { is_expected.to be(true) } - end - end - - context "when version is not resolvable" do - let(:version) { Gem::Version.new("99.18.4") } - - it { is_expected.to be(false) } - - context "with a subdependency" do - let(:dependency_name) { "py" } - let(:dependency_version) { "1.5.3" } - let(:dependency_requirements) { [] } - - it { is_expected.to be(false) } - end - - context "when the original manifest isn't resolvable" do - let(:pipfile_fixture_name) { "conflict_at_current" } - let(:lockfile_fixture_name) { "conflict_at_current.lock" } - let(:version) { Gem::Version.new("99.18.4") } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==2.18.0", - groups: ["default"], - source: nil - }] - end - - it "raises a helpful error" do - expect { resolvable } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message).to match( - "Cannot install -r .* because these package versions have conflicting dependencies" - ) - end - end - end - end - end -end diff --git a/uv/spec/dependabot/uv/update_checker/poetry_version_resolver_spec.rb b/uv/spec/dependabot/uv/update_checker/poetry_version_resolver_spec.rb deleted file mode 100644 index bcc0cfa564..0000000000 --- a/uv/spec/dependabot/uv/update_checker/poetry_version_resolver_spec.rb +++ /dev/null @@ -1,654 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/uv/update_checker/poetry_version_resolver" - -namespace = Dependabot::Uv::UpdateChecker -RSpec.describe namespace::PoetryVersionResolver do - let(:resolver) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: nil - ) - end - - let(:credentials) do - [Dependabot::Credential.new({ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - })] - end - let(:dependency_files) { [pyproject, lockfile] } - let(:pyproject) do - Dependabot::DependencyFile.new( - name: "pyproject.toml", - content: pyproject_content - ) - end - let(:pyproject_content) { fixture("pyproject_files", pyproject_fixture_name) } - let(:pyproject_fixture_name) { "poetry_exact_requirement.toml" } - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "poetry.lock", - content: fixture("poetry_locks", lockfile_fixture_name) - ) - end - let(:lockfile_fixture_name) { "exact_version.lock" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "pip" - ) - end - let(:dependency_name) { "requests" } - let(:dependency_version) { "2.18.0" } - let(:dependency_requirements) do - [{ - file: "pyproject.toml", - requirement: "2.18.0", - groups: ["dependencies"], - source: nil - }] - end - - describe "#latest_resolvable_version" do - subject(:latest_resolvable_version) { resolver.latest_resolvable_version(requirement: updated_requirement) } - - let(:updated_requirement) { ">=2.18.0,<=2.18.4" } - - context "without a lockfile (but with a latest version)" do - let(:dependency_files) { [pyproject] } - let(:dependency_version) { nil } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "with a dependency defined under dev-dependencies" do - let(:pyproject_content) do - super().gsub("[tool.poetry.dependencies]", "[tool.poetry.dev-dependencies]") - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "with a dependency defined under a group" do - let(:pyproject_content) do - super().gsub("[tool.poetry.dependencies]", "[tool.poetry.group.dev.dependencies]") - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "with a dependency defined under a non-dev group" do - let(:pyproject_content) do - super().gsub("[tool.poetry.dependencies]", "[tool.poetry.group.docs.dependencies]") - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - - context "with a lockfile" do - let(:dependency_files) { [pyproject, lockfile] } - let(:dependency_version) { "2.18.0" } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - - context "when not unlocking the requirement" do - let(:updated_requirement) { "==2.18.0" } - - it { is_expected.to eq(Gem::Version.new("2.18.0")) } - end - - context "when the lockfile is named poetry.lock" do - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "poetry.lock", - content: fixture("poetry_locks", lockfile_fixture_name) - ) - end - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - - context "when the pyproject.toml needs to be sanitized" do - let(:pyproject_fixture_name) { "needs_sanitization.toml" } - - it { is_expected.to eq(Gem::Version.new("2.18.4")) } - end - end - end - - context "when the latest version isn't allowed" do - let(:updated_requirement) { ">=2.18.0,<=2.18.3" } - - it { is_expected.to eq(Gem::Version.new("2.18.3")) } - end - - context "when the latest version is nil" do - let(:updated_requirement) { ">=2.18.0" } - - it { is_expected.to be >= Gem::Version.new("2.19.0") } - end - - context "with a subdependency" do - let(:dependency_name) { "idna" } - let(:dependency_version) { "2.5" } - let(:dependency_requirements) { [] } - let(:updated_requirement) { ">=2.5,<=2.7" } - - # Resolution blocked by requests - it { is_expected.to eq(Gem::Version.new("2.5")) } - - context "when dependency can be updated, but not to the latest version" do - let(:pyproject_fixture_name) { "latest_subdep_blocked.toml" } - let(:lockfile_fixture_name) { "latest_subdep_blocked.lock" } - - it { is_expected.to eq(Gem::Version.new("2.6")) } - end - - context "when dependency shouldn't be in the lockfile at all" do - let(:dependency_name) { "cryptography" } - let(:dependency_version) { "2.4.2" } - let(:dependency_requirements) { [] } - let(:updated_requirement) { ">=2.4.2,<=2.5" } - let(:lockfile_fixture_name) { "extra_dependency.lock" } - - # Ideally we would ignore sub-dependencies that shouldn't be in the - # lockfile, but determining that is hard. It's fine for us to update - # them instead - they'll be removed in another (unrelated) PR - it { is_expected.to eq(Gem::Version.new("2.5")) } - end - end - - context "with a legacy Python" do - let(:pyproject_fixture_name) { "python_2.toml" } - let(:lockfile_fixture_name) { "python_2.lock" } - - it "raises an error" do - expect { latest_resolvable_version }.to raise_error(Dependabot::ToolVersionNotSupported) - end - end - - context "with a minimum python set that satisfies the running python" do - let(:pyproject_fixture_name) { "python_lower_bound.toml" } - let(:lockfile_fixture_name) { "python_lower_bound.toml" } - - let(:pyproject_nested) do - Dependabot::DependencyFile.new( - name: "a-dependency/pyproject.toml", - content: fixture("pyproject_files", "python_lower_bound_nested.toml") - ) - end - - let(:dependency_name) { "black" } - let(:dependency_version) { "22.6.0" } - let(:updated_requirement) { "==23.7.0" } - - let(:dependency_files) { [pyproject, lockfile, pyproject_nested] } - - it { is_expected.to eq(Gem::Version.new("23.7.0")) } - end - - context "with a dependency file that includes a git dependency" do - let(:pyproject_fixture_name) { "git_dependency.toml" } - let(:lockfile_fixture_name) { "git_dependency.lock" } - let(:dependency_name) { "pytest" } - let(:dependency_version) { "3.7.4" } - let(:dependency_requirements) do - [{ - file: "pyproject.toml", - requirement: "*", - groups: ["dependencies"], - source: nil - }] - end - let(:updated_requirement) { ">=3.7.4,<=3.9.0" } - - it { is_expected.to eq(Gem::Version.new("3.8.2")) } - - context "when repo has no lockfile" do - let(:dependency_files) { [pyproject] } - - context "when dependency has a bad reference, and there is no lockfile" do - let(:pyproject_fixture_name) { "git_dependency_bad_ref.toml" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::GitDependencyReferenceNotFound) do |err| - expect(err.dependency).to eq("toml") - end - end - end - - context "when dependency is unreachable" do - let(:pyproject_fixture_name) { "git_dependency_unreachable.toml" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::GitDependenciesNotReachable) do |error| - expect(error.dependency_urls) - .to eq(["https://github.com/greysteil/unreachable.git"]) - end - end - end - end - end - - context "with a conflict at the latest version" do - let(:pyproject_fixture_name) { "conflict_at_latest.toml" } - let(:lockfile_fixture_name) { "conflict_at_latest.lock" } - let(:dependency_version) { "2.6.0" } - let(:dependency_requirements) do - [{ - file: "pyproject.toml", - requirement: "2.6.0", - groups: ["dependencies"], - source: nil - }] - end - let(:updated_requirement) { ">=2.6.0,<=2.18.4" } - - # Conflict with chardet is introduced in v2.16.0 - it { is_expected.to eq(Gem::Version.new("2.15.1")) } - end - - context "when version is resolvable only if git references are preserved", :slow do - let(:pyproject_fixture_name) { "git_conflict.toml" } - let(:lockfile_fixture_name) { "git_conflict.lock" } - let(:dependency_name) { "django-widget-tweaks" } - let(:dependency_version) { "1.4.2" } - let(:dependency_requirements) do - [{ - file: "pyproject.toml", - requirement: "^1.4", - groups: ["dependencies"], - source: nil - }] - end - let(:updated_requirement) { ">=1.4.2,<=1.4.3" } - - it { is_expected.to be >= Gem::Version.new("1.4.3") } - end - - context "when version is not resolvable" do - let(:dependency_files) { [pyproject] } - let(:pyproject_fixture_name) { "solver_problem.toml" } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("depends on black (^18), version solving failed") - end - end - - context "when dealing with a yanked dependency" do - let(:pyproject_fixture_name) { "yanked_version.toml" } - let(:lockfile_fixture_name) { "yanked_version.lock" } - - context "with a lockfile" do - let(:dependency_files) { [pyproject, lockfile] } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Package croniter (0.3.26) not found") - end - end - end - - context "without a lockfile" do - let(:dependency_files) { [pyproject] } - - it "raises a helpful error" do - expect { latest_resolvable_version } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("depends on croniter (0.3.26) which doesn't match any versions") - end - end - end - end - end - end - - describe "#resolvable?" do - subject(:resolvable) { resolver.resolvable?(version: version) } - - let(:version) { Gem::Version.new("2.18.4") } - - context "when version is resolvable" do - let(:version) { Gem::Version.new("2.18.4") } - - it { is_expected.to be(true) } - - context "with a subdependency" do - let(:dependency_name) { "idna" } - let(:dependency_version) { "2.5" } - let(:dependency_requirements) { [] } - let(:pyproject_fixture_name) { "latest_subdep_blocked.toml" } - let(:lockfile_fixture_name) { "latest_subdep_blocked.lock" } - let(:version) { Gem::Version.new("2.6") } - - it { is_expected.to be(true) } - end - end - - context "when version is not resolvable" do - let(:version) { Gem::Version.new("99.18.4") } - - it { is_expected.to be(false) } - - context "with a subdependency" do - let(:dependency_name) { "idna" } - let(:dependency_version) { "2.5" } - let(:dependency_requirements) { [] } - let(:pyproject_fixture_name) { "latest_subdep_blocked.toml" } - let(:lockfile_fixture_name) { "latest_subdep_blocked.lock" } - let(:version) { Gem::Version.new("2.7") } - - it { is_expected.to be(false) } - end - - context "when the original manifest isn't resolvable" do - let(:dependency_files) { [pyproject] } - let(:pyproject_fixture_name) { "solver_problem.toml" } - - it "raises a helpful error" do - expect { resolvable } - .to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("depends on black (^18), version solving failed") - end - end - end - end - end - - describe "handles SharedHelpers::HelperSubprocessFailed errors raised by version resolver" do - subject(:poetry_error_handler) { error_handler.handle_poetry_error(exception) } - - let(:error_handler) do - Dependabot::Uv::PoetryErrorHandler.new( - dependencies: dependency, - dependency_files: dependency_files - ) - end - let(:exception) { Exception.new(response) } - - context "with incompatible constraints mentioned in requirements" do - let(:response) { "Incompatible constraints in requirements of histolab (0.7.0):" } - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Incompatible constraints in requirements of histolab (0.7.0):") - end - end - end - - context "with invalid configuration in pyproject.toml file" do - let(:response) do - "The Poetry configuration is invalid: - - data.group.dev.dependencies.h5 must be valid exactly by one definition (0 matches found)" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("The Poetry configuration is invalid") - end - end - end - - context "with invalid version for dependency mentioned in pyproject.toml file" do - let(:response) do - "Resolving dependencies... - Could not parse version constraint: <0.2.0app" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Could not parse version constraint: <0.2.0app") - end - end - end - - context "with invalid dependency source link in pyproject.toml file" do - let(:response) do - "Updating dependencies - Resolving dependencies... - No valid distribution links found for package: \"llama-cpp-python\" version: \"0.2.82\"" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("No valid distribution links found for package: \"llama-cpp-python\" version: \"0.2.82\"") - end - end - end - - context "with private registry authentication error code 401 file" do - let(:response) do - "Creating virtualenv non-package-mode-r7N_A6Jx-py3.11 in /home/dependabot/.cache/pypoetry/virtualenvs - Updating dependencies - Resolving dependencies... - Source (factorypal): Failed to retrieve metadata at https://fp-pypi.sm00p.com/simple/ - 401 Client Error: Unauthorized for url: https://fp-pypi.sm00p.com/simple/" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) do |error| - expect(error.message) - .to include("https://fp-pypi.sm00p.com") - end - end - end - - context "with private registry authentication 403 Client Error" do - let(:response) do - "Creating virtualenv reimbursement-coverage-api-fKdRenE--py3.12 in /home/dependabot/.cache/pypoetry/virtualenvs - Updating dependencies - Resolving dependencies... - 403 Client Error: for url: https://fp-pypi.sm00p.com/simple/" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) do |error| - expect(error.message) - .to include("https://fp-pypi.sm00p.com") - end - end - end - - context "with private registry authentication 404 Client Error" do - let(:response) do - "NetworkConnectionError('404 Client Error: Not Found for url: https://raw.example.com/example/flow/" \ - "constraints-$%7BAIRFLOW_VERSION%7D/constraints-3.10.txt'" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) do |error| - expect(error.message) - .to include("https://raw.example.com") - end - end - end - - context "with private registry authentication 504 Server Error" do - let(:response) do - "Creating virtualenv alk-service-import-product-TbrdR40A-py3.8 in /home/dependabot/.cache/pypoetry/virtualenvs - Updating dependencies - Resolving dependencies... - - 504 Server Error: for url: https://pypi.com:8443/packages/alk_ci-1.whl#sha256=f9" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::InconsistentRegistryResponse) do |error| - expect(error.message) - .to include("https://pypi.com") - end - end - end - - context "with private index authentication HTTP error 404" do - let(:response) do - "HTTP error 404 while getting " \ - "https://.com/compute-cloud/8e1a9.zip" \ - "Not Found for URL" \ - " https://example.com/compute-cloud/[FILTERED_REPO]/archive/a5bf58e7a37be7503e1a79febf8b555b9d28e1a9.zip" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) do |error| - expect(error.message) - .to include("https://example.com") - end - end - end - - context "with dependency spec version not found in package index" do - let(:response) do - "Creating virtualenv pyiceberg-xBYdM_d2-py3.12 in /home/dependabot/.cache/pypoetry/virtualenvs - Updating dependencies - Resolving dependencies... - Package docutils (0.21.post1) not found." - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("Package docutils (0.21.post1) not found.") - end - end - end - - context "with package 'python' specification is incompatible with dependency" do - let(:response) do - "Resolving dependencies... - The current project's supported Python range (>=3.8,<4.0) is not compatible with some of the required " \ - " packages Python requirement: - scipy requires Python <3.13,>=3.9, so it will not be satisfied for" \ - " Python >=3.8,<3.9 || >=3.13,<4.0" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message) - .to include("scipy requires Python <3.13,>=3.9, so it will not be satisfied for") - end - end - end - - context "with a misconfigured pyproject.toml file" do - let(:response) do - "Creating virtualenv analysis-nlUUV3qa-py3.13 in pypoetry/virtualenvs - Updating dependencies - Resolving dependencies... - list index out of range" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - - context "with a timed out response error" do - let(:response) do - "HTTPSConnectionPool(host='nexus.nee.com', port=443): " \ - "Max retries exceeded with url: (Caused by ProxyError('Unable to connect to proxy'" \ - ", RemoteDisconnected('Remote end closed connection without response')))" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::InconsistentRegistryResponse) - end - end - - context "with a timed out response error" do - let(:response) do - "HTTPSConnectionPool(host='pypi.pymetrics.com', port=443): Read timed out. (read timeout=15)" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::InconsistentRegistryResponse) - end - end - - context "with a 500 server error" do - let(:response) do - "500 Server Error: Internal Server Error for url: http://nexus.bvc.euc1.lan/repository/le/adup-utils/" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::InconsistentRegistryResponse) - end - end - - context "with version solving failed error" do - let(:response) do - "Creating virtualenv mobileweb-h6LnalBm-py3.13 in /home/dependabot/.cache/pypoetry/virtualenvs" \ - "Updating dependencies" \ - "Resolving dependencies..." \ - "Unable to determine package info for cryptography" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - - context "with a self dependency error" do - let(:response) do - "Creating virtualenv mobileweb-h6LnalBm-py3.13 in /home/dependabot/.cache/pypoetry/virtualenvs" \ - "Updating dependencies" \ - "Resolving dependencies..." \ - "Package 'tensorflow-macos' is listed as a dependency of itself." - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - - context "with a variation of incompatible constraints error" do - let(:response) do - "Creating virtualenv mobileweb-h6LnalBm-py3.13 in /home/dependabot/.cache/pypoetry/virtualenvs" \ - "Updating dependencies" \ - "Resolving dependencies..." \ - "Incompatible constraints in requirements of reflector (0.1.0):" \ - "types-setuptools (==75.8.0.20250210)" \ - "types-setuptools (>=69.1.0.20240308,<70.0.0.0)" \ - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - - context "with a project is listed a dependency" do - let(:response) do - "Creating virtualenv kiota-serialization-multipart-GzD6BRdm-py3.13 in " \ - "/home/dependabot/.cache/pypoetry/virtualenvs" \ - "Updating dependencies" \ - "Resolving dependencies..." \ - "Path tmp/20250109-1637-dc4aky/json for kiota-serialization-json does not exist" - end - - it "raises a helpful error" do - expect { poetry_error_handler }.to raise_error(Dependabot::DependencyFileNotResolvable) - end - end - end -end diff --git a/uv/spec/dependabot/uv/update_checker_spec.rb b/uv/spec/dependabot/uv/update_checker_spec.rb index ac8af58bc6..697de2a0be 100644 --- a/uv/spec/dependabot/uv/update_checker_spec.rb +++ b/uv/spec/dependabot/uv/update_checker_spec.rb @@ -26,7 +26,7 @@ name: dependency_name, version: dependency_version, requirements: dependency_requirements, - package_manager: "pip" + package_manager: "uv" ) end let(:requirements_fixture_name) { "version_specified.txt" } @@ -42,13 +42,6 @@ content: fixture("pyproject_files", pyproject_fixture_name) ) end - let(:pipfile_fixture_name) { "exact_version" } - let(:pipfile) do - Dependabot::DependencyFile.new( - name: "Pipfile", - content: fixture("pipfile_files", pipfile_fixture_name) - ) - end let(:dependency_files) { [requirements_file] } let(:requirements_update_strategy) { nil } let(:security_advisories) { [] } @@ -102,45 +95,6 @@ it { is_expected.to be_falsey } end - - context "when a dependency in a poetry-based Python library and also in an additional requirements file" do - let(:dependency_files) { [pyproject, requirements_file] } - let(:pyproject_fixture_name) { "tilde_version.toml" } - - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "1.2.3", - requirements: [{ - file: "pyproject.toml", - requirement: "^1.0.0", - groups: [], - source: nil - }, { - file: "requirements.txt", - requirement: "==1.2.8", - groups: [], - source: nil - }], - package_manager: "pip" - ) - end - - let(:pypi_url) { "https://pypi.org/simple/requests/" } - let(:pypi_response) do - fixture("pypi", "pypi_simple_response_requests.html") - end - - before do - stub_request(:get, "https://pypi.org/pypi/pendulum/json/") - .to_return( - status: 200, - body: fixture("pypi", "pypi_response_pendulum.json") - ) - end - - it { is_expected.to be_truthy } - end end describe "#latest_version" do @@ -174,7 +128,7 @@ [ Dependabot::SecurityAdvisory.new( dependency_name: dependency_name, - package_manager: "pip", + package_manager: "uv", vulnerable_versions: ["<= 2.1.0"] ) ] @@ -274,12 +228,8 @@ instance_double(described_class::PipCompileVersionResolver) allow(described_class::PipCompileVersionResolver).to receive(:new) .and_return(dummy_resolver) - expect(dummy_resolver) - .to receive(:resolvable?) - .and_return(false) - expect(dummy_resolver) - .to receive(:latest_resolvable_version) - .and_return(Gem::Version.new("2.5.0")) + allow(dummy_resolver) + .to receive_messages(resolvable?: false, latest_resolvable_version: Gem::Version.new("2.5.0")) expect(checker.latest_resolvable_version) .to eq(Gem::Version.new("2.5.0")) end @@ -316,11 +266,11 @@ context "when the latest version is not resolvable" do it "delegates to PipCompileVersionResolver" do - expect(dummy_resolver) + allow(dummy_resolver) .to receive(:resolvable?) .and_return(false) - expect(dummy_resolver) + allow(dummy_resolver) .to receive(:latest_resolvable_version) .with(requirement: ">=1.22,<=1.24.2") .and_return(Gem::Version.new("1.24.2")) @@ -331,7 +281,7 @@ context "when the latest version is resolvable" do it "returns the latest version" do - expect(dummy_resolver) + allow(dummy_resolver) .to receive(:resolvable?) .and_return(true) @@ -341,86 +291,6 @@ end end end - - context "with a Pipfile" do - let(:dependency_files) { [pipfile] } - let(:dependency_requirements) do - [{ - file: "Pipfile", - requirement: "==2.18.0", - groups: [], - source: nil - }] - end - - it "delegates to PipenvVersionResolver" do - dummy_resolver = - instance_double(described_class::PipenvVersionResolver) - allow(described_class::PipenvVersionResolver).to receive(:new) - .and_return(dummy_resolver) - expect(dummy_resolver) - .to receive(:latest_resolvable_version) - .with(requirement: ">=2.0.0,<=2.6.0") - .and_return(Gem::Version.new("2.5.0")) - expect(checker.latest_resolvable_version) - .to eq(Gem::Version.new("2.5.0")) - end - end - - context "with a pyproject.toml" do - let(:dependency_files) { [pyproject] } - let(:dependency_requirements) do - [{ - file: "pyproject.toml", - requirement: "2.18.0", - groups: [], - source: nil - }] - end - - let(:dependency_files) { [pyproject] } - let(:dependency_requirements) do - [{ - file: "pyproject.toml", - requirement: "2.18.0", - groups: [], - source: nil - }] - end - - context "when including poetry dependencies" do - let(:pyproject_fixture_name) { "poetry_exact_requirement.toml" } - - it "delegates to PoetryVersionResolver" do - dummy_resolver = - instance_double(described_class::PoetryVersionResolver) - allow(described_class::PoetryVersionResolver).to receive(:new) - .and_return(dummy_resolver) - expect(dummy_resolver) - .to receive(:latest_resolvable_version) - .with(requirement: ">=2.0.0,<=2.6.0") - .and_return(Gem::Version.new("2.5.0")) - expect(checker.latest_resolvable_version) - .to eq(Gem::Version.new("2.5.0")) - end - end - - context "when including pep621 dependencies" do - let(:pyproject_fixture_name) { "pep621_exact_requirement.toml" } - - it "delegates to PipVersionResolver" do - dummy_resolver = - instance_double(described_class::PipVersionResolver) - allow(described_class::PipVersionResolver).to receive(:new) - .and_return(dummy_resolver) - expect(dummy_resolver) - .to receive(:latest_resolvable_version) - .and_return(Gem::Version.new("2.5.0")) - expect(checker.latest_resolvable_version) - .to eq(Gem::Version.new("2.5.0")) - end - end - end end describe "#preferred_resolvable_version" do @@ -433,7 +303,7 @@ [ Dependabot::SecurityAdvisory.new( dependency_name: dependency_name, - package_manager: "pip", + package_manager: "uv", vulnerable_versions: ["<= 2.1.0"] ) ] @@ -476,7 +346,7 @@ [ Dependabot::SecurityAdvisory.new( dependency_name: dependency_name, - package_manager: "pip", + package_manager: "uv", vulnerable_versions: ["< 17.4.0"] ) ] @@ -495,7 +365,7 @@ name: "luigi", version: version, requirements: requirements, - package_manager: "pip" + package_manager: "uv" ) end @@ -521,62 +391,6 @@ .to eq(Gem::Version.new("2.6.0")) end end - - context "with a Pipfile" do - let(:dependency_files) { [pipfile, lockfile] } - let(:version) { "2.18.0" } - let(:requirements) do - [{ - file: "Pipfile", - requirement: "==2.18.0", - groups: [], - source: nil - }] - end - let(:lockfile) do - Dependabot::DependencyFile.new( - name: "Pipfile.lock", - content: fixture("pipfile_files", "exact_version.lock") - ) - end - - it "delegates to PipenvVersionResolver" do - dummy_resolver = - instance_double(described_class::PipenvVersionResolver) - allow(described_class::PipenvVersionResolver).to receive(:new) - .and_return(dummy_resolver) - expect(dummy_resolver) - .to receive(:latest_resolvable_version) - .with(requirement: "==2.18.0") - .and_return(Gem::Version.new("2.18.0")) - expect(checker.latest_resolvable_version_with_no_unlock) - .to eq(Gem::Version.new("2.18.0")) - end - - context "with a requirement from a setup.py" do - let(:requirements) do - [{ - file: "setup.py", - requirement: nil, - groups: ["install_requires"], - source: nil - }] - end - - it "delegates to PipenvVersionResolver" do - dummy_resolver = - instance_double(described_class::PipenvVersionResolver) - allow(described_class::PipenvVersionResolver).to receive(:new) - .and_return(dummy_resolver) - expect(dummy_resolver) - .to receive(:latest_resolvable_version) - .with(requirement: nil) - .and_return(Gem::Version.new("2.18.0")) - expect(checker.latest_resolvable_version_with_no_unlock) - .to eq(Gem::Version.new("2.18.0")) - end - end - end end describe "#updated_requirements" do @@ -595,7 +409,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -613,7 +427,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -624,54 +438,6 @@ let(:dependency_files) { [pyproject] } let(:pyproject_fixture_name) { "tilde_version.toml" } - context "when updating a dependency inside" do - let(:dependency) do - Dependabot::Dependency.new( - name: "requests", - version: "1.2.3", - requirements: [{ - file: "pyproject.toml", - requirement: "~1.0.0", - groups: [], - source: nil - }], - package_manager: "pip" - ) - end - - let(:pypi_url) { "https://pypi.org/simple/requests/" } - let(:pypi_response) do - fixture("pypi", "pypi_simple_response_requests.html") - end - - context "when dealing with a library" do - before do - stub_request(:get, "https://pypi.org/pypi/pendulum/json/") - .to_return( - status: 200, - body: fixture("pypi", "pypi_response_pendulum.json") - ) - end - - its([:requirement]) { is_expected.to eq(">=1.0,<2.20") } - end - - context "when dealing with a non-library" do - before do - stub_request(:get, "https://pypi.org/pypi/pendulum/json/") - .to_return(status: 404) - end - - its([:requirement]) { is_expected.to eq("~2.19.1") } - end - - context "when dealing with a poetry in non-package mode" do - let(:pyproject_fixture_name) { "poetry_non_package_mode.toml" } - - its([:requirement]) { is_expected.to eq("~2.19.1") } - end - end - context "when updating a dependency in an additional requirements file" do let(:dependency_files) { super().append(requirements_file) } @@ -698,7 +464,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -755,7 +521,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end @@ -813,7 +579,7 @@ groups: [], source: nil }], - package_manager: "pip" + package_manager: "uv" ) end diff --git a/uv/spec/dependabot/python_spec.rb b/uv/spec/dependabot/uv_spec.rb similarity index 75% rename from uv/spec/dependabot/python_spec.rb rename to uv/spec/dependabot/uv_spec.rb index 85e28d2b27..35681efc2a 100644 --- a/uv/spec/dependabot/python_spec.rb +++ b/uv/spec/dependabot/uv_spec.rb @@ -6,5 +6,5 @@ require_common_spec "shared_examples_for_autoloading" RSpec.describe Dependabot::Uv do - it_behaves_like "it registers the required classes", "pip" + it_behaves_like "it registers the required classes", "uv" end