Simplify Set#inspect output

As Set is now a core collection class, it should have special inspect
output.  Ideally, inspect output should be suitable to eval, similar
to array and hash (assuming the elements are also suitable to eval):

  set = Set[1, 2, 3]
  eval(set.inspect) == set # should be true

The simplest way to do this is to use the Set[] syntax.

This deliberately does not use any subclass name in the output,
similar to array and hash. It is more important that users know they
are dealing with a set than which subclass:

  Class.new(Set)[]
  # this does: Set[]
  # not: #<Class:0x00000c21c78699e0>[]

This inspect change breaks the power_assert bundled gem tests, so
add power_assert to TEST_BUNDLED_GEMS_ALLOW_FAILURES in the workflows.

Implements [Feature #21389]
This commit is contained in:
Jeremy Evans 2025-05-30 17:55:05 -07:00
parent 3b602c952d
commit 3a9c091cf3
16 changed files with 53 additions and 33 deletions

View file

@ -104,7 +104,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
if: ${{ steps.diff.outputs.gems }} if: ${{ steps.diff.outputs.gems }}
- name: Commit - name: Commit

View file

@ -146,7 +146,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }}
LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }}

View file

@ -153,7 +153,7 @@ jobs:
timeout-minutes: ${{ matrix.gc.timeout || 40 }} timeout-minutes: ${{ matrix.gc.timeout || 40 }}
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }}
LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }}

View file

@ -87,7 +87,7 @@ jobs:
EXCLUDES: '../src/test/.excludes-parsey' EXCLUDES: '../src/test/.excludes-parsey'
RUN_OPTS: ${{ matrix.run_opts || '--parser=parse.y' }} RUN_OPTS: ${{ matrix.run_opts || '--parser=parse.y' }}
SPECOPTS: ${{ matrix.specopts || '-T --parser=parse.y' }} SPECOPTS: ${{ matrix.specopts || '-T --parser=parse.y' }}
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
- uses: ./.github/actions/slack - uses: ./.github/actions/slack
with: with:

View file

@ -133,7 +133,7 @@ jobs:
timeout-minutes: ${{ matrix.timeout || 40 }} timeout-minutes: ${{ matrix.timeout || 40 }}
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }}
LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }}

View file

@ -153,7 +153,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
SYNTAX_SUGGEST_TIMEOUT: '5' SYNTAX_SUGGEST_TIMEOUT: '5'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }}

View file

@ -201,7 +201,7 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,rbs,repl_type_completor,power_assert'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
SYNTAX_SUGGEST_TIMEOUT: '5' SYNTAX_SUGGEST_TIMEOUT: '5'
YJIT_BINDGEN_DIFF_OPTS: '--exit-code' YJIT_BINDGEN_DIFF_OPTS: '--exit-code'

View file

@ -150,7 +150,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,power_assert'
SYNTAX_SUGGEST_TIMEOUT: '5' SYNTAX_SUGGEST_TIMEOUT: '5'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
TESTS: ${{ matrix.tests }} TESTS: ${{ matrix.tests }}

View file

@ -172,7 +172,7 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
env: env:
RUBY_TESTOPTS: '-q --tty=no' RUBY_TESTOPTS: '-q --tty=no'
TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof,power_assert'
PRECHECK_BUNDLED_GEMS: 'no' PRECHECK_BUNDLED_GEMS: 'no'
SYNTAX_SUGGEST_TIMEOUT: '5' SYNTAX_SUGGEST_TIMEOUT: '5'
ZJIT_BINDGEN_DIFF_OPTS: '--exit-code' ZJIT_BINDGEN_DIFF_OPTS: '--exit-code'

8
set.c
View file

@ -537,7 +537,7 @@ static int
set_inspect_i(st_data_t key, st_data_t arg) set_inspect_i(st_data_t key, st_data_t arg)
{ {
VALUE str = (VALUE)arg; VALUE str = (VALUE)arg;
if (RSTRING_LEN(str) > 8) { if (RSTRING_LEN(str) > 4) {
rb_str_buf_cat_ascii(str, ", "); rb_str_buf_cat_ascii(str, ", ");
} }
rb_str_buf_append(str, rb_inspect((VALUE)key)); rb_str_buf_append(str, rb_inspect((VALUE)key));
@ -550,10 +550,10 @@ set_inspect(VALUE set, VALUE dummy, int recur)
{ {
VALUE str; VALUE str;
if (recur) return rb_usascii_str_new2("#<Set: {...}>"); if (recur) return rb_usascii_str_new2("Set[...]");
str = rb_str_buf_new2("#<Set: {"); str = rb_str_buf_new2("Set[");
set_iter(set, set_inspect_i, str); set_iter(set, set_inspect_i, str);
rb_str_buf_cat2(str, "}>"); rb_str_buf_cat2(str, "]");
return str; return str;
} }

View file

@ -30,9 +30,9 @@ describe "Enumerable#to_set" do
it "does not need explicit `require 'set'`" do it "does not need explicit `require 'set'`" do
output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1') output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1')
puts [1, 2, 3].to_set puts [1, 2, 3].to_set.to_a.inspect
RUBY RUBY
output.chomp.should == "#<Set: {1, 2, 3}>" output.chomp.should == "[1, 2, 3]"
end end
end end

View file

@ -95,7 +95,7 @@ describe "Set#compare_by_identity" do
set = Set.new.freeze set = Set.new.freeze
-> { -> {
set.compare_by_identity set.compare_by_identity
}.should raise_error(FrozenError, "can't modify frozen Set: #<Set: {}>") }.should raise_error(FrozenError, /can't modify frozen Set: (#<)?Set(\[|: {)[\]}]>?/)
end end
end end

View file

@ -3,8 +3,8 @@ require_relative '../../spec_helper'
describe 'Set' do describe 'Set' do
it 'is available without explicit requiring' do it 'is available without explicit requiring' do
output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1') output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1')
puts Set.new([1, 2, 3]) puts Set.new([1, 2, 3]).to_a.inspect
RUBY RUBY
output.chomp.should == "#<Set: {1, 2, 3}>" output.chomp.should == "[1, 2, 3]"
end end
end end

View file

@ -7,14 +7,33 @@ describe :set_inspect, shared: true do
Set[:a, "b", Set[?c]].send(@method).should be_kind_of(String) Set[:a, "b", Set[?c]].send(@method).should be_kind_of(String)
end end
ruby_version_is "3.5" do
it "does include the elements of the set" do
Set["1"].send(@method).should == 'Set["1"]'
end
end
ruby_version_is ""..."3.5" do
it "does include the elements of the set" do it "does include the elements of the set" do
Set["1"].send(@method).should == '#<Set: {"1"}>' Set["1"].send(@method).should == '#<Set: {"1"}>'
end end
end
it "puts spaces between the elements" do it "puts spaces between the elements" do
Set["1", "2"].send(@method).should include('", "') Set["1", "2"].send(@method).should include('", "')
end end
ruby_version_is "3.5" do
it "correctly handles cyclic-references" do
set1 = Set[]
set2 = Set[set1]
set1 << set2
set1.send(@method).should be_kind_of(String)
set1.send(@method).should include("Set[...]")
end
end
ruby_version_is ""..."3.5" do
it "correctly handles cyclic-references" do it "correctly handles cyclic-references" do
set1 = Set[] set1 = Set[]
set2 = Set[set1] set2 = Set[set1]
@ -22,4 +41,5 @@ describe :set_inspect, shared: true do
set1.send(@method).should be_kind_of(String) set1.send(@method).should be_kind_of(String)
set1.send(@method).should include("#<Set: {...}>") set1.send(@method).should include("#<Set: {...}>")
end end
end
end end

View file

@ -839,24 +839,24 @@ class TC_Set < Test::Unit::TestCase
def test_inspect def test_inspect
set1 = Set[1, 2] set1 = Set[1, 2]
assert_equal('#<Set: {1, 2}>', set1.inspect) assert_equal('Set[1, 2]', set1.inspect)
set2 = Set[Set[0], 1, 2, set1] set2 = Set[Set[0], 1, 2, set1]
assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2}>}>', set2.inspect) assert_equal('Set[Set[0], 1, 2, Set[1, 2]]', set2.inspect)
set1.add(set2) set1.add(set2)
assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2, #<Set: {...}>}>}>', set2.inspect) assert_equal('Set[Set[0], 1, 2, Set[1, 2, Set[...]]]', set2.inspect)
end end
def test_to_s def test_to_s
set1 = Set[1, 2] set1 = Set[1, 2]
assert_equal('#<Set: {1, 2}>', set1.to_s) assert_equal('Set[1, 2]', set1.to_s)
set2 = Set[Set[0], 1, 2, set1] set2 = Set[Set[0], 1, 2, set1]
assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2}>}>', set2.to_s) assert_equal('Set[Set[0], 1, 2, Set[1, 2]]', set2.to_s)
set1.add(set2) set1.add(set2)
assert_equal('#<Set: {#<Set: {0}>, 1, 2, #<Set: {1, 2, #<Set: {...}>}>}>', set2.to_s) assert_equal('Set[Set[0], 1, 2, Set[1, 2, Set[...]]]', set2.to_s)
end end
def test_compare_by_identity def test_compare_by_identity

View file

@ -10,7 +10,7 @@ github_actions = ENV["GITHUB_ACTIONS"] == "true"
allowed_failures = ENV['TEST_BUNDLED_GEMS_ALLOW_FAILURES'] || '' allowed_failures = ENV['TEST_BUNDLED_GEMS_ALLOW_FAILURES'] || ''
if RUBY_PLATFORM =~ /mswin|mingw/ if RUBY_PLATFORM =~ /mswin|mingw/
allowed_failures = [allowed_failures, "rbs,debug,irb"].join(',') allowed_failures = [allowed_failures, "rbs,debug,irb,power_assert"].join(',')
end end
allowed_failures = allowed_failures.split(',').uniq.reject(&:empty?) allowed_failures = allowed_failures.split(',').uniq.reject(&:empty?)