4

Suppose you want to call a method foo on object bar, but somehow while typing the method invocation you intuitively treated foo as a property and you typed bar.foo instead of bar.foo() (with parenthesis). Now, both are syntactically correct, so no error is raised, but semantically very different. It happened to me several times already (my experience in Ruby makes it even worse) and caused me dearly in terms of long and confusing debugging sessions.

Is there a way to make Python interpreter print a warning in such cases - whenever you access an attribute which is callable, but you haven't actually called it?

For the record - I thought about overriding __getattribute__ but it's messy and ultimately won't achieve the goal since function invocation via () happens after __getattribute__ has returned.

  • You want a global setting that warns this in *all* cases? – Alex Hall Apr 01 '18 at 21:47
  • I can't think of anything better so yes, I guess I do. – Maciej Satkiewicz Apr 01 '18 at 21:57
  • As we can read in the docs, functions (callables) are Python first class objects. If you find a solution for your problem, it would be very interesting, although I believe it would do more harm than good. It is more likely you would want to access a method like an attribute than to be warned about not calling it (because of a typo). Good luck! – Gomes J. A. Apr 01 '18 at 22:26

2 Answers2

2

This can't be done in all cases because sometimes you don't want to call the method, e.g. you might want to store it as a callable to be used later, like callback = object.method.

But you can use static analysis tools such as pylint or PyCharm (my recommendation) that warn you if you write a statement that looks pointless, e.g. object.method without any assignment.

Furthermore if you write x = obj.get_x but meant get_x(), then later when you try to use x a static analysis tool may be able to warn you (if you're lucky) that x is a method but an instance of X is expected.

Alex Hall
  • 34,833
  • 5
  • 57
  • 89
  • Thanks for the answer! I agree, sometimes you do want to store callable - but usually you don't do this that often, hence logging all the instances of such situation in your code could be a reliable tool while debugging. I tried pylint, but I guess I'm not that lucky - it ignores examples like this `x = obj.get_x`. Other cases would be list comprehensions - writing `[obj.get_x for obj in objs]` instead of `[obj.get_x() for obj in objs]` - that seems really tricky for static analysis tools. I just tired PyCharm and it was no better. Btw. I prefer plain Sublime instead of IDE's ;P – Maciej Satkiewicz Apr 02 '18 at 00:57
2

It was quite challenging, but I think i get it done! My code isn't very complicated, but you need to be well aware of metaclasses.

Metaclass and wrapper (WarnIfNotCalled.py):

class Notifier:                                                             
    def __init__(self, name, obj, callback):                                
        self.callback = callback                                            
        self.name = name                                                    
        self.obj = obj                                                      
        self.called = False                                                 
    def __call__(self, *args, **kwargs):                                    
        self.callback(self.obj, *args, **kwargs)                            
        self.called = True                                                  
    def __del__(self):                                                      
        if not self.called:                                                 
            print("Warning! {} function hasn't been called!".format(self.name))

class WarnIfNotCalled(type):                                                
    def __new__(cls, name, bases, dct):                                     
        dct_func = {}                                                       
        for name, val in dct.copy().items():                                
            if name.startswith('__') or not callable(val):                  
                continue                                                    
            else:                                                           
                dct_func[name] = val                                        
                del dct[name]                                               
        def getattr(self, name):                                            
            if name in dct_func:                                            
                return Notifier(name, self, dct_func[name])                 
        dct['__getattr__'] = getattr                                        
        return super(WarnIfNotCalled, cls).__new__(cls, name, bases, dct) 

It's very easy to use - just specify a metaclass

from WarnIfNotCalled import WarnIfNotCalled

class A(metaclass = WarnIfNotCalled):                                       
    def foo(self):                                                          
        print("foo has been called")                                        
    def bar(self, x):                                                       
        print("bar has been called and x =", x)

If you didn't forget to call these functions, everything works as usual

a = A()                                                                         

a.foo()                                                                         
a.bar(5)

Output:

foo has been called
bar has been called and x = 5

But if you DID forget:

a = A()                                                                         

a.foo                                                                           
a.bar  

You see the following

Warning! foo function hasn't been called!
Warning! bar function hasn't been called!

Happy debugging!

Scarabyte
  • 303
  • 1
  • 5
  • 1
    Ok, I think it's not the perfect solution, because it requires setting the class's metaclass. Still, I salute that you faced the challenge! I'm working on my solution, I hope I'll be able to post something as good as yours. – Right leg Apr 01 '18 at 23:38
  • Thanks for your reply! I think its surprisingly close to what I'm looking for. As I understand it - every invocation of `a.foo` returns a new Notifier instance and then we rely on the `__del__` hook to notify us if we called it. Is it reliable? Can I assume that `__del__` will always be executed? What about things like `x = a.foo` or `[a.foo for a in aas]`? More importantly - I would like to be able to debug such a thing quickly over many classes - how would I do that? I guess replacing the `type.__new__` method in the beginning of my script would work but I think it's not possible... – Maciej Satkiewicz Apr 02 '18 at 01:11