After an evaluation, GNOME has moved from Bugzilla to GitLab. Learn more about GitLab.
No new issues can be reported in GNOME Bugzilla anymore.
To report an issue in a GNOME project, go to GNOME GitLab.
Do not go to GNOME Gitlab for: Bluefish, Doxygen, GnuCash, GStreamer, java-gnome, LDTP, NetworkManager, Tomboy.
Bug 723081 - Unable to connect Gtk signals
Unable to connect Gtk signals
Status: RESOLVED NOTABUG
Product: pygobject
Classification: Bindings
Component: general
unspecified
Other Linux
: Normal normal
: ---
Assigned To: Nobody's working on this now (help wanted and appreciated)
Python bindings maintainers
Depends on:
Blocks:
 
 
Reported: 2014-01-27 10:01 UTC by Javier Hernández
Modified: 2014-02-17 16:30 UTC
See Also:
GNOME target: ---
GNOME version: ---


Attachments
test case (8.84 KB, application/x-compressed-tar)
2014-02-13 01:14 UTC, Javier Hernández
Details

Description Javier Hernández 2014-01-27 10:01:54 UTC
Hi

I'm facing some problems when trying to connect the signals from a UI.
Currently I am getting:

Traceback (most recent call last):
  • File "/usr/lib/python3.3/site-packages/gi/overrides/Gtk.py", line 394 in _full_callback
    handler, args = self._extract_handler_and_args(obj_or_map, handler_name)
  • File "/usr/lib/python3.3/site-packages/gi/overrides/Gtk.py", line 364 in _extract_handler_and_args
    if isinstance(obj_or_map, collections.Mapping):
  • File "/usr/lib/python3.3/abc.py", line 189 in __instancecheck__
    return any(cls.__subclasscheck__(c) for c in {subclass, subtype})
  • File "/usr/lib/python3.3/abc.py", line 189 in <genexpr>
    return any(cls.__subclasscheck__(c) for c in {subclass, subtype})
  • File "/usr/lib/python3.3/abc.py", line 218 in __subclasscheck__
    if issubclass(subclass, rcls):
TypeError: issubclass() arg 1 must be a class

The affected code is https://git.gnome.org/browse/accerciser/tree/plugins/event_monitor.py#n100

I'm wondering if the problem is related to the fact that my class inherits from another class and I could be missing something relevant in my code, since I can successfully connect the signals by not running the 'isinstance' evaluation.

Thanks in advance,
Javi
Comment 1 Simon Feltman 2014-02-01 07:28:42 UTC
I took a look through the code and noticed multiple inheritance with the GObject base (GtkScrolledWindow) as a secondary class in the MRO. I'm not sure if this is the problem but it does stick out to me a bit:

class ViewportPlugin(Plugin, gtk.ScrolledWindow):
  def __init__(...)
    Plugin.__init__(self, node, message_manager)
    gtk.ScrolledWindow.__init__(self)


You might try flipping this around to read:

class ViewportPlugin(gtk.ScrolledWindow, Plugin):
  def __init__(...)
    gtk.ScrolledWindow.__init__(self)
    Plugin.__init__(self, node, message_manager)


It would also be helpful to attempt a simplified reproducer of the bug (preferably a failing unittest). The class hierarchy seems to read as follows:

EventMonitor(ViewportPlugin)
ViewportPlugin(Plugin, gtk.ScrolledWindow)
Plugin(Tools)
Tools(object)
Comment 2 Javier Hernández 2014-02-11 19:37:43 UTC
Hi Simon,

After investigating this I've just confirmed that the problem is not related to multiple inheritance, but related to the fact that accerciser's Plugin class wraps all callable objects from its plugins (including the callbacks we want to use as handlers for the signals). This is the code wrapping the methods [1]

I'll try to figure out how to avoid this error from Accerciser's source code. If you have any clue/idea about it, don't hesitate to dump your thoughts here.


Best,
Javi

[1]: https://git.gnome.org/browse/accerciser/tree/src/lib/accerciser/plugin/base_plugin.py#n112
Comment 3 Javier Hernández 2014-02-13 01:14:11 UTC
Created attachment 268971 [details]
test case

Hi again.

Attached you'll find a test case that reproduces the bug that I've isolated from accerciser.

Hope this helps to clarify what's going wrong with this.
Comment 4 Simon Feltman 2014-02-13 04:55:14 UTC
(In reply to comment #3)
> Hope this helps to clarify what's going wrong with this.

It does thanks. The problem is __getattribute__ is wrapping all callable attributes of a plugin instance. This includes the __class__ attribute which also happens to be callable. So when isinstance(plugin, collections.Mapping) is called, it looks at the __class__ attribute and instead of receiving the instances class object, it gets back an instance of _PluginMethodWrapper which is going to be problematic for the Python internals. Basically it breaks duck typing of how an instance should behave:

>>> o = VeryParentClass()
>>> issubclass(o.__class__, VeryParentClass)
TypeError: issubclass() arg 1 must be a class

You could list specific attrs which should not be wrapped:
   if callable(obj) and name not in {'__class__', '__new__', '__init__', ...}:
      ...

or more generally:
   if callable(obj) and not (name.startswith('__') and name.endswith('__')):
      ...

I'm guessing the architecture here is to make the application more robust in regards to plugins which might raise exceptions? The implementation seems like it will have maintenance cost beyond this particular problem. I tend to prefer composition with a proxy like accessor over inheritance:

####

# Plugin API
import traceback
import functools

def logexception(func):
    @functools.wraps(func)
    def newfunc(*args, **kwargs):
        # use Exception otherwise KeyboardInterrupt won't get through
        try:
            return func(*args, **kwargs)
        except Exception:
            traceback.print_exc()
    return newfunc

class Plugin(object):
    def method(self):
        raise NotImplementedError

class PluginAccessor(Plugin):
    def __init__(self, plugin):
        self.plugin = plugin

    # Explicitly wrap each function of the plugin API (preferred approach)
    @logexception
    def method(self):
        return self.plugin.method()

    # Alternatively use __getattr__ for wrapping all non-builtin attributes
    # which is safer than using __getattribute___
    def __getattr__(self, name):
        func = getattr(self.plugin, name)
        if callable(func):
            return logexception(func)
        return func

# Some external plugin
class MyPlugin(Plugin):
    def method(self):
        raise ValueError('unintended plugin error')

    def other_method(self):
        raise ValueError('another unintended plugin error')


# Always work with a plugin instance through a PluginAccessor
# this should also be usable when passed to Builder.connect_signals
accessor = PluginAccessor(MyPlugin())
accessor.method()
accessor.other_method()
print(issubclass(accessor.__class__, Plugin))  # True
print("exceptions in plugins won't bubble to the main program")

####
Comment 5 Javier Hernández 2014-02-17 16:29:58 UTC
Hi again.

(In reply to comment #4)
> (In reply to comment #3)
> > Hope this helps to clarify what's going wrong with this.
> 
> It does thanks. The problem is __getattribute__ is wrapping all callable
> attributes of a plugin instance. This includes the __class__ attribute which
> also happens to be callable. So when isinstance(plugin, collections.Mapping) is
> called, it looks at the __class__ attribute and instead of receiving the
> instances class object, it gets back an instance of _PluginMethodWrapper which
> is going to be problematic for the Python internals. Basically it breaks duck
> typing of how an instance should behave:
> 
> >>> o = VeryParentClass()
> >>> issubclass(o.__class__, VeryParentClass)
> TypeError: issubclass() arg 1 must be a class
> 
> You could list specific attrs which should not be wrapped:
>    if callable(obj) and name not in {'__class__', '__new__', '__init__', ...}:
>       ...
> 
> or more generally:
>    if callable(obj) and not (name.startswith('__') and name.endswith('__')):
>       ...
> 

You're absolutely right, I didn't realized that attributes such as __class__ were being wrapped too (and its consequences) :/

Now I can get the code working just by not wrapping the __class__ attribute.


> I'm guessing the architecture here is to make the application more robust in
> regards to plugins which might raise exceptions? 

Yes, that's it. We wrap all callable objects from plugins in order to raise the exceptions in an homogeneous way.


> The implementation seems like it will have maintenance cost beyond this particular problem. I tend to prefer
> composition with a proxy like accessor over inheritance:
> 
> ####
> 
> # Plugin API
> import traceback
> import functools
> 
> def logexception(func):
>     @functools.wraps(func)
>     def newfunc(*args, **kwargs):
>         # use Exception otherwise KeyboardInterrupt won't get through
>         try:
>             return func(*args, **kwargs)
>         except Exception:
>             traceback.print_exc()
>     return newfunc
> 
> class Plugin(object):
>     def method(self):
>         raise NotImplementedError
> 
> class PluginAccessor(Plugin):
>     def __init__(self, plugin):
>         self.plugin = plugin
> 
>     # Explicitly wrap each function of the plugin API (preferred approach)
>     @logexception
>     def method(self):
>         return self.plugin.method()
> 
>     # Alternatively use __getattr__ for wrapping all non-builtin attributes
>     # which is safer than using __getattribute___
>     def __getattr__(self, name):
>         func = getattr(self.plugin, name)
>         if callable(func):
>             return logexception(func)
>         return func
> 
> # Some external plugin
> class MyPlugin(Plugin):
>     def method(self):
>         raise ValueError('unintended plugin error')
> 
>     def other_method(self):
>         raise ValueError('another unintended plugin error')
> 
> 
> # Always work with a plugin instance through a PluginAccessor
> # this should also be usable when passed to Builder.connect_signals
> accessor = PluginAccessor(MyPlugin())
> accessor.method()
> accessor.other_method()
> print(issubclass(accessor.__class__, Plugin))  # True
> print("exceptions in plugins won't bubble to the main program")
> 
> ####

Totally agree. This looks like a better approach to me so I'll try to move the code ASAP.

Thanks fo helping me on this.
Take care!