Merge pull request #49765 from andrewn617/configurable-inspect

Make the output of `ActiveRecord::Core#inspect` configurable.
This commit is contained in:
Rafael Mendonça França 2023-11-08 15:08:36 -05:00 committed by GitHub
commit ee42128bc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 63 deletions

@ -1,3 +1,34 @@
* Make the output of `ActiveRecord::Core#inspect` configurable.
By default, calling `inspect` on a record will yield a formatted string including just the `id`.
```ruby
Post.first.inspect #=> "#<Post id: 1>"
```
The attributes to be included in the output of `inspect` can be configured with
`ActiveRecord::Core#attributes_for_inspect`.
```ruby
Post.attributes_for_inspect = [:id, :title]
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
```
With the `attributes_for_inspect` set to `:all`, `inspect` will list all the record's attributes.
```ruby
Post.attributes_for_inspect = :all
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
```
In development and test mode, `attributes_for_inspect` will be set to `:all` by default.
You can also call `full_inspect` to get an inspection with all the attributes.
The attributes in `attribute_for_inspect` will also be used for `pretty_print`.
*Andrew Novoselac*
* Don't mark Float::INFINITY as changed when reassigning it
When saving a record with a float infinite value, it shouldn't mark as changed

@ -102,6 +102,9 @@ def self.configurations
class_attribute :shard_selector, instance_accessor: false, default: nil
# Specifies the attributes that will be included in the output of the #inspect method
class_attribute :attributes_for_inspect, instance_accessor: false, default: [:id]
def self.application_record_class? # :nodoc:
if ActiveRecord.application_record_class
self == ActiveRecord.application_record_class
@ -681,21 +684,14 @@ def connection_handler
self.class.connection_handler
end
# Returns the contents of the record as a nicely formatted string.
# Returns the attributes specified by <tt>.attributes_for_inspect</tt> as a nicely formatted string.
def inspect
# We check defined?(@attributes) not to issue warnings if the object is
# allocated but not initialized.
inspection = if defined?(@attributes) && @attributes
attribute_names.filter_map do |name|
if _has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
end.join(", ")
else
"not initialized"
inspect_with_attributes(attributes_for_inspect)
end
"#<#{self.class} #{inspection}>"
# Returns the full contents of the record as a nicely formatted string.
def full_inspect
inspect_with_attributes(attribute_names)
end
# 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)
return super if custom_inspect_method_defined?
pp.object_address_group(self) do
if defined?(@attributes) && @attributes
attr_names = self.class.attribute_names.select { |name| _has_attribute?(name) }
attr_names = attributes_for_inspect.select { |name| _has_attribute?(name.to_s) }
pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
attr_name = attr_name.to_s
pp.breakable " "
pp.group(1) do
pp.text attr_name
@ -771,5 +768,26 @@ def pretty_print(pp)
def inspection_filter
self.class.inspection_filter
end
def inspect_with_attributes(attributes_to_list)
# We check defined?(@attributes) not to issue warnings if the object is
# allocated but not initialized.
inspection = if defined?(@attributes) && @attributes
attributes_to_list.filter_map do |name|
name = name.to_s
if _has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
end.join(", ")
else
"not initialized"
end
"#<#{self.class} #{inspection}>"
end
def attributes_for_inspect
self.class.attributes_for_inspect == :all ? attribute_names : self.class.attributes_for_inspect
end
end
end

@ -458,5 +458,15 @@ class Railtie < Rails::Railtie # :nodoc:
end
end
end
initializer "active_record.attributes_for_inspect" do |app|
ActiveSupport.on_load(:active_record) do
if app.config.consider_all_requests_local
if app.config.active_record.attributes_for_inspect.nil?
ActiveRecord::Base.attributes_for_inspect = :all
end
end
end
end
end
end

@ -343,7 +343,7 @@ def deserialize(*)
end
test "attributes not backed by database columns appear in inspect" do
inspection = OverloadedType.new.inspect
inspection = OverloadedType.new.full_inspect
assert_includes inspection, "non_existent_decimal"
end

@ -17,18 +17,28 @@ def test_inspect_class
assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
end
def test_inspect_instance
def test_inspect_instance_includes_just_id_by_default
topic = topics(:first)
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", 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
assert_equal %(#<Topic id: 1>), topic.inspect
end
def test_inspect_includes_attributes_from_attributes_for_inspect
Topic.with(attributes_for_inspect: [:id, :title, :author_name]) do
topic = topics(:first)
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David">), topic.inspect
end
end
def test_inspect_instance_with_lambda_date_formatter
before = Time::DATE_FORMATS[:inspect]
Topic.with(attributes_for_inspect: [:id, :last_read]) do
Time::DATE_FORMATS[:inspect] = ->(date) { "my_format" }
topic = topics(:first)
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", 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
assert_equal %(#<Topic id: 1, last_read: "2004-04-15">), topic.inspect
end
ensure
Time::DATE_FORMATS[:inspect] = before
end
@ -38,9 +48,11 @@ def test_inspect_new_instance
end
def test_inspect_limited_select_instance
Topic.with(attributes_for_inspect: [:id, :title]) do
assert_equal %(#<Topic id: 1>), Topic.all.merge!(select: "id", where: "id = 1").first.inspect
assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect
end
end
def test_inspect_instance_with_non_primary_key_id_attribute
topic = topics(:first).becomes(TitlePrimaryKeyTopic)
@ -51,9 +63,27 @@ def test_inspect_class_without_table
assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
end
def test_inspect_with_attributes_for_inspect_all_lists_all_attributes
Topic.with(attributes_for_inspect: :all) do
topic = topics(:first)
assert_equal <<~STRING.squish, topic.inspect
#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", 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)}">
STRING
end
end
def test_inspect_relation_with_virtual_field
relation = Topic.limit(1).select("1 as virtual_field")
assert_match(/virtual_field: 1/, relation.inspect)
assert_match(/virtual_field: 1/, relation.first.full_inspect)
end
def test_full_inspect_lists_all_attributes
topic = topics(:first)
assert_equal <<~STRING.squish, topic.full_inspect
#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", 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)}">
STRING
end
def test_pretty_print_new
@ -61,32 +91,24 @@ def test_pretty_print_new
actual = +""
PP.pp(topic, StringIO.new(actual))
expected = <<~PRETTY
#<Topic:0xXXXXXX
id: nil,
title: nil,
author_name: nil,
author_email_address: "test@test.com",
written_on: nil,
bonus_time: nil,
last_read: nil,
content: nil,
important: nil,
binary_content: nil,
approved: true,
replies_count: 0,
unique_replies_count: 0,
parent_id: nil,
parent_title: nil,
type: nil,
group: nil,
created_at: nil,
updated_at: nil>
#<Topic:0xXXXXXX id: nil>
PRETTY
assert actual.start_with?(expected.split("XXXXXX").first)
assert actual.end_with?(expected.split("XXXXXX").last)
end
def test_pretty_print_persisted
topic = topics(:first)
actual = +""
PP.pp(topic, StringIO.new(actual))
expected = <<~PRETTY
#<Topic:0x\\w+ id: 1>
PRETTY
assert_match(/\A#{expected}\z/, actual)
end
def test_pretty_print_full
Topic.with(attributes_for_inspect: :all) do
topic = topics(:first)
actual = +""
PP.pp(topic, StringIO.new(actual))
@ -114,6 +136,7 @@ def test_pretty_print_persisted
PRETTY
assert_match(/\A#{expected}\z/, actual)
end
end
def test_pretty_print_uninitialized
topic = Topic.allocate

@ -11,12 +11,15 @@ class FilterAttributesTest < ActiveRecord::TestCase
fixtures :"admin/users", :"admin/accounts"
setup do
@previous_attributes_for_inspect = ActiveRecord::Base.attributes_for_inspect
ActiveRecord::Base.attributes_for_inspect = :all
@previous_filter_attributes = ActiveRecord::Base.filter_attributes
ActiveRecord::Base.filter_attributes = [:name]
ActiveRecord.use_yaml_unsafe_load = true
end
teardown do
ActiveRecord::Base.attributes_for_inspect = @previous_attributes_for_inspect
ActiveRecord::Base.filter_attributes = @previous_filter_attributes
end

@ -132,6 +132,8 @@ class SymbolIgnoredDeveloper < ActiveRecord::Base
class AuditLog < ActiveRecord::Base
belongs_to :developer, validate: true
belongs_to :unvalidated_developer, class_name: "Developer"
self.attributes_for_inspect = [:id, :message]
end
class AuditLogRequired < ActiveRecord::Base

@ -4718,6 +4718,44 @@ def view_test
assert_equal(:html4, Rails.application.config.dom_testing_default_html_version)
end
test "sets ActiveRecord::Base.attributes_for_inspect to [:id] when config.consider_all_requests_local = false" do
add_to_config "config.consider_all_requests_local = false"
app "production"
assert_equal [:id], ActiveRecord::Base.attributes_for_inspect
end
test "sets ActiveRecord::Base.attributes_for_inspect to :all when config.consider_all_requests_local = true" do
add_to_config "config.consider_all_requests_local = true"
app "development"
assert_equal :all, ActiveRecord::Base.attributes_for_inspect
end
test "app configuration takes precedence over default" do
add_to_config "config.consider_all_requests_local = true"
add_to_config "config.active_record.attributes_for_inspect = [:foo]"
app "development"
assert_equal [:foo], ActiveRecord::Base.attributes_for_inspect
end
test "model's configuration takes precedence over default" do
add_to_config "config.consider_all_requests_local = true"
app_file "app/models/foo.rb", <<-RUBY
class Foo < ApplicationRecord
self.attributes_for_inspect = [:foo]
end
RUBY
app "development"
assert_equal [:foo], Foo.attributes_for_inspect
end
private
def set_custom_config(contents, config_source = "custom".inspect)
app_file "config/custom.yml", contents