Skip to content

Commit 639b23b

Browse files
committed
add/delete str/dt/cat dynamically from __dir__ (fix for pandas-dev#9627)
1 parent ce5df98 commit 639b23b

File tree

7 files changed

+70
-9
lines changed

7 files changed

+70
-9
lines changed

doc/source/whatsnew/v0.16.1.txt

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Enhancements
6262

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

65+
- Add/delete ``str/dt/cat`` accessors dynamically from ``__dir__``. (:issue:`9910`)
66+
6567
- ``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>`
6668

6769
.. _whatsnew_0161.enhancements.categoricalindex:

pandas/core/base.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,22 @@ def __unicode__(self):
8686
# Should be overwritten by base classes
8787
return object.__repr__(self)
8888

89-
def _local_dir(self):
90-
""" provide addtional __dir__ for this object """
91-
return []
89+
def _dir_additions(self):
90+
""" add addtional __dir__ for this object """
91+
return set()
92+
93+
def _dir_deletions(self):
94+
""" delete unwanted __dir__ for this object """
95+
return set()
9296

9397
def __dir__(self):
9498
"""
9599
Provide method name lookup and completion
96100
Only provide 'public' methods
97101
"""
98-
return list(sorted(list(set(dir(type(self)) + self._local_dir()))))
102+
rv = set(dir(type(self)))
103+
rv = (rv - self._dir_deletions()) | self._dir_additions()
104+
return sorted(rv)
99105

100106
def _reset_cache(self, key=None):
101107
"""
@@ -518,6 +524,16 @@ def _make_str_accessor(self):
518524

519525
str = AccessorProperty(StringMethods, _make_str_accessor)
520526

527+
def _dir_additions(self):
528+
return set()
529+
530+
def _dir_deletions(self):
531+
try:
532+
getattr(self, 'str')
533+
except AttributeError:
534+
return set(['str'])
535+
return set()
536+
521537
_shared_docs['drop_duplicates'] = (
522538
"""Return %(klass)s with duplicate values removed
523539

pandas/core/generic.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,10 @@ def __unicode__(self):
146146
prepr = '[%s]' % ','.join(map(com.pprint_thing, self))
147147
return '%s(%s)' % (self.__class__.__name__, prepr)
148148

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

154154
@property
155155
def _constructor_sliced(self):

pandas/core/groupby.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,8 @@ def _set_result_index_ordered(self, result):
498498
result.index = self.obj.index
499499
return result
500500

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

504504
def __getattr__(self, attr):
505505
if attr in self._internal_names_set:

pandas/core/series.py

+15
Original file line numberDiff line numberDiff line change
@@ -2521,6 +2521,21 @@ def _make_cat_accessor(self):
25212521

25222522
cat = base.AccessorProperty(CategoricalAccessor, _make_cat_accessor)
25232523

2524+
def _dir_deletions(self):
2525+
return self._accessors
2526+
2527+
def _dir_additions(self):
2528+
rv = set()
2529+
# these accessors are mutually exclusive, so break loop when one exists
2530+
for accessor in self._accessors:
2531+
try:
2532+
getattr(self, accessor)
2533+
rv.add(accessor)
2534+
break
2535+
except AttributeError:
2536+
pass
2537+
return rv
2538+
25242539
Series._setup_axes(['index'], info_axis=0, stat_axis=0,
25252540
aliases={'rows': 0})
25262541
Series._add_numeric_operations()

pandas/tests/test_index.py

+8
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,14 @@ def test_str_attribute(self):
12831283
expected = Series(range(2), index=['a1', 'a2'])
12841284
tm.assert_series_equal(s[s.index.str.startswith('a')], expected)
12851285

1286+
def test_tab_completion(self):
1287+
# GH 9910
1288+
idx = Index(list('abcd'))
1289+
self.assertTrue('str' in idx.__dir__())
1290+
1291+
idx = Index(range(4))
1292+
self.assertTrue('str' not in idx.__dir__())
1293+
12861294
def test_indexing_doesnt_change_class(self):
12871295
idx = Index([1, 2, 3, 'a', 'b', 'c'])
12881296

pandas/tests/test_series.py

+20
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,26 @@ def test_dt_accessor_api(self):
242242
s.dt
243243
self.assertFalse(hasattr(s, 'dt'))
244244

245+
def test_tab_completion(self):
246+
# GH 9910
247+
s = Series(list('abcd'))
248+
# Series of str values should have .str but not .dt/.cat in __dir__
249+
self.assertTrue('str' in s.__dir__())
250+
self.assertTrue('dt' not in s.__dir__())
251+
self.assertTrue('cat' not in s.__dir__())
252+
253+
# similiarly for .dt
254+
s = Series(date_range('1/1/2015', periods=5))
255+
self.assertTrue('dt' in s.__dir__())
256+
self.assertTrue('str' not in s.__dir__())
257+
self.assertTrue('cat' not in s.__dir__())
258+
259+
# similiarly for .cat
260+
s = Series(list('abbcd'), dtype="category")
261+
self.assertTrue('cat' in s.__dir__())
262+
self.assertTrue('str' not in s.__dir__())
263+
self.assertTrue('dt' not in s.__dir__())
264+
245265
def test_binop_maybe_preserve_name(self):
246266

247267
# names match, preserve

0 commit comments

Comments
 (0)