Skip to content

Commit ee42128

Browse files
authored
Merge pull request rails#49765 from andrewn617/configurable-inspect
Make the output of `ActiveRecord::Core#inspect` configurable.
2 parents e94c5a3 + e50182a commit ee42128

File tree

8 files changed

+188
-63
lines changed

8 files changed

+188
-63
lines changed

activerecord/CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
* Make the output of `ActiveRecord::Core#inspect` configurable.
2+
3+
By default, calling `inspect` on a record will yield a formatted string including just the `id`.
4+
5+
```ruby
6+
Post.first.inspect #=> "#<Post id: 1>"
7+
```
8+
9+
The attributes to be included in the output of `inspect` can be configured with
10+
`ActiveRecord::Core#attributes_for_inspect`.
11+
12+
```ruby
13+
Post.attributes_for_inspect = [:id, :title]
14+
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
15+
```
16+
17+
With the `attributes_for_inspect` set to `:all`, `inspect` will list all the record's attributes.
18+
19+
```ruby
20+
Post.attributes_for_inspect = :all
21+
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
22+
```
23+
24+
In development and test mode, `attributes_for_inspect` will be set to `:all` by default.
25+
26+
You can also call `full_inspect` to get an inspection with all the attributes.
27+
28+
The attributes in `attribute_for_inspect` will also be used for `pretty_print`.
29+
30+
*Andrew Novoselac*
31+
132
* Don't mark Float::INFINITY as changed when reassigning it
233

334
When saving a record with a float infinite value, it shouldn't mark as changed

activerecord/lib/active_record/core.rb

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ def self.configurations
102102

103103
class_attribute :shard_selector, instance_accessor: false, default: nil
104104

105+
# Specifies the attributes that will be included in the output of the #inspect method
106+
class_attribute :attributes_for_inspect, instance_accessor: false, default: [:id]
107+
105108
def self.application_record_class? # :nodoc:
106109
if ActiveRecord.application_record_class
107110
self == ActiveRecord.application_record_class
@@ -681,21 +684,14 @@ def connection_handler
681684
self.class.connection_handler
682685
end
683686

684-
# Returns the contents of the record as a nicely formatted string.
687+
# Returns the attributes specified by <tt>.attributes_for_inspect</tt> as a nicely formatted string.
685688
def inspect
686-
# We check defined?(@attributes) not to issue warnings if the object is
687-
# allocated but not initialized.
688-
inspection = if defined?(@attributes) && @attributes
689-
attribute_names.filter_map do |name|
690-
if _has_attribute?(name)
691-
"#{name}: #{attribute_for_inspect(name)}"
692-
end
693-
end.join(", ")
694-
else
695-
"not initialized"
696-
end
689+
inspect_with_attributes(attributes_for_inspect)
690+
end
697691

698-
"#<#{self.class} #{inspection}>"
692+
# Returns the full contents of the record as a nicely formatted string.
693+
def full_inspect
694+
inspect_with_attributes(attribute_names)
699695
end
700696

701697
# Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
@@ -704,8 +700,9 @@ def pretty_print(pp)
704700
return super if custom_inspect_method_defined?
705701
pp.object_address_group(self) do
706702
if defined?(@attributes) && @attributes
707-
attr_names = self.class.attribute_names.select { |name| _has_attribute?(name) }
703+
attr_names = attributes_for_inspect.select { |name| _has_attribute?(name.to_s) }
708704
pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
705+
attr_name = attr_name.to_s
709706
pp.breakable " "
710707
pp.group(1) do
711708
pp.text attr_name
@@ -771,5 +768,26 @@ def pretty_print(pp)
771768
def inspection_filter
772769
self.class.inspection_filter
773770
end
771+
772+
def inspect_with_attributes(attributes_to_list)
773+
# We check defined?(@attributes) not to issue warnings if the object is
774+
# allocated but not initialized.
775+
inspection = if defined?(@attributes) && @attributes
776+
attributes_to_list.filter_map do |name|
777+
name = name.to_s
778+
if _has_attribute?(name)
779+
"#{name}: #{attribute_for_inspect(name)}"
780+
end
781+
end.join(", ")
782+
else
783+
"not initialized"
784+
end
785+
786+
"#<#{self.class} #{inspection}>"
787+
end
788+
789+
def attributes_for_inspect
790+
self.class.attributes_for_inspect == :all ? attribute_names : self.class.attributes_for_inspect
791+
end
774792
end
775793
end

activerecord/lib/active_record/railtie.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,5 +458,15 @@ class Railtie < Rails::Railtie # :nodoc:
458458
end
459459
end
460460
end
461+
462+
initializer "active_record.attributes_for_inspect" do |app|
463+
ActiveSupport.on_load(:active_record) do
464+
if app.config.consider_all_requests_local
465+
if app.config.active_record.attributes_for_inspect.nil?
466+
ActiveRecord::Base.attributes_for_inspect = :all
467+
end
468+
end
469+
end
470+
end
461471
end
462472
end

activerecord/test/cases/attributes_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def deserialize(*)
343343
end
344344

345345
test "attributes not backed by database columns appear in inspect" do
346-
inspection = OverloadedType.new.inspect
346+
inspection = OverloadedType.new.full_inspect
347347

348348
assert_includes inspection, "non_existent_decimal"
349349
end

activerecord/test/cases/core_test.rb

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ def test_inspect_class
1717
assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
1818
end
1919

20-
def test_inspect_instance
20+
def test_inspect_instance_includes_just_id_by_default
2121
topic = topics(:first)
22-
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "[email protected]", written_on: "#{topic.written_on.to_fs(:inspect)}", bonus_time: "#{topic.bonus_time.to_fs(:inspect)}", last_read: "#{topic.last_read.to_fs(:inspect)}", content: "Have a nice day", important: nil, binary_content: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_fs(:inspect)}", updated_at: "#{topic.updated_at.to_fs(:inspect)}">), topic.inspect
22+
assert_equal %(#<Topic id: 1>), topic.inspect
23+
end
24+
25+
def test_inspect_includes_attributes_from_attributes_for_inspect
26+
Topic.with(attributes_for_inspect: [:id, :title, :author_name]) do
27+
topic = topics(:first)
28+
29+
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David">), topic.inspect
30+
end
2331
end
2432

2533
def test_inspect_instance_with_lambda_date_formatter
2634
before = Time::DATE_FORMATS[:inspect]
27-
Time::DATE_FORMATS[:inspect] = ->(date) { "my_format" }
28-
topic = topics(:first)
2935

30-
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "[email protected]", written_on: "my_format", bonus_time: "my_format", last_read: "2004-04-15", content: "Have a nice day", important: nil, binary_content: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "my_format", updated_at: "my_format">), topic.inspect
36+
Topic.with(attributes_for_inspect: [:id, :last_read]) do
37+
Time::DATE_FORMATS[:inspect] = ->(date) { "my_format" }
38+
topic = topics(:first)
3139

40+
assert_equal %(#<Topic id: 1, last_read: "2004-04-15">), topic.inspect
41+
end
3242
ensure
3343
Time::DATE_FORMATS[:inspect] = before
3444
end
@@ -38,8 +48,10 @@ def test_inspect_new_instance
3848
end
3949

4050
def test_inspect_limited_select_instance
41-
assert_equal %(#<Topic id: 1>), Topic.all.merge!(select: "id", where: "id = 1").first.inspect
42-
assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect
51+
Topic.with(attributes_for_inspect: [:id, :title]) do
52+
assert_equal %(#<Topic id: 1>), Topic.all.merge!(select: "id", where: "id = 1").first.inspect
53+
assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect
54+
end
4355
end
4456

4557
def test_inspect_instance_with_non_primary_key_id_attribute
@@ -51,36 +63,35 @@ def test_inspect_class_without_table
5163
assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
5264
end
5365

66+
def test_inspect_with_attributes_for_inspect_all_lists_all_attributes
67+
Topic.with(attributes_for_inspect: :all) do
68+
topic = topics(:first)
69+
70+
assert_equal <<~STRING.squish, topic.inspect
71+
#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "[email protected]", written_on: "#{topic.written_on.to_fs(:inspect)}", bonus_time: "#{topic.bonus_time.to_fs(:inspect)}", last_read: "#{topic.last_read.to_fs(:inspect)}", content: "Have a nice day", important: nil, binary_content: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_fs(:inspect)}", updated_at: "#{topic.updated_at.to_fs(:inspect)}">
72+
STRING
73+
end
74+
end
75+
5476
def test_inspect_relation_with_virtual_field
5577
relation = Topic.limit(1).select("1 as virtual_field")
56-
assert_match(/virtual_field: 1/, relation.inspect)
78+
assert_match(/virtual_field: 1/, relation.first.full_inspect)
79+
end
80+
81+
def test_full_inspect_lists_all_attributes
82+
topic = topics(:first)
83+
84+
assert_equal <<~STRING.squish, topic.full_inspect
85+
#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "[email protected]", written_on: "#{topic.written_on.to_fs(:inspect)}", bonus_time: "#{topic.bonus_time.to_fs(:inspect)}", last_read: "#{topic.last_read.to_fs(:inspect)}", content: "Have a nice day", important: nil, binary_content: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_fs(:inspect)}", updated_at: "#{topic.updated_at.to_fs(:inspect)}">
86+
STRING
5787
end
5888

5989
def test_pretty_print_new
6090
topic = Topic.new
6191
actual = +""
6292
PP.pp(topic, StringIO.new(actual))
6393
expected = <<~PRETTY
64-
#<Topic:0xXXXXXX
65-
id: nil,
66-
title: nil,
67-
author_name: nil,
68-
author_email_address: "[email protected]",
69-
written_on: nil,
70-
bonus_time: nil,
71-
last_read: nil,
72-
content: nil,
73-
important: nil,
74-
binary_content: nil,
75-
approved: true,
76-
replies_count: 0,
77-
unique_replies_count: 0,
78-
parent_id: nil,
79-
parent_title: nil,
80-
type: nil,
81-
group: nil,
82-
created_at: nil,
83-
updated_at: nil>
94+
#<Topic:0xXXXXXX id: nil>
8495
PRETTY
8596
assert actual.start_with?(expected.split("XXXXXX").first)
8697
assert actual.end_with?(expected.split("XXXXXX").last)
@@ -91,30 +102,42 @@ def test_pretty_print_persisted
91102
actual = +""
92103
PP.pp(topic, StringIO.new(actual))
93104
expected = <<~PRETTY
94-
#<Topic:0x\\w+
95-
id: 1,
96-
title: "The First Topic",
97-
author_name: "David",
98-
author_email_address: "[email protected]",
99-
written_on: 2003-07-16 14:28:11(?:\.2233)? UTC,
100-
bonus_time: 2000-01-01 14:28:00 UTC,
101-
last_read: Thu, 15 Apr 2004,
102-
content: "Have a nice day",
103-
important: nil,
104-
binary_content: nil,
105-
approved: false,
106-
replies_count: 1,
107-
unique_replies_count: 0,
108-
parent_id: nil,
109-
parent_title: nil,
110-
type: nil,
111-
group: nil,
112-
created_at: [^,]+,
113-
updated_at: [^,>]+>
105+
#<Topic:0x\\w+ id: 1>
114106
PRETTY
115107
assert_match(/\A#{expected}\z/, actual)
116108
end
117109

110+
def test_pretty_print_full
111+
Topic.with(attributes_for_inspect: :all) do
112+
topic = topics(:first)
113+
actual = +""
114+
PP.pp(topic, StringIO.new(actual))
115+
expected = <<~PRETTY
116+
#<Topic:0x\\w+
117+
id: 1,
118+
title: "The First Topic",
119+
author_name: "David",
120+
author_email_address: "[email protected]",
121+
written_on: 2003-07-16 14:28:11(?:\.2233)? UTC,
122+
bonus_time: 2000-01-01 14:28:00 UTC,
123+
last_read: Thu, 15 Apr 2004,
124+
content: "Have a nice day",
125+
important: nil,
126+
binary_content: nil,
127+
approved: false,
128+
replies_count: 1,
129+
unique_replies_count: 0,
130+
parent_id: nil,
131+
parent_title: nil,
132+
type: nil,
133+
group: nil,
134+
created_at: [^,]+,
135+
updated_at: [^,>]+>
136+
PRETTY
137+
assert_match(/\A#{expected}\z/, actual)
138+
end
139+
end
140+
118141
def test_pretty_print_uninitialized
119142
topic = Topic.allocate
120143
actual = +""

activerecord/test/cases/filter_attributes_test.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ class FilterAttributesTest < ActiveRecord::TestCase
1111
fixtures :"admin/users", :"admin/accounts"
1212

1313
setup do
14+
@previous_attributes_for_inspect = ActiveRecord::Base.attributes_for_inspect
15+
ActiveRecord::Base.attributes_for_inspect = :all
1416
@previous_filter_attributes = ActiveRecord::Base.filter_attributes
1517
ActiveRecord::Base.filter_attributes = [:name]
1618
ActiveRecord.use_yaml_unsafe_load = true
1719
end
1820

1921
teardown do
22+
ActiveRecord::Base.attributes_for_inspect = @previous_attributes_for_inspect
2023
ActiveRecord::Base.filter_attributes = @previous_filter_attributes
2124
end
2225

activerecord/test/models/developer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class SymbolIgnoredDeveloper < ActiveRecord::Base
132132
class AuditLog < ActiveRecord::Base
133133
belongs_to :developer, validate: true
134134
belongs_to :unvalidated_developer, class_name: "Developer"
135+
136+
self.attributes_for_inspect = [:id, :message]
135137
end
136138

137139
class AuditLogRequired < ActiveRecord::Base

railties/test/application/configuration_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4718,6 +4718,44 @@ def view_test
47184718
assert_equal(:html4, Rails.application.config.dom_testing_default_html_version)
47194719
end
47204720

4721+
test "sets ActiveRecord::Base.attributes_for_inspect to [:id] when config.consider_all_requests_local = false" do
4722+
add_to_config "config.consider_all_requests_local = false"
4723+
4724+
app "production"
4725+
4726+
assert_equal [:id], ActiveRecord::Base.attributes_for_inspect
4727+
end
4728+
4729+
test "sets ActiveRecord::Base.attributes_for_inspect to :all when config.consider_all_requests_local = true" do
4730+
add_to_config "config.consider_all_requests_local = true"
4731+
4732+
app "development"
4733+
4734+
assert_equal :all, ActiveRecord::Base.attributes_for_inspect
4735+
end
4736+
4737+
test "app configuration takes precedence over default" do
4738+
add_to_config "config.consider_all_requests_local = true"
4739+
add_to_config "config.active_record.attributes_for_inspect = [:foo]"
4740+
4741+
app "development"
4742+
4743+
assert_equal [:foo], ActiveRecord::Base.attributes_for_inspect
4744+
end
4745+
4746+
test "model's configuration takes precedence over default" do
4747+
add_to_config "config.consider_all_requests_local = true"
4748+
app_file "app/models/foo.rb", <<-RUBY
4749+
class Foo < ApplicationRecord
4750+
self.attributes_for_inspect = [:foo]
4751+
end
4752+
RUBY
4753+
4754+
app "development"
4755+
4756+
assert_equal [:foo], Foo.attributes_for_inspect
4757+
end
4758+
47214759
private
47224760
def set_custom_config(contents, config_source = "custom".inspect)
47234761
app_file "config/custom.yml", contents

0 commit comments

Comments
 (0)