Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

__all__ feature, improvement to dir2, and tests for both #1529

Merged
merged 5 commits into from

3 participants

Tim Couper Thomas Kluyver Fernando Perez
Tim Couper

The 2 patches (relating to __all__ and dir2) are applied, plus the CBool change and a couple of tests to confirm the correct behaviour of complete with, and without, limit_to__all__ being set

Thomas Kluyver
Owner

N.B. For background, see discussion on PRs #1497 and #1528.

Fernando Perez
Owner

I've realized that this feature is potentially a bit dangerous, as modules don't always keep their __all__ too well. For example, numpy doesn't list float in __all__, presumably b/c it's a builtin. But this means that with this new feature on, users could be mislead into thinking that numpy.float isn't a valid dtype identifier, since they would see other similar ones but not float itself.

In any case, since the feature is off by default, it won't bite anyone by accident. Advanced users can decide to activate it and it will be their responsibility to be aware of what happens. Furthermore, since it can be easily toggled at runtime with %config, I'm not too worried.

The implementation looks clean, and with good tests, so I'm merging it (after review and also interactive testing). Many thanks for the contribution and the persistence of going through 3 PRs :) And also thanks @takluyver for your diligent work on it!

Fernando Perez fperez merged commit 47367a0 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 27, 2012
  1. Tim Couper

    added __all__ to completer.py and added basic tests for test_dir2

    drtimcouper authored
    Signed-off-by: Tim Couper <drtimcouper@gmail.com>
  2. Tim Couper

    Added test_dir2 for the dir2 (bonus) tests

    drtimcouper authored
    Signed-off-by: Tim Couper <drtimcouper@gmail.com>
  3. Tim Couper

    Changes to dir2 to remove duplicates fix: put limit_to__all__ default…

    drtimcouper authored
    … to 0 fix: the doctest to reflect the new limit_to__all__
    
    Signed-off-by: Tim Couper <drtimcouper@gmail.com>
  4. Tim Couper
  5. Tim Couper
This page is out of date. Refresh to see the latest.
31 IPython/core/completer.py
View
@@ -336,7 +336,7 @@ def attr_matches(self, text):
#io.rprint('Completer->attr_matches, txt=%r' % text) # dbg
# Another option, seems to work great. Catches things like ''.<tab>
m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text)
-
+
if m:
expr, attr = m.group(1, 3)
elif self.greedy:
@@ -346,7 +346,7 @@ def attr_matches(self, text):
expr, attr = m2.group(1,2)
else:
return []
-
+
try:
obj = eval(expr, self.namespace)
except:
@@ -355,7 +355,10 @@ def attr_matches(self, text):
except:
return []
- words = dir2(obj)
+ if self.limit_to__all__ and hasattr(obj, '__all__'):
+ words = get__all__entries(obj)
+ else:
+ words = dir2(obj)
try:
words = generics.complete_object(obj, words)
@@ -371,6 +374,16 @@ def attr_matches(self, text):
return res
+def get__all__entries(obj):
+ """returns the strings in the __all__ attribute"""
+ try:
+ words = getattr(obj,'__all__')
+ except:
+ return []
+
+ return [w for w in words if isinstance(w, basestring)]
+
+
class IPCompleter(Completer):
"""Extension of the completer class with IPython-specific features"""
@@ -403,6 +416,16 @@ def _greedy_changed(self, name, old, new):
When 0: nothing will be excluded.
"""
)
+ limit_to__all__ = CBool(default_value=False, config=True,
+ help="""Instruct the completer to use __all__ for the completion
+
+ Specifically, when completing on ``object.<tab>``.
+
+ When True: only those names in obj.__all__ will be included.
+
+ When False [default]: the __all__ attribute is ignored
+ """
+ )
def __init__(self, shell=None, namespace=None, global_namespace=None,
alias_table=None, use_readline=True,
@@ -602,7 +625,7 @@ def alias_matches(self, text):
def python_matches(self,text):
"""Match attributes or global python names"""
-
+
#io.rprint('Completer->python_matches, txt=%r' % text) # dbg
if "." in text:
try:
6 IPython/core/magic.py
View
@@ -3753,6 +3753,12 @@ def magic_config(self, s):
Whether to merge completion results into a single list
If False, only the completion results from the first non-empty completer
will be returned.
+ IPCompleter.limit_to__all__=<CBool>
+ Current: False
+ Instruct the completer to use __all__ for the completion
+ Specifically, when completing on ``object.<tab>``.
+ When True: only those names in obj.__all__ will be included.
+ When False [default]: the __all__ attribute is ignored
IPCompleter.greedy=<CBool>
Current: False
Activate greedy completion
38 IPython/core/tests/test_completer.py
View
@@ -229,4 +229,40 @@ def test_omit__names():
nt.assert_false('ip.__str__' in matches)
nt.assert_false('ip._hidden_attr' in matches)
del ip._hidden_attr
-
+
+
+def test_limit_to__all__False_ok():
+ ip = get_ipython()
+ c = ip.Completer
+ ip.ex('class D: x=24')
+ ip.ex('d=D()')
+ cfg = Config()
+ cfg.IPCompleter.limit_to__all__ = False
+ c.update_config(cfg)
+ s, matches = c.complete('d.')
+ nt.assert_true('d.x' in matches)
+
+def test_limit_to__all__True_ok():
+ ip = get_ipython()
+ c = ip.Completer
+ ip.ex('class D: x=24')
+ ip.ex('d=D()')
+ ip.ex("d.__all__=['z']")
+ cfg = Config()
+ cfg.IPCompleter.limit_to__all__ = True
+ c.update_config(cfg)
+ s, matches = c.complete('d.')
+ nt.assert_true('d.z' in matches)
+ nt.assert_false('d.x' in matches)
+
+def test_get__all__entries_ok():
+ class A(object):
+ __all__ = ['x', 1]
+ words = completer.get__all__entries(A())
+ nt.assert_equal(words, ['x'])
+
+def test_get__all__entries_no__all__ok():
+ class A(object):
+ pass
+ words = completer.get__all__entries(A())
+ nt.assert_equal(words, [])
57 IPython/utils/dir2.py
View
@@ -19,7 +19,7 @@
def get_class_members(cls):
ret = dir(cls)
- if hasattr(cls,'__bases__'):
+ if hasattr(cls, '__bases__'):
try:
bases = cls.__bases__
except AttributeError:
@@ -46,49 +46,28 @@ def dir2(obj):
# Start building the attribute list via dir(), and then complete it
# with a few extra special-purpose calls.
- words = dir(obj)
- if hasattr(obj,'__class__'):
- words.append('__class__')
- words.extend(get_class_members(obj.__class__))
- #if '__base__' in words: 1/0
+ words = set(dir(obj))
- # Some libraries (such as traits) may introduce duplicates, we want to
- # track and clean this up if it happens
- may_have_dupes = False
+ if hasattr(obj, '__class__'):
+ #words.add('__class__')
+ words |= set(get_class_members(obj.__class__))
- # this is the 'dir' function for objects with Enthought's traits
- if hasattr(obj, 'trait_names'):
- try:
- words.extend(obj.trait_names())
- may_have_dupes = True
- except TypeError:
- # This will happen if `obj` is a class and not an instance.
- pass
- except AttributeError:
- # `obj` lied to hasatter (e.g. Pyro), ignore
- pass
-
- # Support for PyCrust-style _getAttributeNames magic method.
- if hasattr(obj, '_getAttributeNames'):
- try:
- words.extend(obj._getAttributeNames())
- may_have_dupes = True
- except TypeError:
- # `obj` is a class and not an instance. Ignore
- # this error.
- pass
- except AttributeError:
- # `obj` lied to hasatter (e.g. Pyro), ignore
- pass
- if may_have_dupes:
- # eliminate possible duplicates, as some traits may also
- # appear as normal attributes in the dir() call.
- words = list(set(words))
- words.sort()
+ # for objects with Enthought's traits, add trait_names() list
+ # for PyCrust-style, add _getAttributeNames() magic method list
+ for attr in ('trait_names', '_getAttributeNames'):
+ if hasattr(obj, attr):
+ try:
+ func = getattr(obj, attr)
+ if callable(func):
+ words |= set(func())
+ except:
+ # TypeError: obj is class not instance
+ pass
# filter out non-string attributes which may be stuffed by dir() calls
# and poor coding in third-party modules
- return [w for w in words if isinstance(w, basestring)]
+ words = [w for w in words if isinstance(w, basestring)]
+ return sorted(words)
52 IPython/utils/tests/test_dir2.py
View
@@ -0,0 +1,52 @@
+import nose.tools as nt
+from IPython.utils.dir2 import dir2
+
+
+class Base(object):
+ x = 1
+ z = 23
+
+
+def test_base():
+ res = dir2(Base())
+ assert ('x' in res)
+ assert ('z' in res)
+ assert ('y' not in res)
+ assert ('__class__' in res)
+ nt.assert_equal(res.count('x'), 1)
+ nt.assert_equal(res.count('__class__'), 1)
+
+def test_SubClass():
+
+ class SubClass(Base):
+ y = 2
+
+ res = dir2(SubClass())
+ assert ('y' in res)
+ nt.assert_equal(res.count('y'), 1)
+ nt.assert_equal(res.count('x'), 1)
+
+
+def test_SubClass_with_trait_names_method():
+
+ class SubClass(Base):
+ y = 2
+ def trait_names(self):
+ return ['t', 'umbrella']
+
+ res = dir2(SubClass())
+ assert('trait_names' in res)
+ assert('umbrella' in res)
+ nt.assert_equal(res[-6:], ['t', 'trait_names','umbrella', 'x','y','z'])
+ nt.assert_equal(res.count('t'), 1)
+
+
+def test_SubClass_with_trait_names_attr():
+ # usecase: trait_names is used in a class describing psychological classification
+
+ class SubClass(Base):
+ y = 2
+ trait_names = 44
+
+ res = dir2(SubClass())
+ assert('trait_names' in res)
Something went wrong with that request. Please try again.