diff --git a/Library/Homebrew/livecheck/strategy/pypi.rb b/Library/Homebrew/livecheck/strategy/pypi.rb index 86f27df8a8..c598b33168 100644 --- a/Library/Homebrew/livecheck/strategy/pypi.rb +++ b/Library/Homebrew/livecheck/strategy/pypi.rb @@ -1,19 +1,21 @@ # typed: strict # frozen_string_literal: true +require "json" +require "utils/curl" + module Homebrew module Livecheck module Strategy # The {Pypi} strategy identifies versions of software at pypi.org by - # checking project pages for archive files. + # using the JSON API endpoint. # - # PyPI URLs have a standard format but the hexadecimal text between - # `/packages/` and the filename varies: + # PyPI URLs have a standard format: # # * `https://files.pythonhosted.org/packages////example-1.2.3.tar.gz` # - # As such, the default regex only targets the filename at the end of the - # URL. + # This method uses the `info.version` field in the JSON response to + # determine the latest stable version. # # @api public class Pypi @@ -44,10 +46,8 @@ module Homebrew URL_MATCH_REGEX.match?(url) end - # Extracts information from a provided URL and uses it to generate - # various input values used by the strategy to check for new versions. - # Some of these values act as defaults and can be overridden in a - # `livecheck` block. + # Extracts the package name from the provided URL and generates the + # PyPI JSON API endpoint. # # @param url [String] the URL used to generate values # @return [Hash] @@ -58,40 +58,48 @@ module Homebrew match = File.basename(url).match(FILENAME_REGEX) return values if match.blank? - # It's not technically necessary to have the `#files` fragment at the - # end of the URL but it makes the debug output a bit more useful. - values[:url] = "https://pypi.org/project/#{T.must(match[:package_name]).gsub(/%20|_/, "-")}/#files" - - # Use `\.t` instead of specific tarball extensions (e.g. .tar.gz) - suffix = T.must(match[:suffix]).sub(Strategy::TARBALL_EXTENSION_REGEX, ".t") - regex_suffix = Regexp.escape(suffix).gsub("\\-", "-") - - # Example regex: `%r{href=.*?/packages.*?/example[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i` - regex_name = Regexp.escape(T.must(match[:package_name])).gsub(/\\[_-]/, "[_-]") - values[:regex] = - %r{href=.*?/packages.*?/#{regex_name}[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)#{regex_suffix}}i + package_name = T.must(match[:package_name]).gsub(/[_-]/, "-") + values[:url] = "https://pypi.org/project/#{package_name}/#files" + values[:regex] = %r{href=.*?/packages.*?/#{package_name}[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i values end - # Generates a URL and regex (if one isn't provided) and passes them - # to {PageMatch.find_versions} to identify versions in the content. + # Fetches the latest version of the package from the PyPI JSON API. # # @param url [String] the URL of the content to check - # @param regex [Regexp] a regex used for matching versions in content + # @param regex [Regexp] a regex used for matching versions in content (optional) # @return [Hash] sig { params( - url: String, - regex: T.nilable(Regexp), - unused: T.untyped, - block: T.nilable(Proc), + url: String, + regex: T.nilable(Regexp), + _unused: T.untyped, + _block: T.nilable(Proc), ).returns(T::Hash[Symbol, T.untyped]) } - def self.find_versions(url:, regex: nil, **unused, &block) - generated = generate_input_values(url) + def self.find_versions(url:, regex: nil, **_unused, &_block) + match_data = { matches: {}, regex:, url: } - PageMatch.find_versions(url: generated[:url], regex: regex || generated[:regex], **unused, &block) + generated = generate_input_values(url) + return match_data if generated.blank? + + match_data[:url] = generated[:url] + + # Parse JSON and get the latest version + begin + response = Utils::Curl.curl_output(generated[:url]) + data = JSON.parse(response.stdout, symbolize_names: true) + latest_version = data.dig(:info, :version) + rescue => e + puts "Error fetching version from PyPI: #{e.message}" + return {} + end + + # Return the version if found + return {} if latest_version.blank? + + { matches: { latest_version => Version.new(latest_version) } } end end end diff --git a/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb b/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb index 2cdd945c11..f42a3d174c 100644 --- a/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb +++ b/Library/Homebrew/test/livecheck/strategy/pypi_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Homebrew::Livecheck::Strategy::Pypi do let(:generated) do { url: "https://pypi.org/project/example-package/#files", - regex: %r{href=.*?/packages.*?/example[_-]package[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i, + regex: %r{href=.*?/packages.*?/example-package[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i, } end