Skip to content

Commit

Permalink
Merge pull request #19094 from Homebrew/utils-sorbet-strict
Browse files Browse the repository at this point in the history
Bump some `utils/` files to Sorbet `typed: strict`
  • Loading branch information
issyl0 authored Jan 22, 2025
2 parents 55475cb + 66e5084 commit fa40d2f
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 88 deletions.
34 changes: 29 additions & 5 deletions Library/Homebrew/utils/analytics.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true

require "context"
Expand Down Expand Up @@ -60,7 +60,7 @@ def deferred_curl(url, args)
puts Utils.popen_read(curl, *args, url)
else
pid = spawn curl, *args, url
Process.detach T.must(pid)
Process.detach(pid)
end
end

Expand Down Expand Up @@ -161,52 +161,63 @@ def report_test_bot_test(step_command_short, passed)
report_influx(:test_bot_test, tags, fields)
end

sig { returns(T::Boolean) }
def influx_message_displayed?
config_true?(:influxanalyticsmessage)
end

sig { returns(T::Boolean) }
def messages_displayed?
config_true?(:analyticsmessage) &&
config_true?(:caskanalyticsmessage) &&
influx_message_displayed?
return false unless config_true?(:analyticsmessage)
return false unless config_true?(:caskanalyticsmessage)

influx_message_displayed?
end

sig { returns(T::Boolean) }
def disabled?
return true if Homebrew::EnvConfig.no_analytics?

config_true?(:analyticsdisabled)
end

sig { returns(T::Boolean) }
def not_this_run?
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"].present?
end

sig { returns(T::Boolean) }
def no_message_output?
# Used by Homebrew/install
ENV["HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT"].present?
end

sig { void }
def messages_displayed!
Homebrew::Settings.write :analyticsmessage, true
Homebrew::Settings.write :caskanalyticsmessage, true
Homebrew::Settings.write :influxanalyticsmessage, true
end

sig { void }
def enable!
Homebrew::Settings.write :analyticsdisabled, false
delete_uuid!
messages_displayed!
end

sig { void }
def disable!
Homebrew::Settings.write :analyticsdisabled, true
delete_uuid!
end

sig { void }
def delete_uuid!
Homebrew::Settings.delete :analyticsuuid
end

sig { params(args: Homebrew::Cmd::Info::Args, filter: T.nilable(String)).void }
def output(args:, filter: nil)
require "api"

Expand Down Expand Up @@ -244,6 +255,7 @@ def output(args:, filter: nil)
table_output(category, days, results, os_version:, cask_install:)
end

sig { params(json: T::Hash[String, T.untyped], args: Homebrew::Cmd::Info::Args).void }
def output_analytics(json, args:)
full_analytics = args.analytics? || verbose?

Expand Down Expand Up @@ -273,6 +285,7 @@ def output_analytics(json, args:)
# It relies on screen scraping some GitHub HTML that's not available as an API.
# This seems very likely to break in the future.
# That said, it's the only way to get the data we want right now.
sig { params(formula: Formula, args: Homebrew::Cmd::Info::Args).void }
def output_github_packages_downloads(formula, args:)
return unless args.github_packages_downloads?
return unless formula.core_formula?
Expand Down Expand Up @@ -316,6 +329,7 @@ def output_github_packages_downloads(formula, args:)
puts "#{number_readable(thirty_day_download_count)} (30 days)"
end

sig { params(formula: Formula, args: Homebrew::Cmd::Info::Args).void }
def formula_output(formula, args:)
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?

Expand All @@ -331,6 +345,7 @@ def formula_output(formula, args:)
nil
end

sig { params(cask: Cask::Cask, args: Homebrew::Cmd::Info::Args).void }
def cask_output(cask, args:)
return if Homebrew::EnvConfig.no_analytics? || Homebrew::EnvConfig.no_github_api?

Expand Down Expand Up @@ -388,6 +403,12 @@ def default_package_fields
end
end

sig {
params(
category: String, days: String, results: T::Hash[String, Integer], os_version: T::Boolean,
cask_install: T::Boolean
).void
}
def table_output(category, days, results, os_version: false, cask_install: false)
oh1 "#{category} (#{days} days)"
total_count = results.values.inject("+")
Expand Down Expand Up @@ -475,14 +496,17 @@ def table_output(category, days, results, os_version: false, cask_install: false
"#{formatted_total_count_footer} | #{formatted_total_percent_footer}%"
end

sig { params(key: Symbol).returns(T::Boolean) }
def config_true?(key)
Homebrew::Settings.read(key) == "true"
end

sig { params(count: Integer).returns(String) }
def format_count(count)
count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end

sig { params(percent: T.any(Integer, Float)).returns(String) }
def format_percent(percent)
format("%<percent>.2f", percent:)
end
Expand Down
11 changes: 7 additions & 4 deletions Library/Homebrew/utils/fork.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true

require "fcntl"
require "utils/socket"

module Utils
sig { params(child_error: T::Hash[String, T.untyped]).returns(Exception) }
def self.rewrite_child_error(child_error)
inner_class = Object.const_get(child_error["json_class"])
error = if child_error["cmd"] && inner_class == ErrorDuringExecution
Expand Down Expand Up @@ -33,7 +34,11 @@ def self.rewrite_child_error(child_error)
# When using this function, remember to call `exec` as soon as reasonably possible.
# This function does not protect against the pitfalls of what you can do pre-exec in a fork.
# See `man fork` for more information.
def self.safe_fork(directory: nil, yield_parent: false)
sig {
params(directory: T.nilable(String), yield_parent: T::Boolean,
_blk: T.proc.params(arg0: T.nilable(String)).void).void
}
def self.safe_fork(directory: nil, yield_parent: false, &_blk)
require "json/add/exception"

block = proc do |tmpdir|
Expand Down Expand Up @@ -80,8 +85,6 @@ def self.safe_fork(directory: nil, yield_parent: false)
exit!(true)
end

pid = T.must(pid)

begin
yield(nil) if yield_parent

Expand Down
57 changes: 38 additions & 19 deletions Library/Homebrew/utils/pypi.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true

require "utils/inreplace"
Expand All @@ -14,20 +14,20 @@ module PyPI
class Package
sig { params(package_string: String, is_url: T::Boolean, python_name: String).void }
def initialize(package_string, is_url: false, python_name: "python")
@pypi_info = nil
@pypi_info = T.let(nil, T.nilable(T::Array[String]))
@package_string = package_string
@is_url = is_url
@is_pypi_url = package_string.start_with? PYTHONHOSTED_URL_PREFIX
@is_pypi_url = T.let(package_string.start_with?(PYTHONHOSTED_URL_PREFIX), T::Boolean)
@python_name = python_name
end

sig { returns(String) }
sig { returns(T.nilable(String)) }
def name
basic_metadata if @name.blank?
@name
end

sig { returns(T::Array[T.nilable(String)]) }
sig { returns(T.nilable(T::Array[String])) }
def extras
basic_metadata if @extras.blank?
@extras
Expand All @@ -43,7 +43,7 @@ def version
def version=(new_version)
raise ArgumentError, "can't update version for non-PyPI packages" unless valid_pypi_package?

@version = new_version
@version = T.let(new_version, T.nilable(String))
end

sig { returns(T::Boolean) }
Expand Down Expand Up @@ -97,8 +97,10 @@ def pypi_info(new_version: nil)
sig { returns(String) }
def to_s
if valid_pypi_package?
out = name
out += "[#{extras.join(",")}]" if extras.present?
out = T.must(name)
if (pypi_extras = extras.presence)
out += "[#{pypi_extras.join(",")}]"
end
out += "==#{version}" if version.present?
out
else
Expand Down Expand Up @@ -132,14 +134,15 @@ def <=>(other)
private

# Returns [name, [extras], version] for this package.
sig { returns(T.nilable(T.any(String, T::Array[String]))) }
def 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?

@name ||= PyPI.normalize_python_package match[1]
@extras ||= []
@version ||= match[2]
@name ||= T.let(PyPI.normalize_python_package(T.must(match[1])), T.nilable(String))
@extras ||= T.let([], T.nilable(T::Array[String]))
@version ||= T.let(match[2], T.nilable(String))
elsif @is_url
ensure_formula_installed!(@python_name)

Expand All @@ -162,9 +165,9 @@ def basic_metadata

metadata = JSON.parse(pip_output)["install"].first["metadata"]

@name ||= PyPI.normalize_python_package metadata["name"]
@extras ||= []
@version ||= metadata["version"]
@name ||= T.let(PyPI.normalize_python_package(metadata["name"]), T.nilable(String))
@extras ||= T.let([], T.nilable(T::Array[String]))
@version ||= T.let(metadata["version"], T.nilable(String))
else
if @package_string.include? "=="
name, version = @package_string.split("==")
Expand All @@ -180,7 +183,7 @@ def basic_metadata
extras = []
end

@name ||= PyPI.normalize_python_package name
@name ||= T.let(PyPI.normalize_python_package(T.must(name)), T.nilable(String))
@extras ||= extras
@version ||= version
end
Expand Down Expand Up @@ -248,7 +251,7 @@ def self.update_python_resources!(formula, version: nil, package_name: nil, extr
missing_msg = "formulae required to update \"#{formula.name}\" resources: #{missing_dependencies.join(", ")}"
odie "Missing #{missing_msg}" unless install_dependencies
ohai "Installing #{missing_msg}"
missing_dependencies.each(&method(:ensure_formula_installed!))
missing_dependencies.each(&:ensure_formula_installed!)
end

python_deps = formula.deps
Expand Down Expand Up @@ -327,12 +330,21 @@ def self.update_python_resources!(formula, version: nil, package_name: nil, extr
# Resolve the dependency tree of all input packages
show_info = !print_only && !silent
ohai "Retrieving PyPI dependencies for \"#{input_packages.join(" ")}\"..." if show_info
found_packages = pip_report(input_packages, python_name:, print_stderr: verbose && show_info)

print_stderr = if verbose && show_info
true
else
false
end

found_packages = pip_report(input_packages, python_name:, print_stderr:)
# Resolve the dependency tree of excluded packages to prune the above
exclude_packages.delete_if { |package| found_packages.exclude? package }
ohai "Retrieving PyPI dependencies for excluded \"#{exclude_packages.join(" ")}\"..." if show_info
exclude_packages = pip_report(exclude_packages, python_name:, print_stderr: verbose && show_info)
exclude_packages += [Package.new(main_package.name)] unless main_package.nil?
exclude_packages = pip_report(exclude_packages, python_name:, print_stderr:)
if (main_package_name = main_package&.name)
exclude_packages += [Package.new(main_package_name)]
end

new_resource_blocks = ""
found_packages.sort.each do |package|
Expand Down Expand Up @@ -404,12 +416,18 @@ def self.update_python_resources!(formula, version: nil, package_name: nil, extr
true
end

sig { params(name: String).returns(String) }
def self.normalize_python_package(name)
# This normalization is defined in the PyPA packaging specifications;
# https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization
name.gsub(/[-_.]+/, "-").downcase
end

sig {
params(
packages: T::Array[Package], python_name: String, print_stderr: T::Boolean,
).returns(T::Array[Package])
}
def self.pip_report(packages, python_name: "python", print_stderr: false)
return [] if packages.blank?

Expand All @@ -430,6 +448,7 @@ def self.pip_report(packages, python_name: "python", print_stderr: false)
pip_report_to_packages(JSON.parse(pip_output)).uniq
end

sig { params(report: T::Hash[String, T.untyped]).returns(T::Array[Package]) }
def self.pip_report_to_packages(report)
return [] if report.blank?

Expand Down
32 changes: 19 additions & 13 deletions Library/Homebrew/utils/shell.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true

module Utils
Expand Down Expand Up @@ -67,7 +67,10 @@ def profile
return "#{ENV["HOMEBREW_ZDOTDIR"]}/.zshrc" if ENV["HOMEBREW_ZDOTDIR"].present?
end

SHELL_PROFILE_MAP.fetch(preferred, "~/.profile")
shell = preferred
return "~/.profile" if shell.nil?

SHELL_PROFILE_MAP.fetch(shell, "~/.profile")
end

sig { params(variable: String, value: String).returns(T.nilable(String)) }
Expand Down Expand Up @@ -98,17 +101,20 @@ def prepend_path_in_profile(path)
end
end

SHELL_PROFILE_MAP = {
bash: "~/.profile",
csh: "~/.cshrc",
fish: "~/.config/fish/config.fish",
ksh: "~/.kshrc",
mksh: "~/.kshrc",
rc: "~/.rcrc",
sh: "~/.profile",
tcsh: "~/.tcshrc",
zsh: "~/.zshrc",
}.freeze
SHELL_PROFILE_MAP = T.let(
{
bash: "~/.profile",
csh: "~/.cshrc",
fish: "~/.config/fish/config.fish",
ksh: "~/.kshrc",
mksh: "~/.kshrc",
rc: "~/.rcrc",
sh: "~/.profile",
tcsh: "~/.tcshrc",
zsh: "~/.zshrc",
}.freeze,
T::Hash[Symbol, String],
)

UNSAFE_SHELL_CHAR = %r{([^A-Za-z0-9_\-.,:/@~+\n])}

Expand Down
Loading

0 comments on commit fa40d2f

Please sign in to comment.