From cf95840ed4258aa5b51b860acac3d52069f411da Mon Sep 17 00:00:00 2001 From: Tim Couper Date: Tue, 27 Mar 2012 11:52:14 +0100 Subject: [PATCH 1/5] added __all__ to completer.py and added basic tests for test_dir2 Signed-off-by: Tim Couper --- IPython/core/completer.py | 30 ++++++++++++++++++++++++---- IPython/core/tests/test_completer.py | 14 ++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index ae96e720ae8..f2428e82017 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -336,7 +336,6 @@ def attr_matches(self, text): #io.rprint('Completer->attr_matches, txt=%r' % text) # dbg # Another option, seems to work great. Catches things like ''. m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text) - if m: expr, attr = m.group(1, 3) elif self.greedy: @@ -346,7 +345,7 @@ def attr_matches(self, text): expr, attr = m2.group(1,2) else: return [] - + try: obj = eval(expr, self.namespace) except: @@ -355,7 +354,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 +373,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 +415,16 @@ def _greedy_changed(self, name, old, new): When 0: nothing will be excluded. """ ) + limit_to__all__ = Enum((0,1), default_value=1, config=True, + help="""Instruct the completer to use __all__ for the completion + + Specifically, when completing on ``object.``. + + When 1: only those names in obj.__all__ will be included. + + When 0 [default]: the values in the __all__ attribute are ignored + """ + ) def __init__(self, shell=None, namespace=None, global_namespace=None, alias_table=None, use_readline=True, @@ -602,7 +624,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: diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 34cdff02e0b..2f9ea481a9a 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -18,6 +18,7 @@ from IPython.external.decorators import knownfailureif from IPython.utils.tempdir import TemporaryDirectory from IPython.utils.generics import complete_object +from IPython.testing.globalipapp import get_ipython #----------------------------------------------------------------------------- # Test functions @@ -229,4 +230,15 @@ def test_omit__names(): nt.assert_false('ip.__str__' in matches) nt.assert_false('ip._hidden_attr' in matches) del ip._hidden_attr - \ No newline at end of file + +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, []) \ No newline at end of file From e1b3c3de6256d3ce239cb19acff4188b19cc392e Mon Sep 17 00:00:00 2001 From: Tim Couper Date: Tue, 27 Mar 2012 11:59:59 +0100 Subject: [PATCH 2/5] Added test_dir2 for the dir2 (bonus) tests Signed-off-by: Tim Couper --- IPython/core/completer.py | 3 +- IPython/core/tests/test_completer.py | 3 +- IPython/utils/tests/test_dir2.py | 51 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 IPython/utils/tests/test_dir2.py diff --git a/IPython/core/completer.py b/IPython/core/completer.py index f2428e82017..da2a86615a4 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -336,6 +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 ''. m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text) + if m: expr, attr = m.group(1, 3) elif self.greedy: @@ -345,7 +346,7 @@ def attr_matches(self, text): expr, attr = m2.group(1,2) else: return [] - + try: obj = eval(expr, self.namespace) except: diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 2f9ea481a9a..ab2c7b6053e 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -18,7 +18,6 @@ from IPython.external.decorators import knownfailureif from IPython.utils.tempdir import TemporaryDirectory from IPython.utils.generics import complete_object -from IPython.testing.globalipapp import get_ipython #----------------------------------------------------------------------------- # Test functions @@ -241,4 +240,4 @@ def test_get__all__entries_no__all__ok(): class A(object): pass words = completer.get__all__entries(A()) - nt.assert_equal(words, []) \ No newline at end of file + nt.assert_equal(words, []) diff --git a/IPython/utils/tests/test_dir2.py b/IPython/utils/tests/test_dir2.py new file mode 100644 index 00000000000..c06e982c35a --- /dev/null +++ b/IPython/utils/tests/test_dir2.py @@ -0,0 +1,51 @@ +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'), 2) # duplicates + nt.assert_equal(res.count('__class__'), 4) # duplicates + +def test_SubClass(): + + class SubClass(Base): + y = 2 + + res = dir2(SubClass()) + assert ('y' in res) + nt.assert_equal(res.count('y'), 2) # duplicates, + nt.assert_equal(res.count('x'), 3) # duplicates, but fewer than above! + + +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.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) From 1ff2bdd6e792e3c537c64ec38531c68968719ffa Mon Sep 17 00:00:00 2001 From: Tim Couper Date: Tue, 27 Mar 2012 14:44:04 +0100 Subject: [PATCH 3/5] 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 --- IPython/core/completer.py | 2 +- IPython/core/magic.py | 7 ++++ IPython/utils/dir2.py | 57 ++++++++++---------------------- IPython/utils/tests/test_dir2.py | 9 ++--- 4 files changed, 31 insertions(+), 44 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index da2a86615a4..cb298c2fd93 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -416,7 +416,7 @@ def _greedy_changed(self, name, old, new): When 0: nothing will be excluded. """ ) - limit_to__all__ = Enum((0,1), default_value=1, config=True, + limit_to__all__ = Enum((0,1), default_value=0, config=True, help="""Instruct the completer to use __all__ for the completion Specifically, when completing on ``object.``. diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 4996c2019fd..71650243248 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -3753,6 +3753,13 @@ 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__= + Current: 0 + Choices: (0, 1) + Instruct the completer to use __all__ for the completion + Specifically, when completing on ``object.``. + When 1: only those names in obj.__all__ will be included. + When 0 [default]: the values in the __all__ attribute are ignored IPCompleter.greedy= Current: False Activate greedy completion diff --git a/IPython/utils/dir2.py b/IPython/utils/dir2.py index 54d6cf6ba66..818baadabe4 100644 --- a/IPython/utils/dir2.py +++ b/IPython/utils/dir2.py @@ -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) diff --git a/IPython/utils/tests/test_dir2.py b/IPython/utils/tests/test_dir2.py index c06e982c35a..3d0ba61b80f 100644 --- a/IPython/utils/tests/test_dir2.py +++ b/IPython/utils/tests/test_dir2.py @@ -13,8 +13,8 @@ def test_base(): assert ('z' in res) assert ('y' not in res) assert ('__class__' in res) - nt.assert_equal(res.count('x'), 2) # duplicates - nt.assert_equal(res.count('__class__'), 4) # duplicates + nt.assert_equal(res.count('x'), 1) + nt.assert_equal(res.count('__class__'), 1) def test_SubClass(): @@ -23,8 +23,8 @@ class SubClass(Base): res = dir2(SubClass()) assert ('y' in res) - nt.assert_equal(res.count('y'), 2) # duplicates, - nt.assert_equal(res.count('x'), 3) # duplicates, but fewer than above! + nt.assert_equal(res.count('y'), 1) + nt.assert_equal(res.count('x'), 1) def test_SubClass_with_trait_names_method(): @@ -37,6 +37,7 @@ def trait_names(self): 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) From abd9e16be6a595b9dd7bfbe143e4ebddfc3f72b5 Mon Sep 17 00:00:00 2001 From: Tim Couper Date: Tue, 27 Mar 2012 21:24:46 +0100 Subject: [PATCH 4/5] Changed type of limit_to__all__ from Enum to CBool --- IPython/core/completer.py | 6 +++--- IPython/core/magic.py | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index cb298c2fd93..8ebf1263bf6 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -416,14 +416,14 @@ def _greedy_changed(self, name, old, new): When 0: nothing will be excluded. """ ) - limit_to__all__ = Enum((0,1), default_value=0, config=True, + limit_to__all__ = CBool(default_value=False, config=True, help="""Instruct the completer to use __all__ for the completion Specifically, when completing on ``object.``. - When 1: only those names in obj.__all__ will be included. + When True: only those names in obj.__all__ will be included. - When 0 [default]: the values in the __all__ attribute are ignored + When False [default]: the __all__ attribute is ignored """ ) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 71650243248..ed867a753f0 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -3753,13 +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__= - Current: 0 - Choices: (0, 1) + IPCompleter.limit_to__all__= + Current: False Instruct the completer to use __all__ for the completion Specifically, when completing on ``object.``. - When 1: only those names in obj.__all__ will be included. - When 0 [default]: the values in the __all__ attribute are ignored + When True: only those names in obj.__all__ will be included. + When False [default]: the __all__ attribute is ignored IPCompleter.greedy= Current: False Activate greedy completion From d0e7b4db57b5fbeeab9b1cb3d106f00abeb2d1fd Mon Sep 17 00:00:00 2001 From: Tim Couper Date: Tue, 27 Mar 2012 21:50:19 +0100 Subject: [PATCH 5/5] added tests for limit_to__all__ for False and True cases --- IPython/core/tests/test_completer.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index ab2c7b6053e..2198d892aac 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -230,6 +230,31 @@ def test_omit__names(): 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]