New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure associations are inherited even after subclasses are defined #39473
base: main
Are you sure you want to change the base?
Conversation
This is a common `class_attribute` implementation issue (e.g. rails#39467, rails#39468, rails#38871). For now, we should take care of the change propagation by themselves if we want to ensure the propagation after the `class_attribute` are accessed in subclasses. Fixes rails#20678.
I've partly picked `LiloHash` from rails#39487. Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
if default.is_a?(Hash) | ||
class_methods << inplace_updatable_class_attribute(name) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very interesting idea! I originally chose the class_store
approach because I didn't want to add (yet another) option to class_attribute
, and because some options did not seem suitable (e.g. instance_writer: true
). But I really like your approach, so I changed to the same!
descendants.each do |subclass| | ||
next if subclass.#{name}.equal?(#{name}) | ||
|
||
if !subclass.#{name}.key?(key) || prev_value&.equal?(subclass.#{name}[key]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can produce different outcomes for flyweight / frozen objects versus other objects. For example:
A = Class.new
A.class_attribute :things, default: {}
B = Class.new(A)
A.inplace_update_things(:point, [0, 0])
B.things = B.things.dup
B.inplace_update_things(:point, [0, 0])
A.inplace_update_things(:point, [1, 1])
B.things[:point] # => [0, 0]
A.inplace_update_things(:x, 0)
B.things = B.things.dup
B.inplace_update_things(:x, 0)
A.inplace_update_things(:x, 1)
B.things[:x] # => 1
My approach to fix this was to track overridden keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this does not perfectly detect overrides.
But I suppose this works well enough as a small utility that solves such like internal problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this small utility is focused to avoid allocation as much as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I am not a fan of extra allocation either. But I felt it was necessary because even if such problems are unlikely, they would be hard to diagnose when they did occur. Also, because it is implemented as an automatic feature of class_attribute
, it seems more public, and so I wanted to make it more foolproof.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added ff3e8f4 which should prevent Set allocation when the superclass's class_attribute
value is empty?
or undefined. If the update method is called on the superclass later (and its class_attribute
value becomes non-empty), the subclass's Set will be allocated and properly back-filled.
class LiloHash < Hash | ||
def []=(key, value) | ||
delete(key) | ||
super | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
This reverts commit 5f0e546. The purpose of the commit is to demonstrate how to generalize. In this case, the generalize allocates extra `descendants` array so I've reverted the commit.
68aef08
to
f153b8e
Compare
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This is a common
class_attribute
implementation issue (e.g. #39467,#39468, #38871).
For now, we should take care of the change propagation by themselves if
we want to ensure the propagation after the
class_attribute
areaccessed in subclasses.
Fixes #20678.