Repository: pandas-dev/pandas

Issue:
KeyError: 'trait_names' in py3k
When tab completing in ipython notebook on `pd.options...`, nothing shows up, and on the backend (in iterm) I see a `KeyError: 'trait_names'`, with a longer traceback.

pd.version.version == '0.12.0'
python version == 3.3.2
IPython.version_info == (2, 0, 0, 'dev')

Pandas is latest from pip, and ipython is latest from git. It may be an IPython problem.

Traceback [here](https://gist.github.com/d10genes/6936334).

pandas.options should raise an AttributeError subclass
 `hasattr` expects `AttributeError` when looking up. Let's make it simple and backward compatible, replace `KeyError` with this class:

``` python
class OptionNotFound(AttributeError, KeyError):
    """Exception for pandas.options, backwards compatible with KeyError checks"""
```

cc @d10genes this should fix your problem.

simple example:

``` python
In [5]: class OptionNotFoundError(AttributeError, KeyError): pass

In [6]: class MyObject(self):
   ...:
KeyboardInterrupt

In [6]: class MyObject(object):
   ...:     @property
   ...:     def prop(self):
   ...:         raise OptionNotFoundError("HEYO!")
   ...:

In [7]: a = MyObject()

In [8]: hasattr(a, "prop")
Out[8]: False

In [9]: OptionNotFoundError = KeyError

In [10]: class MyObject(object):
    @property
    def prop(self):
        raise OptionNotFoundError("HEYO!")
   ....:

In [11]: a = MyObject()

In [12]: hasattr(a, "prop")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-12-7205b5401c9b> in <module>()
----> 1 hasattr(a, "prop")

<ipython-input-10-7a717a2e41df> in prop(self)
      2     @property
      3     def prop(self):
----> 4         raise OptionNotFoundError("HEYO!")
      5

KeyError: 'HEYO!'

In [13]:
```



Discussion:
`trait_names` does not appear in the 0.12.0 codebase, so I'm guessing it's an IPython problem.

Can you make sure this doesn't happen in Python 3 with a release version of IPython? Also, please try doing `dir(pd.options)` and see what happens.

Actually, I found it - this is what does it:

```
~/py-env/p3k/lib/python3.3/site-packages/IPython/utils/dir2.py in dir2(obj=<pandas.core.config.DictWrapper object>)
     59     # for PyCrust-style, add _getAttributeNames() magic method list
     60     for attr in ('trait_names', '_getAttributeNames'):
---> 61         if hasattr(obj, attr):
```

The problem is that the options object raises KeyError rather than AttributeError, which screws up hasattr.

``` python
In [4]: pd.options.banana
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-4-83a5f2c8a3eb> in <module>()
----> 1 pd.options.banana

/Users/jtratner/projects/python/pandas/pandas/core/config.py in __getattr__(self, key)
    203             prefix += "."
    204         prefix += key
--> 205         v = object.__getattribute__(self, "d")[key]
    206         if isinstance(v, dict):
    207             return DictWrapper(v, prefix)

KeyError: 'banana'
```

Basically, it's a misbehaving Python object.



Resolving patch:
diff --git a/pandas/core/config.py b/pandas/core/config.py
--- a/pandas/core/config.py
+++ b/pandas/core/config.py
@@ -65,6 +65,11 @@
 _reserved_keys = ['all']  # keys which have a special meaning
 
 
+class OptionError(AttributeError, KeyError):
+    """Exception for pandas.options, backwards compatible with KeyError
+    checks"""
+
+
 ##########################################
 # User API
 
@@ -73,9 +78,9 @@ def _get_single_key(pat, silent):
     if len(keys) == 0:
         if not silent:
             _warn_if_deprecated(pat)
-        raise KeyError('No such keys(s): %r' % pat)
+        raise OptionError('No such keys(s): %r' % pat)
     if len(keys) > 1:
-        raise KeyError('Pattern matched multiple keys')
+        raise OptionError('Pattern matched multiple keys')
     key = keys[0]
 
     if not silent:
@@ -147,7 +152,7 @@ def _describe_option(pat='', _print_desc=True):
 
     keys = _select_options(pat)
     if len(keys) == 0:
-        raise KeyError('No such keys(s)')
+        raise OptionError('No such keys(s)')
 
     s = u('')
     for k in keys:  # filter by pat
@@ -164,7 +169,7 @@ def _reset_option(pat):
     keys = _select_options(pat)
 
     if len(keys) == 0:
-        raise KeyError('No such keys(s)')
+        raise OptionError('No such keys(s)')
 
     if len(keys) > 1 and len(pat) < 4 and pat != 'all':
         raise ValueError('You must specify at least 4 characters when '
@@ -195,7 +200,7 @@ def __setattr__(self, key, val):
         if key in self.d and not isinstance(self.d[key], dict):
             _set_option(prefix, val)
         else:
-            raise KeyError("You can only set the value of existing options")
+            raise OptionError("You can only set the value of existing options")
 
     def __getattr__(self, key):
         prefix = object.__getattribute__(self, "prefix")
@@ -211,6 +216,7 @@ def __getattr__(self, key):
     def __dir__(self):
         return list(self.d.keys())
 
+
 # For user convenience,  we'd like to have the available options described
 # in the docstring. For dev convenience we'd like to generate the docstrings
 # dynamically instead of maintaining them by hand. To this, we use the
@@ -255,7 +261,7 @@ def __doc__(self):
 
 Raises
 ------
-KeyError if no such option exists
+OptionError if no such option exists
 
 {opts_desc}
 """
@@ -281,7 +287,7 @@ def __doc__(self):
 
 Raises
 ------
-KeyError if no such option exists
+OptionError if no such option exists
 
 {opts_desc}
 """
@@ -398,9 +404,9 @@ def register_option(key, defval, doc='', validator=None, cb=None):
     key = key.lower()
 
     if key in _registered_options:
-        raise KeyError("Option '%s' has already been registered" % key)
+        raise OptionError("Option '%s' has already been registered" % key)
     if key in _reserved_keys:
-        raise KeyError("Option '%s' is a reserved key" % key)
+        raise OptionError("Option '%s' is a reserved key" % key)
 
     # the default value should be legal
     if validator:
@@ -418,14 +424,14 @@ def register_option(key, defval, doc='', validator=None, cb=None):
     cursor = _global_config
     for i, p in enumerate(path[:-1]):
         if not isinstance(cursor, dict):
-            raise KeyError("Path prefix to option '%s' is already an option"
+            raise OptionError("Path prefix to option '%s' is already an option"
                            % '.'.join(path[:i]))
         if p not in cursor:
             cursor[p] = {}
         cursor = cursor[p]
 
     if not isinstance(cursor, dict):
-        raise KeyError("Path prefix to option '%s' is already an option"
+        raise OptionError("Path prefix to option '%s' is already an option"
                        % '.'.join(path[:-1]))
 
     cursor[path[-1]] = defval  # initialize
@@ -470,14 +476,14 @@ def deprecate_option(key, msg=None, rkey=None, removal_ver=None):
 
     Raises
     ------
-    KeyError - if key has already been deprecated.
+    OptionError - if key has already been deprecated.
 
     """
 
     key = key.lower()
 
     if key in _deprecated_options:
-        raise KeyError("Option '%s' has already been defined as deprecated."
+        raise OptionError("Option '%s' has already been defined as deprecated."
                        % key)
 
     _deprecated_options[key] = DeprecatedOption(key, msg, rkey, removal_ver)


Test patch: