Skip to content
This repository

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

Merged
merged 5 commits into from about 2 years ago

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
Collaborator

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 April 15, 2012
Fernando Perez fperez closed this April 15, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 5 unique commits by 1 author.

Mar 27, 2012
Tim Couper added __all__ to completer.py and added basic tests for test_dir2
Signed-off-by: Tim Couper <drtimcouper@gmail.com>
cf95840
Tim Couper Added test_dir2 for the dir2 (bonus) tests
Signed-off-by: Tim Couper <drtimcouper@gmail.com>
e1b3c3d
Tim Couper Changes to dir2 to remove duplicates fix: put limit_to__all__ default…
… to 0 fix: the doctest to reflect the new limit_to__all__

Signed-off-by: Tim Couper <drtimcouper@gmail.com>
1ff2bdd
Tim Couper Changed type of limit_to__all__ from Enum to CBool abd9e16
Tim Couper added tests for limit_to__all__ for False and True cases d0e7b4d
This page is out of date. Refresh to see the latest.
31  IPython/core/completer.py
@@ -336,7 +336,7 @@ def attr_matches(self, text):
336 336
         #io.rprint('Completer->attr_matches, txt=%r' % text) # dbg
337 337
         # Another option, seems to work great. Catches things like ''.<tab>
338 338
         m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text)
339  
-
  339
+    
340 340
         if m:
341 341
             expr, attr = m.group(1, 3)
342 342
         elif self.greedy:
@@ -346,7 +346,7 @@ def attr_matches(self, text):
346 346
             expr, attr = m2.group(1,2)
347 347
         else:
348 348
             return []
349  
-
  349
+    
350 350
         try:
351 351
             obj = eval(expr, self.namespace)
352 352
         except:
@@ -355,7 +355,10 @@ def attr_matches(self, text):
355 355
             except:
356 356
                 return []
357 357
 
358  
-        words = dir2(obj)
  358
+        if self.limit_to__all__ and hasattr(obj, '__all__'):
  359
+            words = get__all__entries(obj)
  360
+        else: 
  361
+            words = dir2(obj)
359 362
 
360 363
         try:
361 364
             words = generics.complete_object(obj, words)
@@ -371,6 +374,16 @@ def attr_matches(self, text):
371 374
         return res
372 375
 
373 376
 
  377
+def get__all__entries(obj):
  378
+    """returns the strings in the __all__ attribute"""
  379
+    try:
  380
+        words = getattr(obj,'__all__')
  381
+    except:
  382
+        return []
  383
+    
  384
+    return [w for w in words if isinstance(w, basestring)]
  385
+
  386
+
374 387
 class IPCompleter(Completer):
375 388
     """Extension of the completer class with IPython-specific features"""
376 389
 
@@ -403,6 +416,16 @@ def _greedy_changed(self, name, old, new):
403 416
         When 0: nothing will be excluded.
404 417
         """
405 418
     )
  419
+    limit_to__all__ = CBool(default_value=False, config=True,
  420
+        help="""Instruct the completer to use __all__ for the completion
  421
+        
  422
+        Specifically, when completing on ``object.<tab>``.
  423
+        
  424
+        When True: only those names in obj.__all__ will be included.
  425
+        
  426
+        When False [default]: the __all__ attribute is ignored 
  427
+        """
  428
+    )
406 429
 
407 430
     def __init__(self, shell=None, namespace=None, global_namespace=None,
408 431
                  alias_table=None, use_readline=True,
@@ -602,7 +625,7 @@ def alias_matches(self, text):
602 625
 
603 626
     def python_matches(self,text):
604 627
         """Match attributes or global python names"""
605  
-
  628
+        
606 629
         #io.rprint('Completer->python_matches, txt=%r' % text) # dbg
607 630
         if "." in text:
608 631
             try:
6  IPython/core/magic.py
@@ -3753,6 +3753,12 @@ def magic_config(self, s):
3753 3753
                 Whether to merge completion results into a single list
3754 3754
                 If False, only the completion results from the first non-empty completer
3755 3755
                 will be returned.
  3756
+            IPCompleter.limit_to__all__=<CBool>
  3757
+                Current: False
  3758
+                Instruct the completer to use __all__ for the completion
  3759
+                Specifically, when completing on ``object.<tab>``.
  3760
+                When True: only those names in obj.__all__ will be included.
  3761
+                When False [default]: the __all__ attribute is ignored
3756 3762
             IPCompleter.greedy=<CBool>
3757 3763
                 Current: False
3758 3764
                 Activate greedy completion
38  IPython/core/tests/test_completer.py
@@ -229,4 +229,40 @@ def test_omit__names():
229 229
     nt.assert_false('ip.__str__' in matches)
230 230
     nt.assert_false('ip._hidden_attr' in matches)
231 231
     del ip._hidden_attr
232  
-    
  232
+
  233
+
  234
+def test_limit_to__all__False_ok():
  235
+    ip = get_ipython()
  236
+    c = ip.Completer
  237
+    ip.ex('class D: x=24')
  238
+    ip.ex('d=D()')
  239
+    cfg = Config()
  240
+    cfg.IPCompleter.limit_to__all__ = False
  241
+    c.update_config(cfg)
  242
+    s, matches = c.complete('d.')
  243
+    nt.assert_true('d.x' in matches) 
  244
+
  245
+def test_limit_to__all__True_ok():
  246
+    ip = get_ipython()
  247
+    c = ip.Completer
  248
+    ip.ex('class D: x=24')
  249
+    ip.ex('d=D()')
  250
+    ip.ex("d.__all__=['z']")
  251
+    cfg = Config()
  252
+    cfg.IPCompleter.limit_to__all__ = True
  253
+    c.update_config(cfg)
  254
+    s, matches = c.complete('d.')
  255
+    nt.assert_true('d.z' in matches) 
  256
+    nt.assert_false('d.x' in matches)
  257
+
  258
+def test_get__all__entries_ok():
  259
+  class A(object):
  260
+    __all__ = ['x', 1]
  261
+  words = completer.get__all__entries(A())
  262
+  nt.assert_equal(words, ['x'])
  263
+
  264
+def test_get__all__entries_no__all__ok():
  265
+  class A(object):
  266
+      pass
  267
+  words = completer.get__all__entries(A())
  268
+  nt.assert_equal(words, [])
57  IPython/utils/dir2.py
@@ -19,7 +19,7 @@
19 19
 
20 20
 def get_class_members(cls):
21 21
     ret = dir(cls)
22  
-    if hasattr(cls,'__bases__'):
  22
+    if hasattr(cls, '__bases__'):
23 23
         try:
24 24
             bases = cls.__bases__
25 25
         except AttributeError:
@@ -46,49 +46,28 @@ def dir2(obj):
46 46
 
47 47
     # Start building the attribute list via dir(), and then complete it
48 48
     # with a few extra special-purpose calls.
49  
-    words = dir(obj)
50 49
 
51  
-    if hasattr(obj,'__class__'):
52  
-        words.append('__class__')
53  
-        words.extend(get_class_members(obj.__class__))
54  
-    #if '__base__' in words: 1/0
  50
+    words = set(dir(obj))
55 51
 
56  
-    # Some libraries (such as traits) may introduce duplicates, we want to
57  
-    # track and clean this up if it happens
58  
-    may_have_dupes = False
  52
+    if hasattr(obj, '__class__'):
  53
+        #words.add('__class__')
  54
+        words |= set(get_class_members(obj.__class__))
59 55
 
60  
-    # this is the 'dir' function for objects with Enthought's traits
61  
-    if hasattr(obj, 'trait_names'):
62  
-        try:
63  
-            words.extend(obj.trait_names())
64  
-            may_have_dupes = True
65  
-        except TypeError:
66  
-            # This will happen if `obj` is a class and not an instance.
67  
-            pass
68  
-        except AttributeError:
69  
-            # `obj` lied to hasatter (e.g. Pyro), ignore
70  
-            pass
71  
-
72  
-    # Support for PyCrust-style _getAttributeNames magic method.
73  
-    if hasattr(obj, '_getAttributeNames'):
74  
-        try:
75  
-            words.extend(obj._getAttributeNames())
76  
-            may_have_dupes = True
77  
-        except TypeError:
78  
-            # `obj` is a class and not an instance.  Ignore
79  
-            # this error.
80  
-            pass
81  
-        except AttributeError:
82  
-            # `obj` lied to hasatter (e.g. Pyro), ignore
83  
-            pass
84 56
 
85  
-    if may_have_dupes:
86  
-        # eliminate possible duplicates, as some traits may also
87  
-        # appear as normal attributes in the dir() call.
88  
-        words = list(set(words))
89  
-        words.sort()
  57
+    # for objects with Enthought's traits, add trait_names() list
  58
+    # for PyCrust-style, add _getAttributeNames() magic method list
  59
+    for attr in ('trait_names', '_getAttributeNames'):
  60
+        if hasattr(obj, attr):
  61
+            try:
  62
+                func = getattr(obj, attr)
  63
+                if callable(func):
  64
+                    words |= set(func())
  65
+            except:
  66
+                # TypeError: obj is class not instance
  67
+                pass
90 68
 
91 69
     # filter out non-string attributes which may be stuffed by dir() calls
92 70
     # and poor coding in third-party modules
93  
-    return [w for w in words if isinstance(w, basestring)]
94 71
 
  72
+    words = [w for w in words if isinstance(w, basestring)]
  73
+    return sorted(words)
52  IPython/utils/tests/test_dir2.py
... ...
@@ -0,0 +1,52 @@
  1
+import nose.tools as nt
  2
+from IPython.utils.dir2 import dir2
  3
+
  4
+
  5
+class Base(object):
  6
+    x = 1
  7
+    z = 23
  8
+
  9
+
  10
+def test_base():
  11
+    res = dir2(Base())
  12
+    assert ('x' in res)
  13
+    assert ('z' in res)
  14
+    assert ('y' not in res)
  15
+    assert ('__class__' in res)
  16
+    nt.assert_equal(res.count('x'), 1)
  17
+    nt.assert_equal(res.count('__class__'), 1)
  18
+
  19
+def test_SubClass():
  20
+
  21
+    class SubClass(Base):
  22
+        y = 2
  23
+
  24
+    res = dir2(SubClass())
  25
+    assert ('y' in res)
  26
+    nt.assert_equal(res.count('y'), 1)
  27
+    nt.assert_equal(res.count('x'), 1)
  28
+
  29
+
  30
+def test_SubClass_with_trait_names_method():
  31
+
  32
+    class SubClass(Base):
  33
+        y = 2
  34
+        def trait_names(self):
  35
+            return ['t', 'umbrella']
  36
+
  37
+    res = dir2(SubClass())
  38
+    assert('trait_names' in res)
  39
+    assert('umbrella' in res)
  40
+    nt.assert_equal(res[-6:], ['t', 'trait_names','umbrella', 'x','y','z'])
  41
+    nt.assert_equal(res.count('t'), 1)
  42
+
  43
+
  44
+def test_SubClass_with_trait_names_attr():
  45
+    # usecase: trait_names is used in a class describing psychological classification
  46
+
  47
+    class SubClass(Base):
  48
+        y = 2
  49
+        trait_names = 44
  50
+
  51
+    res = dir2(SubClass())
  52
+    assert('trait_names' in res)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.