diff --git a/python/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb b/python/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb index 15f38ffbc5e..05d3975a4e5 100644 --- a/python/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb +++ b/python/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb @@ -32,6 +32,8 @@ class PipCompileVersionResolver "pip._internal.exceptions.InstallationSubprocessError: Command errored out with exit status 1:" # See https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata PYTHON_PACKAGE_NAME_REGEX = /[A-Za-z0-9_\-]+/.freeze + RESOLUTION_IMPOSSIBLE_ERROR = "ResolutionImpossible" + ERROR_REGEX = /(?<=ERROR\:\W).*$/.freeze attr_reader :dependency, :dependency_files, :credentials @@ -76,16 +78,13 @@ def fetch_latest_resolvable_version_string(requirement:) # Shell out to pip-compile. # This is slow, as pip-compile needs to do installs. run_pip_compile_command( - "pyenv exec pip-compile --allow-unsafe -v "\ - "#{pip_compile_options(filename)} -P #{dependency.name} "\ - "#{filename}" + "pyenv exec pip-compile -v #{pip_compile_options(filename)} -P #{dependency.name} #{filename}" ) # Run pip-compile a second time, without an update argument, # to ensure it handles markers correctly write_original_manifest_files unless dependency.top_level? run_pip_compile_command( - "pyenv exec pip-compile --allow-unsafe "\ - "#{pip_compile_options(filename)} #{filename}" + "pyenv exec pip-compile #{pip_compile_options(filename)} #{filename}" ) end @@ -114,7 +113,7 @@ def compilation_error?(error) # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/PerceivedComplexity def handle_pip_compile_errors(error) - if error.message.include?("Could not find a version") + if error.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 @@ -138,7 +137,7 @@ def handle_pip_compile_errors(error) return end - if error.message.include?("Could not find a version ") && + if error.message.include?(RESOLUTION_IMPOSSIBLE_ERROR) && !error.message.match?(/#{Regexp.quote(dependency.name)}/i) # Sometimes pip-tools gets confused and can't work around # sub-dependency incompatibilities. Ignore those cases. @@ -179,7 +178,7 @@ def check_original_requirements_resolvable filenames_to_compile.each do |filename| run_pip_compile_command( - "pyenv exec pip-compile #{pip_compile_options(filename)} --allow-unsafe #{filename}" + "pyenv exec pip-compile #{pip_compile_options(filename)} #{filename}" ) end @@ -188,7 +187,7 @@ def check_original_requirements_resolvable # Pick the error message that includes resolvability errors, this might be the cause from # handle_pip_compile_errors (it's unclear if we should always pick the cause here) error_message = [e.message, e.cause&.message].compact.find do |msg| - ["UnsupportedConstraint", "Could not find a version"].any? { |err| msg.include?(err) } + msg.include?(RESOLUTION_IMPOSSIBLE_ERROR) end cleaned_message = clean_error_message(error_message || "") @@ -220,6 +219,7 @@ def run_command(command, env: python_env) def pip_compile_options(filename) options = @build_isolation ? ["--build-isolation"] : ["--no-build-isolation"] options += pip_compile_index_options + options += ["--resolver backtracking", "--allow-unsafe"] if (requirements_file = compiled_file_for_filename(filename)) options << "--output-file=#{requirements_file.name}" @@ -353,25 +353,8 @@ def normalise(name) NameNormaliser.normalise(name) end - VERBOSE_ERROR_OUTPUT_LINES = [ - "Traceback", - "Using indexes:", - "Current constraints:", - "Finding the best candidates:", - "Finding secondary dependencies:", - "\n", - " " - ].freeze - def clean_error_message(message) - msg_lines = message.lines - msg = msg_lines. - take_while { |l| !l.start_with?("During handling of") }. - drop_while { |l| l.start_with?(*VERBOSE_ERROR_OUTPUT_LINES) }. - join.strip - - # Redact any URLs, as they may include credentials - msg.gsub(/http.*?(?=\s)/, "") + message.scan(ERROR_REGEX).last end def filenames_to_compile diff --git a/python/spec/dependabot/python/update_checker/pip_compile_version_resolver_spec.rb b/python/spec/dependabot/python/update_checker/pip_compile_version_resolver_spec.rb index 0f87758aec0..f412c89a600 100644 --- a/python/spec/dependabot/python/update_checker/pip_compile_version_resolver_spec.rb +++ b/python/spec/dependabot/python/update_checker/pip_compile_version_resolver_spec.rb @@ -140,41 +140,6 @@ end end - context "when unlocking causes a conflict (in the sub-dependencies)" do - let(:manifest_fixture_name) { "unresolvable_if_unpinned.in" } - let(:generated_fixture_name) do - "pip_compile_unresolvable_if_unpinned.txt" - end - let(:dependency_name) { "boto3" } - let(:dependency_version) { "1.7.84" } - let(:updated_requirement) { ">= 1.7.84, <= 1.9.28" } - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: ">=1.7,<1.8", - groups: [], - source: nil - }] - end - it { is_expected.to be nil } - - context "and updating would cause a conflict" do - let(:dependency_name) { "moto" } - let(:dependency_version) { "1.3.6" } - let(:updated_requirement) { ">= 1.3.6, <= 1.3.7" } - - let(:dependency_requirements) do - [{ - file: "requirements/test.in", - requirement: nil, - groups: [], - source: nil - }] - end - it { is_expected.to be nil } - end - end - context "with multiple requirement.in files" do let(:dependency_files) do [manifest_file, manifest_file2, generated_file, generated_file2] @@ -219,37 +184,14 @@ expect { subject }. to raise_error(Dependabot::DependencyFileNotResolvable) do |error| expect(error.message). - to include("Could not find a version that matches boto3") + 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 requirement" do - let(:manifest_fixture_name) { "unresolvable.in" } - let(:dependency_files) { [manifest_file] } - let(:dependency_name) { "boto3" } - let(:dependency_version) { nil } - let(:updated_requirement) { ">= 0, <= 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 { subject }. - to raise_error(Dependabot::DependencyFileNotResolvable) do |error| - expect(error.message). - to include("Could not find a version that matches boto3") - end - end - end - context "with an unresolvable project" do let(:dependency_files) { project_dependency_files("unresolvable") } let(:dependency) do @@ -273,7 +215,7 @@ expect { subject }. to raise_error(Dependabot::DependencyFileNotResolvable) do |error| expect(error.message). - to include("Could not find a version that matches jupyter-server") + to include("Could not find a version that satisfies the requirement jupyter-server<=18.1.0,>=17.3.0") end end end @@ -438,7 +380,8 @@ expect { subject }. to raise_error(Dependabot::DependencyFileNotResolvable) do |error| expect(error.message). - to include("Could not find a version that matches boto3") + 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 diff --git a/python/spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in b/python/spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in deleted file mode 100644 index 19860ccbe10..00000000000 --- a/python/spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in +++ /dev/null @@ -1,3 +0,0 @@ -boto3>=1.7,<1.8 -botocore<1.11.0 -moto diff --git a/python/spec/fixtures/pip_compile_files/unresolvable_if_unpinned.txt b/python/spec/fixtures/pip_compile_files/unresolvable_if_unpinned.txt deleted file mode 100644 index d5192c6e00f..00000000000 --- a/python/spec/fixtures/pip_compile_files/unresolvable_if_unpinned.txt +++ /dev/null @@ -1,101 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in -# -aws-xray-sdk==0.95 - # via moto -boto3==1.7.84 - # via - # -r spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in - # moto -boto==2.49.0 - # via moto -botocore==1.10.84 - # via - # -r spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in - # boto3 - # moto - # s3transfer -certifi==2021.5.30 - # via requests -cffi==1.14.5 - # via cryptography -chardet==4.0.0 - # via requests -cookies==2.2.1 - # via moto -cryptography==3.4.7 - # via moto -docker==5.0.0 - # via moto -docutils==0.17.1 - # via botocore -ecdsa==0.17.0 - # via python-jose -future==0.18.2 - # via python-jose -idna==2.10 - # via requests -jinja2==3.0.1 - # via moto -jmespath==0.10.0 - # via - # boto3 - # botocore -jsondiff==1.1.1 - # via moto -jsonpickle==2.0.0 - # via aws-xray-sdk -markupsafe==2.0.1 - # via jinja2 -mock==4.0.3 - # via moto -moto==1.3.6 - # via -r spec/fixtures/pip_compile_files/unresolvable_if_unpinned.in -pyaml==20.4.0 - # via moto -pycparser==2.20 - # via cffi -pycryptodome==3.10.1 - # via python-jose -python-dateutil==2.8.1 - # via - # botocore - # moto -python-jose==2.0.2 - # via moto -pytz==2021.1 - # via moto -pyyaml==5.4.1 - # via pyaml -requests==2.25.1 - # via - # aws-xray-sdk - # docker - # moto - # responses -responses==0.13.3 - # via moto -s3transfer==0.1.13 - # via boto3 -six==1.16.0 - # via - # ecdsa - # moto - # python-dateutil - # python-jose - # responses -urllib3==1.26.5 - # via - # requests - # responses -websocket-client==1.0.1 - # via docker -werkzeug==2.0.1 - # via moto -wrapt==1.12.1 - # via aws-xray-sdk -xmltodict==0.12.0 - # via moto