Skip to content

add/delete str/dt/cat dynamically from __dir__ (fix for #9627) #9910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v0.16.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Enhancements

- Trying to write an excel file now raises ``NotImplementedError`` if the ``DataFrame`` has a ``MultiIndex`` instead of writing a broken Excel file. (:issue:`9794`)

- Add/delete ``str/dt/cat`` accessors dynamically from ``__dir__``. (:issue:`9910`)

- ``DataFrame`` and ``Series`` now have ``_constructor_expanddim`` property as overridable constructor for one higher dimensionality data. This should be used only when it is really needed, see :ref:`here <ref-subclassing-pandas>`

.. _whatsnew_0161.enhancements.categoricalindex:
Expand Down
24 changes: 20 additions & 4 deletions pandas/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,22 @@ def __unicode__(self):
# Should be overwritten by base classes
return object.__repr__(self)

def _local_dir(self):
""" provide addtional __dir__ for this object """
return []
def _dir_additions(self):
""" add addtional __dir__ for this object """
return set()

def _dir_deletions(self):
""" delete unwanted __dir__ for this object """
return set()

def __dir__(self):
"""
Provide method name lookup and completion
Only provide 'public' methods
"""
return list(sorted(list(set(dir(type(self)) + self._local_dir()))))
rv = set(dir(type(self)))
rv = (rv - self._dir_deletions()) | self._dir_additions()
return sorted(rv)

def _reset_cache(self, key=None):
"""
Expand Down Expand Up @@ -518,6 +524,16 @@ def _make_str_accessor(self):

str = AccessorProperty(StringMethods, _make_str_accessor)

def _dir_additions(self):
return set()

def _dir_deletions(self):
try:
getattr(self, 'str')
except AttributeError:
return set(['str'])
return set()

_shared_docs['drop_duplicates'] = (
"""Return %(klass)s with duplicate values removed

Expand Down
6 changes: 3 additions & 3 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ def __unicode__(self):
prepr = '[%s]' % ','.join(map(com.pprint_thing, self))
return '%s(%s)' % (self.__class__.__name__, prepr)

def _local_dir(self):
def _dir_additions(self):
""" add the string-like attributes from the info_axis """
return [c for c in self._info_axis
if isinstance(c, string_types) and isidentifier(c)]
return set([c for c in self._info_axis
if isinstance(c, string_types) and isidentifier(c)])

@property
def _constructor_sliced(self):
Expand Down
4 changes: 2 additions & 2 deletions pandas/core/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,8 @@ def _set_result_index_ordered(self, result):
result.index = self.obj.index
return result

def _local_dir(self):
return sorted(set(self.obj._local_dir() + list(self._apply_whitelist)))
def _dir_additions(self):
return self.obj._dir_additions() | self._apply_whitelist

def __getattr__(self, attr):
if attr in self._internal_names_set:
Expand Down
15 changes: 15 additions & 0 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,21 @@ def _make_cat_accessor(self):

cat = base.AccessorProperty(CategoricalAccessor, _make_cat_accessor)

def _dir_deletions(self):
return self._accessors

def _dir_additions(self):
rv = set()
# these accessors are mutually exclusive, so break loop when one exists
for accessor in self._accessors:
try:
getattr(self, accessor)
rv.add(accessor)
break
except AttributeError:
pass
return rv

Series._setup_axes(['index'], info_axis=0, stat_axis=0,
aliases={'rows': 0})
Series._add_numeric_operations()
Expand Down
8 changes: 8 additions & 0 deletions pandas/tests/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,14 @@ def test_str_attribute(self):
expected = Series(range(2), index=['a1', 'a2'])
tm.assert_series_equal(s[s.index.str.startswith('a')], expected)

def test_tab_completion(self):
# GH 9910
idx = Index(list('abcd'))
self.assertTrue('str' in dir(idx))

idx = Index(range(4))
self.assertTrue('str' not in dir(idx))

def test_indexing_doesnt_change_class(self):
idx = Index([1, 2, 3, 'a', 'b', 'c'])

Expand Down
20 changes: 20 additions & 0 deletions pandas/tests/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,26 @@ def test_dt_accessor_api(self):
s.dt
self.assertFalse(hasattr(s, 'dt'))

def test_tab_completion(self):
# GH 9910
s = Series(list('abcd'))
# Series of str values should have .str but not .dt/.cat in __dir__
self.assertTrue('str' in dir(s))
self.assertTrue('dt' not in dir(s))
self.assertTrue('cat' not in dir(s))

# similiarly for .dt
s = Series(date_range('1/1/2015', periods=5))
self.assertTrue('dt' in dir(s))
self.assertTrue('str' not in dir(s))
self.assertTrue('cat' not in dir(s))

# similiarly for .cat
s = Series(list('abbcd'), dtype="category")
self.assertTrue('cat' in dir(s))
self.assertTrue('str' not in dir(s))
self.assertTrue('dt' not in dir(s))

def test_binop_maybe_preserve_name(self):

# names match, preserve
Expand Down