From 08c927b6a17fa36368e389d552fa8bb3f9240611 Mon Sep 17 00:00:00 2001 From: Sam Ford <1584702+samford@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:22:57 -0500 Subject: [PATCH 1/2] Json: Allow nil regex block argument This updates the block-handling logic in `Json::versions_from_content` to naively pass the regex value when the block has two parameters. Up to now, we have been ensuring that `regex` is not `nil` and this makes sense with existing usage (the `Crate` strategy's default block, formulae/cask `strategy` blocks). However, we need to allow a `nil` `regex` value to make it possible to add an optional `regex` parameter in the `Pypi::DEFAULT_BLOCK` Proc. This is necessary to allow the `Pypi` strategy to work with an optional regex from a `livecheck` block again [without creating an additional `DEFAULT_BLOCK` variant with a regex parameter]. --- Library/Homebrew/livecheck/strategy/json.rb | 10 +++++----- Library/Homebrew/test/livecheck/strategy/json_spec.rb | 5 ----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Library/Homebrew/livecheck/strategy/json.rb b/Library/Homebrew/livecheck/strategy/json.rb index a4631c58e2327..b8f05e29ae725 100644 --- a/Library/Homebrew/livecheck/strategy/json.rb +++ b/Library/Homebrew/livecheck/strategy/json.rb @@ -58,8 +58,10 @@ def self.parse_json(content) end # Parses JSON text and identifies versions using a `strategy` block. - # If a regex is provided, it will be passed as the second argument to - # the `strategy` block (after the parsed JSON data). + # If the block has two parameters, the parsed JSON data will be used as + # the first argument and the regex (if any) will be the second. + # Otherwise, only the parsed JSON data will be passed to the block. + # # @param content [String] the JSON text to parse and check # @param regex [Regexp, nil] a regex used for matching versions in the # content @@ -77,10 +79,8 @@ def self.versions_from_content(content, regex = nil, &block) json = parse_json(content) return [] if json.blank? - block_return_value = if regex.present? + block_return_value = if block.arity == 2 yield(json, regex) - elsif block.arity == 2 - raise "Two arguments found in `strategy` block but no regex provided." else yield(json) end diff --git a/Library/Homebrew/test/livecheck/strategy/json_spec.rb b/Library/Homebrew/test/livecheck/strategy/json_spec.rb index dc18a8f074512..5949d83e19bf0 100644 --- a/Library/Homebrew/test/livecheck/strategy/json_spec.rb +++ b/Library/Homebrew/test/livecheck/strategy/json_spec.rb @@ -107,11 +107,6 @@ expect(json.versions_from_content(content_simple, regex) { next }).to eq([]) end - it "errors if a block uses two arguments but a regex is not given" do - expect { json.versions_from_content(content_simple) { |json, regex| json["version"][regex, 1] } } - .to raise_error("Two arguments found in `strategy` block but no regex provided.") - end - it "errors on an invalid return type from a block" do expect { json.versions_from_content(content_simple, regex) { 123 } } .to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG) From 270313f6490bdeee73b7db748580d11f52ca1247 Mon Sep 17 00:00:00 2001 From: Sam Ford <1584702+samford@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:47:50 -0500 Subject: [PATCH 2/2] Pypi: Restore regex support We recently updated the `Pypi` strategy to use the PyPI JSON API and the default strategy behavior no longer relies on a regex, so the initial implementation didn't include regex handling. This restores support for a `livecheck` block regex by updating the `DEFAULT_BLOCK` logic to handle an optional regex. This allows us to use a regex to omit parts of the `info.version` value without having to duplicate the default block logic in a `strategy` block only to use a regex. This isn't currently necessary for any existing formulae using the `Pypi` strategy but we have a few that needed a custom regex with the previous strategy approach, so they may need this functionality in the future. Besides that, restoring regex support to `Pypi` ensures that `livecheck`/`strategy` blocks work in a fairly consistent manner across strategies. --- Library/Homebrew/livecheck/strategy/pypi.rb | 10 ++++--- .../test/livecheck/strategy/pypi_spec.rb | 26 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Library/Homebrew/livecheck/strategy/pypi.rb b/Library/Homebrew/livecheck/strategy/pypi.rb index 66351f884c221..8711a2264a539 100644 --- a/Library/Homebrew/livecheck/strategy/pypi.rb +++ b/Library/Homebrew/livecheck/strategy/pypi.rb @@ -20,10 +20,14 @@ class Pypi # The default `strategy` block used to extract version information when # a `strategy` block isn't provided. - DEFAULT_BLOCK = T.let(proc do |json| - json.dig("info", "version").presence + DEFAULT_BLOCK = T.let(proc do |json, regex| + version = json.dig("info", "version") + next if version.blank? + + regex ? version[regex, 1] : version end.freeze, T.proc.params( - arg0: T::Hash[String, T.untyped], + json: T::Hash[String, T.untyped], + regex: T.nilable(Regexp), ).returns(T.nilable(String))) # The `Regexp` used to extract the package name and suffix (e.g. file diff --git a/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb b/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb index be93644477108..d1f44376bb549 100644 --- a/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb +++ b/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb @@ -8,7 +8,7 @@ let(:pypi_url) { "https://files.pythonhosted.org/packages/ab/cd/efg/example-package-1.2.3.tar.gz" } let(:non_pypi_url) { "https://brew.sh/test" } - let(:regex) { /^v?(\d+(?:\.\d+)+)$/i } + let(:regex) { /^v?(\d+(?:\.\d+)+)/i } let(:generated) do { @@ -17,25 +17,26 @@ end # This is a limited subset of a PyPI JSON API response object, for the sake - # of testing. + # of testing. Typical versions use a `1.2.3` format but this adds a suffix, + # so we can test regex matching. let(:content) do <<~JSON { "info": { - "version": "1.2.3" + "version": "1.2.3-456" } } JSON end - let(:matches) { ["1.2.3"] } + let(:matches) { ["1.2.3-456"] } let(:find_versions_return_hash) do { matches: { - "1.2.3" => Version.new("1.2.3"), + "1.2.3-456" => Version.new("1.2.3-456"), }, - regex: nil, + regex:, url: generated[:url], } end @@ -76,10 +77,17 @@ { cached:, cached_default: cached.merge({ matches: {} }), + cached_regex: cached.merge({ + matches: { "1.2.3" => Version.new("1.2.3") }, + regex:, + }), } end it "finds versions in provided content" do + expect(pypi.find_versions(url: pypi_url, regex:, provided_content: content)) + .to eq(match_data[:cached_regex]) + expect(pypi.find_versions(url: pypi_url, provided_content: content)) .to eq(match_data[:cached]) end @@ -92,7 +100,7 @@ next if match.blank? match[1] - end).to eq(match_data[:cached].merge({ regex: })) + end).to eq(match_data[:cached_regex]) expect(pypi.find_versions(url: pypi_url, provided_content: content) do |json| json.dig("info", "version").presence @@ -100,10 +108,14 @@ end it "returns default match_data when block doesn't return version information" do + no_match_regex = /will_not_match/i + expect(pypi.find_versions(url: pypi_url, provided_content: '{"info":{"version":""}}')) .to eq(match_data[:cached_default]) expect(pypi.find_versions(url: pypi_url, provided_content: '{"other":true}')) .to eq(match_data[:cached_default]) + expect(pypi.find_versions(url: pypi_url, regex: no_match_regex, provided_content: content)) + .to eq(match_data[:cached_default].merge({ regex: no_match_regex })) end it "returns default match_data when url is blank" do