Repository: ipython/ipython

Issue:
_repr_html_ exception reporting corner case when using type(foo)
@michaelaye reported this in https://github.com/pydata/pandas/issues/6225,
but it seems to be an ipython issues, resulting from the recent https://github.com/ipython/ipython/pull/4745.

``` python
In [11]: class Foo(object):
    ...:     def _repr_html_(self):
    ...:         return "Me"
    ...: x=Foo()
In [12]: type(x)
/home/user1/src/ipython/IPython/core/formatters.py:199: FormatterWarning: 
Exception in text/html formatter: unbound method _repr_html_() must 
be called with Foo instance as first argument (got nothing instead)
  FormatterWarning,
```



Discussion:
Looks like we check for the existence of a `_repr_foo_` attribute, but not whether it can be called with no arguments.


Resolving patch:
diff --git a/IPython/config/loader.py b/IPython/config/loader.py
--- a/IPython/config/loader.py
+++ b/IPython/config/loader.py
@@ -246,11 +246,13 @@ def __getitem__(self, key):
                 c = Config()
                 dict.__setitem__(self, key, c)
                 return c
-            else:
+            elif not key.startswith('_'):
                 # undefined, create lazy value, used for container methods
                 v = LazyConfigValue()
                 dict.__setitem__(self, key, v)
                 return v
+            else:
+                raise KeyError
 
     def __setitem__(self, key, value):
         if _is_section_key(key):
diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py
--- a/IPython/core/formatters.py
+++ b/IPython/core/formatters.py
@@ -25,7 +25,9 @@
 
 # Stdlib imports
 import abc
+import inspect
 import sys
+import types
 import warnings
 
 from IPython.external.decorator import decorator
@@ -52,6 +54,32 @@
 # The main DisplayFormatter class
 #-----------------------------------------------------------------------------
 
+
+def _valid_formatter(f):
+    """Return whether an object is a valid formatter
+    
+    Cases checked:
+    
+    - bound methods             OK
+    - unbound methods           NO
+    - callable with zero args   OK
+    """
+    if isinstance(f, type(str.find)):
+        # unbound methods on compiled classes have type method_descriptor
+        return False
+    elif isinstance(f, types.BuiltinFunctionType):
+        # bound methods on compiled classes have type builtin_function
+        return True
+    elif callable(f):
+        # anything that works with zero args should be okay
+        try:
+            inspect.getcallargs(f)
+        except TypeError:
+            return False
+        else:
+            return True
+    return False
+
 class DisplayFormatter(Configurable):
 
     # When set to true only the default plain text formatter will be used.
@@ -312,7 +340,8 @@ def __call__(self, obj):
                 return printer(obj)
             # Finally look for special method names
             method = pretty._safe_getattr(obj, self.print_method, None)
-            if method is not None:
+            # print_method must be a bound method:
+            if _valid_formatter(method):
                 return method()
             return None
         else:


Test patch:
diff --git a/IPython/config/tests/test_loader.py b/IPython/config/tests/test_loader.py
--- a/IPython/config/tests/test_loader.py
+++ b/IPython/config/tests/test_loader.py
@@ -369,6 +369,14 @@ def test_getattr_not_section(self):
         assert isinstance(foo, LazyConfigValue)
         self.assertIn('foo', cfg)
 
+    def test_getattr_private_missing(self):
+        cfg = Config()
+        self.assertNotIn('_repr_html_', cfg)
+        with self.assertRaises(AttributeError):
+            _ = cfg._repr_html_
+        self.assertNotIn('_repr_html_', cfg)
+        self.assertEqual(len(cfg), 0)
+
     def test_getitem_not_section(self):
         cfg = Config()
         self.assertNotIn('foo', cfg)
diff --git a/IPython/core/tests/test_formatters.py b/IPython/core/tests/test_formatters.py
--- a/IPython/core/tests/test_formatters.py
+++ b/IPython/core/tests/test_formatters.py
@@ -8,6 +8,7 @@
     numpy = None
 import nose.tools as nt
 
+from IPython.config import Config
 from IPython.core.formatters import (
     PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key
 )
@@ -289,3 +290,33 @@ def test_pdf_formatter():
     pdf = MakePDF()
     f = PDFFormatter()
     nt.assert_equal(f(pdf), 'PDF')
+
+def test_print_method_bound():
+    f = HTMLFormatter()
+    class MyHTML(object):
+        def _repr_html_(self):
+            return "hello"
+
+    with capture_output() as captured:
+        result = f(MyHTML)
+    nt.assert_is(result, None)
+    nt.assert_not_in("FormatterWarning", captured.stderr)
+
+    with capture_output() as captured:
+        result = f(MyHTML())
+    nt.assert_equal(result, "hello")
+    nt.assert_equal(captured.stderr, "")
+
+def test_format_config():
+    """config objects don't pretend to support fancy reprs with lazy attrs"""
+    f = HTMLFormatter()
+    cfg = Config()
+    with capture_output() as captured:
+        result = f(cfg)
+    nt.assert_is(result, None)
+    nt.assert_equal(captured.stderr, "")
+
+    with capture_output() as captured:
+        result = f(Config)
+    nt.assert_is(result, None)
+    nt.assert_equal(captured.stderr, "")