From 8a9202135b4d9c0faf9c1d2b5ae5469b7f841681 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 23 Jul 2018 03:54:10 +0200 Subject: [PATCH] Move import logic around a bit --- jedi/evaluate/imports.py | 217 ++++++++++++++++++++------------------- jedi/plugins/base.py | 8 ++ jedi/plugins/typeshed.py | 33 ++++++ 3 files changed, 152 insertions(+), 106 deletions(-) create mode 100644 jedi/plugins/typeshed.py diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 99791dc9..80bf1313 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -12,6 +12,7 @@ This module also supports import autocompletion, which means to complete statements like ``from datetim`` (cursor at the end would return ``datetime``). """ import os +from functools import partial from parso.python import tree from parso.tree import search_ancestor @@ -21,7 +22,6 @@ from jedi._compatibility import (FileNotFoundError, ImplicitNSInfo, force_unicode, unicode) from jedi import debug from jedi import settings -from jedi.common.utils import traverse_parents from jedi.parser_utils import get_cached_code_lines from jedi.evaluate import sys_path from jedi.evaluate import helpers @@ -282,114 +282,14 @@ class Importer(object): if not self.import_path: return NO_CONTEXTS - return self._do_import(self.import_path, self.sys_path_with_modifications()) + return import_module( + self._evaluator, + self.import_path, + self.sys_path_with_modifications(), + partial(_add_error, self.module_context), - def _do_import(self, import_path, sys_path): - """ - This method is very similar to importlib's `_gcd_import`. - """ - import_parts = [ - force_unicode(i.value if isinstance(i, tree.Name) else i) - for i in import_path - ] - - # Handle "magic" Flask extension imports: - # ``flask.ext.foo`` is really ``flask_foo`` or ``flaskext.foo``. - if len(import_path) > 2 and import_parts[:2] == ['flask', 'ext']: - # New style. - ipath = ('flask_' + str(import_parts[2]),) + import_path[3:] - modules = self._do_import(ipath, sys_path) - if modules: - return modules - else: - # Old style - return self._do_import(('flaskext',) + import_path[2:], sys_path) - - if import_parts[0] in settings.auto_import_modules: - module = _load_module( - self._evaluator, - import_names=import_parts, - sys_path=sys_path, - ) - return ContextSet(module) - - module_name = '.'.join(import_parts) - try: - return ContextSet(self._evaluator.module_cache.get(module_name)) - except KeyError: - pass - - if len(import_path) > 1: - # This is a recursive way of importing that works great with - # the module cache. - bases = self._do_import(import_path[:-1], sys_path) - if not bases: - return NO_CONTEXTS - # We can take the first element, because only the os special - # case yields multiple modules, which is not important for - # further imports. - parent_module = list(bases)[0] - - # This is a huge exception, we follow a nested import - # ``os.path``, because it's a very important one in Python - # that is being achieved by messing with ``sys.modules`` in - # ``os``. - if import_parts == ['os', 'path']: - return parent_module.py__getattribute__('path') - - try: - method = parent_module.py__path__ - except AttributeError: - # The module is not a package. - _add_error(self.module_context, import_path[-1]) - return NO_CONTEXTS - else: - paths = method() - debug.dbg('search_module %s in paths %s', module_name, paths) - for path in paths: - # At the moment we are only using one path. So this is - # not important to be correct. - if not isinstance(path, list): - path = [path] - code, module_path, is_pkg = self._evaluator.compiled_subprocess.get_module_info( - string=import_parts[-1], - path=path, - full_name=module_name, - is_global_search=False, - ) - if module_path is not None: - break - else: - _add_error(self.module_context, import_path[-1]) - return NO_CONTEXTS - else: - debug.dbg('global search_module %s in %s', import_parts[-1], self.file_path) - # Override the sys.path. It works only good that way. - # Injecting the path directly into `find_module` did not work. - code, module_path, is_pkg = self._evaluator.compiled_subprocess.get_module_info( - string=import_parts[-1], - full_name=module_name, - sys_path=sys_path, - is_global_search=True, - ) - if module_path is None: - # The module is not a package. - _add_error(self.module_context, import_path[-1]) - return NO_CONTEXTS - - module = _load_module( - self._evaluator, module_path, code, sys_path, - import_names=import_parts, - safe_module_name=True, ) - if module is None: - # The file might raise an ImportError e.g. and therefore not be - # importable. - return NO_CONTEXTS - - return ContextSet(module) - def _generate_name(self, name, in_module=None): # Create a pseudo import to be able to follow them. if in_module is None: @@ -478,6 +378,111 @@ class Importer(object): return names +def import_module(evaluator, import_path, sys_path, add_error_callback): + """ + This method is very similar to importlib's `_gcd_import`. + """ + import_parts = [ + force_unicode(i.value if isinstance(i, tree.Name) else i) + for i in import_path + ] + + if len(import_path) > 2 and import_parts[:2] == ['flask', 'ext']: + # New style. + ipath = ('flask_' + str(import_parts[2]),) + import_path[3:] + modules = import_module(evaluator, ipath, sys_path, add_error_callback) + if modules: + return modules + else: + # Old style + return import_module(evaluator, ('flaskext',) + import_path[2:], sys_path, add_error_callback) + + if import_parts[0] in settings.auto_import_modules: + module = _load_module( + evaluator, + import_names=import_parts, + sys_path=sys_path, + ) + return ContextSet(module) + + module_name = '.'.join(import_parts) + try: + return ContextSet(evaluator.module_cache.get(module_name)) + except KeyError: + pass + + if len(import_path) > 1: + # This is a recursive way of importing that works great with + # the module cache. + bases = import_module(evaluator, import_path[:-1], sys_path, add_error_callback) + if not bases: + return NO_CONTEXTS + # We can take the first element, because only the os special + # case yields multiple modules, which is not important for + # further imports. + parent_module = list(bases)[0] + + # This is a huge exception, we follow a nested import + # ``os.path``, because it's a very important one in Python + # that is being achieved by messing with ``sys.modules`` in + # ``os``. + if import_parts == ['os', 'path']: + return parent_module.py__getattribute__('path') + + try: + method = parent_module.py__path__ + except AttributeError: + # The module is not a package. + add_error_callback(import_path[-1]) + return NO_CONTEXTS + else: + paths = method() + debug.dbg('search_module %s in paths %s', module_name, paths) + for path in paths: + # At the moment we are only using one path. So this is + # not important to be correct. + if not isinstance(path, list): + path = [path] + code, module_path, is_pkg = evaluator.compiled_subprocess.get_module_info( + string=import_parts[-1], + path=path, + full_name=module_name, + is_global_search=False, + ) + if module_path is not None: + break + else: + add_error_callback(import_path[-1]) + return NO_CONTEXTS + else: + debug.dbg('global search_module %s', import_parts[-1]) + # Override the sys.path. It works only good that way. + # Injecting the path directly into `find_module` did not work. + code, module_path, is_pkg = evaluator.compiled_subprocess.get_module_info( + string=import_parts[-1], + full_name=module_name, + sys_path=sys_path, + is_global_search=True, + ) + if module_path is None: + # The module is not a package. + add_error_callback(import_path[-1]) + return NO_CONTEXTS + + module = _load_module( + evaluator, module_path, code, sys_path, + import_names=import_parts, + safe_module_name=True, + ) + + if module is None: + # The file might raise an ImportError e.g. and therefore not be + # importable. + return NO_CONTEXTS + + return ContextSet(module) + + def _load_module(evaluator, path=None, code=None, sys_path=None, import_names=None, safe_module_name=False): if import_names is None: diff --git a/jedi/plugins/base.py b/jedi/plugins/base.py index 62288981..21033b57 100644 --- a/jedi/plugins/base.py +++ b/jedi/plugins/base.py @@ -11,3 +11,11 @@ class BasePlugin(object): Decorates the execute(context, arguments) function. """ return callback + + def import_module(callback): + """ + Decorates the + import_module(evaluator, import_path, sys_path, add_error_callback) + function. + """ + return callback diff --git a/jedi/plugins/typeshed.py b/jedi/plugins/typeshed.py new file mode 100644 index 00000000..624d3f63 --- /dev/null +++ b/jedi/plugins/typeshed.py @@ -0,0 +1,33 @@ +from jedi.plugins.base import BasePlugin +from jedi.evaluate.base_context import Context, ContextSet + + +class TypeshedPlugin(BasePlugin): + def foo(): + return + + +class TypeshedProxy(object): + def __init__(self, parent_context, context, typeshed_context): + self.parent_context = parent_context + self._context = context + self._typeshed_context = typeshed_context + + # We have to overwrite everything that has to do with trailers, name + # lookups and filters to make it possible to route name lookups towards + # compiled objects and the rest towards tree node contexts. + def py__getattribute__(self, *args, **kwargs): + context_results = self._context.py__getattribute__( + *args, **kwargs + ) + typeshed_results = self._typeshed_context = py__getattribute__( + *args, **kwargs + ) + print(context_results, typeshed_results) + return context_results + + def __getattr__(self, name): + return getattr(self._context, name) + + def __repr__(self): + return '<%s: %s>' % (type(self).__name__, self.access_handle.get_repr())