Repository: ipython/ipython

Issue:
interact doesn't work with instance methods
This code:

``` python
from IPython.html.widgets import interact

class Foo(object):
    def show(self, x):
        print x

f = Foo()

interact(f.show, x=(1,10))
```

produces this exception:

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-58-b03b8685dfc0> in <module>()
      7 f = Foo()
      8 
----> 9 interact(f.show, x=(1,10))

/home/fperez/usr/lib/python2.7/site-packages/IPython/html/widgets/interaction.pyc in interact(__interact_f, **kwargs)
    235         f = __interact_f
    236         w = interactive(f, **kwargs)
--> 237         f.widget = w
    238         display(w)
    239         return f

AttributeError: 'instancemethod' object has no attribute 'widget'
```



Discussion:
Setting arbitrary attributes works on functions, but apparently not on methods. Simple fix is just to catch AttributeError and ignore it - it's not essential that the widget be attached to the object. Though maybe it needs to be stored somewhere so it doesn't fall out of scope and get GCed.

We just talked about it and I like @minrk's suggestion of wrapping it in a `lambda` if it's a method. That way the behavior from the outside still appears the same, in that we can return a function with those attributes.

People may have written code that assumes the widget is attached, so it's best to expose a uniform interface regardless of whether they started with a function or method.

You can achieve it without `lambda` if you use `f.__dict__["widget"]=w`, but this looks very unpythonic (and I did not test it in python3). Also wrapping the method into a `lambda` will not eliminate the problem that no deep copy of the object has been created. If the caller decides to modify the original object then the lambda function will also change its behaviour. So I would come up with one of the following:

```
def attach(callab):
    ''' uses lambda to wrap the method and does not need __dict__'''
    from copy import deepcopy
    from inspect import ismethod,isfunction
    if ismethod(callab):
        ccallab=deepcopy(callab)
        f=lambda x: ccallab(x)
    elif isfunction(callab):
        f=callab
    else:
        raise Exception()
    f.widget="dummy"
    return f

def attach2(callab):
    ''' uses no lambda to wrap the method but does need __dict__'''
    from copy import deepcopy
    from inspect import ismethod,isfunction
    if ismethod(callab):
        ccallab=deepcopy(callab)
        f=ccallab
    elif isfunction(callab):
        f=callab
    else:
        raise Exception()
    f.__dict__["widget"]="dummy"
    return f
```

EDIT:
I thought a while about this and making a copy does not seem to be apropriate, my bad. The reason why the `f.widget = w` does not work for methods in the first place is because of its `__setattr__` function which behaves differently than a normal function's `__setattr__` (does not allow for new entries to be created). So circumventing this call by `f.__dict__["widget"]=w` should be fine.

the workaround I found is wrapping the instance method with a regular function, which has the instance of this object as a global variable and calls the instance method within the function:

```
class Tank(object): #our tank object which has volume, accepts intake and outflow
    def __init__(self,volume):
        self.volume=self.initvol=volume
        self.dt=1
    def flow(self,qin,qout,dt=1):
        if dt!=self.dt:
            self.dt=dt
            self.volume=self.initvol
        self.volume+=(qin-qout)*dt
        print self.volume
        return self.volume
t=Tank(10)
print t.flow(10,20,2)
def flow(qin,qout,dt):
    global t
    return t.flow(qin,qout,dt)
interact(flow,qin=(0,10),qout=(0,10),dt=(0,10)) 
```


Resolving patch:
diff --git a/IPython/html/widgets/interaction.py b/IPython/html/widgets/interaction.py
--- a/IPython/html/widgets/interaction.py
+++ b/IPython/html/widgets/interaction.py
@@ -1,16 +1,7 @@
 """Interact with functions using widgets."""
 
-#-----------------------------------------------------------------------------
-# Copyright (c) 2013, the IPython Development Team.
-#
+# Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
-#
-# The full license is in the file COPYING.txt, distributed with this software.
-#-----------------------------------------------------------------------------
-
-#-----------------------------------------------------------------------------
-# Imports
-#-----------------------------------------------------------------------------
 
 from __future__ import print_function
 
@@ -30,10 +21,6 @@
 
 empty = Parameter.empty
 
-#-----------------------------------------------------------------------------
-# Classes and Functions
-#-----------------------------------------------------------------------------
-
 
 def _matches(o, pattern):
     """Match a pattern of types in a sequence."""
@@ -251,7 +238,13 @@ def interact(__interact_f=None, **kwargs):
         #        ...
         f = __interact_f
         w = interactive(f, **kwargs)
-        f.widget = w
+        try:
+            f.widget = w
+        except AttributeError:
+            # some things (instancemethods) can't have attributes attached,
+            # so wrap in a lambda
+            f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
+            f.widget = w
         display(w)
         return f
     else:
@@ -260,10 +253,7 @@ def interact(__interact_f=None, **kwargs):
         # def f(*args, **kwargs):
         #     ...
         def dec(f):
-            w = interactive(f, **kwargs)
-            f.widget = w
-            display(w)
-            return f
+            return interact(f, **kwargs)
         return dec
 
 def interact_manual(__interact_f=None, **kwargs):


Test patch:
diff --git a/IPython/html/widgets/tests/test_interaction.py b/IPython/html/widgets/tests/test_interaction.py
--- a/IPython/html/widgets/tests/test_interaction.py
+++ b/IPython/html/widgets/tests/test_interaction.py
@@ -365,6 +365,23 @@ def foo(a):
         value=5,
     )
 
+@nt.with_setup(clear_display)
+def test_interact_instancemethod():
+    class Foo(object):
+        def show(self, x):
+            print(x)
+
+    f = Foo()
+    
+    with tt.monkeypatch(interaction, 'display', record_display):
+        g = interact(f.show, x=(1,10))
+    nt.assert_equal(len(displayed), 1)
+    w = displayed[0].children[0]
+    check_widget(w,
+        cls=widgets.IntSlider,
+        value=5,
+    )
+
 @nt.with_setup(clear_display)
 def test_decorator_no_call():
     with tt.monkeypatch(interaction, 'display', record_display):