ronaldoussoren / pyobjc

The Python <-> Objective-C Bridge with bindings for macOS frameworks
https://pyobjc.readthedocs.io
563 stars 47 forks source link

AttributeError: 'super' object has no attribute ... #549

Closed typemytype closed 1 year ago

typemytype commented 1 year ago

Describe the bug

Calling super() in a subclasses of NSObject (like a NSView) triggers an error.

import AppKit

class Sub(AppKit.NSView):

    def viewDidMoveToWindow(self):
        super().viewDidMoveToWindow()

print("has viewDidMoveToWindow:", "viewDidMoveToWindow" in dir(AppKit.NSView))

s = Sub.alloc().init()
s.viewDidMoveToWindow()

Platform information

this worked fine in pyobjc 9.0.1

thanks!!

ronaldoussoren commented 1 year ago

You must import objc.super using from objc import super to reliably use super with Cocoa classes.

I see I have to change something in the documentation, this should be mentioned in PyObjC's introduction but it isn't.

Background: PyObjC's proxy for Cocoa classes dynamically looks for methods as needed, both for performance reasons and because Cocoa classes can change at runtime without a way to cheaply detect this. Because of this the __dict__ of those Cocoa proxy classes tend to be incomplete. The implementation of builtin.super assumes that the __dict__ is always up-to-date, and hence can fail when using Cocoa classes.

I've written a PEP that tries to fix this PEP 447, but now think this is too complex and that a better solution is possible. I haven't gotten around to fully flesh out a proposal for that though (and in any case, such a proposal would end up in Python 3.13 at the earliest).

justvanrossum commented 1 year ago

Can objc.super be reliably used for non Cocoa classes, too? That is, can we do from objc import super in modules that use super() for both Cocoa classes and ordinary Python classes?

typemytype commented 1 year ago

this still fails with pyobjc 9.1.1 and objc.super()


import AppKit
import objc

print(objc.__version__)

class Sub(AppKit.NSObject):

    def init(self):
        print("init")
        self = objc.super().init()
        return self

s = Sub.alloc().init()
    self = objc.super().init()
           ^^^^^^^^^^^^
RuntimeError: super(): __class__ cell not found
sys:1: UninitializedDeallocWarning: leaking an uninitialized object of type Sub
ronaldoussoren commented 1 year ago

Can objc.super be reliably used for non Cocoa classes, too? That is, can we do from objc import super in modules that use super() for both Cocoa classes and ordinary Python classes?

Yes, you can. objc.super is a subclass of builtin.super that overrides the attribute resolution with some special casing for Cocoa classes and behaves like the builtin super class otherwise.

ronaldoussoren commented 1 year ago

this still fails with pyobjc 9.1.1 and objc.super()

import AppKit
import objc

print(objc.__version__)

class Sub(AppKit.NSObject):

    def init(self):
        print("init")
        self = objc.super().init()
        return self

s = Sub.alloc().init()
    self = objc.super().init()
           ^^^^^^^^^^^^
RuntimeError: super(): __class__ cell not found
sys:1: UninitializedDeallocWarning: leaking an uninitialized object of type Sub

That's because you really have to use from objc import super to be able to use the zero-argument form of super. Python's compiler special-cases code generation for zero-argument calls to super(), and not for other forms. That's the cause of the error.


import AppKit
import objc
from objc import super

print(objc.__version__)

class Sub(AppKit.NSObject):

    def init(self):
        print("init")
        self = super().init()
        return self

s = Sub.alloc().init()
typemytype commented 1 year ago

thanks for the explanation!

glyph commented 1 year ago

In my case, this was baffling to discover, because it looked like it worked correctly locally in local & alias builds for me, but then failed in release builds. I wonder if it would be possible to at least have a linter for this somehow, if a clearer error when using the wrong super() is not possible?

ronaldoussoren commented 1 year ago

In my case, this was baffling to discover, because it looked like it worked correctly locally in local & alias builds for me, but then failed in release builds. I wonder if it would be possible to at least have a linter for this somehow, if a clearer error when using the wrong super() is not possible?

I'm afraid not. builtin.super walks the MRO and pokes directly into the class __dict__ with PyDict_* APIs. This means a class cannot observe that this happens.

I've considered changing the type of the __dict__ attribute to one with a __missing__ implementation but AFAIK that's not something supported by CPython. A longer term solution requires changes to CPython itself. My current idea is PEP 447 is too invasive and to add a __super__ method instead, but haven't had time yet to fully consider the impact of this, let alone write a PEP.

ronaldoussoren commented 1 year ago

A linter is possible, but not something I can work on in the short term.

ronaldoussoren commented 1 year ago

There now is some documentation. Leaving the issue open for now to look into linter possibilities.

ronaldoussoren commented 1 year ago

Hmmm...

Because of the way calls to argument less super is implemented in CPython it might be possible to detect this at runtime. In particular: there will be a __classcell__ key in the class dict when one or more methods use argument less super. The hard part is to reliably detect what super binds to in the class.

Something that could be good enough (in objc._transform.processClassDict):


    if "__classcell__" in class_dict:
       if "__module__" in class_dict:
           mod = sys.modules[class_dict["__module__"]]
           if not hasattr(mod, "super") or mod.super is not objc.super:
               warnings.warn("Objective-C subclass uses super() but super is not objc.super", ...)

Probably with configuring the used warning to always report. The code above is completely untested at this time.

ronaldoussoren commented 1 year ago
import objc

NSObject = objc.lookUpClass("NSObject")

class MyObject(NSObject):
    def init(self):
        self = super().init()
        if self is None:
            return None

        self.a = 42
        return self
$ python3 a.py

/a.py:5: ObjCSuperWarning: Objective-C subclass uses super(), but super is not objc.super
  class MyObject(NSObject):
ronaldoussoren commented 1 year ago

Changeset d6e9b63 warns about using the wrong super at runtime.

That's not entirely ideal because the warning will mostly be invisible when using a tool like py2app to create a standalone application, but makes it a lot easier to detect the issue (and this also found a recent bug in the PyObjC testsuite itself...)

typemytype commented 1 year ago

super! thanks