diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e5677dc814..4a88128f6f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -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 #=> "#" + ``` + + 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 #=> "#" + ``` + + 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 #=> "#" + ``` + + 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 diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index dd25889880..c26b49a6a7 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -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 .attributes_for_inspect 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" - end + 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 pp record @@ -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 diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index ce84fef68b..dc07737b0d 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -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 diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index b2bd7a0bc4..be1fda81c1 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -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 diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 24187c36fd..d6c86786b4 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -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.inspect + assert_equal %(#), 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.inspect + end end def test_inspect_instance_with_lambda_date_formatter before = Time::DATE_FORMATS[:inspect] - Time::DATE_FORMATS[:inspect] = ->(date) { "my_format" } - topic = topics(:first) - assert_equal %(#), topic.inspect + Topic.with(attributes_for_inspect: [:id, :last_read]) do + Time::DATE_FORMATS[:inspect] = ->(date) { "my_format" } + topic = topics(:first) + assert_equal %(#), topic.inspect + end ensure Time::DATE_FORMATS[:inspect] = before end @@ -38,8 +48,10 @@ def test_inspect_new_instance end def test_inspect_limited_select_instance - assert_equal %(#), Topic.all.merge!(select: "id", where: "id = 1").first.inspect - assert_equal %(#), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect + Topic.with(attributes_for_inspect: [:id, :title]) do + assert_equal %(#), Topic.all.merge!(select: "id", where: "id = 1").first.inspect + assert_equal %(#), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect + end end def test_inspect_instance_with_non_primary_key_id_attribute @@ -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 + # + 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 + # + STRING end def test_pretty_print_new @@ -61,26 +91,7 @@ def test_pretty_print_new actual = +"" PP.pp(topic, StringIO.new(actual)) expected = <<~PRETTY - # + # PRETTY assert actual.start_with?(expected.split("XXXXXX").first) assert actual.end_with?(expected.split("XXXXXX").last) @@ -91,30 +102,42 @@ def test_pretty_print_persisted actual = +"" PP.pp(topic, StringIO.new(actual)) expected = <<~PRETTY - #]+> + # 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)) + expected = <<~PRETTY + #]+> + PRETTY + assert_match(/\A#{expected}\z/, actual) + end + end + def test_pretty_print_uninitialized topic = Topic.allocate actual = +"" diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb index 748c7c8fc0..b407f5e9ba 100644 --- a/activerecord/test/cases/filter_attributes_test.rb +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -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 diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 22361a49a2..d2844547ad 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -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 diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index e3e32e2f7d..347f420ac6 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -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