Skip to content

Commit

Permalink
fix(livecheck/pypi): update to use json endpoint to query version
Browse files Browse the repository at this point in the history
Signed-off-by: Rui Chen <[email protected]>
  • Loading branch information
chenrui333 committed Dec 7, 2024
1 parent 405ceda commit 67b555f
Showing 1 changed file with 30 additions and 26 deletions.
56 changes: 30 additions & 26 deletions Library/Homebrew/livecheck/strategy/pypi.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
# typed: strict
# frozen_string_literal: true

require "open-uri"
require "json"

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/<hex>/<hex>/<long_hex>/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
Expand Down Expand Up @@ -44,10 +45,8 @@ def self.match?(url)
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]
Expand All @@ -58,27 +57,17 @@ def self.generate_input_values(url)
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/pypi/#{package_name}/json"
values[:package_name] = package_name

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(
Expand All @@ -89,9 +78,24 @@ def self.generate_input_values(url)
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, **unused, &block)
# Extract package name and JSON API URL
generated = generate_input_values(url)
return {} if generated[:url].nil?

# Parse JSON and get the latest version
begin
response = URI.open(generated[:url]).read

Check failure

Code scanning / CodeQL

Use of `Kernel.open` or `IO.read` or similar sinks with a non-constant value Critical

Call to URI.open with a non-constant value. Consider replacing it with URI().open.
data = JSON.parse(response, 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?

PageMatch.find_versions(url: generated[:url], regex: regex || generated[:regex], **unused, &block)
{ matches: { latest_version => Version.new(latest_version) } }
end
end
end
Expand Down

0 comments on commit 67b555f

Please sign in to comment.