diff --git a/doc/source/whatsnew/v0.16.1.txt b/doc/source/whatsnew/v0.16.1.txt index 3561822baf5eb..08d8ef9116367 100755 --- a/doc/source/whatsnew/v0.16.1.txt +++ b/doc/source/whatsnew/v0.16.1.txt @@ -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 ` .. _whatsnew_0161.enhancements.categoricalindex: diff --git a/pandas/core/base.py b/pandas/core/base.py index c0233a5a33308..9c27f3c7a2cc3 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -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): """ @@ -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 diff --git a/pandas/core/generic.py b/pandas/core/generic.py index f3d7c48c7d1f1..681cfc0f7a416 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -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): diff --git a/pandas/core/groupby.py b/pandas/core/groupby.py index e5b1a96f81677..afdfe98b79a97 100644 --- a/pandas/core/groupby.py +++ b/pandas/core/groupby.py @@ -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: diff --git a/pandas/core/series.py b/pandas/core/series.py index 685d44acafe53..4ad5e06693221 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -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() diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py index 1d59d1f3fbfe3..3c9dbd2e48cb6 100644 --- a/pandas/tests/test_index.py +++ b/pandas/tests/test_index.py @@ -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']) diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index b5ada4cf39b5e..f1a9e23796804 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -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