From 13c1f79d5cdf52dc842893993dab26a932f14e83 Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 12:40:35 +0200 Subject: [PATCH 1/8] A stab at davidhalter/jedi#361 (Flask extension imports) Both new-style and old-style extensions work, but only when imported with a 'from'. There are two skipped tests of the full dotted name imports. Also, our fixture has a normal flaskext package, whereas in practice the flaskext module is injected from a pth file and does not have __init__.py, we need to figure out to handle that. --- jedi/evaluate/imports.py | 19 ++++++++++ .../flask-site-packages/flask_baz/__init__.py | 1 + .../flask-site-packages/flask_foo.py | 2 ++ .../flask-site-packages/flaskext/__init__.py | 0 .../flask-site-packages/flaskext/bar.py | 2 ++ .../flaskext/moo/__init__.py | 1 + test/test_evaluate/test_imports.py | 36 +++++++++++++++++++ 7 files changed, 61 insertions(+) create mode 100644 test/test_evaluate/flask-site-packages/flask_baz/__init__.py create mode 100644 test/test_evaluate/flask-site-packages/flask_foo.py create mode 100644 test/test_evaluate/flask-site-packages/flaskext/__init__.py create mode 100644 test/test_evaluate/flask-site-packages/flaskext/bar.py create mode 100644 test/test_evaluate/flask-site-packages/flaskext/moo/__init__.py diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 1d7c4937..8eb20670 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -343,6 +343,25 @@ 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 [part._string 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_' + part._string, 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_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..4f929448 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,36 @@ 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 +]) +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()] + + +@pytest.mark.xfail +@pytest.mark.parametrize("script,name", [ + ("import flask.ext.foo; flask.ext.foo.", "Foo"), + ("import flask.ext.bar; flask.ext.bar.", "Foo"), + ("import flask.ext.baz; flask.ext.baz.", "Foo"), + ("import flask.ext.moo; flask.ext.moo.", "Foo"), +]) +def test_flask_ext_more(script, name): + assert name in [c.name for c in jedi.Script(script).completions()] From a18f8a7cbb834b8b188eff0ac30d6e94770a5d45 Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 13:27:34 +0200 Subject: [PATCH 2/8] Make tests terser. pytest rules! --- test/test_evaluate/test_imports.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 4f929448..86ccba79 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -43,19 +43,12 @@ def teardown_function(function): ("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 + 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()] - - -@pytest.mark.xfail -@pytest.mark.parametrize("script,name", [ - ("import flask.ext.foo; flask.ext.foo.", "Foo"), - ("import flask.ext.bar; flask.ext.bar.", "Foo"), - ("import flask.ext.baz; flask.ext.baz.", "Foo"), - ("import flask.ext.moo; flask.ext.moo.", "Foo"), -]) -def test_flask_ext_more(script, name): - assert name in [c.name for c in jedi.Script(script).completions()] From 5edd2274b2f9e8a617e289b33de5cd20175f96f6 Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 13:43:20 +0200 Subject: [PATCH 3/8] Fix an exception in the flask ext code. --- jedi/evaluate/imports.py | 3 ++- test/test_evaluate/test_imports.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 8eb20670..018447a5 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -345,7 +345,8 @@ class _Importer(object): def follow_file_system(self): # Handle "magic" Flask extension imports: # ``flask.ext.foo`` is really ``flask_foo`` or ``flaskext.foo``. - if [part._string for part in self.import_path[:2]] == ['flask', 'ext']: + if len(self.import_path) > 2 and \ + [part._string for part in self.import_path[:2]] == ['flask', 'ext']: orig_path = tuple(self.import_path) part = orig_path[2] pos = (part._line, part._column) diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 86ccba79..2946e7fd 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -52,3 +52,7 @@ 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()] + + +def test_flask_regression(): + jedi.Script("from flask.ext.").completions() From 7f45bfe689a38fc8a324d18a540b9101f72970a4 Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 16:08:26 +0200 Subject: [PATCH 4/8] More on #361: enumerate new-style flask extensions. --- jedi/evaluate/imports.py | 9 ++++++++- test/test_evaluate/test_imports.py | 5 ++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 018447a5..0110ac68 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -109,6 +109,13 @@ class ImportWrapper(pr.Base): m = _load_module(rel_path) names += m.get_defined_names() else: + if self.import_path == ('flask', 'ext'): + # List Flask extensions in ``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)) if on_import_stmt and isinstance(scope, pr.Module) \ and scope.path.endswith('__init__.py'): pkg_path = os.path.dirname(scope.path) @@ -325,7 +332,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]): diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 2946e7fd..c21e8593 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -43,6 +43,8 @@ def teardown_function(function): ("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.", "baz"), 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")), @@ -53,6 +55,3 @@ def test_flask_ext(script, name): """ assert name in [c.name for c in jedi.Script(script).completions()] - -def test_flask_regression(): - jedi.Script("from flask.ext.").completions() From 733eee94b6aea238561e4a6ee3101767a518d74a Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 16:18:24 +0200 Subject: [PATCH 5/8] Fix breaking tests. --- jedi/evaluate/imports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 0110ac68..b6dc43e7 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -353,13 +353,13 @@ class _Importer(object): # Handle "magic" Flask extension imports: # ``flask.ext.foo`` is really ``flask_foo`` or ``flaskext.foo``. if len(self.import_path) > 2 and \ - [part._string for part in self.import_path[:2]] == ['flask', 'ext']: + [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_' + part._string, part.parent, pos), + pr.NamePart('flask_' + str(part), part.parent, pos), ) + orig_path[3:] return self._real_follow_file_system() except ModuleNotFound as e: From ab486ba84f7b29a5ce3175cf6d2bcbaf231fa72e Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 17:00:17 +0200 Subject: [PATCH 6/8] List old-style flask extensions, too. --- jedi/evaluate/imports.py | 7 ++++++- test/test_evaluate/test_imports.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index b6dc43e7..94ddfd4f 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -110,12 +110,17 @@ class ImportWrapper(pr.Base): names += m.get_defined_names() else: if self.import_path == ('flask', 'ext'): - # List Flask extensions in ``flask_foo`` + # 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) diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index c21e8593..e695df79 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -44,7 +44,9 @@ def teardown_function(function): ("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")), From 25978cf591c75faaab21b9159579a2e1e18c8734 Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 17:35:50 +0200 Subject: [PATCH 7/8] Mentioned framework support in the docs. --- docs/docs/features.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/features.rst b/docs/docs/features.rst index 321cbf71..fcc8e1b2 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 From ecb208517499b6406ab30a6d1e45ba2ff848d678 Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Sun, 27 Jul 2014 17:59:09 +0200 Subject: [PATCH 8/8] Add flask.ext to the test fixture so tests pass even without flask installed. --- test/test_evaluate/flask-site-packages/flask/__init__.py | 1 + test/test_evaluate/flask-site-packages/flask/ext/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 test/test_evaluate/flask-site-packages/flask/__init__.py create mode 100644 test/test_evaluate/flask-site-packages/flask/ext/__init__.py 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 @@ +