From 4e096e8eca4a81711f8afe32e47073fb54c306a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net> Date: Tue, 18 Oct 2022 22:52:45 +0200 Subject: [PATCH 1/3] Refactor overly complicated update checker logic --- python/lib/dependabot/python/update_checker.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/python/lib/dependabot/python/update_checker.rb b/python/lib/dependabot/python/update_checker.rb index 55a0747ba05..2bd80d504f7 100644 --- a/python/lib/dependabot/python/update_checker.rb +++ b/python/lib/dependabot/python/update_checker.rb @@ -205,13 +205,11 @@ def current_requirement_string reqs = dependency.requirements return if reqs.none? - requirement = - case resolver_type - when :pipenv then reqs.find { |r| r[:file] == "Pipfile" } - when :poetry then reqs.find { |r| r[:file] == "pyproject.toml" } - when :pip_compile then reqs.find { |r| r[:file].end_with?(".in") } - when :requirements then reqs.find { |r| r[:file].end_with?(".txt") } - end + requirement = reqs.find do |r| + file = r[:file] + + file == "Pipfile" || file == "pyproject.toml" || file.end_with?(".in") || file.end_with?(".txt") + end requirement&.fetch(:requirement) end From ebfc87990bc29a0d87d5ccee76f4f41ec84e6853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net> Date: Tue, 18 Oct 2022 22:48:00 +0200 Subject: [PATCH 2/3] Extract some helpers for readability and later reuse --- .../lib/dependabot/python/update_checker.rb | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/python/lib/dependabot/python/update_checker.rb b/python/lib/dependabot/python/update_checker.rb index 2bd80d504f7..06b23a2eb90 100644 --- a/python/lib/dependabot/python/update_checker.rb +++ b/python/lib/dependabot/python/update_checker.rb @@ -89,7 +89,7 @@ def lowest_resolvable_security_fix_version def updated_requirements RequirementsUpdater.new( - requirements: dependency.requirements, + requirements: requirements, latest_resolvable_version: preferred_resolvable_version&.to_s, update_strategy: requirements_update_strategy, has_lockfile: !(pipfile_lock || poetry_lock || pyproject_lock).nil? @@ -133,8 +133,7 @@ def fetch_lowest_resolvable_security_fix_version end def resolver_type - reqs = dependency.requirements - req_files = reqs.map { |r| r.fetch(:file) } + reqs = requirements # If there are no requirements then this is a sub-dependency. It # must come from one of Pipenv, Poetry or pip-tools, and can't come @@ -143,9 +142,9 @@ 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 req_files.any?("Pipfile") - return :poetry if req_files.any?("pyproject.toml") - return :pip_compile if req_files.any? { |f| f.end_with?(".in") } + return :pipenv if updating_pipfile? + return :poetry if updating_pyproject? + return :pip_compile if updating_in_file? if dependency.version && !exact_requirement?(reqs) subdependency_resolver @@ -202,7 +201,7 @@ def resolver_args end def current_requirement_string - reqs = dependency.requirements + reqs = requirements return if reqs.none? requirement = reqs.find do |r| @@ -234,7 +233,7 @@ def updated_version_req_lower_bound return ">= #{dependency.version}" if dependency.version version_for_requirement = - dependency.requirements.filter_map { |r| r[:requirement] }. + requirements.filter_map { |r| r[:requirement] }. reject { |req_string| req_string.start_with?("<") }. select { |req_string| req_string.match?(VERSION_REGEX) }. map { |req_string| req_string.match(VERSION_REGEX) }. @@ -281,6 +280,26 @@ def poetry_library? false end + def updating_pipfile? + requirement_files.any?("Pipfile") + end + + def updating_pyproject? + requirement_files.any?("pyproject.toml") + end + + def updating_in_file? + requirement_files.any? { |f| f.end_with?(".in") } + end + + def requirement_files + requirements.map { |r| r.fetch(:file) } + end + + def requirements + dependency.requirements + end + def normalised_name(name) NameNormaliser.normalise(name) end From 7d91b710a4cb928f784cee066783eeb7467a19ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net> Date: Thu, 20 Oct 2022 02:15:31 +0200 Subject: [PATCH 3/3] Fix crash when updating libraries with multiple manifests If a library has both a pyproject.toml file and a standard requirements.txt file, we'd end up using the `:widen` strategy for the dependencies in the `requirements.txt` file and eventually crashing with an error like the following: ``` /home/dependabot/dependabot-core/common/lib/dependabot/update_checkers/base.rb:266:in `block in preferred_version_resolvable_with_unlock?': undefined method `[]' for nil:NilClass (NoMethodError) updated_requirements.none? { |r| r[:requirement] == :unfixable } ^^^^^^^^^^^^^^ from /home/dependabot/dependabot-core/common/lib/dependabot/update_checkers/base.rb:266:in `none?' from /home/dependabot/dependabot-core/common/lib/dependabot/update_checkers/base.rb:266:in `preferred_version_resolvable_with_unlock?' from /home/dependabot/dependabot-core/common/lib/dependabot/update_checkers/base.rb:249:in `numeric_version_can_update?' from /home/dependabot/dependabot-core/common/lib/dependabot/update_checkers/base.rb:199:in `version_can_update?' from /home/dependabot/dependabot-core/common/lib/dependabot/update_checkers/base.rb:44:in `can_update?' from bin/dry-run.rb:709:in `block in <main>' from bin/dry-run.rb:661:in `each' from bin/dry-run.rb:661:in `<main>' ``` I think the crash happens because the requirements.txt file updater does not supoort the `:widen` strategy. So my fix is to fallback to `increase` in this case, since requirements.txt files usually include pinned dependencies so widening probably doesn't make much sense there. --- .../lib/dependabot/python/update_checker.rb | 2 +- .../dependabot/python/update_checker_spec.rb | 78 +++++++++++-------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/python/lib/dependabot/python/update_checker.rb b/python/lib/dependabot/python/update_checker.rb index 06b23a2eb90..91628470684 100644 --- a/python/lib/dependabot/python/update_checker.rb +++ b/python/lib/dependabot/python/update_checker.rb @@ -259,7 +259,7 @@ def latest_version_finder end def poetry_library? - return false unless pyproject + return false unless updating_pyproject? # Hit PyPi and check whether there are details for a library with a # matching name and description diff --git a/python/spec/dependabot/python/update_checker_spec.rb b/python/spec/dependabot/python/update_checker_spec.rb index 447a44a1f8e..50590b1c039 100644 --- a/python/spec/dependabot/python/update_checker_spec.rb +++ b/python/spec/dependabot/python/update_checker_spec.rb @@ -57,7 +57,7 @@ ) end let(:requirements_fixture_name) { "version_specified.txt" } - let(:dependency) do + let(:requirements_dependency) do Dependabot::Dependency.new( name: dependency_name, version: dependency_version, @@ -76,6 +76,8 @@ }] end + let(:dependency) { requirements_dependency } + describe "#can_update?" do subject { checker.can_update?(requirements_to_unlock: :own) } @@ -507,44 +509,56 @@ let(:dependency_files) { [requirements_file, pyproject] } let(:pyproject_fixture_name) { "caret_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 - }], - package_manager: "pip" - ) - end + context "and 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 + let(:pypi_url) { "https://pypi.org/simple/requests/" } + let(:pypi_response) do + fixture("pypi", "pypi_simple_response_requests.html") + end - context "for 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") - ) + context "for 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,<3") } end - its([:requirement]) { is_expected.to eq(">=1,<3") } - end + context "for a non-library" do + before do + stub_request(:get, "https://pypi.org/pypi/pendulum/json/"). + to_return(status: 404) + end - context "for a non-library" do - before do - stub_request(:get, "https://pypi.org/pypi/pendulum/json/"). - to_return(status: 404) + its([:requirement]) { is_expected.to eq("^2.19.1") } end + end + + context "and updating a dependency in an additional requirements file" do + let(:dependency_files) { super().append(requirements_file) } - its([:requirement]) { is_expected.to eq("^2.19.1") } + let(:dependency) { requirements_dependency } + + it "does not get affected by whether it's a library or not and updates using the :increase strategy" do + expect(subject[:requirement]).to eq("==2.6.0") + end end end