attr_json
attr_json copied to clipboard
Defining attr_json attributes on the parent class results in "NoMethodError: super: no superclass method"
(note from maintainer: There are a variety of things that could cause a NoMethodError. If you have such an error but don't know why, please file a separate Issue or, even better, start at topic in Discussions, instead of adding a "me too" here. Thank you, and thanks for getting in touch!)
I'm trying to upgrade to the latest attr_json
and running into an issue that was not happening with the previous version of attr_json (we've been using this gem for many years).
I have several cases where I define the attr_json
attribute in the parent (abstract) class, then use it in the child class. With the new version, I can only get it to work if I define the attribute in each of the child classes.
Is this expected behaviour, and if so, can you suggest how to get around this?
Hi @marclennox , thank you for getting in touch with an error report! And I'm glad to hear you've been using it for years!
What you describe does not sound like expected behavior, it sounds like a bug. If so, I'm sorry we didn't catch it before the 2.0.0 release!
But I'm having trouble reproducing. Subclasses inheriting the attr_json defined in the parent seem to work fine for me -- I even do it in my own app. If something broke for you upgrading, then probably there is some kind of bug, but I'm sorry, I haven't been able to reproduce it.
Can you give me a "failing test case", or the skeleton of one with some code that can reproduce the problem? Are your classes AttrJson::Record
or AttrJson::Model
?
OK, I'll have to try and create a reproducible test case for you. I'm using AttrJson::Record.
It should also be noted that it's just the setter
method that is throwing this error. The getter
method works fine.
Thanks! I cannot reproduce, setter works fine for me too.
I suspect there is something specific in your code that triggers it where the simplest possible reproduction does not -- which doesn't mean it's not an attr_json bug.
Reproduction would be great, but in the meantime if you could send a whole stack trace for the error raised, that might be a clue.
The super
method I am guessing it is -- should be calling the "setter" method that was provided by the call to Rails attribute
.
attr_json 2.0 always creates an underlying Rails attribute, for more natural/consistent Rails integration. And relies on it being there and being called by the setter, for some behaviors to work. Such as rails dirty tracking, and full casting.
So if it's not there.... things might break, guarding the call to super with a condition to check super exists may not be sufficient.
So I'm very curious under what conditions the super method ordinarily provided by rails attribute
would not exist. I can't reproduce it. And might need to in order to fix things for those conditions. Which I am definitely interested in doing! So appreciate your help!
Thanks @jrochkind! I am really glad that you have adopted the Rails attributes API for this gem. I will be honest and say that I was hoping this would happen for some time. Let me see if I can gather some more clues for you as to what is going on.
Full stack trace, if it helps
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/activemodel-7.0.4.2/lib/active_model/attribute_methods.rb:455:in `method_missing'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/attr_json-2.0.0/lib/attr_json/record.rb:205:in `block (2 levels) in attr_json'
(pry):6:in `__pry__'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:290:in `eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:290:in `evaluate_ruby'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:659:in `handle_line'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:261:in `block (2 levels) in eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:260:in `catch'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:260:in `block in eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:259:in `catch'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:259:in `eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:77:in `block in repl'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:67:in `loop'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:67:in `repl'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:38:in `block in start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/input_lock.rb:61:in `__with_ownership'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/input_lock.rb:78:in `with_ownership'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:38:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:15:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-byebug-3.10.1/lib/pry-byebug/pry_ext.rb:15:in `start_with_pry_byebug'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_class.rb:194:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands/console/console_command.rb:70:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands/console/console_command.rb:19:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands/console/console_command.rb:102:in `perform'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/thor-1.2.1/lib/thor/command.rb:27:in `run'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/thor-1.2.1/lib/thor/invocation.rb:127:in `invoke_command'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/thor-1.2.1/lib/thor.rb:392:in `dispatch'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/command/base.rb:87:in `perform'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/command.rb:48:in `invoke'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands.rb:18:in `<main>'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
bin/rails:6:in `<main>'
Interestingly, the Rails attribute shows up if I invoke the attribute_names
method, so the attribute is defined at the AR level.
An aside, but
I am really glad that you have adopted the Rails attributes API for this gem. I will be honest and say that I was hoping this would happen for some time.
Did you know about the feature in attr_json
to pass rails_attribute: true
to attr_json
to get an accompanying rails attribute? Pretty much what happens now is you always get this. And I polished off a few other rough edges, that I noticed trying to look at things from all sides, but nobody had reported before (if they had, I would have tried to resolve them sooner!)
So it's not really too much different now than if you had just done rails_attribute: true
, but either way I'm glad attr_json
is working for you, and sorry for the regression!
OK, I did not know that! Is this something that was added perhaps in the past year?
I tried downgrading attr_json back to pre-2.0.0 and using the rails_attribute: true
option. No errors in this case, and I can see the attribute in the list of model attributes.
It was added in 1.2.0, released June 22, 2020. Actually that CHANGELOG may have been for an improvement to it, it may have been added even earlier, before I started maintaining a reliable CHANGELOG.
But anyway there's a lot to try and document/advertise in attr_json, I am not surprised if you missed it. One reason why I made it just the always-on default now -- plus it simplifies the code a bit. Several years later, it seems to me there's no reason not to want this.
The rails attribute syncing should work even better/more reliably in 2.0.0 .
Clearly some kind of regression has happened in 2.0.0 though. I'd love to fix it if we can figure out how to reproduce it.
This is definitely a weird issue. I was playing around with injecting breakpoints in the underlying Rails attribute definition methods, and after returning to the main console, I was able to instantiate the object and use the setter method without error. I can only get it to work though if I place a breakpoint deep inside these underlying methods, so it almost seems like it's a timing issue.
I've been able to get around this issue temporarily using a hack. Instead of directly calling attr_json
in the abstract base class, I use the Ruby inherited
callback to call it on all child classes. In order to prevent attr_json
from being called multiple times, I manually prevent it from being called on any base classes.
class Tracking < ApplicationRecord
self.abstract_class = true
def self.inherited(child)
super
return if %w[Tracking Tracking::Location].include?(child.name)
child.attr_json :sync_interval, :integer
end
...
Hi @marclennox , thanks for investigating this! I was unexpectedly away from work this last week with an unplanned situation.
If there is a bug in attr_json, I would really like to fix it! But I'm still confused about what's going on, and unable to reproduce it myself. (I definitely call attr_json
in my own code in base classes that are then used by sub-classes! And if I make a simple test setup doing what you say, it also has no problems for me).
At first I thought you meant the problem only exhibited when you put a breakpoint in, but I don't think that's what you meant, you mean it's hard to investigate with a breakpoint because things seem to change in presence of breakpoint. (I have myself noticed some issues with trying to use debugger breakpoints nearby Rails attribute
stuff, that I think are there with or without attr_json, and are annoying!)
No luck in making a reproduction case can you can share? If you can share your actual codebase with me, and instructions for reproducing from a console with that, I'd be willing to take a look at it too!
@marclennox I still can't reproduce, but I wonder -- do you use the ActiveRecord becomes
method in your code anywhere?
I wonder if the error is due to that, and the same thing as was also reported at #189 -- I was able to reproduce that one, with use of becomes
.
No we don't use that. I haven't done any investigation since, as we were able to get around it using the self.interited(child)
method as described in my previous comment. At some point I'll see what I can find, just haven't had the time to spend on it. Thanks @jrochkind
@garrywilliams I have deleted your comments and moved them to a separate issue at #198, because I do not believe it is the same thing.
I am encountering similar situation, see follow case code:
Environments:
- rails 6.0
- ruby 2.7.7
- attr_json 2.0.1
# application_record.rb
include ::AttrJson::Record
attr_json_config(default_container_attribute: :data)
# animal.rb
class Animal < ApplicationRecord
class << self
def base_class
Animal
end
end
attr_json :foots_count, :integer
end
# dog.rb
class Dog < Animal
class << self
def base_class
Dog
end
end
end
dog = Dog.new
dog.foots_count # nil
dog.foots_count = 4 # NoMethodError: super
It only happen in test environment, I found three ways to avoid (or resolve) it.
- set
confog.eager_load = true
# config/environments/test.rb config.eager_load = true
- change
Dog
's.base_class
toAnimal
# dog.rb class << self def base_class Animal end end
- run
Animal.new
beforeDog.new
Animal.new dog = Dog.new dog.foots_count = 4
I guess it caused by rails autoloading, in production, eager_load default value is true, so it would not happen. In my case, I have to set different base_class for other purpose such as polymorphic. In way 3, when superclass (Animal) call first, its rails attribute(foots_count) loaded to AR model, so call super will not raise NoMethodError.
These three ways is not best way for resolving, now I am trying to figure out how rails autoload to cause this problem :(
@marsz Thanks for getting in touch! I'm not sure if this is the same case or not.
But is it possible your case (and maybe OP case) is this Rails-documented issue with Single-Table Inheritance in general, and not special to attr_json? https://edgeguides.rubyonrails.org/autoloading_and_reloading_constants.html#single-table-inheritance.
Your comments on it showing up in test environment (and maybe development too?) but not production reminded me of this. And the things you say solve it seem like they could be this. What do you think?
There is a bit of an annoying thing there in Rails with single-table inheritance. You get it with or without attr_json, it's not an attr_json issue. Perhaps I should mention it in the README, although there's already so much in the README I worry that adding more doesn't increase the chances of anyone seeing anything I add!
I'm not sure if it makes sense that @marclennox's original issue is this too or not, anyt houghts @marclennox ?