From f923b337b72c29574eb0f8178a9aafef642b84e5 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sat, 3 Jun 2023 23:09:49 -0400 Subject: [PATCH 01/12] utils/pypi: support for non-pythonhosted urls Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index 225e30c440..567314da52 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -19,13 +19,34 @@ module PyPI @pypi_info = nil if is_url - match = if package_string.start_with?(PYTHONHOSTED_URL_PREFIX) - File.basename(package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/) - end - raise ArgumentError, "Package should be a valid PyPI URL" if match.blank? + if package_string.start_with?(PYTHONHOSTED_URL_PREFIX) + match = File.basename(package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/) + + raise ArgumentError, "Package should be a valid PyPI URL" if match.blank? + + @name = PyPI.normalize_python_package(match[1]) + @version = match[2] + else + # The URL might be a source distribution hosted somewhere; + # try and use `pip install -q --no-deps --dry-run --report ...` to get its + # name and version. + command = + [Formula["python"].bin/"python3", "-m", "pip", "install", "-q", "--no-deps", "--dry-run", "--ignore-installed", "--report", + "/dev/stdout", package_string] + pip_output = Utils.popen_read({ "PIP_REQUIRE_VIRTUALENV" => "false" }, *command) + unless $CHILD_STATUS.success? + raise ArgumentError, <<~EOS + Unable to determine dependencies for "#{package_string}" because of a failure when running + `#{command.join(" ")}`. + Please update the resources for "#{formula.name}" manually. + EOS + end + + metadata = JSON.parse(pip_output)["install"].first["metadata"] + @name = PyPI.normalize_python_package metadata["name"] + @version = metadata["version"] + end - @name = PyPI.normalize_python_package(match[1]) - @version = match[2] return end From 43ba30e8a9af4591152f7cfbf5070ad07006c7be Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sat, 3 Jun 2023 23:13:26 -0400 Subject: [PATCH 02/12] utils/pypi: style Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index 567314da52..b5d77bece0 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -31,8 +31,8 @@ module PyPI # try and use `pip install -q --no-deps --dry-run --report ...` to get its # name and version. command = - [Formula["python"].bin/"python3", "-m", "pip", "install", "-q", "--no-deps", "--dry-run", "--ignore-installed", "--report", - "/dev/stdout", package_string] + [Formula["python"].bin/"python3", "-m", "pip", "install", "-q", "--no-deps", + "--dry-run", "--ignore-installed", "--report", "/dev/stdout", package_string] pip_output = Utils.popen_read({ "PIP_REQUIRE_VIRTUALENV" => "false" }, *command) unless $CHILD_STATUS.success? raise ArgumentError, <<~EOS From d8b6bb2d4c81d895eb3b639173cf52a9d8634ba8 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sat, 3 Jun 2023 23:15:44 -0400 Subject: [PATCH 03/12] utils/pypi: more docs Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index b5d77bece0..8db565ade9 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -30,6 +30,9 @@ module PyPI # The URL might be a source distribution hosted somewhere; # try and use `pip install -q --no-deps --dry-run --report ...` to get its # name and version. + # Note that this is different from the (similar) `pip install --report` we + # do below, in that it uses `--no-deps` because we only care about resolving + # this specific URL's project metadata. command = [Formula["python"].bin/"python3", "-m", "pip", "install", "-q", "--no-deps", "--dry-run", "--ignore-installed", "--report", "/dev/stdout", package_string] From 009ebff85cf92e4b86a408c512dc4852df225f6a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sat, 3 Jun 2023 23:17:59 -0400 Subject: [PATCH 04/12] utils/pypi: trim exception Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index 8db565ade9..52f0526bab 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -41,7 +41,6 @@ module PyPI raise ArgumentError, <<~EOS Unable to determine dependencies for "#{package_string}" because of a failure when running `#{command.join(" ")}`. - Please update the resources for "#{formula.name}" manually. EOS end From f8d6dd4b94c2b04d132d02b2dbedaea3b6179f83 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sat, 3 Jun 2023 23:47:41 -0400 Subject: [PATCH 05/12] utils/pypi: ensure Python Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index 52f0526bab..c2643132b0 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -27,6 +27,8 @@ module PyPI @name = PyPI.normalize_python_package(match[1]) @version = match[2] else + ensure_formula_installed!("python") + # The URL might be a source distribution hosted somewhere; # try and use `pip install -q --no-deps --dry-run --report ...` to get its # name and version. @@ -64,7 +66,9 @@ module PyPI @extras = T.must(match[2]).split "," end - # Get name, URL, SHA-256 checksum, and latest version for a given PyPI package. + # Get name, URL, SHA-256 checksum, and latest version for a given package. + # This only works for packages from PyPI or from a PyPI URL; packages + # derived from non-PyPI URLs will produce `nil` here. sig { params(version: T.nilable(T.any(String, Version))).returns(T.nilable(T::Array[String])) } def pypi_info(version: nil) return @pypi_info if @pypi_info.present? && version.blank? From ec0361fd9bf08f65e0d407b8a1768d1e794a6fe9 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sun, 4 Jun 2023 00:01:41 -0400 Subject: [PATCH 06/12] test: add another URL test for `update_pypi_url` Signed-off-by: William Woodruff --- Library/Homebrew/test/utils/pypi_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/test/utils/pypi_spec.rb b/Library/Homebrew/test/utils/pypi_spec.rb index 8193c15b69..ad0657c8a7 100644 --- a/Library/Homebrew/test/utils/pypi_spec.rb +++ b/Library/Homebrew/test/utils/pypi_spec.rb @@ -176,8 +176,12 @@ describe PyPI do expect(described_class.update_pypi_url(old_package_url, "0.0.0")).to be_nil end - it "returns nil for non-pypi urls" do + it "returns nil for nonexistent urls" do expect(described_class.update_pypi_url("https://brew.sh/foo-1.0.tgz", "1.1")).to be_nil end + + it "returns nil for non-pypi urls" do + expect(described_class.update_pypi_url("https://github.com/pypa/pip-audit/releases/download/v2.5.6/v2.5.6.tar.gz", "1.1")).to be_nil + end end end From 685693a8fe2793f7c9aabd505e610bbc1f12d681 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sun, 4 Jun 2023 00:27:34 -0400 Subject: [PATCH 07/12] utils/pypi: enforce non-pypi urls Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index c2643132b0..dc722fbade 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -8,8 +8,10 @@ module PyPI PYTHONHOSTED_URL_PREFIX = "https://files.pythonhosted.org/packages/" private_constant :PYTHONHOSTED_URL_PREFIX - # PyPI Package - # + + # Represents a Python package. + # This package can be a PyPI package (either by name/version or PyPI distribution URL), + # or it can be a non-PyPI URL. # @api private class Package attr_accessor :name, :extras, :version @@ -49,6 +51,7 @@ module PyPI metadata = JSON.parse(pip_output)["install"].first["metadata"] @name = PyPI.normalize_python_package metadata["name"] @version = metadata["version"] + @from_pypi = false end return @@ -100,6 +103,7 @@ module PyPI sig { returns(T::Boolean) } def valid_pypi_package? + return false unless @from_pypi info = pypi_info info.present? && info.is_a?(Array) end From 0b3a5d0f6cb6f6091dda04aa5d34b4c7ff74ff06 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sun, 4 Jun 2023 00:36:23 -0400 Subject: [PATCH 08/12] utils/pypi: set default `from_pypi` Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index dc722fbade..aeb3e52940 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -19,6 +19,7 @@ module PyPI sig { params(package_string: String, is_url: T::Boolean).void } def initialize(package_string, is_url: false) @pypi_info = nil + @from_pypi = true if is_url if package_string.start_with?(PYTHONHOSTED_URL_PREFIX) From af6f728eb42f4715f8365f4598ba3e5ef92121aa Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sun, 4 Jun 2023 23:31:40 -0400 Subject: [PATCH 09/12] utils, test: rewrite PyPI::Package This rewrites the `Package` class from the ground up to better accomodate non-PyPI URLs. The existing APIs are largely preserved, but with clearer invariants around when they can or can't be used (e.g., `#pypi_info`). Signed-off-by: William Woodruff --- Library/Homebrew/test/utils/pypi_spec.rb | 86 +++++++--- Library/Homebrew/utils/pypi.rb | 192 ++++++++++++----------- 2 files changed, 166 insertions(+), 112 deletions(-) diff --git a/Library/Homebrew/test/utils/pypi_spec.rb b/Library/Homebrew/test/utils/pypi_spec.rb index ad0657c8a7..33cf00348b 100644 --- a/Library/Homebrew/test/utils/pypi_spec.rb +++ b/Library/Homebrew/test/utils/pypi_spec.rb @@ -3,14 +3,17 @@ require "utils/pypi" describe PyPI do - let(:package_url) do + let(:pypi_package_url) do "https://files.pythonhosted.org/packages/b0/3f/2e1dad67eb172b6443b5eb37eb885a054a55cfd733393071499514140282/" \ "snakemake-5.29.0.tar.gz" end - let(:old_package_url) do + let(:old_pypi_package_url) do "https://files.pythonhosted.org/packages/6f/c4/da52bfdd6168ea46a0fe2b7c983b6c34c377a8733ec177cc00b197a96a9f/" \ "snakemake-5.28.0.tar.gz" end + let(:non_pypi_package_url) do + "https://github.com/pypa/pip-audit/releases/download/v2.5.6/v2.5.6.tar.gz" + end describe PyPI::Package do let(:package_checksum) { "47417307d08ecb0707b3b29effc933bd63d8c8e3ab15509c62b685b7614c6568" } @@ -22,7 +25,8 @@ describe PyPI do let(:package_with_extra) { described_class.new("snakemake[foo]") } let(:package_with_extra_and_version) { described_class.new("snakemake[foo]==5.28.0") } let(:package_with_different_capitalization) { described_class.new("SNAKEMAKE") } - let(:package_from_url) { described_class.new(package_url, is_url: true) } + let(:package_from_pypi_url) { described_class.new(pypi_package_url, is_url: true) } + let(:package_from_non_pypi_url) { described_class.new(non_pypi_package_url, is_url: true) } let(:other_package) { described_class.new("virtualenv==20.2.0") } describe "initialize" do @@ -66,12 +70,50 @@ describe PyPI do expect(described_class.new("foo[bar,baz]==1.2.3").version).to eq "1.2.3" end - it "initializes name from url" do - expect(described_class.new(package_url, is_url: true).name).to eq "snakemake" + it "initializes name from PyPI url" do + expect(described_class.new(pypi_package_url, is_url: true).name).to eq "snakemake" end - it "initializes version from url" do - expect(described_class.new(package_url, is_url: true).version).to eq "5.29.0" + it "initializes version from PyPI url" do + expect(described_class.new(pypi_package_url, is_url: true).version).to eq "5.29.0" + end + end + + describe ".version=" do + it "sets for package names" do + package = described_class.new("snakemake==5.28.0") + expect(package.version).to eq "5.28.0" + + package.version = "5.29.0" + expect(package.version).to eq "5.29.0" + end + + it "sets for PyPI package URLs" do + package = described_class.new(old_pypi_package_url, is_url: true) + expect(package.version).to eq "5.28.0" + + package.version = "5.29.0" + expect(package.version).to eq "5.29.0" + end + + it "fails for non-PYPI package URLs" do + package = described_class.new(non_pypi_package_url, is_url: true) + + expect {package.version = "1.2.3" }.to raise_error(ArgumentError) + end + end + + describe ".valid_pypi_package?" do + it "is true for package names" do + expect(package.valid_pypi_package?).to be true + end + + it "is true for PyPI URLs" do + expect(package_from_pypi_url.valid_pypi_package?).to be true + end + + it "is false for non-PyPI URLs" do + expect(package_from_non_pypi_url.valid_pypi_package?).to be false end end @@ -81,7 +123,8 @@ describe PyPI do end it "gets pypi info from a package name and specified version" do - expect(package.pypi_info(version: "5.29.0")).to eq ["snakemake", package_url, package_checksum, "5.29.0"] + expect(package.pypi_info(new_version: "5.29.0")).to eq ["snakemake", pypi_package_url, package_checksum, + "5.29.0"] end it "gets pypi info from a package name with extra" do @@ -89,26 +132,27 @@ describe PyPI do end it "gets pypi info from a package name and version" do - expect(package_with_version.pypi_info).to eq ["snakemake", old_package_url, old_package_checksum, "5.28.0"] + expect(package_with_version.pypi_info).to eq ["snakemake", old_pypi_package_url, old_package_checksum, + "5.28.0"] end it "gets pypi info from a package name with overridden version" do - expected_result = ["snakemake", package_url, package_checksum, "5.29.0"] - expect(package_with_version.pypi_info(version: "5.29.0")).to eq expected_result + expected_result = ["snakemake", pypi_package_url, package_checksum, "5.29.0"] + expect(package_with_version.pypi_info(new_version: "5.29.0")).to eq expected_result end it "gets pypi info from a package name, extras, and version" do - expected_result = ["snakemake", old_package_url, old_package_checksum, "5.28.0"] + expected_result = ["snakemake", old_pypi_package_url, old_package_checksum, "5.28.0"] expect(package_with_extra_and_version.pypi_info).to eq expected_result end it "gets pypi info from a url" do - expect(package_from_url.pypi_info).to eq ["snakemake", package_url, package_checksum, "5.29.0"] + expect(package_from_pypi_url.pypi_info).to eq ["snakemake", pypi_package_url, package_checksum, "5.29.0"] end it "gets pypi info from a url with overridden version" do - expected_result = ["snakemake", old_package_url, old_package_checksum, "5.28.0"] - expect(package_from_url.pypi_info(version: "5.28.0")).to eq expected_result + expected_result = ["snakemake", old_pypi_package_url, old_package_checksum, "5.28.0"] + expect(package_from_pypi_url.pypi_info(new_version: "5.28.0")).to eq expected_result end end @@ -130,7 +174,7 @@ describe PyPI do end it "returns string representation of package from url" do - expect(package_from_url.to_s).to eq "snakemake==5.29.0" + expect(package_from_pypi_url.to_s).to eq "snakemake==5.29.0" end end @@ -169,19 +213,15 @@ describe PyPI do describe "update_pypi_url", :needs_network do it "updates url to new version" do - expect(described_class.update_pypi_url(old_package_url, "5.29.0")).to eq package_url + expect(described_class.update_pypi_url(old_pypi_package_url, "5.29.0")).to eq pypi_package_url end it "returns nil for invalid versions" do - expect(described_class.update_pypi_url(old_package_url, "0.0.0")).to be_nil - end - - it "returns nil for nonexistent urls" do - expect(described_class.update_pypi_url("https://brew.sh/foo-1.0.tgz", "1.1")).to be_nil + expect(described_class.update_pypi_url(old_pypi_package_url, "0.0.0")).to be_nil end it "returns nil for non-pypi urls" do - expect(described_class.update_pypi_url("https://github.com/pypa/pip-audit/releases/download/v2.5.6/v2.5.6.tar.gz", "1.1")).to be_nil + expect(described_class.update_pypi_url(non_pypi_package_url, "1.1")).to be_nil end end end diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index aeb3e52940..28c257930e 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -8,80 +8,59 @@ module PyPI PYTHONHOSTED_URL_PREFIX = "https://files.pythonhosted.org/packages/" private_constant :PYTHONHOSTED_URL_PREFIX - # Represents a Python package. # This package can be a PyPI package (either by name/version or PyPI distribution URL), # or it can be a non-PyPI URL. # @api private class Package - attr_accessor :name, :extras, :version - sig { params(package_string: String, is_url: T::Boolean).void } def initialize(package_string, is_url: false) @pypi_info = nil - @from_pypi = true + @package_string = package_string + @is_url = is_url + @is_pypi_url = package_string.start_with? PYTHONHOSTED_URL_PREFIX + end - if is_url - if package_string.start_with?(PYTHONHOSTED_URL_PREFIX) - match = File.basename(package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/) + sig { returns(String) } + def name + @name ||= basic_metadata[0] + end - raise ArgumentError, "Package should be a valid PyPI URL" if match.blank? + sig { returns(T::Array[T.nilable(String)]) } + def extras + @extras ||= basic_metadata[1] + end - @name = PyPI.normalize_python_package(match[1]) - @version = match[2] - else - ensure_formula_installed!("python") + sig { returns(String) } + def version + @version ||= basic_metadata[2] + end - # The URL might be a source distribution hosted somewhere; - # try and use `pip install -q --no-deps --dry-run --report ...` to get its - # name and version. - # Note that this is different from the (similar) `pip install --report` we - # do below, in that it uses `--no-deps` because we only care about resolving - # this specific URL's project metadata. - command = - [Formula["python"].bin/"python3", "-m", "pip", "install", "-q", "--no-deps", - "--dry-run", "--ignore-installed", "--report", "/dev/stdout", package_string] - pip_output = Utils.popen_read({ "PIP_REQUIRE_VIRTUALENV" => "false" }, *command) - unless $CHILD_STATUS.success? - raise ArgumentError, <<~EOS - Unable to determine dependencies for "#{package_string}" because of a failure when running - `#{command.join(" ")}`. - EOS - end + sig { params(new_version: String).void } + def version=(new_version) + raise ArgumentError, "can't update version for non-PyPI packages" unless valid_pypi_package? - metadata = JSON.parse(pip_output)["install"].first["metadata"] - @name = PyPI.normalize_python_package metadata["name"] - @version = metadata["version"] - @from_pypi = false - end + @version = new_version + end - return - end - - if package_string.include? "==" - @name, @version = package_string.split("==") - else - @name = package_string - end - - return unless (match = T.must(@name).match(/^(.*?)\[(.+)\]$/)) - - @name = match[1] - @extras = T.must(match[2]).split "," + sig { returns(T::Boolean) } + def valid_pypi_package? + @is_pypi_url || !@is_url end # Get name, URL, SHA-256 checksum, and latest version for a given package. # This only works for packages from PyPI or from a PyPI URL; packages # derived from non-PyPI URLs will produce `nil` here. - sig { params(version: T.nilable(T.any(String, Version))).returns(T.nilable(T::Array[String])) } - def pypi_info(version: nil) - return @pypi_info if @pypi_info.present? && version.blank? + sig { params(new_version: T.nilable(T.any(String, Version))).returns(T.nilable(T::Array[String])) } + def pypi_info(new_version: nil) + return unless valid_pypi_package? + return @pypi_info if @pypi_info.present? && new_version.blank? - version ||= @version - metadata_url = if version.present? - "https://pypi.org/pypi/#{@name}/#{version}/json" + new_version ||= version + metadata_url = if new_version.present? + "https://pypi.org/pypi/#{name}/#{new_version}/json" else - "https://pypi.org/pypi/#{@name}/json" + "https://pypi.org/pypi/#{name}/json" end out, _, status = curl_output metadata_url, "--location", "--fail" @@ -102,24 +81,22 @@ module PyPI ] end - sig { returns(T::Boolean) } - def valid_pypi_package? - return false unless @from_pypi - info = pypi_info - info.present? && info.is_a?(Array) - end - sig { returns(String) } def to_s - out = @name - out += "[#{@extras.join(",")}]" if @extras.present? - out += "==#{@version}" if @version.present? - out + if valid_pypi_package? + out = name + out += "[#{extras.join(",")}]" if extras.present? + out += "==#{version}" if version.present? + out + else + @package_string + end end sig { params(other: Package).returns(T::Boolean) } def same_package?(other) - T.must(@name.tr("_", "-").casecmp(other.name.tr("_", "-"))).zero? + # These names are pre-normalized, so we can compare them directly. + name == other.name end # Compare only names so we can use .include? and .uniq on a Package array @@ -131,12 +108,62 @@ module PyPI sig { returns(Integer) } def hash - @name.tr("_", "-").downcase.hash + name.hash end sig { params(other: Package).returns(T.nilable(Integer)) } def <=>(other) - @name <=> other.name + name <=> other.name + end + + private + + # Returns [name, [extras], version] for this package. + def basic_metadata + @basic_metadata ||= if @is_pypi_url + match = File.basename(@package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/) + raise ArgumentError, "Package should be a valid PyPI URL" if match.blank? + + [PyPI.normalize_python_package(match[1]), [], match[2]] + elsif @is_url + ensure_formula_installed!("python") + + # The URL might be a source distribution hosted somewhere; + # try and use `pip install -q --no-deps --dry-run --report ...` to get its + # name and version. + # Note that this is different from the (similar) `pip install --report` we + # do below, in that it uses `--no-deps` because we only care about resolving + # this specific URL's project metadata. + command = + [Formula["python"].bin/"python3", "-m", "pip", "install", "-q", "--no-deps", + "--dry-run", "--ignore-installed", "--report", "/dev/stdout", @package_string] + pip_output = Utils.popen_read({ "PIP_REQUIRE_VIRTUALENV" => "false" }, *command) + unless $CHILD_STATUS.success? + raise ArgumentError, <<~EOS + Unable to determine metadata for "#{@package_string}" because of a failure when running + `#{command.join(" ")}`. + EOS + end + + metadata = JSON.parse(pip_output)["install"].first["metadata"] + [PyPI.normalize_python_package(metadata["name"]), [], metadata["version"]] + else + if @package_string.include? "==" + name, version = @package_string.split("==") + else + name = @package_string + version = nil + end + + if (match = T.must(name).match(/^(.*?)\[(.+)\]$/)) + name = match[1] + extras = T.must(match[2]).split "," + + [PyPI.normalize_python_package(name), extras, version] + else + [PyPI.normalize_python_package(name), [], version] + end + end end end @@ -146,7 +173,7 @@ module PyPI return unless package.valid_pypi_package? - _, url = package.pypi_info(version: version) + _, url = package.pypi_info(new_version: version) url rescue ArgumentError nil @@ -191,30 +218,17 @@ module PyPI main_package = if package_name.present? Package.new(package_name) else - begin - Package.new(formula.stable.url, is_url: true) - rescue ArgumentError - nil + Package.new(formula.stable.url, is_url: true) + end + + if version.present? + if main_package.valid_pypi_package? + main_package.version = version + else + odie "The main package is not a PyPI package. Please update its URL manually." end end - if main_package.blank? - return if ignore_non_pypi_packages - - odie <<~EOS - Could not infer PyPI package name from URL: - #{Formatter.url(formula.stable.url)} - EOS - end - - unless main_package.valid_pypi_package? - return if ignore_non_pypi_packages - - odie "\"#{main_package}\" is not available on PyPI." - end - - main_package.version = version if version.present? - extra_packages = (extra_packages || []).map { |p| Package.new p } exclude_packages = (exclude_packages || []).map { |p| Package.new p } exclude_packages += %W[#{main_package.name} argparse pip setuptools wsgiref].map { |p| Package.new p } From 791536348291436bb151d02af5bd3bdc9d20e00d Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sun, 4 Jun 2023 23:33:46 -0400 Subject: [PATCH 10/12] test: `brew style --fix` Signed-off-by: William Woodruff --- Library/Homebrew/test/utils/pypi_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Homebrew/test/utils/pypi_spec.rb b/Library/Homebrew/test/utils/pypi_spec.rb index 33cf00348b..e3ca858d1d 100644 --- a/Library/Homebrew/test/utils/pypi_spec.rb +++ b/Library/Homebrew/test/utils/pypi_spec.rb @@ -99,7 +99,7 @@ describe PyPI do it "fails for non-PYPI package URLs" do package = described_class.new(non_pypi_package_url, is_url: true) - expect {package.version = "1.2.3" }.to raise_error(ArgumentError) + expect { package.version = "1.2.3" }.to raise_error(ArgumentError) end end From 7067f72eb0a43626be753126a45e86a1b575fccf Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sun, 4 Jun 2023 23:49:20 -0400 Subject: [PATCH 11/12] utils/pypi: fix sig Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index 28c257930e..e5c9b0580a 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -31,7 +31,7 @@ module PyPI @extras ||= basic_metadata[1] end - sig { returns(String) } + sig { returns(T.nilable(String)) } def version @version ||= basic_metadata[2] end From 0f40e224bdabf13520dfba333257e9a156b1661c Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 5 Jun 2023 10:16:19 -0400 Subject: [PATCH 12/12] utils/pypi: refactor instance variables Signed-off-by: William Woodruff --- Library/Homebrew/utils/pypi.rb | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index e5c9b0580a..69332c2af2 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -23,17 +23,20 @@ module PyPI sig { returns(String) } def name - @name ||= basic_metadata[0] + basic_metadata if @name.blank? + @name end sig { returns(T::Array[T.nilable(String)]) } def extras - @extras ||= basic_metadata[1] + basic_metadata if @extras.blank? + @extras end sig { returns(T.nilable(String)) } def version - @version ||= basic_metadata[2] + basic_metadata if @version.blank? + @version end sig { params(new_version: String).void } @@ -120,11 +123,13 @@ module PyPI # Returns [name, [extras], version] for this package. def basic_metadata - @basic_metadata ||= if @is_pypi_url + if @is_pypi_url match = File.basename(@package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/) raise ArgumentError, "Package should be a valid PyPI URL" if match.blank? - [PyPI.normalize_python_package(match[1]), [], match[2]] + @name = PyPI.normalize_python_package match[1] + @extras = [] + @version = match[2] elsif @is_url ensure_formula_installed!("python") @@ -146,7 +151,10 @@ module PyPI end metadata = JSON.parse(pip_output)["install"].first["metadata"] - [PyPI.normalize_python_package(metadata["name"]), [], metadata["version"]] + + @name = PyPI.normalize_python_package metadata["name"] + @extras = [] + @version = metadata["version"] else if @package_string.include? "==" name, version = @package_string.split("==") @@ -158,11 +166,13 @@ module PyPI if (match = T.must(name).match(/^(.*?)\[(.+)\]$/)) name = match[1] extras = T.must(match[2]).split "," - - [PyPI.normalize_python_package(name), extras, version] else - [PyPI.normalize_python_package(name), [], version] + extras = [] end + + @name = PyPI.normalize_python_package name + @extras = extras + @version = version end end end