Skip to content

Commit

Permalink
Pypi: Rework to use Json::find_versions
Browse files Browse the repository at this point in the history
This reworks the new `Pypi` JSON API implementation to use
`Json::find_versions` in `Pypi::find_versions`, borrowing some of the
approach from the `Crate` strategy.

Besides that, this pares down the fields in the
`::generate_input_values` return hash to only `:url`, as we're not
using a generated regex to match version information in this setup.

This adds a `provided_content` parameter to `::find_versions` as part
of this process and I will expand the `Pypi` tests to increase
coverage (like the `Crates` tests) in a later PR. 75% of `Pypi` checks
are failing at the moment (with some returning inaccurate version
information), so the current priority is getting this fix merged in
the short-term.
  • Loading branch information
samford committed Dec 8, 2024
1 parent d49e01b commit e71b86e
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 42 deletions.
75 changes: 35 additions & 40 deletions Library/Homebrew/livecheck/strategy/pypi.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
# 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
# using the JSON API endpoint.
# The {Pypi} strategy identifies the newest version of a PyPI package by
# checking the JSON API endpoint for the project and using the
# `info.version` field from the response.
#
# PyPI URLs have a standard format:
# `https://files.pythonhosted.org/packages/<hex>/<hex>/<long_hex>/example-1.2.3.tar.gz`
#
# * `https://files.pythonhosted.org/packages/<hex>/<hex>/<long_hex>/example-1.2.3.tar.gz`
#
# This method uses the `info.version` field in the JSON response to
# determine the latest stable version.
# Upstream documentation for the PyPI JSON API can be found at:
# https://docs.pypi.org/api/json/#get-a-project
#
# @api public
class Pypi
NICE_NAME = "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")

Check warning on line 24 in Library/Homebrew/livecheck/strategy/pypi.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/livecheck/strategy/pypi.rb#L24

Added line #L24 was not covered by tests
end.freeze, T.proc.params(
arg0: T::Hash[String, T.untyped],
).returns(T.nilable(String)))

# The `Regexp` used to extract the package name and suffix (e.g. file
# extension) from the URL basename.
FILENAME_REGEX = /
Expand All @@ -46,8 +51,8 @@ def self.match?(url)
URL_MATCH_REGEX.match?(url)
end

# Extracts the package name from the provided URL and generates the
# PyPI JSON API endpoint.
# Extracts the package name from the provided URL and uses it to
# generate the PyPI JSON API URL for the project.
#
# @param url [String] the URL used to generate values
# @return [Hash]
Expand All @@ -58,48 +63,38 @@ def self.generate_input_values(url)
match = File.basename(url).match(FILENAME_REGEX)
return values if match.blank?

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[:url] = "https://pypi.org/pypi/#{T.must(match[:package_name]).gsub(/%20|_/, "-")}/json"

values
end

# Fetches the latest version of the package from the PyPI JSON API.
# Generates a PyPI JSON API URL for the project and identifies new
# versions using {Json#find_versions} with a block.
#
# @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content (optional)
# @param regex [Regexp] a regex used for matching versions in content
# @param provided_content [String, nil] content to check instead of
# fetching
# @return [Hash]
sig {
params(
url: String,
regex: T.nilable(Regexp),
_unused: T.untyped,
_block: T.nilable(Proc),
url: String,
regex: T.nilable(Regexp),
provided_content: T.nilable(String),
unused: T.untyped,
block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url:, regex: nil, **_unused, &_block)
match_data = { matches: {}, regex:, url: }

def self.find_versions(url:, regex: nil, provided_content: nil, **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) } }
Json.find_versions(

Check warning on line 91 in Library/Homebrew/livecheck/strategy/pypi.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/livecheck/strategy/pypi.rb#L91

Added line #L91 was not covered by tests
url: generated[:url],
regex:,
provided_content:,
**unused,
&block || DEFAULT_BLOCK
)
end
end
end
Expand Down
3 changes: 1 addition & 2 deletions Library/Homebrew/test/livecheck/strategy/pypi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@

let(:generated) do
{
url: "https://pypi.org/project/example-package/#files",
regex: %r{href=.*?/packages.*?/example-package[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i,
url: "https://pypi.org/pypi/example-package/json",
}
end

Expand Down

0 comments on commit e71b86e

Please sign in to comment.