Merge pull request #49765 from andrewn617/configurable-inspect
Make the output of `ActiveRecord::Core#inspect` configurable.
This commit is contained in:
commit
ee42128bc1
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user