diff --git a/.gitignore b/.gitignore index b4fce685..da611dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /dist/ jedi.egg-info/ record.json +/.cache/ diff --git a/jedi/evaluate/stdlib.py b/jedi/evaluate/stdlib.py index adb2a3ef..fec8a501 100644 --- a/jedi/evaluate/stdlib.py +++ b/jedi/evaluate/stdlib.py @@ -1,12 +1,18 @@ """ Implementations of standard library functions, because it's not possible to understand them with Jedi. + +To add a new implementation, create a function and add it to the +``_implemented`` dict at the bottom of this module. + """ +import collections from jedi._compatibility import unicode from jedi.evaluate import compiled from jedi.evaluate import representation as er from jedi.evaluate import iterable from jedi.evaluate.helpers import FakeArray, FakeStatement +from jedi.parser import Parser from jedi.parser import representation as pr from jedi import debug @@ -107,6 +113,49 @@ def builtins_reversed(evaluator, obj, params): return [er.Instance(evaluator, obj, objects)] +def collections_namedtuple(evaluator, obj, params): + """ + Implementation of the namedtuple function. + + This has to be done by processing the namedtuple class template and + evaluating the result. + + .. note:: |jedi| only supports namedtuples on Python >2.6. + + """ + # Namedtuples are not supported on Python 2.6 + if not hasattr(collections, '_class_template'): + return [] + + # Process arguments + name = _follow_param(evaluator, params, 0)[0].obj + _fields = _follow_param(evaluator, params, 1)[0] + if isinstance(_fields, compiled.CompiledObject): + fields = _fields.obj.replace(',', ' ').split() + elif isinstance(_fields, iterable.Array): + try: + fields = [v.obj for v in _fields.values()] + except AttributeError: + return [] + else: + return [] + + # Build source + source = collections._class_template.format( + typename=name, + field_names=fields, + num_fields=len(fields), + arg_list=', '.join(fields), + repr_fmt=', '.join(collections._repr_template.format(name=name) for name in fields), + field_defs='\n'.join(collections._field_template.format(index=index, name=name) + for index, name in enumerate(fields)) + ) + + # Parse source + generated_class = Parser(unicode(source)).module.subscopes[0] + return [er.Class(evaluator, generated_class)] + + def _return_first_param(evaluator, obj, params): if len(params) == 1: return _follow_param(evaluator, params, 0) @@ -128,4 +177,7 @@ _implemented = { 'load': lambda *args: [], 'loads': lambda *args: [], }, + 'collections': { + 'namedtuple': collections_namedtuple, + }, } diff --git a/test/test_integration_stdlib.py b/test/test_integration_stdlib.py new file mode 100644 index 00000000..a32a09ce --- /dev/null +++ b/test/test_integration_stdlib.py @@ -0,0 +1,37 @@ +""" +Tests of various stdlib related things that could not be tested +with "Black Box Tests". +""" +import pytest +from jedi import Script +from jedi._compatibility import is_py26 + + +@pytest.mark.parametrize(['letter', 'expected'], [ + ('n', ['name']), + ('s', ['smart']), +]) +def test_namedtuple_str(letter, expected): + source = "import collections\n" + \ + "Person = collections.namedtuple('Person', 'name smart')\n" + \ + "dave = Person('Dave', False)\n" + \ + "dave.%s" % letter + result = Script(source).completions() + completions = set(r.name for r in result) + if is_py26: + assert completions == set() + else: + assert completions == set(expected) + + +def test_namedtuple_list(): + source = "import collections\n" + \ + "Cat = collections.namedtuple('Person', ['legs', u'length', 'large'])\n" + \ + "garfield = Cat(4, '85cm', True)\n" + \ + "garfield.l" + result = Script(source).completions() + completions = set(r.name for r in result) + if is_py26: + assert completions == set() + else: + assert completions == set(['legs', 'length', 'large']) diff --git a/tox.ini b/tox.ini index b553234b..f59efa3c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py26, py27, py32, py33, py34 [testenv] deps = pytest>=2.3.5 + pytest-cache # docopt for sith doctests docopt # coloroma for colored debug output