Repository: rootpy/rootpy

Issue:
`snake_case_methods` breaks descriptors
This bug caused a regression in 01edd2d99feede77e74b3ae87a2c95ed0bdf33dd. Compare the output of this commit and the one beforehand:

```
[01edd2^] $ python -c "import rootpy.ROOT as R; print R.File.open"
<function root_open at 0x14d2488>
[01edd2] $ python -c "import rootpy.ROOT as R; print R.File.open"
<unbound method File.root_open>
```

The latter is not usable and breaks my code.
# What's the problem?

The problem is due to the way that [`snake_case_methods`](https://github.com/rootpy/rootpy/blob/master/rootpy/decorators.py#L155) (SCM) works. I thought I had fixed this in 29626c704158eab8cfcf00963b1627e11b4d04bd, but apparently not.

The root cause is in the way that SCM aliases existing methods. Prior to 29626c7, it did this by `getattr`ing the target function and `setattr`'ing it on the target class. The problem is that `getattr` actually "dereferences" the descriptor (`__get__`) of target function. That means that `staticmethod`s become ordinary functions, which then get tacked onto the target class and suddenly they look like unbound methods.

I tried to fix this by making it a `__dict__` lookup on the target class, which prevents the descriptor machinary from kicking in, but then 01edd2d shows that this isn't sufficient, because of inheritance - the target function might not be in the `cls.__dict__` but in one of its super classes - so we need to visit the super classes in order according to the mro, and look inside the `__dict__` of each one.
# What's needed to prevent this from happening again?

Two test cases:
1. Check that File.open works correctly
2. Check that `snake_case_methods` does the right thing.



Discussion:

Resolving patch:
diff --git a/rootpy/decorators.py b/rootpy/decorators.py
--- a/rootpy/decorators.py
+++ b/rootpy/decorators.py
@@ -133,6 +133,7 @@ def snake_case_methods(cls, debug=False):
             duplicate_idx.add(idx)
         except ValueError:
             seen.append(n)
+    
     for i, (name, member) in enumerate(members):
         if i in duplicate_idx:
             continue
@@ -140,7 +141,7 @@ def snake_case_methods(cls, debug=False):
         if name[0] == '_' or name.islower():
             continue
         # Is this a method of the ROOT base class?
-        if inspect.ismethod(member):
+        if inspect.ismethod(member) or inspect.isfunction(member):
             # convert CamelCase to snake_case
             new_name = camel_to_snake(name)
             if debug:
@@ -149,9 +150,20 @@ def snake_case_methods(cls, debug=False):
                     raise ValueError(
                             '%s is already a method for %s' %
                             (new_name, cls.__name__))
-
-            # Note, use a.__dict__[x] instead of get getattr(a, x) is needed
-            # here because "getattr" breaks static methods. Fall back to getattr.
-            value = cls.__dict__.get(name, getattr(cls, name))
+            
+            # Use a __dict__ lookup rather than getattr because we _want_ to
+            # obtain the _descriptor_, and not what the descriptor gives us when
+            # it is `getattr`'d.
+            value = None
+            for c in cls.mro():
+                if name in c.__dict__:
+                    value = c.__dict__[name]
+                    break
+            # <neo>Woah, a use for for-else</neo>
+            else:
+                # Weird. Maybe the item lives somewhere else, such as on the
+                # metaclass?
+                value = getattr(cls, name)
+            
             setattr(cls, new_name, value)
     return cls
diff --git a/rootpy/io/file.py b/rootpy/io/file.py
--- a/rootpy/io/file.py
+++ b/rootpy/io/file.py
@@ -463,7 +463,7 @@ def __repr__(self):
 
         return self.__str__()
 
-
+@snake_case_methods
 class _FileBase(_DirectoryBase):
 
     # Override .Open


Test patch:
diff --git a/rootpy/io/tests/test_file.py b/rootpy/io/tests/test_file.py
--- a/rootpy/io/tests/test_file.py
+++ b/rootpy/io/tests/test_file.py
@@ -48,6 +48,9 @@ def test_memfile():
         hist.Write()
         assert_equal(f['test'], hist)
 
+def test_file_open():
+    with MemFile.open("test", "recreate"):
+        pass
 
 def test_file_get():
 
diff --git a/rootpy/tests/test_decorators.py b/rootpy/tests/test_decorators.py
--- a/rootpy/tests/test_decorators.py
+++ b/rootpy/tests/test_decorators.py
@@ -6,7 +6,7 @@
                                snake_case_methods)
 from rootpy.io import TemporaryFile
 import rootpy
-from nose.tools import assert_true, raises
+from nose.tools import assert_equal, assert_true, raises
 
 
 def test_snake_case_methods():
@@ -31,6 +31,35 @@ def write(self): pass
     assert_true(hasattr(B, 'other_method'))
 
 
+def test_snake_case_methods_descriptor():
+
+    def f(_): pass
+
+    class A(object):
+        Prop = property(f)
+        Sm = staticmethod(f)
+        Cm = classmethod(f)
+        M = f
+    
+    class B(A):
+        cm = A.__dict__["Cm"]
+        m = A.__dict__["M"]
+        prop = A.__dict__["Prop"]
+        sm = A.__dict__["Sm"]
+    
+    @snake_case_methods
+    class snakeB(A):
+        pass
+        
+    # Ensure that no accidental descriptor dereferences happened inside
+    # `snake_case_methods`. This is checked by making sure that the types
+    # are the same between B and snakeB.
+    
+    for member in dir(snakeB):
+        if member.startswith("_"): continue
+        assert_equal(type(getattr(B, member)), type(getattr(snakeB, member)))
+        
+
 class Foo(Object, ROOT.TH1D):
 
     @method_file_check