diff --git a/docs/docs/features.rst b/docs/docs/features.rst index 27072995..937c15ed 100644 --- a/docs/docs/features.rst +++ b/docs/docs/features.rst @@ -41,6 +41,7 @@ Supported Python Features - simple/usual ``sys.path`` modifications - ``isinstance`` checks for if/while/assert - namespace packages (includes ``pkgutil`` and ``pkg_resources`` namespaces) +- Django / Flask / Buildout support Unsupported Features diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 1d7c4937..94ddfd4f 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -109,6 +109,18 @@ class ImportWrapper(pr.Base): m = _load_module(rel_path) names += m.get_defined_names() else: + if self.import_path == ('flask', 'ext'): + # List Flask extensions like ``flask_foo`` + for mod in self._get_module_names(): + modname = str(mod) + if modname.startswith('flask_'): + extname = modname[len('flask_'):] + names.append(self._generate_name(extname)) + # Now the old style: ``flaskext.foo`` + for dir in self._importer.sys_path_with_modifications(): + flaskext = os.path.join(dir, 'flaskext') + if os.path.isdir(flaskext): + names += self._get_module_names([flaskext]) if on_import_stmt and isinstance(scope, pr.Module) \ and scope.path.endswith('__init__.py'): pkg_path = os.path.dirname(scope.path) @@ -325,7 +337,7 @@ class _Importer(object): # `from gunicorn import something`. But gunicorn is not in the # sys.path. Therefore look if gunicorn is a parent directory, #56. in_path = [] - if self.import_path: + if self.import_path and self.file_path is not None: parts = self.file_path.split(os.path.sep) for i, p in enumerate(parts): if p == unicode(self.import_path[0]): @@ -343,6 +355,26 @@ class _Importer(object): @memoize_default(NO_DEFAULT) def follow_file_system(self): + # Handle "magic" Flask extension imports: + # ``flask.ext.foo`` is really ``flask_foo`` or ``flaskext.foo``. + if len(self.import_path) > 2 and \ + [str(part) for part in self.import_path[:2]] == ['flask', 'ext']: + orig_path = tuple(self.import_path) + part = orig_path[2] + pos = (part._line, part._column) + try: + self.import_path = ( + pr.NamePart('flask_' + str(part), part.parent, pos), + ) + orig_path[3:] + return self._real_follow_file_system() + except ModuleNotFound as e: + self.import_path = ( + pr.NamePart('flaskext', part.parent, pos), + ) + orig_path[2:] + return self._real_follow_file_system() + return self._real_follow_file_system() + + def _real_follow_file_system(self): if self.file_path: sys_path_mod = list(self.sys_path_with_modifications()) if not self.module.has_explicit_absolute_import: diff --git a/test/test_evaluate/flask-site-packages/flask/__init__.py b/test/test_evaluate/flask-site-packages/flask/__init__.py new file mode 100644 index 00000000..e876bc15 --- /dev/null +++ b/test/test_evaluate/flask-site-packages/flask/__init__.py @@ -0,0 +1 @@ + diff --git a/test/test_evaluate/flask-site-packages/flask/ext/__init__.py b/test/test_evaluate/flask-site-packages/flask/ext/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/test_evaluate/flask-site-packages/flask/ext/__init__.py @@ -0,0 +1 @@ + diff --git a/test/test_evaluate/flask-site-packages/flask_baz/__init__.py b/test/test_evaluate/flask-site-packages/flask_baz/__init__.py new file mode 100644 index 00000000..e9b3fffe --- /dev/null +++ b/test/test_evaluate/flask-site-packages/flask_baz/__init__.py @@ -0,0 +1 @@ +Baz = 1 diff --git a/test/test_evaluate/flask-site-packages/flask_foo.py b/test/test_evaluate/flask-site-packages/flask_foo.py new file mode 100644 index 00000000..0b910b80 --- /dev/null +++ b/test/test_evaluate/flask-site-packages/flask_foo.py @@ -0,0 +1,2 @@ +class Foo(object): + pass diff --git a/test/test_evaluate/flask-site-packages/flaskext/__init__.py b/test/test_evaluate/flask-site-packages/flaskext/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_evaluate/flask-site-packages/flaskext/bar.py b/test/test_evaluate/flask-site-packages/flaskext/bar.py new file mode 100644 index 00000000..6629f9ae --- /dev/null +++ b/test/test_evaluate/flask-site-packages/flaskext/bar.py @@ -0,0 +1,2 @@ +class Bar(object): + pass diff --git a/test/test_evaluate/flask-site-packages/flaskext/moo/__init__.py b/test/test_evaluate/flask-site-packages/flaskext/moo/__init__.py new file mode 100644 index 00000000..266e8093 --- /dev/null +++ b/test/test_evaluate/flask-site-packages/flaskext/moo/__init__.py @@ -0,0 +1 @@ +Moo = 1 diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 89e71570..e695df79 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -1,3 +1,6 @@ +import os +import sys + import pytest import jedi @@ -23,3 +26,34 @@ def test_import_not_in_sys_path(): assert a[0].name == 'str' a = jedi.Script(path='module.py', line=7).goto_definitions() assert a[0].name == 'str' + + +def setup_function(function): + sys.path.append(os.path.join( + os.path.dirname(__file__), 'flask-site-packages')) + + +def teardown_function(function): + path = os.path.join(os.path.dirname(__file__), 'flask-site-packages') + sys.path.remove(path) + + +@pytest.mark.parametrize("script,name", [ + ("from flask.ext import foo; foo.", "Foo"), # flask_foo.py + ("from flask.ext import bar; bar.", "Bar"), # flaskext/bar.py + ("from flask.ext import baz; baz.", "Baz"), # flask_baz/__init__.py + ("from flask.ext import moo; moo.", "Moo"), # flaskext/moo/__init__.py + ("from flask.ext.", "foo"), + ("from flask.ext.", "bar"), + ("from flask.ext.", "baz"), + ("from flask.ext.", "moo"), + pytest.mark.xfail(("import flask.ext.foo; flask.ext.foo.", "Foo")), + pytest.mark.xfail(("import flask.ext.bar; flask.ext.bar.", "Foo")), + pytest.mark.xfail(("import flask.ext.baz; flask.ext.baz.", "Foo")), + pytest.mark.xfail(("import flask.ext.moo; flask.ext.moo.", "Foo")), +]) +def test_flask_ext(script, name): + """flask.ext.foo is really imported from flaskext.foo or flask_foo. + """ + assert name in [c.name for c in jedi.Script(script).completions()] +