diff --git a/news/1 Enhancements/1400.md b/news/1 Enhancements/1400.md new file mode 100644 index 000000000000..47b3cf611c88 --- /dev/null +++ b/news/1 Enhancements/1400.md @@ -0,0 +1 @@ +Intergrate Jedi 0.12. See https://github.com/davidhalter/jedi/issues/1063#issuecomment-381417297 for details. \ No newline at end of file diff --git a/news/2 Fixes/1033.md b/news/2 Fixes/1033.md new file mode 100644 index 000000000000..31fec8720909 --- /dev/null +++ b/news/2 Fixes/1033.md @@ -0,0 +1 @@ +Fix go to definition functionality across files. \ No newline at end of file diff --git a/pythonFiles/completion.py b/pythonFiles/completion.py index e530be32b367..2a0d6e3d095b 100644 --- a/pythonFiles/completion.py +++ b/pythonFiles/completion.py @@ -570,7 +570,7 @@ def _process_request(self, request): if lookup == 'definitions': defs = [] try: - defs = self._get_definitionsx(script.goto_assignments(follow_imports=False), request['id']) + defs = self._get_definitionsx(script.goto_definitions(follow_imports=False), request['id']) except: pass try: diff --git a/pythonFiles/jedi/__init__.py b/pythonFiles/jedi/__init__.py index 1a1080ad2fd4..ff2de906d9cd 100644 --- a/pythonFiles/jedi/__init__.py +++ b/pythonFiles/jedi/__init__.py @@ -36,8 +36,12 @@ good text editor, while still having very good IDE features for Python. """ -__version__ = '0.11.1' +__version__ = '0.12.0' from jedi.api import Script, Interpreter, set_debug_function, \ preload_module, names from jedi import settings +from jedi.api.environment import find_virtualenvs, find_system_environments, \ + get_default_environment, InvalidPythonEnvironment, create_environment, \ + get_system_environment +from jedi.api.exceptions import InternalError diff --git a/pythonFiles/jedi/_compatibility.py b/pythonFiles/jedi/_compatibility.py index 52a20fe2c07c..8b55fb8f4a1d 100644 --- a/pythonFiles/jedi/_compatibility.py +++ b/pythonFiles/jedi/_compatibility.py @@ -1,25 +1,25 @@ """ -To ensure compatibility from Python ``2.6`` - ``3.3``, a module has been +To ensure compatibility from Python ``2.7`` - ``3.x``, a module has been created. Clearly there is huge need to use conforming syntax. """ +import binascii +import errno import sys -import imp import os import re import pkgutil import warnings +import inspect +import subprocess try: import importlib except ImportError: pass -# Cannot use sys.version.major and minor names, because in Python 2.6 it's not -# a namedtuple. is_py3 = sys.version_info[0] >= 3 is_py33 = is_py3 and sys.version_info[1] >= 3 is_py34 = is_py3 and sys.version_info[1] >= 4 is_py35 = is_py3 and sys.version_info[1] >= 5 -is_py26 = not is_py3 and sys.version_info[1] < 7 py_version = int(str(sys.version_info[0]) + str(sys.version_info[1])) @@ -35,28 +35,24 @@ def close(self): del self.loader -def find_module_py34(string, path=None, fullname=None): - implicit_namespace_pkg = False +def find_module_py34(string, path=None, full_name=None): spec = None loader = None spec = importlib.machinery.PathFinder.find_spec(string, path) - if hasattr(spec, 'origin'): - origin = spec.origin - implicit_namespace_pkg = origin == 'namespace' - - # We try to disambiguate implicit namespace pkgs with non implicit namespace pkgs - if implicit_namespace_pkg: - fullname = string if not path else fullname - implicit_ns_info = ImplicitNSInfo(fullname, spec.submodule_search_locations._path) - return None, implicit_ns_info, False - - # we have found the tail end of the dotted path - if hasattr(spec, 'loader'): + if spec is not None: + # We try to disambiguate implicit namespace pkgs with non implicit namespace pkgs + if not spec.has_location: + full_name = string if not path else full_name + implicit_ns_info = ImplicitNSInfo(full_name, spec.submodule_search_locations._path) + return None, implicit_ns_info, False + + # we have found the tail end of the dotted path loader = spec.loader return find_module_py33(string, path, loader) -def find_module_py33(string, path=None, loader=None, fullname=None): + +def find_module_py33(string, path=None, loader=None, full_name=None): loader = loader or importlib.machinery.PathFinder.find_module(string, path) if loader is None and path is None: # Fallback to find builtins @@ -74,7 +70,7 @@ def find_module_py33(string, path=None, loader=None, fullname=None): raise ImportError("Originally " + repr(e)) if loader is None: - raise ImportError("Couldn't find a loader for {0}".format(string)) + raise ImportError("Couldn't find a loader for {}".format(string)) try: is_package = loader.is_package(string) @@ -109,7 +105,10 @@ def find_module_py33(string, path=None, loader=None, fullname=None): return module_file, module_path, is_package -def find_module_pre_py33(string, path=None, fullname=None): +def find_module_pre_py33(string, path=None, full_name=None): + # This import is here, because in other places it will raise a + # DeprecationWarning. + import imp try: module_file, module_path, description = imp.find_module(string, path) module_type = description[2] @@ -127,14 +126,7 @@ def find_module_pre_py33(string, path=None, fullname=None): if loader: is_package = loader.is_package(string) is_archive = hasattr(loader, 'archive') - try: - module_path = loader.get_filename(string) - except AttributeError: - # fallback for py26 - try: - module_path = loader._get_filename(string) - except AttributeError: - continue + module_path = loader.get_filename(string) if is_package: module_path = os.path.dirname(module_path) if is_archive: @@ -142,14 +134,14 @@ def find_module_pre_py33(string, path=None, fullname=None): file = None if not is_package or is_archive: file = DummyFile(loader, string) - return (file, module_path, is_package) + return file, module_path, is_package except ImportError: pass - raise ImportError("No module named {0}".format(string)) + raise ImportError("No module named {}".format(string)) find_module = find_module_py33 if is_py33 else find_module_pre_py33 -find_module = find_module_py34 if is_py34 else find_module +find_module = find_module_py34 if is_py34 else find_module find_module.__doc__ = """ Provides information about a module. @@ -161,12 +153,80 @@ def find_module_pre_py33(string, path=None, fullname=None): """ +def _iter_modules(paths, prefix=''): + # Copy of pkgutil.iter_modules adapted to work with namespaces + + for path in paths: + importer = pkgutil.get_importer(path) + + if not isinstance(importer, importlib.machinery.FileFinder): + # We're only modifying the case for FileFinder. All the other cases + # still need to be checked (like zip-importing). Do this by just + # calling the pkgutil version. + for mod_info in pkgutil.iter_modules([path], prefix): + yield mod_info + continue + + # START COPY OF pkutils._iter_file_finder_modules. + if importer.path is None or not os.path.isdir(importer.path): + return + + yielded = {} + + try: + filenames = os.listdir(importer.path) + except OSError: + # ignore unreadable directories like import does + filenames = [] + filenames.sort() # handle packages before same-named modules + + for fn in filenames: + modname = inspect.getmodulename(fn) + if modname == '__init__' or modname in yielded: + continue + + # jedi addition: Avoid traversing special directories + if fn.startswith('.') or fn == '__pycache__': + continue + + path = os.path.join(importer.path, fn) + ispkg = False + + if not modname and os.path.isdir(path) and '.' not in fn: + modname = fn + # A few jedi modifications: Don't check if there's an + # __init__.py + try: + os.listdir(path) + except OSError: + # ignore unreadable directories like import does + continue + ispkg = True + + if modname and '.' not in modname: + yielded[modname] = 1 + yield importer, prefix + modname, ispkg + # END COPY + +iter_modules = _iter_modules if py_version >= 34 else pkgutil.iter_modules + + class ImplicitNSInfo(object): """Stores information returned from an implicit namespace spec""" def __init__(self, name, paths): self.name = name self.paths = paths + +if is_py3: + all_suffixes = importlib.machinery.all_suffixes +else: + def all_suffixes(): + # Is deprecated and raises a warning in Python 3.6. + import imp + return [suffix for suffix, _, _ in imp.get_suffixes()] + + # unicode function try: unicode = unicode @@ -208,7 +268,7 @@ def use_metaclass(meta, *bases): """ Create a class with a metaclass. """ if not bases: bases = (object,) - return meta("HackClass", bases, {}) + return meta("Py2CompatibilityMetaClass", bases, {}) try: @@ -219,19 +279,37 @@ def use_metaclass(meta, *bases): encoding = 'ascii' -def u(string): +def u(string, errors='strict'): """Cast to unicode DAMMIT! Written because Python2 repr always implicitly casts to a string, so we have to cast back to a unicode (and we now that we always deal with valid unicode, because we check that in the beginning). """ - if is_py3: - return str(string) - - if not isinstance(string, unicode): - return unicode(str(string), 'UTF-8') + if isinstance(string, bytes): + return unicode(string, encoding='UTF-8', errors=errors) return string + +def cast_path(obj): + """ + Take a bytes or str path and cast it to unicode. + + Apparently it is perfectly fine to pass both byte and unicode objects into + the sys.path. This probably means that byte paths are normal at other + places as well. + + Since this just really complicates everything and Python 2.7 will be EOL + soon anyway, just go with always strings. + """ + return u(obj, errors='replace') + + +def force_unicode(obj): + # Intentionally don't mix those two up, because those two code paths might + # be different in the future (maybe windows?). + return cast_path(obj) + + try: import builtins # module name in python 3 except ImportError: @@ -242,11 +320,6 @@ def u(string): def literal_eval(string): - # py3.0, py3.1 and py32 don't support unicode literals. Support those, I - # don't want to write two versions of the tokenizer. - if is_py3 and sys.version_info.minor < 3: - if re.match('[uU][\'"]', string): - string = string[1:] return ast.literal_eval(string) @@ -260,6 +333,11 @@ def literal_eval(string): except NameError: FileNotFoundError = IOError +try: + NotADirectoryError = NotADirectoryError +except NameError: + NotADirectoryError = IOError + def no_unicode_pprint(dct): """ @@ -273,6 +351,13 @@ def no_unicode_pprint(dct): print(re.sub("u'", "'", s)) +def print_to_stderr(*args): + if is_py3: + eval("print(*args, file=sys.stderr)") + else: + print >> sys.stderr, args + + def utf8_repr(func): """ ``__repr__`` methods in Python 2 don't allow unicode objects to be @@ -289,3 +374,142 @@ def wrapper(self): return func else: return wrapper + + +if is_py3: + import queue +else: + import Queue as queue + + +import pickle +if sys.version_info[:2] == (3, 3): + """ + Monkeypatch the unpickler in Python 3.3. This is needed, because the + argument `encoding='bytes'` is not supported in 3.3, but badly needed to + communicate with Python 2. + """ + + class NewUnpickler(pickle._Unpickler): + dispatch = dict(pickle._Unpickler.dispatch) + + def _decode_string(self, value): + # Used to allow strings from Python 2 to be decoded either as + # bytes or Unicode strings. This should be used only with the + # STRING, BINSTRING and SHORT_BINSTRING opcodes. + if self.encoding == "bytes": + return value + else: + return value.decode(self.encoding, self.errors) + + def load_string(self): + data = self.readline()[:-1] + # Strip outermost quotes + if len(data) >= 2 and data[0] == data[-1] and data[0] in b'"\'': + data = data[1:-1] + else: + raise pickle.UnpicklingError("the STRING opcode argument must be quoted") + self.append(self._decode_string(pickle.codecs.escape_decode(data)[0])) + dispatch[pickle.STRING[0]] = load_string + + def load_binstring(self): + # Deprecated BINSTRING uses signed 32-bit length + len, = pickle.struct.unpack('>> defs[0].type + >>> defs = [str(d.type) for d in defs] # It's unicode and in Py2 has u before it. + >>> defs[0] 'module' - >>> defs[1].type + >>> defs[1] 'class' - >>> defs[2].type + >>> defs[2] 'instance' - >>> defs[3].type + >>> defs[3] 'function' """ @@ -159,7 +157,7 @@ def to_reverse(): except IndexError: pass - if name.api_type == 'module': + if name.api_type in 'module': module_contexts = name.infer() if module_contexts: module_context, = module_contexts @@ -259,7 +257,7 @@ def docstring(self, raw=False, fast=True): @property def description(self): """A textual description of the object.""" - return u(self._name.string_name) + return self._name.string_name @property def full_name(self): @@ -324,9 +322,9 @@ def get_param_names(context): param_names = param_names[1:] elif isinstance(context, (instance.AbstractInstanceContext, ClassContext)): if isinstance(context, ClassContext): - search = '__init__' + search = u'__init__' else: - search = '__call__' + search = u'__call__' names = context.get_function_slot_names(search) if not names: return [] @@ -377,8 +375,7 @@ def get_line_code(self, before=0, after=0): if self.in_builtin_module(): return '' - path = self._name.get_root_context().py__file__() - lines = parser_cache[self._evaluator.grammar._hashed][path].lines + lines = self._name.get_root_context().code_lines index = self._name.start_pos[0] - 1 start_index = max(index - before, 0) @@ -406,7 +403,7 @@ def _complete(self, like_name): and self.type == 'Function': append = '(' - if isinstance(self._name, ParamName) and self._stack is not None: + if self._name.api_type == 'param' and self._stack is not None: node_names = list(self._stack.get_node_names(self._evaluator.grammar._pgen_grammar)) if 'trailer' in node_names and 'argument' not in node_names: append += '=' @@ -525,7 +522,7 @@ def description(self): if typ == 'function': # For the description we want a short and a pythonic way. typ = 'def' - return typ + ' ' + u(self._name.string_name) + return typ + ' ' + self._name.string_name elif typ == 'param': code = search_ancestor(tree_name, 'param').get_code( include_prefix=False, @@ -533,7 +530,6 @@ def description(self): ) return typ + ' ' + code - definition = tree_name.get_definition() or tree_name # Remove the prefix, because that's not what we want for get_code # here. @@ -555,7 +551,7 @@ def desc_with_module(self): .. todo:: Add full path. This function is should return a `module.class.function` path. """ - position = '' if self.in_builtin_module else '@%s' % (self.line) + position = '' if self.in_builtin_module else '@%s' % self.line return "%s:%s%s" % (self.module_name, self.description, position) @memoize_method diff --git a/pythonFiles/jedi/api/completion.py b/pythonFiles/jedi/api/completion.py index 559a4d3f8320..c88a031e4679 100644 --- a/pythonFiles/jedi/api/completion.py +++ b/pythonFiles/jedi/api/completion.py @@ -2,6 +2,7 @@ from parso.python import tree from parso.tree import search_ancestor, Leaf +from jedi._compatibility import Parameter from jedi import debug from jedi import settings from jedi.api import classes @@ -18,24 +19,21 @@ def get_call_signature_param_names(call_signatures): for call_sig in call_signatures: for p in call_sig.params: # Allow protected access, because it's a public API. - tree_name = p._name.tree_name - # Compiled modules typically don't allow keyword arguments. - if tree_name is not None: - # Allow access on _definition here, because it's a - # public API and we don't want to make the internal - # Name object public. - tree_param = tree.search_ancestor(tree_name, 'param') - if tree_param.star_count == 0: # no *args/**kwargs - yield p._name + if p._name.get_kind() in (Parameter.POSITIONAL_OR_KEYWORD, + Parameter.KEYWORD_ONLY): + yield p._name def filter_names(evaluator, completion_names, stack, like_name): comp_dct = {} + if settings.case_insensitive_completion: + like_name = like_name.lower() for name in completion_names: - if settings.case_insensitive_completion \ - and name.string_name.lower().startswith(like_name.lower()) \ - or name.string_name.startswith(like_name): + string = name.string_name + if settings.case_insensitive_completion: + string = string.lower() + if string.startswith(like_name): new = classes.Completion( evaluator, name, @@ -208,7 +206,7 @@ def _get_context_completions(self): def _get_keyword_completion_names(self, keywords_): for k in keywords_: - yield keywords.keyword(self._evaluator, k).name + yield keywords.KeywordName(self._evaluator, k) def _global_completions(self): context = get_user_scope(self._module_context, self._position) diff --git a/pythonFiles/jedi/api/environment.py b/pythonFiles/jedi/api/environment.py new file mode 100644 index 000000000000..51b390f36ab4 --- /dev/null +++ b/pythonFiles/jedi/api/environment.py @@ -0,0 +1,393 @@ +""" +Environments are a way to activate different Python versions or Virtualenvs for +static analysis. The Python binary in that environment is going to be executed. +""" +import os +import re +import sys +import hashlib +import filecmp +from subprocess import PIPE +from collections import namedtuple +# When dropping Python 2.7 support we should consider switching to +# `shutil.which`. +from distutils.spawn import find_executable + +from jedi._compatibility import GeneralizedPopen +from jedi.cache import memoize_method, time_cache +from jedi.evaluate.compiled.subprocess import get_subprocess, \ + EvaluatorSameProcess, EvaluatorSubprocess + +import parso + +_VersionInfo = namedtuple('VersionInfo', 'major minor micro') + +_SUPPORTED_PYTHONS = ['3.6', '3.5', '3.4', '3.3', '2.7'] +_SAFE_PATHS = ['/usr/bin', '/usr/local/bin'] +_CURRENT_VERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor) + + +class InvalidPythonEnvironment(Exception): + """ + If you see this exception, the Python executable or Virtualenv you have + been trying to use is probably not a correct Python version. + """ + + +class _BaseEnvironment(object): + @memoize_method + def get_grammar(self): + version_string = '%s.%s' % (self.version_info.major, self.version_info.minor) + return parso.load_grammar(version=version_string) + + @property + def _sha256(self): + try: + return self._hash + except AttributeError: + self._hash = _calculate_sha256_for_file(self.executable) + return self._hash + + +class Environment(_BaseEnvironment): + """ + This class is supposed to be created by internal Jedi architecture. You + should not create it directly. Please use create_environment or the other + functions instead. It is then returned by that function. + """ + def __init__(self, path, executable): + self.path = os.path.abspath(path) + """ + The path to an environment, matches ``sys.prefix``. + """ + self.executable = os.path.abspath(executable) + """ + The Python executable, matches ``sys.executable``. + """ + self.version_info = self._get_version() + """ + + Like ``sys.version_info``. A tuple to show the current Environment's + Python version. + """ + + def _get_version(self): + try: + process = GeneralizedPopen([self.executable, '--version'], stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + retcode = process.poll() + if retcode: + raise InvalidPythonEnvironment() + except OSError: + raise InvalidPythonEnvironment() + + # Until Python 3.4 wthe version string is part of stderr, after that + # stdout. + output = stdout + stderr + match = re.match(br'Python (\d+)\.(\d+)\.(\d+)', output) + if match is None: + raise InvalidPythonEnvironment("--version not working") + + return _VersionInfo(*[int(m) for m in match.groups()]) + + def __repr__(self): + version = '.'.join(str(i) for i in self.version_info) + return '<%s: %s in %s>' % (self.__class__.__name__, version, self.path) + + def get_evaluator_subprocess(self, evaluator): + return EvaluatorSubprocess(evaluator, self._get_subprocess()) + + def _get_subprocess(self): + return get_subprocess(self.executable) + + @memoize_method + def get_sys_path(self): + """ + The sys path for this environment. Does not include potential + modifications like ``sys.path.append``. + + :returns: list of str + """ + # It's pretty much impossible to generate the sys path without actually + # executing Python. The sys path (when starting with -S) itself depends + # on how the Python version was compiled (ENV variables). + # If you omit -S when starting Python (normal case), additionally + # site.py gets executed. + return self._get_subprocess().get_sys_path() + + +class SameEnvironment(Environment): + def __init__(self): + super(SameEnvironment, self).__init__(sys.prefix, sys.executable) + + def _get_version(self): + return _VersionInfo(*sys.version_info[:3]) + + +class InterpreterEnvironment(_BaseEnvironment): + def __init__(self): + self.version_info = _VersionInfo(*sys.version_info[:3]) + + def get_evaluator_subprocess(self, evaluator): + return EvaluatorSameProcess(evaluator) + + def get_sys_path(self): + return sys.path + + +def _get_virtual_env_from_var(): + var = os.environ.get('VIRTUAL_ENV') + if var is not None: + if var == sys.prefix: + return SameEnvironment() + + try: + return create_environment(var) + except InvalidPythonEnvironment: + pass + + +def _calculate_sha256_for_file(path): + sha256 = hashlib.sha256() + with open(path, 'rb') as f: + for block in iter(lambda: f.read(filecmp.BUFSIZE), b''): + sha256.update(block) + return sha256.hexdigest() + + +def get_default_environment(): + """ + Tries to return an active Virtualenv. If there is no VIRTUAL_ENV variable + set it will return the latest Python version installed on the system. This + makes it possible to use as many new Python features as possible when using + autocompletion and other functionality. + + :returns: :class:`Environment` + """ + virtual_env = _get_virtual_env_from_var() + if virtual_env is not None: + return virtual_env + + for environment in find_system_environments(): + return environment + + # If no Python Environment is found, use the environment we're already + # using. + return SameEnvironment() + + +@time_cache(seconds=10 * 60) # 10 Minutes +def get_cached_default_environment(): + return get_default_environment() + + +def find_virtualenvs(paths=None, **kwargs): + """ + :param paths: A list of paths in your file system to be scanned for + Virtualenvs. It will search in these paths and potentially execute the + Python binaries. Also the VIRTUAL_ENV variable will be checked if it + contains a valid Virtualenv. + :param safe: Default True. In case this is False, it will allow this + function to execute potential `python` environments. An attacker might + be able to drop an executable in a path this function is searching by + default. If the executable has not been installed by root, it will not + be executed. + + :yields: :class:`Environment` + """ + def py27_comp(paths=None, safe=True): + if paths is None: + paths = [] + + _used_paths = set() + + # Using this variable should be safe, because attackers might be able + # to drop files (via git) but not environment variables. + virtual_env = _get_virtual_env_from_var() + if virtual_env is not None: + yield virtual_env + _used_paths.add(virtual_env.path) + + for directory in paths: + if not os.path.isdir(directory): + continue + + directory = os.path.abspath(directory) + for path in os.listdir(directory): + path = os.path.join(directory, path) + if path in _used_paths: + # A path shouldn't be evaluated twice. + continue + _used_paths.add(path) + + try: + executable = _get_executable_path(path, safe=safe) + yield Environment(path, executable) + except InvalidPythonEnvironment: + pass + + return py27_comp(paths, **kwargs) + + +def find_system_environments(): + """ + Ignores virtualenvs and returns the Python versions that were installed on + your system. This might return nothing, if you're running Python e.g. from + a portable version. + + The environments are sorted from latest to oldest Python version. + + :yields: :class:`Environment` + """ + for version_string in _SUPPORTED_PYTHONS: + try: + yield get_system_environment(version_string) + except InvalidPythonEnvironment: + pass + + +# TODO: the logic to find the Python prefix is much more complicated than that. +# See Modules/getpath.c for UNIX and PC/getpathp.c for Windows in CPython's +# source code. A solution would be to deduce it by running the Python +# interpreter and printing the value of sys.prefix. +def _get_python_prefix(executable): + if os.name != 'nt': + return os.path.dirname(os.path.dirname(executable)) + landmark = os.path.join('Lib', 'os.py') + prefix = os.path.dirname(executable) + while prefix: + if os.path.join(prefix, landmark): + return prefix + prefix = os.path.dirname(prefix) + raise InvalidPythonEnvironment( + "Cannot find prefix of executable %s." % executable) + + +# TODO: this function should probably return a list of environments since +# multiple Python installations can be found on a system for the same version. +def get_system_environment(version): + """ + Return the first Python environment found for a string of the form 'X.Y' + where X and Y are the major and minor versions of Python. + + :raises: :exc:`.InvalidPythonEnvironment` + :returns: :class:`Environment` + """ + exe = find_executable('python' + version) + if exe: + if exe == sys.executable: + return SameEnvironment() + return Environment(_get_python_prefix(exe), exe) + + if os.name == 'nt': + for prefix, exe in _get_executables_from_windows_registry(version): + return Environment(prefix, exe) + raise InvalidPythonEnvironment("Cannot find executable python%s." % version) + + +def create_environment(path, safe=True): + """ + Make it possible to create an environment by hand. + + :raises: :exc:`.InvalidPythonEnvironment` + :returns: :class:`Environment` + """ + return Environment(path, _get_executable_path(path, safe=safe)) + + +def _get_executable_path(path, safe=True): + """ + Returns None if it's not actually a virtual env. + """ + + if os.name == 'nt': + python = os.path.join(path, 'Scripts', 'python.exe') + else: + python = os.path.join(path, 'bin', 'python') + if not os.path.exists(python): + raise InvalidPythonEnvironment("%s seems to be missing." % python) + + if safe and not _is_safe(python): + raise InvalidPythonEnvironment("The python binary is potentially unsafe.") + return python + + +def _get_executables_from_windows_registry(version): + # The winreg module is named _winreg on Python 2. + try: + import winreg + except ImportError: + import _winreg as winreg + + # TODO: support Python Anaconda. + sub_keys = [ + r'SOFTWARE\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Python\PythonCore\{version}-32\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}-32\InstallPath' + ] + for root_key in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]: + for sub_key in sub_keys: + sub_key = sub_key.format(version=version) + try: + with winreg.OpenKey(root_key, sub_key) as key: + prefix = winreg.QueryValueEx(key, '')[0] + exe = os.path.join(prefix, 'python.exe') + if os.path.isfile(exe): + yield prefix, exe + except WindowsError: + pass + + +def _is_safe(executable_path): + # Resolve sym links. A venv typically is a symlink to a known Python + # binary. Only virtualenvs copy symlinks around. + real_path = os.path.realpath(executable_path) + + if _is_unix_safe_simple(real_path): + return True + + # Just check the list of known Python versions. If it's not in there, + # it's likely an attacker or some Python that was not properly + # installed in the system. + for environment in find_system_environments(): + if environment.executable == real_path: + return True + + # If the versions don't match, just compare the binary files. If we + # don't do that, only venvs will be working and not virtualenvs. + # venvs are symlinks while virtualenvs are actual copies of the + # Python files. + # This still means that if the system Python is updated and the + # virtualenv's Python is not (which is probably never going to get + # upgraded), it will not work with Jedi. IMO that's fine, because + # people should just be using venv. ~ dave + if environment._sha256 == _calculate_sha256_for_file(real_path): + return True + return False + + +def _is_unix_safe_simple(real_path): + if _is_unix_admin(): + # In case we are root, just be conservative and + # only execute known paths. + return any(real_path.startswith(p) for p in _SAFE_PATHS) + + uid = os.stat(real_path).st_uid + # The interpreter needs to be owned by root. This means that it wasn't + # written by a user and therefore attacking Jedi is not as simple. + # The attack could look like the following: + # 1. A user clones a repository. + # 2. The repository has an innocent looking folder called foobar. jedi + # searches for the folder and executes foobar/bin/python --version if + # there's also a foobar/bin/activate. + # 3. The bin/python is obviously not a python script but a bash script or + # whatever the attacker wants. + return uid == 0 + + +def _is_unix_admin(): + try: + return os.getuid() == 0 + except AttributeError: + return False # Windows diff --git a/pythonFiles/jedi/api/exceptions.py b/pythonFiles/jedi/api/exceptions.py new file mode 100644 index 000000000000..99cebdb7ddb5 --- /dev/null +++ b/pythonFiles/jedi/api/exceptions.py @@ -0,0 +1,10 @@ +class _JediError(Exception): + pass + + +class InternalError(_JediError): + pass + + +class WrongVersion(_JediError): + pass diff --git a/pythonFiles/jedi/api/helpers.py b/pythonFiles/jedi/api/helpers.py index 2c4d8e0d10fc..221fc4dfe0d4 100644 --- a/pythonFiles/jedi/api/helpers.py +++ b/pythonFiles/jedi/api/helpers.py @@ -7,12 +7,13 @@ from parso.python.parser import Parser from parso.python import tree -from parso import split_lines from jedi._compatibility import u from jedi.evaluate.syntax_tree import eval_atom from jedi.evaluate.helpers import evaluate_call_of_leaf -from jedi.cache import time_cache +from jedi.evaluate.compiled import get_string_context_set +from jedi.evaluate.base_context import ContextSet +from jedi.cache import call_signature_time_cache CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name']) @@ -44,7 +45,7 @@ def _get_code(code_lines, start_pos, end_pos): lines[-1] = lines[-1][:end_pos[1]] # Remove first line indentation. lines[0] = lines[0][start_pos[1]:] - return '\n'.join(lines) + return ''.join(lines) class OnErrorLeaf(Exception): @@ -53,28 +54,11 @@ def error_leaf(self): return self.args[0] -def _is_on_comment(leaf, position): - comment_lines = split_lines(leaf.prefix) - difference = leaf.start_pos[0] - position[0] - prefix_start_pos = leaf.get_start_pos_of_prefix() - if difference == 0: - indent = leaf.start_pos[1] - elif position[0] == prefix_start_pos[0]: - indent = prefix_start_pos[1] - else: - indent = 0 - line = comment_lines[-difference - 1][:position[1] - indent] - return '#' in line - - def _get_code_for_stack(code_lines, module_node, position): leaf = module_node.get_leaf_for_position(position, include_prefixes=True) # It might happen that we're on whitespace or on a comment. This means # that we would not get the right leaf. if leaf.start_pos >= position: - if _is_on_comment(leaf, position): - return u('') - # If we're not on a comment simply get the previous leaf and proceed. leaf = leaf.get_previous_leaf() if leaf is None: @@ -125,6 +109,9 @@ def tokenize_without_endmarker(code): for token_ in tokens: if token_.string == safeword: raise EndMarkerReached() + elif token_.prefix.endswith(safeword): + # This happens with comments. + raise EndMarkerReached() else: yield token_ @@ -134,7 +121,7 @@ def tokenize_without_endmarker(code): # completion. # Use Z as a prefix because it's not part of a number suffix. safeword = 'ZZZ_USER_WANTS_TO_COMPLETE_HERE_WITH_JEDI' - code = code + safeword + code = code + ' ' + safeword p = Parser(grammar._pgen_grammar, error_recovery=True) try: @@ -208,6 +195,8 @@ def evaluate_goto_definition(evaluator, context, leaf): return evaluate_call_of_leaf(context, leaf) elif isinstance(leaf, tree.Literal): return eval_atom(context, leaf) + elif leaf.type in ('fstring_string', 'fstring_start', 'fstring_end'): + return get_string_context_set(evaluator) return [] @@ -294,14 +283,14 @@ def get_call_signature_details(module, position): return None -@time_cache("call_signatures_validity") +@call_signature_time_cache("call_signatures_validity") def cache_call_signatures(evaluator, context, bracket_leaf, code_lines, user_pos): """This function calculates the cache key.""" - index = user_pos[0] - 1 + line_index = user_pos[0] - 1 - before_cursor = code_lines[index][:user_pos[1]] - other_lines = code_lines[bracket_leaf.start_pos[0]:index] - whole = '\n'.join(other_lines + [before_cursor]) + before_cursor = code_lines[line_index][:user_pos[1]] + other_lines = code_lines[bracket_leaf.start_pos[0]:line_index] + whole = ''.join(other_lines + [before_cursor]) before_bracket = re.match(r'.*\(', whole, re.DOTALL) module_path = context.get_root_context().py__file__() diff --git a/pythonFiles/jedi/api/interpreter.py b/pythonFiles/jedi/api/interpreter.py index 202f345e94b9..c9b7bd69bbe0 100644 --- a/pythonFiles/jedi/api/interpreter.py +++ b/pythonFiles/jedi/api/interpreter.py @@ -5,24 +5,34 @@ from jedi.evaluate.context import ModuleContext from jedi.evaluate import compiled from jedi.evaluate.compiled import mixed +from jedi.evaluate.compiled.access import create_access_path from jedi.evaluate.base_context import Context +def _create(evaluator, obj): + return compiled.create_from_access_path( + evaluator, create_access_path(evaluator, obj) + ) + + class NamespaceObject(object): def __init__(self, dct): self.__dict__ = dct class MixedModuleContext(Context): - resets_positions = True type = 'mixed_module' - def __init__(self, evaluator, tree_module, namespaces, path): + def __init__(self, evaluator, tree_module, namespaces, path, code_lines): self.evaluator = evaluator self._namespaces = namespaces self._namespace_objects = [NamespaceObject(n) for n in namespaces] - self._module_context = ModuleContext(evaluator, tree_module, path=path) + self._module_context = ModuleContext( + evaluator, tree_module, + path=path, + code_lines=code_lines + ) self.tree_node = tree_module def get_node(self): @@ -33,7 +43,7 @@ def get_filters(self, *args, **kwargs): yield filter for namespace_obj in self._namespace_objects: - compiled_object = compiled.create(self.evaluator, namespace_obj) + compiled_object = _create(self.evaluator, namespace_obj) mixed_object = mixed.MixedObject( self.evaluator, parent_context=self, @@ -43,5 +53,9 @@ def get_filters(self, *args, **kwargs): for filter in mixed_object.get_filters(*args, **kwargs): yield filter + @property + def code_lines(self): + return self._module_context.code_lines + def __getattr__(self, name): return getattr(self._module_context, name) diff --git a/pythonFiles/jedi/api/keywords.py b/pythonFiles/jedi/api/keywords.py index a1bc4e7f8556..2991a0f81a56 100644 --- a/pythonFiles/jedi/api/keywords.py +++ b/pythonFiles/jedi/api/keywords.py @@ -1,10 +1,7 @@ import pydoc -import keyword -from jedi._compatibility import is_py3, is_py35 from jedi.evaluate.utils import ignored from jedi.evaluate.filters import AbstractNameDefinition -from parso.python.tree import Leaf try: from pydoc_data import topics as pydoc_topics @@ -17,87 +14,30 @@ # pydoc_data module in its file python3x.zip. pydoc_topics = None -if is_py3: - if is_py35: - # in python 3.5 async and await are not proper keywords, but for - # completion pursposes should as as though they are - keys = keyword.kwlist + ["async", "await"] - else: - keys = keyword.kwlist -else: - keys = keyword.kwlist + ['None', 'False', 'True'] - - -def has_inappropriate_leaf_keyword(pos, module): - relevant_errors = filter( - lambda error: error.first_pos[0] == pos[0], - module.error_statement_stacks) - - for error in relevant_errors: - if error.next_token in keys: - return True - - return False - - -def completion_names(evaluator, stmt, pos, module): - keyword_list = all_keywords(evaluator) - - if not isinstance(stmt, Leaf) or has_inappropriate_leaf_keyword(pos, module): - keyword_list = filter( - lambda keyword: not keyword.only_valid_as_leaf, - keyword_list - ) - return [keyword.name for keyword in keyword_list] - - -def all_keywords(evaluator, pos=(0, 0)): - return set([Keyword(evaluator, k, pos) for k in keys]) - - -def keyword(evaluator, string, pos=(0, 0)): - if string in keys: - return Keyword(evaluator, string, pos) - else: - return None - def get_operator(evaluator, string, pos): return Keyword(evaluator, string, pos) -keywords_only_valid_as_leaf = ( - 'continue', - 'break', -) - - class KeywordName(AbstractNameDefinition): - api_type = 'keyword' + api_type = u'keyword' def __init__(self, evaluator, name): self.evaluator = evaluator self.string_name = name - self.parent_context = evaluator.BUILTINS - - def eval(self): - return set() + self.parent_context = evaluator.builtins_module def infer(self): return [Keyword(self.evaluator, self.string_name, (0, 0))] class Keyword(object): - api_type = 'keyword' + api_type = u'keyword' def __init__(self, evaluator, name, pos): self.name = KeywordName(evaluator, name) self.start_pos = pos - self.parent = evaluator.BUILTINS - - @property - def only_valid_as_leaf(self): - return self.name.value in keywords_only_valid_as_leaf + self.parent = evaluator.builtins_module @property def names(self): diff --git a/pythonFiles/jedi/api/project.py b/pythonFiles/jedi/api/project.py new file mode 100644 index 000000000000..ca6992b5db7f --- /dev/null +++ b/pythonFiles/jedi/api/project.py @@ -0,0 +1,200 @@ +import os +import json + +from jedi._compatibility import FileNotFoundError, NotADirectoryError +from jedi.api.environment import SameEnvironment, \ + get_cached_default_environment +from jedi.api.exceptions import WrongVersion +from jedi._compatibility import force_unicode +from jedi.evaluate.sys_path import discover_buildout_paths +from jedi.evaluate.cache import evaluator_as_method_param_cache +from jedi.common.utils import traverse_parents + +_CONFIG_FOLDER = '.jedi' +_CONTAINS_POTENTIAL_PROJECT = 'setup.py', '.git', '.hg', 'requirements.txt', 'MANIFEST.in' + +_SERIALIZER_VERSION = 1 + + +def _remove_duplicates_from_path(path): + used = set() + for p in path: + if p in used: + continue + used.add(p) + yield p + + +def _force_unicode_list(lst): + return list(map(force_unicode, lst)) + + +class Project(object): + # TODO serialize environment + _serializer_ignore_attributes = ('_environment',) + _environment = None + + @staticmethod + def _get_json_path(base_path): + return os.path.join(base_path, _CONFIG_FOLDER, 'project.json') + + @classmethod + def load(cls, path): + """ + :param path: The path of the directory you want to use as a project. + """ + with open(cls._get_json_path(path)) as f: + version, data = json.load(f) + + if version == 1: + self = cls.__new__() + self.__dict__.update(data) + return self + else: + raise WrongVersion( + "The Jedi version of this project seems newer than what we can handle." + ) + + def __init__(self, path, **kwargs): + """ + :param path: The base path for this project. + :param sys_path: list of str. You can override the sys path if you + want. By default the ``sys.path.`` is generated from the + environment (virtualenvs, etc). + :param smart_sys_path: If this is enabled (default), adds paths from + local directories. Otherwise you will have to rely on your packages + being properly configured on the ``sys.path``. + """ + def py2_comp(path, environment=None, sys_path=None, + smart_sys_path=True, _django=False): + self._path = path + if isinstance(environment, SameEnvironment): + self._environment = environment + + self._sys_path = sys_path + self._smart_sys_path = smart_sys_path + self._django = _django + + py2_comp(path, **kwargs) + + def _get_base_sys_path(self, environment=None): + if self._sys_path is not None: + return self._sys_path + + # The sys path has not been set explicitly. + if environment is None: + environment = self.get_environment() + + sys_path = environment.get_sys_path() + try: + sys_path.remove('') + except ValueError: + pass + return sys_path + + @evaluator_as_method_param_cache() + def _get_sys_path(self, evaluator, environment=None): + """ + Keep this method private for all users of jedi. However internally this + one is used like a public method. + """ + suffixed = [] + prefixed = [] + + sys_path = list(self._get_base_sys_path(environment)) + if self._smart_sys_path: + prefixed.append(self._path) + + if evaluator.script_path is not None: + suffixed += discover_buildout_paths(evaluator, evaluator.script_path) + + traversed = [] + for parent in traverse_parents(evaluator.script_path): + traversed.append(parent) + if parent == self._path: + # Don't go futher than the project path. + break + + # AFAIK some libraries have imports like `foo.foo.bar`, which + # leads to the conclusion to by default prefer longer paths + # rather than shorter ones by default. + suffixed += reversed(traversed) + + if self._django: + prefixed.append(self._path) + + path = prefixed + sys_path + suffixed + return list(_force_unicode_list(_remove_duplicates_from_path(path))) + + def save(self): + data = dict(self.__dict__) + for attribute in self._serializer_ignore_attributes: + data.pop(attribute, None) + + with open(self._get_json_path(self._path), 'wb') as f: + return json.dump((_SERIALIZER_VERSION, data), f) + + def get_environment(self): + if self._environment is None: + return get_cached_default_environment() + + return self._environment + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self._path) + + +def _is_potential_project(path): + for name in _CONTAINS_POTENTIAL_PROJECT: + if os.path.exists(os.path.join(path, name)): + return True + return False + + +def _is_django_path(directory): + """ Detects the path of the very well known Django library (if used) """ + try: + with open(os.path.join(directory, 'manage.py'), 'rb') as f: + return b"DJANGO_SETTINGS_MODULE" in f.read() + except (FileNotFoundError, NotADirectoryError): + return False + + return False + + +def get_default_project(path=None): + if path is None: + path = os.getcwd() + + check = os.path.realpath(path) + probable_path = None + first_no_init_file = None + for dir in traverse_parents(check, include_current=True): + try: + return Project.load(dir) + except (FileNotFoundError, NotADirectoryError): + pass + + if first_no_init_file is None: + if os.path.exists(os.path.join(dir, '__init__.py')): + # In the case that a __init__.py exists, it's in 99% just a + # Python package and the project sits at least one level above. + continue + else: + first_no_init_file = dir + + if _is_django_path(dir): + return Project(dir, _django=True) + + if probable_path is None and _is_potential_project(dir): + probable_path = dir + + if probable_path is not None: + # TODO search for setup.py etc + return Project(probable_path) + + if first_no_init_file is not None: + return Project(first_no_init_file) + + curdir = path if os.path.isdir(path) else os.path.dirname(path) + return Project(curdir) diff --git a/pythonFiles/jedi/api/replstartup.py b/pythonFiles/jedi/api/replstartup.py index 5bfcc8ce889e..4c44a626b775 100644 --- a/pythonFiles/jedi/api/replstartup.py +++ b/pythonFiles/jedi/api/replstartup.py @@ -11,8 +11,8 @@ [GCC 4.6.1] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os - >>> os.path.join().split().in # doctest: +SKIP - os.path.join().split().index os.path.join().split().insert + >>> os.path.join('a', 'b').split().in # doctest: +SKIP + ..dex ..sert """ import jedi.utils diff --git a/pythonFiles/jedi/cache.py b/pythonFiles/jedi/cache.py index 01138e75a99b..6c0c2a830942 100644 --- a/pythonFiles/jedi/cache.py +++ b/pythonFiles/jedi/cache.py @@ -12,6 +12,7 @@ these variables are being cleaned after every API usage. """ import time +from functools import wraps from jedi import settings from parso.cache import parser_cache @@ -74,7 +75,7 @@ def clear_time_caches(delete_all=False): del tc[key] -def time_cache(time_add_setting): +def call_signature_time_cache(time_add_setting): """ This decorator works as follows: Call it with a setting and after that use the function with a callable that returns the key. @@ -106,8 +107,31 @@ def wrapper(*args, **kwargs): return _temp +def time_cache(seconds): + def decorator(func): + cache = {} + + @wraps(func) + def wrapper(*args, **kwargs): + key = (args, frozenset(kwargs.items())) + try: + created, result = cache[key] + if time.time() < created + seconds: + return result + except KeyError: + pass + result = func(*args, **kwargs) + cache[key] = time.time(), result + return result + + wrapper.clear_cache = lambda: cache.clear() + return wrapper + return decorator + + def memoize_method(method): """A normal memoize function.""" + @wraps(method) def wrapper(self, *args, **kwargs): cache_dict = self.__dict__.setdefault('_memoize_method_dct', {}) dct = cache_dict.setdefault(method, {}) diff --git a/pythonFiles/jedi/common/utils.py b/pythonFiles/jedi/common/utils.py new file mode 100644 index 000000000000..72726a4696e6 --- /dev/null +++ b/pythonFiles/jedi/common/utils.py @@ -0,0 +1,12 @@ +import os + + +def traverse_parents(path, include_current=False): + if not include_current: + path = os.path.dirname(path) + + previous = None + while previous != path: + yield path + previous = path + path = os.path.dirname(path) diff --git a/pythonFiles/jedi/debug.py b/pythonFiles/jedi/debug.py index 8caf1accb17f..a4fd86465bac 100644 --- a/pythonFiles/jedi/debug.py +++ b/pythonFiles/jedi/debug.py @@ -35,7 +35,7 @@ def _lazy_colorama_init(): # need this. initialise.atexit_done = True try: - init() + init(strip=False) except Exception: # Colorama fails with initializing under vim and is buggy in # version 0.3.6. diff --git a/pythonFiles/jedi/evaluate/__init__.py b/pythonFiles/jedi/evaluate/__init__.py index 20461071abdb..3ba52b89f408 100644 --- a/pythonFiles/jedi/evaluate/__init__.py +++ b/pythonFiles/jedi/evaluate/__init__.py @@ -17,7 +17,8 @@ ``eval_expr_stmt``. There's separate logic for autocompletion in the API, the evaluator is all about evaluating an expression. -TODO this paragraph is not what jedi does anymore. +TODO this paragraph is not what jedi does anymore, it's similar, but not the +same. Now you need to understand what follows after ``eval_expr_stmt``. Let's make an example:: @@ -62,10 +63,9 @@ that are not used are just being ignored. """ -import sys - from parso.python import tree import parso +from parso import python_bytes_to_unicode from jedi import debug from jedi import parser_utils @@ -86,31 +86,42 @@ class Evaluator(object): - def __init__(self, grammar, project): - self.grammar = grammar + def __init__(self, project, environment=None, script_path=None): + if environment is None: + environment = project.get_environment() + self.environment = environment + self.script_path = script_path + self.compiled_subprocess = environment.get_evaluator_subprocess(self) + self.grammar = environment.get_grammar() + self.latest_grammar = parso.load_grammar(version='3.6') self.memoize_cache = {} # for memoize decorators - # To memorize modules -> equals `sys.modules`. - self.modules = {} # like `sys.modules`. + self.module_cache = imports.ModuleCache() # does the job of `sys.modules`. self.compiled_cache = {} # see `evaluate.compiled.create()` self.inferred_element_counts = {} self.mixed_cache = {} # see `evaluate.compiled.mixed._create()` self.analysis = [] self.dynamic_params_depth = 0 self.is_analysis = False - self.python_version = sys.version_info[:2] self.project = project - project.add_evaluator(self) + self.access_cache = {} self.reset_recursion_limitations() + self.allow_different_encoding = True - # Constants - self.BUILTINS = compiled.get_special_object(self, 'BUILTINS') + @property + @evaluator_function_cache() + def builtins_module(self): + return compiled.get_special_object(self, u'BUILTINS') def reset_recursion_limitations(self): self.recursion_detector = recursion.RecursionDetector() self.execution_recursion_detector = recursion.ExecutionRecursionDetector(self) + def get_sys_path(self): + """Convenience function""" + return self.project._get_sys_path(self, environment=self.environment) + def eval_element(self, context, element): if isinstance(context, CompForContext): return eval_node(context, element) @@ -124,7 +135,11 @@ def eval_element(self, context, element): if_stmt = None break predefined_if_name_dict = context.predefined_names.get(if_stmt) - if predefined_if_name_dict is None and if_stmt and if_stmt.type == 'if_stmt': + # TODO there's a lot of issues with this one. We actually should do + # this in a different way. Caching should only be active in certain + # cases and this all sucks. + if predefined_if_name_dict is None and if_stmt \ + and if_stmt.type == 'if_stmt' and self.is_analysis: if_stmt_test = if_stmt.children[1] name_dicts = [{}] # If we already did a check, we don't want to do it again -> If @@ -357,3 +372,15 @@ def from_scope_node(scope_node, child_is_funcdef=None, is_nested=True, node_is_o node = node.parent scope_node = parent_scope(node) return from_scope_node(scope_node, is_nested=True, node_is_object=node_is_object) + + def parse_and_get_code(self, code=None, path=None, **kwargs): + if self.allow_different_encoding: + if code is None: + with open(path, 'rb') as f: + code = f.read() + code = python_bytes_to_unicode(code, errors='replace') + + return self.grammar.parse(code=code, path=path, **kwargs), code + + def parse(self, *args, **kwargs): + return self.parse_and_get_code(*args, **kwargs)[0] diff --git a/pythonFiles/jedi/evaluate/analysis.py b/pythonFiles/jedi/evaluate/analysis.py index c825e5fef9e9..ded4e9f20880 100644 --- a/pythonFiles/jedi/evaluate/analysis.py +++ b/pythonFiles/jedi/evaluate/analysis.py @@ -1,9 +1,12 @@ """ Module for statical analysis. """ -from jedi import debug from parso.python import tree + +from jedi._compatibility import force_unicode +from jedi import debug from jedi.evaluate.compiled import CompiledObject +from jedi.evaluate.helpers import is_string CODES = { @@ -114,9 +117,10 @@ def add_attribute_error(name_context, lookup_context, name): # instead of an error, if that happens. typ = Error if isinstance(lookup_context, AbstractInstanceContext): - slot_names = lookup_context.get_function_slot_names('__getattr__') + \ - lookup_context.get_function_slot_names('__getattribute__') + slot_names = lookup_context.get_function_slot_names(u'__getattr__') + \ + lookup_context.get_function_slot_names(u'__getattribute__') for n in slot_names: + # TODO do we even get here? if isinstance(name, CompiledInstanceName) and \ n.parent_context.obj == object: typ = Warning @@ -139,7 +143,7 @@ def _check_for_exception_catch(node_context, jedi_name, exception, payload=None) """ def check_match(cls, exception): try: - return isinstance(cls, CompiledObject) and issubclass(exception, cls.obj) + return isinstance(cls, CompiledObject) and cls.is_super_class(exception) except TypeError: return False @@ -160,7 +164,7 @@ def check_try_for_except(obj, exception): except_classes = node_context.eval_node(node) for cls in except_classes: from jedi.evaluate.context import iterable - if isinstance(cls, iterable.AbstractIterable) and \ + if isinstance(cls, iterable.Sequence) and \ cls.array_type == 'tuple': # multiple exceptions for lazy_context in cls.py__iter__(): @@ -189,8 +193,8 @@ def check_hasattr(node, suite): # Check name key, lazy_context = args[1] names = list(lazy_context.infer()) - assert len(names) == 1 and isinstance(names[0], CompiledObject) - assert names[0].obj == payload[1].value + assert len(names) == 1 and is_string(names[0]) + assert force_unicode(names[0].get_safe_value()) == payload[1].value # Check objects key, lazy_context = args[0] diff --git a/pythonFiles/jedi/evaluate/arguments.py b/pythonFiles/jedi/evaluate/arguments.py index 32b9238c6f4d..beab4c8c9541 100644 --- a/pythonFiles/jedi/evaluate/arguments.py +++ b/pythonFiles/jedi/evaluate/arguments.py @@ -10,6 +10,7 @@ from jedi.evaluate.context import iterable from jedi.evaluate.param import get_params, ExecutedParam + def try_iter_content(types, depth=0): """Helper method for static analysis.""" if depth > 10: @@ -29,6 +30,8 @@ def try_iter_content(types, depth=0): class AbstractArguments(object): context = None + argument_node = None + trailer = None def eval_argument_clinic(self, parameters): """Uses a list with argument clinic information (see PEP 436).""" @@ -95,29 +98,30 @@ def __init__(self, evaluator, context, argument_node, trailer=None): self.trailer = trailer # Can be None, e.g. in a class definition. def _split(self): - if isinstance(self.argument_node, (tuple, list)): - for el in self.argument_node: - yield 0, el - else: - if not (self.argument_node.type == 'arglist' or ( - # in python 3.5 **arg is an argument, not arglist - (self.argument_node.type == 'argument') and - self.argument_node.children[0] in ('*', '**'))): - yield 0, self.argument_node - return - - iterator = iter(self.argument_node.children) - for child in iterator: - if child == ',': - continue - elif child in ('*', '**'): - yield len(child.value), next(iterator) - elif child.type == 'argument' and \ - child.children[0] in ('*', '**'): - assert len(child.children) == 2 - yield len(child.children[0].value), child.children[1] - else: - yield 0, child + if self.argument_node is None: + return + + # Allow testlist here as well for Python2's class inheritance + # definitions. + if not (self.argument_node.type in ('arglist', 'testlist') or ( + # in python 3.5 **arg is an argument, not arglist + (self.argument_node.type == 'argument') and + self.argument_node.children[0] in ('*', '**'))): + yield 0, self.argument_node + return + + iterator = iter(self.argument_node.children) + for child in iterator: + if child == ',': + continue + elif child in ('*', '**'): + yield len(child.value), next(iterator) + elif child.type == 'argument' and \ + child.children[0] in ('*', '**'): + assert len(child.children) == 2 + yield len(child.children[0].value), child.children[1] + else: + yield 0, child def unpack(self, funcdef=None): named_args = [] @@ -126,7 +130,6 @@ def unpack(self, funcdef=None): arrays = self.context.eval_node(el) iterators = [_iterate_star_args(self.context, a, el, funcdef) for a in arrays] - iterators = list(iterators) for values in list(zip_longest(*iterators)): # TODO zip_longest yields None, that means this would raise # an exception? @@ -134,7 +137,7 @@ def unpack(self, funcdef=None): [v for v in values if v is not None] ) elif star_count == 2: - arrays = self._evaluator.eval_element(self.context, el) + arrays = self.context.eval_node(el) for dct in arrays: for key, values in _star_star_dict(self.context, dct, el, funcdef): yield key, values @@ -197,7 +200,11 @@ def get_calling_nodes(self): arguments = param.var_args break - return [arguments.argument_node or arguments.trailer] + if arguments.argument_node is not None: + return [arguments.argument_node] + if arguments.trailer is not None: + return [arguments.trailer] + return [] class ValuesArguments(AbstractArguments): @@ -235,7 +242,7 @@ def _star_star_dict(context, array, input_node, funcdef): # For now ignore this case. In the future add proper iterators and just # make one call without crazy isinstance checks. return {} - elif isinstance(array, iterable.AbstractIterable) and array.array_type == 'dict': + elif isinstance(array, iterable.Sequence) and array.array_type == 'dict': return array.exact_key_items() else: if funcdef is not None: diff --git a/pythonFiles/jedi/evaluate/base_context.py b/pythonFiles/jedi/evaluate/base_context.py index 693a99aae7aa..2c6fe6cd2c88 100644 --- a/pythonFiles/jedi/evaluate/base_context.py +++ b/pythonFiles/jedi/evaluate/base_context.py @@ -1,3 +1,11 @@ +""" +Contexts are the "values" that Python would return. However Contexts are at the +same time also the "contexts" that a user is currently sitting in. + +A ContextSet is typically used to specify the return of a function or any other +static analysis operation. In jedi there are always multiple returns and not +just one. +""" from parso.python.tree import ExprStmt, CompFor from jedi import debug @@ -63,10 +71,13 @@ def execute_evaluated(self, *value_list): arguments = ValuesArguments([ContextSet(value) for value in value_list]) return self.execute(arguments) - def iterate(self, contextualized_node=None): - debug.dbg('iterate') + def iterate(self, contextualized_node=None, is_async=False): + debug.dbg('iterate %s', self) try: - iter_method = self.py__iter__ + if is_async: + iter_method = self.py__aiter__ + else: + iter_method = self.py__iter__ except AttributeError: if contextualized_node is not None: from jedi.evaluate import analysis @@ -81,17 +92,22 @@ def iterate(self, contextualized_node=None): def get_item(self, index_contexts, contextualized_node): from jedi.evaluate.compiled import CompiledObject - from jedi.evaluate.context.iterable import Slice, AbstractIterable + from jedi.evaluate.context.iterable import Slice, Sequence result = ContextSet() for index in index_contexts: - if isinstance(index, (CompiledObject, Slice)): + if isinstance(index, Slice): index = index.obj + if isinstance(index, CompiledObject): + try: + index = index.get_safe_value() + except ValueError: + pass - if type(index) not in (float, int, str, unicode, slice, type(Ellipsis)): + if type(index) not in (float, int, str, unicode, slice, bytes): # If the index is not clearly defined, we have to get all the # possiblities. - if isinstance(self, AbstractIterable) and self.array_type == 'dict': + if isinstance(self, Sequence) and self.array_type == 'dict': result |= self.dict_values() else: result |= iterate_contexts(ContextSet(self)) @@ -139,10 +155,6 @@ def py__getattribute__(self, name_or_str, name_context=None, position=None, return f.filter_name(filters) return f.find(filters, attribute_lookup=not search_global) - return self.evaluator.find_types( - self, name_or_str, name_context, position, search_global, is_goto, - analysis_errors) - def create_context(self, node, node_is_context=False, node_is_object=False): return self.evaluator.create_context(self, node, node_is_context, node_is_object) @@ -169,14 +181,14 @@ def py__doc__(self, include_call_signature=False): return None -def iterate_contexts(contexts, contextualized_node=None): +def iterate_contexts(contexts, contextualized_node=None, is_async=False): """ Calls `iterate`, on all contexts but ignores the ordering and just returns all contexts that the iterate functions yield. """ return ContextSet.from_sets( lazy_context.infer() - for lazy_context in contexts.iterate(contextualized_node) + for lazy_context in contexts.iterate(contextualized_node, is_async=is_async) ) @@ -241,9 +253,9 @@ class ContextSet(BaseContextSet): def py__class__(self): return ContextSet.from_iterable(c.py__class__() for c in self._set) - def iterate(self, contextualized_node=None): + def iterate(self, contextualized_node=None, is_async=False): from jedi.evaluate.lazy_context import get_merged_lazy_context - type_iters = [c.iterate(contextualized_node) for c in self._set] + type_iters = [c.iterate(contextualized_node, is_async=is_async) for c in self._set] for lazy_contexts in zip_longest(*type_iters): yield get_merged_lazy_context( [l for l in lazy_contexts if l is not None] diff --git a/pythonFiles/jedi/evaluate/cache.py b/pythonFiles/jedi/evaluate/cache.py index b7c7cd7e979f..c619e698a3c8 100644 --- a/pythonFiles/jedi/evaluate/cache.py +++ b/pythonFiles/jedi/evaluate/cache.py @@ -59,7 +59,7 @@ def decorator(func): return decorator -def _memoize_meta_class(): +def evaluator_as_method_param_cache(): def decorator(call): return _memoize_default(second_arg_is_evaluator=True)(call) @@ -72,6 +72,6 @@ class CachedMetaClass(type): class initializations. Either you do it this way or with decorators, but with decorators you lose class access (isinstance, etc). """ - @_memoize_meta_class() + @evaluator_as_method_param_cache() def __call__(self, *args, **kwargs): return super(CachedMetaClass, self).__call__(*args, **kwargs) diff --git a/pythonFiles/jedi/evaluate/compiled/__init__.py b/pythonFiles/jedi/evaluate/compiled/__init__.py index f9f2e0781e13..357d26cc87fc 100644 --- a/pythonFiles/jedi/evaluate/compiled/__init__.py +++ b/pythonFiles/jedi/evaluate/compiled/__init__.py @@ -1,638 +1,39 @@ -""" -Imitate the parser representation. -""" -import inspect -import re -import sys -import os -import types -from functools import partial +from jedi._compatibility import unicode +from jedi.evaluate.compiled.context import CompiledObject, CompiledName, \ + CompiledObjectFilter, CompiledContextName, create_from_access_path, \ + create_from_name -from jedi._compatibility import builtins as _builtins, unicode, py_version -from jedi import debug -from jedi.cache import underscore_memoization, memoize_method -from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ - ContextNameMixin -from jedi.evaluate.base_context import Context, ContextSet -from jedi.evaluate.lazy_context import LazyKnownContext -from jedi.evaluate.compiled.getattr_static import getattr_static -from . import fake - -_sep = os.path.sep -if os.path.altsep is not None: - _sep += os.path.altsep -_path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) -del _sep - -# Those types don't exist in typing. -MethodDescriptorType = type(str.replace) -WrapperDescriptorType = type(set.__iter__) -# `object.__subclasshook__` is an already executed descriptor. -object_class_dict = type.__dict__["__dict__"].__get__(object) -ClassMethodDescriptorType = type(object_class_dict['__subclasshook__']) - -ALLOWED_DESCRIPTOR_ACCESS = ( - types.FunctionType, - types.GetSetDescriptorType, - types.MemberDescriptorType, - MethodDescriptorType, - WrapperDescriptorType, - ClassMethodDescriptorType, - staticmethod, - classmethod, -) - -class CheckAttribute(object): - """Raises an AttributeError if the attribute X isn't available.""" - def __init__(self, func): - self.func = func - # Remove the py in front of e.g. py__call__. - self.check_name = func.__name__[2:] - - def __get__(self, instance, owner): - # This might raise an AttributeError. That's wanted. - if self.check_name == '__iter__': - # Python iterators are a bit strange, because there's no need for - # the __iter__ function as long as __getitem__ is defined (it will - # just start with __getitem__(0). This is especially true for - # Python 2 strings, where `str.__iter__` is not even defined. - try: - iter(instance.obj) - except TypeError: - raise AttributeError - else: - getattr(instance.obj, self.check_name) - return partial(self.func, instance) - - -class CompiledObject(Context): - path = None # modules have this attribute - set it to None. - used_names = lambda self: {} # To be consistent with modules. - - def __init__(self, evaluator, obj, parent_context=None, faked_class=None): - super(CompiledObject, self).__init__(evaluator, parent_context) - self.obj = obj - # This attribute will not be set for most classes, except for fakes. - self.tree_node = faked_class - - def get_root_node(self): - # To make things a bit easier with filters we add this method here. - return self.get_root_context() - - @CheckAttribute - def py__call__(self, params): - if inspect.isclass(self.obj): - from jedi.evaluate.context import CompiledInstance - return ContextSet(CompiledInstance(self.evaluator, self.parent_context, self, params)) - else: - return ContextSet.from_iterable(self._execute_function(params)) - - @CheckAttribute - def py__class__(self): - return create(self.evaluator, self.obj.__class__) - - @CheckAttribute - def py__mro__(self): - return (self,) + tuple(create(self.evaluator, cls) for cls in self.obj.__mro__[1:]) - - @CheckAttribute - def py__bases__(self): - return tuple(create(self.evaluator, cls) for cls in self.obj.__bases__) - - def py__bool__(self): - return bool(self.obj) - - def py__file__(self): - try: - return self.obj.__file__ - except AttributeError: - return None - - def is_class(self): - return inspect.isclass(self.obj) - - def py__doc__(self, include_call_signature=False): - return inspect.getdoc(self.obj) or '' - - def get_param_names(self): - obj = self.obj - try: - if py_version < 33: - raise ValueError("inspect.signature was introduced in 3.3") - if py_version == 34: - # In 3.4 inspect.signature are wrong for str and int. This has - # been fixed in 3.5. The signature of object is returned, - # because no signature was found for str. Here we imitate 3.5 - # logic and just ignore the signature if the magic methods - # don't match object. - # 3.3 doesn't even have the logic and returns nothing for str - # and classes that inherit from object. - user_def = inspect._signature_get_user_defined_method - if (inspect.isclass(obj) - and not user_def(type(obj), '__init__') - and not user_def(type(obj), '__new__') - and (obj.__init__ != object.__init__ - or obj.__new__ != object.__new__)): - raise ValueError - - signature = inspect.signature(obj) - except ValueError: # Has no signature - params_str, ret = self._parse_function_doc() - tokens = params_str.split(',') - if inspect.ismethoddescriptor(obj): - tokens.insert(0, 'self') - for p in tokens: - parts = p.strip().split('=') - yield UnresolvableParamName(self, parts[0]) - else: - for signature_param in signature.parameters.values(): - yield SignatureParamName(self, signature_param) - - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, repr(self.obj)) - - @underscore_memoization - def _parse_function_doc(self): - doc = self.py__doc__() - if doc is None: - return '', '' - - return _parse_function_doc(doc) - - @property - def api_type(self): - obj = self.obj - if inspect.isclass(obj): - return 'class' - elif inspect.ismodule(obj): - return 'module' - elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \ - or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj): - return 'function' - # Everything else... - return 'instance' - - @property - def type(self): - """Imitate the tree.Node.type values.""" - cls = self._get_class() - if inspect.isclass(cls): - return 'classdef' - elif inspect.ismodule(cls): - return 'file_input' - elif inspect.isbuiltin(cls) or inspect.ismethod(cls) or \ - inspect.ismethoddescriptor(cls): - return 'funcdef' - - @underscore_memoization - def _cls(self): - """ - We used to limit the lookups for instantiated objects like list(), but - this is not the case anymore. Python itself - """ - # Ensures that a CompiledObject is returned that is not an instance (like list) - return self - - def _get_class(self): - if not fake.is_class_instance(self.obj) or \ - inspect.ismethoddescriptor(self.obj): # slots - return self.obj - - try: - return self.obj.__class__ - except AttributeError: - # happens with numpy.core.umath._UFUNC_API (you get it - # automatically by doing `import numpy`. - return type - - def get_filters(self, search_global=False, is_instance=False, - until_position=None, origin_scope=None): - yield self._ensure_one_filter(is_instance) - - @memoize_method - def _ensure_one_filter(self, is_instance): - """ - search_global shouldn't change the fact that there's one dict, this way - there's only one `object`. - """ - return CompiledObjectFilter(self.evaluator, self, is_instance) - - @CheckAttribute - def py__getitem__(self, index): - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. - return ContextSet() - - return ContextSet(create(self.evaluator, self.obj[index])) - - @CheckAttribute - def py__iter__(self): - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. - return - - for i, part in enumerate(self.obj): - if i > 20: - # Should not go crazy with large iterators - break - yield LazyKnownContext(create(self.evaluator, part)) - - def py__name__(self): - try: - return self._get_class().__name__ - except AttributeError: - return None - - @property - def name(self): - try: - name = self._get_class().__name__ - except AttributeError: - name = repr(self.obj) - return CompiledContextName(self, name) - - def _execute_function(self, params): - from jedi.evaluate import docstrings - if self.type != 'funcdef': - return - for name in self._parse_function_doc()[1].split(): - try: - bltn_obj = getattr(_builtins, name) - except AttributeError: - continue - else: - if bltn_obj is None: - # We want to evaluate everything except None. - # TODO do we? - continue - bltn_obj = create(self.evaluator, bltn_obj) - for result in bltn_obj.execute(params): - yield result - for type_ in docstrings.infer_return_types(self): - yield type_ - - def get_self_attributes(self): - return [] # Instance compatibility - - def get_imports(self): - return [] # Builtins don't have imports - - def dict_values(self): - return ContextSet.from_iterable( - create(self.evaluator, v) for v in self.obj.values() - ) - - -class CompiledName(AbstractNameDefinition): - def __init__(self, evaluator, parent_context, name): - self._evaluator = evaluator - self.parent_context = parent_context - self.string_name = name - - def __repr__(self): - try: - name = self.parent_context.name # __name__ is not defined all the time - except AttributeError: - name = None - return '<%s: (%s).%s>' % (self.__class__.__name__, name, self.string_name) - - @property - def api_type(self): - return next(iter(self.infer())).api_type - - @underscore_memoization - def infer(self): - module = self.parent_context.get_root_context() - return ContextSet(_create_from_name( - self._evaluator, module, self.parent_context, self.string_name - )) - - -class SignatureParamName(AbstractNameDefinition): - api_type = 'param' - - def __init__(self, compiled_obj, signature_param): - self.parent_context = compiled_obj.parent_context - self._signature_param = signature_param - - @property - def string_name(self): - return self._signature_param.name - - def infer(self): - p = self._signature_param - evaluator = self.parent_context.evaluator - contexts = ContextSet() - if p.default is not p.empty: - contexts = ContextSet(create(evaluator, p.default)) - if p.annotation is not p.empty: - annotation = create(evaluator, p.annotation) - contexts |= annotation.execute_evaluated() - return contexts - - -class UnresolvableParamName(AbstractNameDefinition): - api_type = 'param' - - def __init__(self, compiled_obj, name): - self.parent_context = compiled_obj.parent_context - self.string_name = name - - def infer(self): - return ContextSet() - - -class CompiledContextName(ContextNameMixin, AbstractNameDefinition): - def __init__(self, context, name): - self.string_name = name - self._context = context - self.parent_context = context.parent_context - - -class EmptyCompiledName(AbstractNameDefinition): - """ - Accessing some names will raise an exception. To avoid not having any - completions, just give Jedi the option to return this object. It infers to - nothing. - """ - def __init__(self, evaluator, name): - self.parent_context = evaluator.BUILTINS - self.string_name = name - - def infer(self): - return ContextSet() - - -class CompiledObjectFilter(AbstractFilter): - name_class = CompiledName - - def __init__(self, evaluator, compiled_object, is_instance=False): - self._evaluator = evaluator - self._compiled_object = compiled_object - self._is_instance = is_instance - - @memoize_method - def get(self, name): - name = str(name) - obj = self._compiled_object.obj - try: - attr, is_get_descriptor = getattr_static(obj, name) - except AttributeError: - return [] - else: - if is_get_descriptor \ - and not type(attr) in ALLOWED_DESCRIPTOR_ACCESS: - # In case of descriptors that have get methods we cannot return - # it's value, because that would mean code execution. - return [EmptyCompiledName(self._evaluator, name)] - if self._is_instance and name not in dir(obj): - return [] - return [self._create_name(name)] - - def values(self): - obj = self._compiled_object.obj - - names = [] - for name in dir(obj): - names += self.get(name) - - is_instance = self._is_instance or fake.is_class_instance(obj) - # ``dir`` doesn't include the type names. - if not inspect.ismodule(obj) and (obj is not type) and not is_instance: - for filter in create(self._evaluator, type).get_filters(): - names += filter.values() - return names - - def _create_name(self, name): - return self.name_class(self._evaluator, self._compiled_object, name) - - -def dotted_from_fs_path(fs_path, sys_path): - """ - Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e. - compares the path with sys.path and then returns the dotted_path. If the - path is not in the sys.path, just returns None. - """ - if os.path.basename(fs_path).startswith('__init__.'): - # We are calculating the path. __init__ files are not interesting. - fs_path = os.path.dirname(fs_path) - - # prefer - # - UNIX - # /path/to/pythonX.Y/lib-dynload - # /path/to/pythonX.Y/site-packages - # - Windows - # C:\path\to\DLLs - # C:\path\to\Lib\site-packages - # over - # - UNIX - # /path/to/pythonX.Y - # - Windows - # C:\path\to\Lib - path = '' - for s in sys_path: - if (fs_path.startswith(s) and len(path) < len(s)): - path = s - - # - Window - # X:\path\to\lib-dynload/datetime.pyd => datetime - module_path = fs_path[len(path):].lstrip(os.path.sep).lstrip('/') - # - Window - # Replace like X:\path\to\something/foo/bar.py - return _path_re.sub('', module_path).replace(os.path.sep, '.').replace('/', '.') - - -def load_module(evaluator, path=None, name=None): - sys_path = list(evaluator.project.sys_path) - if path is not None: - dotted_path = dotted_from_fs_path(path, sys_path=sys_path) - else: - dotted_path = name - - temp, sys.path = sys.path, sys_path - try: - __import__(dotted_path) - except RuntimeError: - if 'PySide' in dotted_path or 'PyQt' in dotted_path: - # RuntimeError: the PyQt4.QtCore and PyQt5.QtCore modules both wrap - # the QObject class. - # See https://github.com/davidhalter/jedi/pull/483 - return None - raise - except ImportError: - # If a module is "corrupt" or not really a Python module or whatever. - debug.warning('Module %s not importable in path %s.', dotted_path, path) - return None - finally: - sys.path = temp - - # Just access the cache after import, because of #59 as well as the very - # complicated import structure of Python. - module = sys.modules[dotted_path] - - return create(evaluator, module) - - -docstr_defaults = { - 'floating point number': 'float', - 'character': 'str', - 'integer': 'int', - 'dictionary': 'dict', - 'string': 'str', -} +def builtin_from_name(evaluator, string): + builtins = evaluator.builtins_module + return create_from_name(evaluator, builtins, string) -def _parse_function_doc(doc): +def create_simple_object(evaluator, obj): """ - Takes a function and returns the params and return value as a tuple. - This is nothing more than a docstring parser. - - TODO docstrings like utime(path, (atime, mtime)) and a(b [, b]) -> None - TODO docstrings like 'tuple of integers' + Only allows creations of objects that are easily picklable across Python + versions. """ - # parse round parentheses: def func(a, (b,c)) - try: - count = 0 - start = doc.index('(') - for i, s in enumerate(doc[start:]): - if s == '(': - count += 1 - elif s == ')': - count -= 1 - if count == 0: - end = start + i - break - param_str = doc[start + 1:end] - except (ValueError, UnboundLocalError): - # ValueError for doc.index - # UnboundLocalError for undefined end in last line - debug.dbg('no brackets found - no param') - end = 0 - param_str = '' - else: - # remove square brackets, that show an optional param ( = None) - def change_options(m): - args = m.group(1).split(',') - for i, a in enumerate(args): - if a and '=' not in a: - args[i] += '=None' - return ','.join(args) - - while True: - param_str, changes = re.subn(r' ?\[([^\[\]]+)\]', - change_options, param_str) - if changes == 0: - break - param_str = param_str.replace('-', '_') # see: isinstance.__doc__ - - # parse return value - r = re.search('-[>-]* ', doc[end:end + 7]) - if r is None: - ret = '' - else: - index = end + r.end() - # get result type, which can contain newlines - pattern = re.compile(r'(,\n|[^\n-])+') - ret_str = pattern.match(doc, index).group(0).strip() - # New object -> object() - ret_str = re.sub(r'[nN]ew (.*)', r'\1()', ret_str) - - ret = docstr_defaults.get(ret_str, ret_str) - - return param_str, ret - - -def _create_from_name(evaluator, module, compiled_object, name): - obj = compiled_object.obj - faked = None - try: - faked = fake.get_faked(evaluator, module, obj, parent_context=compiled_object, name=name) - if faked.type == 'funcdef': - from jedi.evaluate.context.function import FunctionContext - return FunctionContext(evaluator, compiled_object, faked) - except fake.FakeDoesNotExist: - pass - - try: - obj = getattr(obj, name) - except AttributeError: - # Happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None - return create(evaluator, obj, parent_context=compiled_object, faked=faked) - - -def builtin_from_name(evaluator, string): - bltn_obj = getattr(_builtins, string) - return create(evaluator, bltn_obj) - - -def _a_generator(foo): - """Used to have an object to return for generators.""" - yield 42 - yield foo - - -_SPECIAL_OBJECTS = { - 'FUNCTION_CLASS': type(load_module), - 'METHOD_CLASS': type(CompiledObject.is_class), - 'MODULE_CLASS': type(os), - 'GENERATOR_OBJECT': _a_generator(1.0), - 'BUILTINS': _builtins, -} + assert isinstance(obj, (int, float, str, bytes, unicode, slice, complex)) + return create_from_access_path( + evaluator, + evaluator.compiled_subprocess.create_simple_object(obj) + ) def get_special_object(evaluator, identifier): - obj = _SPECIAL_OBJECTS[identifier] - return create(evaluator, obj, parent_context=create(evaluator, _builtins)) + return create_from_access_path( + evaluator, + evaluator.compiled_subprocess.get_special_object(identifier) + ) -def compiled_objects_cache(attribute_name): - def decorator(func): - """ - This decorator caches just the ids, oopposed to caching the object itself. - Caching the id has the advantage that an object doesn't need to be - hashable. - """ - def wrapper(evaluator, obj, parent_context=None, module=None, faked=None): - cache = getattr(evaluator, attribute_name) - # Do a very cheap form of caching here. - key = id(obj), id(parent_context) - try: - return cache[key][0] - except KeyError: - # TODO this whole decorator is way too ugly - result = func(evaluator, obj, parent_context, module, faked) - # Need to cache all of them, otherwise the id could be overwritten. - cache[key] = result, obj, parent_context, module, faked - return result - return wrapper +def get_string_context_set(evaluator): + return builtin_from_name(evaluator, u'str').execute_evaluated() - return decorator - -@compiled_objects_cache('compiled_cache') -def create(evaluator, obj, parent_context=None, module=None, faked=None): - """ - A very weird interface class to this module. The more options provided the - more acurate loading compiled objects is. - """ - if inspect.ismodule(obj): - if parent_context is not None: - # Modules don't have parents, be careful with caching: recurse. - return create(evaluator, obj) - else: - if parent_context is None and obj is not _builtins: - return create(evaluator, obj, create(evaluator, _builtins)) - - try: - faked = fake.get_faked(evaluator, module, obj, parent_context=parent_context) - if faked.type == 'funcdef': - from jedi.evaluate.context.function import FunctionContext - return FunctionContext(evaluator, parent_context, faked) - except fake.FakeDoesNotExist: - pass - - return CompiledObject(evaluator, obj, parent_context, faked) +def load_module(evaluator, **kwargs): + access_path = evaluator.compiled_subprocess.load_module(**kwargs) + if access_path is None: + return None + return create_from_access_path(evaluator, access_path) diff --git a/pythonFiles/jedi/evaluate/compiled/access.py b/pythonFiles/jedi/evaluate/compiled/access.py new file mode 100644 index 000000000000..97a8dd0b5c3d --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/access.py @@ -0,0 +1,490 @@ +import inspect +import types +import sys +from textwrap import dedent +import operator as op +from collections import namedtuple + +from jedi import debug +from jedi._compatibility import unicode, is_py3, is_py34, builtins, \ + py_version, force_unicode, print_to_stderr +from jedi.evaluate.compiled.getattr_static import getattr_static +from jedi.evaluate.utils import dotted_from_fs_path + + +MethodDescriptorType = type(str.replace) +# These are not considered classes and access is granted even though they have +# a __class__ attribute. +NOT_CLASS_TYPES = ( + types.BuiltinFunctionType, + types.CodeType, + types.FrameType, + types.FunctionType, + types.GeneratorType, + types.GetSetDescriptorType, + types.LambdaType, + types.MemberDescriptorType, + types.MethodType, + types.ModuleType, + types.TracebackType, + MethodDescriptorType +) + +if is_py3: + NOT_CLASS_TYPES += ( + types.MappingProxyType, + types.SimpleNamespace + ) + if is_py34: + NOT_CLASS_TYPES += (types.DynamicClassAttribute,) + + +# Those types don't exist in typing. +MethodDescriptorType = type(str.replace) +WrapperDescriptorType = type(set.__iter__) +# `object.__subclasshook__` is an already executed descriptor. +object_class_dict = type.__dict__["__dict__"].__get__(object) +ClassMethodDescriptorType = type(object_class_dict['__subclasshook__']) + +def _a_generator(foo): + """Used to have an object to return for generators.""" + yield 42 + yield foo + + +_sentinel = object() + +# Maps Python syntax to the operator module. +COMPARISON_OPERATORS = { + '==': op.eq, + '!=': op.ne, + 'is': op.is_, + 'is not': op.is_not, + '<': op.lt, + '<=': op.le, + '>': op.gt, + '>=': op.ge, +} + +_OPERATORS = { + '+': op.add, + '-': op.sub, +} +_OPERATORS.update(COMPARISON_OPERATORS) + +ALLOWED_DESCRIPTOR_ACCESS = ( + types.FunctionType, + types.GetSetDescriptorType, + types.MemberDescriptorType, + MethodDescriptorType, + WrapperDescriptorType, + ClassMethodDescriptorType, + staticmethod, + classmethod, +) + + +def safe_getattr(obj, name, default=_sentinel): + try: + attr, is_get_descriptor = getattr_static(obj, name) + except AttributeError: + if default is _sentinel: + raise + return default + else: + if type(attr) in ALLOWED_DESCRIPTOR_ACCESS: + # In case of descriptors that have get methods we cannot return + # it's value, because that would mean code execution. + return getattr(obj, name) + return attr + + +SignatureParam = namedtuple( + 'SignatureParam', + 'name has_default default has_annotation annotation kind_name' +) + + +def compiled_objects_cache(attribute_name): + def decorator(func): + """ + This decorator caches just the ids, oopposed to caching the object itself. + Caching the id has the advantage that an object doesn't need to be + hashable. + """ + def wrapper(evaluator, obj, parent_context=None): + cache = getattr(evaluator, attribute_name) + # Do a very cheap form of caching here. + key = id(obj) + try: + cache[key] + return cache[key][0] + except KeyError: + # TODO wuaaaarrghhhhhhhh + if attribute_name == 'mixed_cache': + result = func(evaluator, obj, parent_context) + else: + result = func(evaluator, obj) + # Need to cache all of them, otherwise the id could be overwritten. + cache[key] = result, obj, parent_context + return result + return wrapper + + return decorator + + +def create_access(evaluator, obj): + return evaluator.compiled_subprocess.get_or_create_access_handle(obj) + + +def load_module(evaluator, path=None, name=None, sys_path=None): + if sys_path is None: + sys_path = list(evaluator.get_sys_path()) + if path is not None: + dotted_path = dotted_from_fs_path(path, sys_path=sys_path) + else: + dotted_path = name + + temp, sys.path = sys.path, sys_path + try: + __import__(dotted_path) + except ImportError: + # If a module is "corrupt" or not really a Python module or whatever. + debug.warning('Module %s not importable in path %s.', dotted_path, path) + return None + except Exception: + # Since __import__ pretty much makes code execution possible, just + # catch any error here and print it. + import traceback + print_to_stderr("Cannot import:\n%s" % traceback.format_exc()) + return None + finally: + sys.path = temp + + # Just access the cache after import, because of #59 as well as the very + # complicated import structure of Python. + module = sys.modules[dotted_path] + return create_access_path(evaluator, module) + + +class AccessPath(object): + def __init__(self, accesses): + self.accesses = accesses + + # Writing both of these methods here looks a bit ridiculous. However with + # the differences of Python 2/3 it's actually necessary, because we will + # otherwise have a accesses attribute that is bytes instead of unicode. + def __getstate__(self): + return self.accesses + + def __setstate__(self, value): + self.accesses = value + + +def create_access_path(evaluator, obj): + access = create_access(evaluator, obj) + return AccessPath(access.get_access_path_tuples()) + + +def _force_unicode_decorator(func): + return lambda *args, **kwargs: force_unicode(func(*args, **kwargs)) + + +class DirectObjectAccess(object): + def __init__(self, evaluator, obj): + self._evaluator = evaluator + self._obj = obj + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self.get_repr()) + + def _create_access(self, obj): + return create_access(self._evaluator, obj) + + def _create_access_path(self, obj): + return create_access_path(self._evaluator, obj) + + def py__bool__(self): + return bool(self._obj) + + def py__file__(self): + try: + return self._obj.__file__ + except AttributeError: + return None + + def py__doc__(self, include_call_signature=False): + return force_unicode(inspect.getdoc(self._obj)) or u'' + + def py__name__(self): + if not _is_class_instance(self._obj) or \ + inspect.ismethoddescriptor(self._obj): # slots + cls = self._obj + else: + try: + cls = self._obj.__class__ + except AttributeError: + # happens with numpy.core.umath._UFUNC_API (you get it + # automatically by doing `import numpy`. + return None + + try: + return force_unicode(cls.__name__) + except AttributeError: + return None + + def py__mro__accesses(self): + return tuple(self._create_access_path(cls) for cls in self._obj.__mro__[1:]) + + def py__getitem__(self, index): + if type(self._obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): + # Get rid of side effects, we won't call custom `__getitem__`s. + return None + + return self._create_access_path(self._obj[index]) + + def py__iter__list(self): + if type(self._obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): + # Get rid of side effects, we won't call custom `__getitem__`s. + return [] + + lst = [] + for i, part in enumerate(self._obj): + if i > 20: + # Should not go crazy with large iterators + break + lst.append(self._create_access_path(part)) + return lst + + def py__class__(self): + return self._create_access_path(self._obj.__class__) + + def py__bases__(self): + return [self._create_access_path(base) for base in self._obj.__bases__] + + @_force_unicode_decorator + def get_repr(self): + builtins = 'builtins', '__builtin__' + + if inspect.ismodule(self._obj): + return repr(self._obj) + # Try to avoid execution of the property. + if safe_getattr(self._obj, '__module__', default='') in builtins: + return repr(self._obj) + + type_ = type(self._obj) + if type_ == type: + return type.__repr__(self._obj) + + if safe_getattr(type_, '__module__', default='') in builtins: + # Allow direct execution of repr for builtins. + return repr(self._obj) + return object.__repr__(self._obj) + + def is_class(self): + return inspect.isclass(self._obj) + + def ismethoddescriptor(self): + return inspect.ismethoddescriptor(self._obj) + + def dir(self): + return list(map(force_unicode, dir(self._obj))) + + def has_iter(self): + try: + iter(self._obj) + return True + except TypeError: + return False + + def is_allowed_getattr(self, name): + # TODO this API is ugly. + try: + attr, is_get_descriptor = getattr_static(self._obj, name) + except AttributeError: + return False, False + else: + if is_get_descriptor and type(attr) not in ALLOWED_DESCRIPTOR_ACCESS: + # In case of descriptors that have get methods we cannot return + # it's value, because that would mean code execution. + return True, True + return True, False + + def getattr(self, name, default=_sentinel): + try: + return self._create_access(getattr(self._obj, name)) + except AttributeError: + # Happens e.g. in properties of + # PyQt4.QtGui.QStyleOptionComboBox.currentText + # -> just set it to None + if default is _sentinel: + raise + return self._create_access(default) + + def get_safe_value(self): + if type(self._obj) in (bool, bytes, float, int, str, unicode, slice): + return self._obj + raise ValueError("Object is type %s and not simple" % type(self._obj)) + + def get_api_type(self): + obj = self._obj + if self.is_class(): + return u'class' + elif inspect.ismodule(obj): + return u'module' + elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \ + or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj): + return u'function' + # Everything else... + return u'instance' + + def get_access_path_tuples(self): + accesses = [create_access(self._evaluator, o) for o in self._get_objects_path()] + return [(access.py__name__(), access) for access in accesses] + + def _get_objects_path(self): + def get(): + obj = self._obj + yield obj + try: + obj = obj.__objclass__ + except AttributeError: + pass + else: + yield obj + + try: + # Returns a dotted string path. + imp_plz = obj.__module__ + except AttributeError: + # Unfortunately in some cases like `int` there's no __module__ + if not inspect.ismodule(obj): + yield builtins + else: + if imp_plz is None: + # Happens for example in `(_ for _ in []).send.__module__`. + yield builtins + else: + try: + # TODO use sys.modules, __module__ can be faked. + yield sys.modules[imp_plz] + except KeyError: + # __module__ can be something arbitrary that doesn't exist. + yield builtins + + return list(reversed(list(get()))) + + def execute_operation(self, other_access_handle, operator): + other_access = other_access_handle.access + op = _OPERATORS[operator] + return self._create_access_path(op(self._obj, other_access._obj)) + + def needs_type_completions(self): + return inspect.isclass(self._obj) and self._obj != type + + def get_signature_params(self): + obj = self._obj + if py_version < 33: + raise ValueError("inspect.signature was introduced in 3.3") + if py_version == 34: + # In 3.4 inspect.signature are wrong for str and int. This has + # been fixed in 3.5. The signature of object is returned, + # because no signature was found for str. Here we imitate 3.5 + # logic and just ignore the signature if the magic methods + # don't match object. + # 3.3 doesn't even have the logic and returns nothing for str + # and classes that inherit from object. + user_def = inspect._signature_get_user_defined_method + if (inspect.isclass(obj) + and not user_def(type(obj), '__init__') + and not user_def(type(obj), '__new__') + and (obj.__init__ != object.__init__ + or obj.__new__ != object.__new__)): + raise ValueError + + try: + signature = inspect.signature(obj) + except (RuntimeError, TypeError): + # Reading the code of the function in Python 3.6 implies there are + # at least these errors that might occur if something is wrong with + # the signature. In that case we just want a simple escape for now. + raise ValueError + return [ + SignatureParam( + name=p.name, + has_default=p.default is not p.empty, + default=self._create_access_path(p.default), + has_annotation=p.annotation is not p.empty, + annotation=self._create_access_path(p.annotation), + kind_name=str(p.kind) + ) for p in signature.parameters.values() + ] + + def negate(self): + return self._create_access_path(-self._obj) + + def dict_values(self): + return [self._create_access_path(v) for v in self._obj.values()] + + def is_super_class(self, exception): + return issubclass(exception, self._obj) + + def get_dir_infos(self): + """ + Used to return a couple of infos that are needed when accessing the sub + objects of an objects + """ + # TODO is_allowed_getattr might raise an AttributeError + tuples = dict( + (force_unicode(name), self.is_allowed_getattr(name)) + for name in self.dir() + ) + return self.needs_type_completions(), tuples + + +def _is_class_instance(obj): + """Like inspect.* methods.""" + try: + cls = obj.__class__ + except AttributeError: + return False + else: + return cls != type and not issubclass(cls, NOT_CLASS_TYPES) + + +if py_version >= 35: + exec(compile(dedent(""" + async def _coroutine(): pass + _coroutine = _coroutine() + CoroutineType = type(_coroutine) + _coroutine.close() # Prevent ResourceWarning + """), 'blub', 'exec')) + _coroutine_wrapper = _coroutine.__await__() +else: + _coroutine = None + _coroutine_wrapper = None + +if py_version >= 36: + exec(compile(dedent(""" + async def _async_generator(): + yield + _async_generator = _async_generator() + AsyncGeneratorType = type(_async_generator) + """), 'blub', 'exec')) +else: + _async_generator = None + +class _SPECIAL_OBJECTS(object): + FUNCTION_CLASS = types.FunctionType + METHOD_CLASS = type(DirectObjectAccess.py__bool__) + MODULE_CLASS = types.ModuleType + GENERATOR_OBJECT = _a_generator(1.0) + BUILTINS = builtins + COROUTINE = _coroutine + COROUTINE_WRAPPER = _coroutine_wrapper + ASYNC_GENERATOR = _async_generator + + +def get_special_object(evaluator, identifier): + obj = getattr(_SPECIAL_OBJECTS, identifier) + return create_access_path(evaluator, obj) diff --git a/pythonFiles/jedi/evaluate/compiled/context.py b/pythonFiles/jedi/evaluate/compiled/context.py new file mode 100644 index 000000000000..f81509d73f72 --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/context.py @@ -0,0 +1,474 @@ +""" +Imitate the parser representation. +""" +import re +from functools import partial + +from jedi import debug +from jedi._compatibility import force_unicode, Parameter +from jedi.cache import underscore_memoization, memoize_method +from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition, \ + ContextNameMixin +from jedi.evaluate.base_context import Context, ContextSet +from jedi.evaluate.lazy_context import LazyKnownContext +from jedi.evaluate.compiled.access import _sentinel +from jedi.evaluate.cache import evaluator_function_cache +from . import fake + + +class CheckAttribute(object): + """Raises an AttributeError if the attribute X isn't available.""" + def __init__(self, func): + self.func = func + # Remove the py in front of e.g. py__call__. + self.check_name = force_unicode(func.__name__[2:]) + + def __get__(self, instance, owner): + if instance is None: + return self + + # This might raise an AttributeError. That's wanted. + if self.check_name == '__iter__': + # Python iterators are a bit strange, because there's no need for + # the __iter__ function as long as __getitem__ is defined (it will + # just start with __getitem__(0). This is especially true for + # Python 2 strings, where `str.__iter__` is not even defined. + if not instance.access_handle.has_iter(): + raise AttributeError + else: + instance.access_handle.getattr(self.check_name) + return partial(self.func, instance) + + +class CompiledObject(Context): + def __init__(self, evaluator, access_handle, parent_context=None, faked_class=None): + super(CompiledObject, self).__init__(evaluator, parent_context) + self.access_handle = access_handle + # This attribute will not be set for most classes, except for fakes. + self.tree_node = faked_class + + @CheckAttribute + def py__call__(self, params): + if self.tree_node is not None and self.tree_node.type == 'funcdef': + from jedi.evaluate.context.function import FunctionContext + return FunctionContext( + self.evaluator, + parent_context=self.parent_context, + funcdef=self.tree_node + ).py__call__(params) + if self.access_handle.is_class(): + from jedi.evaluate.context import CompiledInstance + return ContextSet(CompiledInstance(self.evaluator, self.parent_context, self, params)) + else: + return ContextSet.from_iterable(self._execute_function(params)) + + @CheckAttribute + def py__class__(self): + return create_from_access_path(self.evaluator, self.access_handle.py__class__()) + + @CheckAttribute + def py__mro__(self): + return (self,) + tuple( + create_from_access_path(self.evaluator, access) + for access in self.access_handle.py__mro__accesses() + ) + + @CheckAttribute + def py__bases__(self): + return tuple( + create_from_access_path(self.evaluator, access) + for access in self.access_handle.py__bases__() + ) + + def py__bool__(self): + return self.access_handle.py__bool__() + + def py__file__(self): + return self.access_handle.py__file__() + + def is_class(self): + return self.access_handle.is_class() + + def py__doc__(self, include_call_signature=False): + return self.access_handle.py__doc__() + + def get_param_names(self): + try: + signature_params = self.access_handle.get_signature_params() + except ValueError: # Has no signature + params_str, ret = self._parse_function_doc() + tokens = params_str.split(',') + if self.access_handle.ismethoddescriptor(): + tokens.insert(0, 'self') + for p in tokens: + parts = p.strip().split('=') + yield UnresolvableParamName(self, parts[0]) + else: + for signature_param in signature_params: + yield SignatureParamName(self, signature_param) + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self.access_handle.get_repr()) + + @underscore_memoization + def _parse_function_doc(self): + doc = self.py__doc__() + if doc is None: + return '', '' + + return _parse_function_doc(doc) + + @property + def api_type(self): + return self.access_handle.get_api_type() + + @underscore_memoization + def _cls(self): + """ + We used to limit the lookups for instantiated objects like list(), but + this is not the case anymore. Python itself + """ + # Ensures that a CompiledObject is returned that is not an instance (like list) + return self + + def get_filters(self, search_global=False, is_instance=False, + until_position=None, origin_scope=None): + yield self._ensure_one_filter(is_instance) + + @memoize_method + def _ensure_one_filter(self, is_instance): + """ + search_global shouldn't change the fact that there's one dict, this way + there's only one `object`. + """ + return CompiledObjectFilter(self.evaluator, self, is_instance) + + @CheckAttribute + def py__getitem__(self, index): + access = self.access_handle.py__getitem__(index) + if access is None: + return ContextSet() + + return ContextSet(create_from_access_path(self.evaluator, access)) + + @CheckAttribute + def py__iter__(self): + for access in self.access_handle.py__iter__list(): + yield LazyKnownContext(create_from_access_path(self.evaluator, access)) + + def py__name__(self): + return self.access_handle.py__name__() + + @property + def name(self): + name = self.py__name__() + if name is None: + name = self.access_handle.get_repr() + return CompiledContextName(self, name) + + def _execute_function(self, params): + from jedi.evaluate import docstrings + from jedi.evaluate.compiled import builtin_from_name + if self.api_type != 'function': + return + + for name in self._parse_function_doc()[1].split(): + try: + # TODO wtf is this? this is exactly the same as the thing + # below. It uses getattr as well. + self.evaluator.builtins_module.access_handle.getattr(name) + except AttributeError: + continue + else: + bltn_obj = builtin_from_name(self.evaluator, name) + for result in bltn_obj.execute(params): + yield result + for type_ in docstrings.infer_return_types(self): + yield type_ + + def dict_values(self): + return ContextSet.from_iterable( + create_from_access_path(self.evaluator, access) + for access in self.access_handle.dict_values() + ) + + def get_safe_value(self, default=_sentinel): + try: + return self.access_handle.get_safe_value() + except ValueError: + if default == _sentinel: + raise + return default + + def execute_operation(self, other, operator): + return create_from_access_path( + self.evaluator, + self.access_handle.execute_operation(other.access_handle, operator) + ) + + def negate(self): + return create_from_access_path(self.evaluator, self.access_handle.negate()) + + def is_super_class(self, exception): + return self.access_handle.is_super_class(exception) + + +class CompiledName(AbstractNameDefinition): + def __init__(self, evaluator, parent_context, name): + self._evaluator = evaluator + self.parent_context = parent_context + self.string_name = name + + def __repr__(self): + try: + name = self.parent_context.name # __name__ is not defined all the time + except AttributeError: + name = None + return '<%s: (%s).%s>' % (self.__class__.__name__, name, self.string_name) + + @property + def api_type(self): + return next(iter(self.infer())).api_type + + @underscore_memoization + def infer(self): + return ContextSet(create_from_name( + self._evaluator, self.parent_context, self.string_name + )) + + +class SignatureParamName(AbstractNameDefinition): + api_type = u'param' + + def __init__(self, compiled_obj, signature_param): + self.parent_context = compiled_obj.parent_context + self._signature_param = signature_param + + @property + def string_name(self): + return self._signature_param.name + + def get_kind(self): + return getattr(Parameter, self._signature_param.kind_name) + + def is_keyword_param(self): + return self._signature_param + + def infer(self): + p = self._signature_param + evaluator = self.parent_context.evaluator + contexts = ContextSet() + if p.has_default: + contexts = ContextSet(create_from_access_path(evaluator, p.default)) + if p.has_annotation: + annotation = create_from_access_path(evaluator, p.annotation) + contexts |= annotation.execute_evaluated() + return contexts + + +class UnresolvableParamName(AbstractNameDefinition): + api_type = u'param' + + def __init__(self, compiled_obj, name): + self.parent_context = compiled_obj.parent_context + self.string_name = name + + def get_kind(self): + return Parameter.POSITIONAL_ONLY + + def infer(self): + return ContextSet() + + +class CompiledContextName(ContextNameMixin, AbstractNameDefinition): + def __init__(self, context, name): + self.string_name = name + self._context = context + self.parent_context = context.parent_context + + +class EmptyCompiledName(AbstractNameDefinition): + """ + Accessing some names will raise an exception. To avoid not having any + completions, just give Jedi the option to return this object. It infers to + nothing. + """ + def __init__(self, evaluator, name): + self.parent_context = evaluator.builtins_module + self.string_name = name + + def infer(self): + return ContextSet() + + +class CompiledObjectFilter(AbstractFilter): + name_class = CompiledName + + def __init__(self, evaluator, compiled_object, is_instance=False): + self._evaluator = evaluator + self._compiled_object = compiled_object + self._is_instance = is_instance + + def get(self, name): + return self._get( + name, + lambda: self._compiled_object.access_handle.is_allowed_getattr(name), + lambda: self._compiled_object.access_handle.dir(), + check_has_attribute=True + ) + + def _get(self, name, allowed_getattr_callback, dir_callback, check_has_attribute=False): + """ + To remove quite a few access calls we introduced the callback here. + """ + has_attribute, is_descriptor = allowed_getattr_callback() + if check_has_attribute and not has_attribute: + return [] + + # Always use unicode objects in Python 2 from here. + name = force_unicode(name) + + if is_descriptor or not has_attribute: + return [self._get_cached_name(name, is_empty=True)] + + if self._is_instance and name not in dir_callback(): + return [] + return [self._get_cached_name(name)] + + @memoize_method + def _get_cached_name(self, name, is_empty=False): + if is_empty: + return EmptyCompiledName(self._evaluator, name) + else: + return self._create_name(name) + + def values(self): + from jedi.evaluate.compiled import builtin_from_name + names = [] + needs_type_completions, dir_infos = self._compiled_object.access_handle.get_dir_infos() + for name in dir_infos: + names += self._get( + name, + lambda: dir_infos[name], + lambda: dir_infos.keys(), + ) + + # ``dir`` doesn't include the type names. + if not self._is_instance and needs_type_completions: + for filter in builtin_from_name(self._evaluator, u'type').get_filters(): + names += filter.values() + return names + + def _create_name(self, name): + return self.name_class(self._evaluator, self._compiled_object, name) + + +docstr_defaults = { + 'floating point number': u'float', + 'character': u'str', + 'integer': u'int', + 'dictionary': u'dict', + 'string': u'str', +} + + +def _parse_function_doc(doc): + """ + Takes a function and returns the params and return value as a tuple. + This is nothing more than a docstring parser. + + TODO docstrings like utime(path, (atime, mtime)) and a(b [, b]) -> None + TODO docstrings like 'tuple of integers' + """ + doc = force_unicode(doc) + # parse round parentheses: def func(a, (b,c)) + try: + count = 0 + start = doc.index('(') + for i, s in enumerate(doc[start:]): + if s == '(': + count += 1 + elif s == ')': + count -= 1 + if count == 0: + end = start + i + break + param_str = doc[start + 1:end] + except (ValueError, UnboundLocalError): + # ValueError for doc.index + # UnboundLocalError for undefined end in last line + debug.dbg('no brackets found - no param') + end = 0 + param_str = u'' + else: + # remove square brackets, that show an optional param ( = None) + def change_options(m): + args = m.group(1).split(',') + for i, a in enumerate(args): + if a and '=' not in a: + args[i] += '=None' + return ','.join(args) + + while True: + param_str, changes = re.subn(r' ?\[([^\[\]]+)\]', + change_options, param_str) + if changes == 0: + break + param_str = param_str.replace('-', '_') # see: isinstance.__doc__ + + # parse return value + r = re.search(u'-[>-]* ', doc[end:end + 7]) + if r is None: + ret = u'' + else: + index = end + r.end() + # get result type, which can contain newlines + pattern = re.compile(r'(,\n|[^\n-])+') + ret_str = pattern.match(doc, index).group(0).strip() + # New object -> object() + ret_str = re.sub(r'[nN]ew (.*)', r'\1()', ret_str) + + ret = docstr_defaults.get(ret_str, ret_str) + + return param_str, ret + + +def create_from_name(evaluator, compiled_object, name): + faked = None + try: + faked = fake.get_faked_with_parent_context(compiled_object, name) + except fake.FakeDoesNotExist: + pass + + access = compiled_object.access_handle.getattr(name, default=None) + return create_cached_compiled_object( + evaluator, access, parent_context=compiled_object, faked=faked + ) + + +def _normalize_create_args(func): + """The cache doesn't care about keyword vs. normal args.""" + def wrapper(evaluator, obj, parent_context=None, faked=None): + return func(evaluator, obj, parent_context, faked) + return wrapper + + +def create_from_access_path(evaluator, access_path): + parent_context = None + for name, access in access_path.accesses: + try: + if parent_context is None: + faked = fake.get_faked_module(evaluator, access_path.accesses[0][0]) + else: + faked = fake.get_faked_with_parent_context(parent_context, name) + except fake.FakeDoesNotExist: + faked = None + + parent_context = create_cached_compiled_object(evaluator, access, parent_context, faked) + return parent_context + + +@_normalize_create_args +@evaluator_function_cache() +def create_cached_compiled_object(evaluator, access_handle, parent_context, faked): + return CompiledObject(evaluator, access_handle, parent_context, faked) diff --git a/pythonFiles/jedi/evaluate/compiled/fake.py b/pythonFiles/jedi/evaluate/compiled/fake.py index 60dbefe4acac..ac43ea286b48 100644 --- a/pythonFiles/jedi/evaluate/compiled/fake.py +++ b/pythonFiles/jedi/evaluate/compiled/fake.py @@ -5,73 +5,59 @@ """ import os -import inspect -import types from itertools import chain -from parso.python import tree +from jedi._compatibility import unicode -from jedi._compatibility import is_py3, builtins, unicode, is_py34 +fake_modules = {} -modules = {} +def _get_path_dict(): + path = os.path.dirname(os.path.abspath(__file__)) + base_path = os.path.join(path, 'fake') + dct = {} + for file_name in os.listdir(base_path): + if file_name.endswith('.pym'): + dct[file_name[:-4]] = os.path.join(base_path, file_name) + return dct -MethodDescriptorType = type(str.replace) -# These are not considered classes and access is granted even though they have -# a __class__ attribute. -NOT_CLASS_TYPES = ( - types.BuiltinFunctionType, - types.CodeType, - types.FrameType, - types.FunctionType, - types.GeneratorType, - types.GetSetDescriptorType, - types.LambdaType, - types.MemberDescriptorType, - types.MethodType, - types.ModuleType, - types.TracebackType, - MethodDescriptorType -) -if is_py3: - NOT_CLASS_TYPES += ( - types.MappingProxyType, - types.SimpleNamespace - ) - if is_py34: - NOT_CLASS_TYPES += (types.DynamicClassAttribute,) +_path_dict = _get_path_dict() class FakeDoesNotExist(Exception): pass -def _load_faked_module(grammar, module): - module_name = module.__name__ - if module_name == '__builtin__' and not is_py3: - module_name = 'builtins' +def _load_faked_module(evaluator, module_name): + try: + return fake_modules[module_name] + except KeyError: + pass + + check_module_name = module_name + if module_name == '__builtin__' and evaluator.environment.version_info.major == 2: + check_module_name = 'builtins' try: - return modules[module_name] + path = _path_dict[check_module_name] except KeyError: - path = os.path.dirname(os.path.abspath(__file__)) - try: - with open(os.path.join(path, 'fake', module_name) + '.pym') as f: - source = f.read() - except IOError: - modules[module_name] = None - return - modules[module_name] = m = grammar.parse(unicode(source)) - - if module_name == 'builtins' and not is_py3: - # There are two implementations of `open` for either python 2/3. - # -> Rename the python2 version (`look at fake/builtins.pym`). - open_func = _search_scope(m, 'open') - open_func.children[1].value = 'open_python3' - open_func = _search_scope(m, 'open_python2') - open_func.children[1].value = 'open' - return m + fake_modules[module_name] = None + return + + with open(path) as f: + source = f.read() + + fake_modules[module_name] = m = evaluator.latest_grammar.parse(unicode(source)) + + if check_module_name != module_name: + # There are two implementations of `open` for either python 2/3. + # -> Rename the python2 version (`look at fake/builtins.pym`). + open_func = _search_scope(m, 'open') + open_func.children[1].value = 'open_python3' + open_func = _search_scope(m, 'open_python2') + open_func.children[1].value = 'open' + return m def _search_scope(scope, obj_name): @@ -80,134 +66,17 @@ def _search_scope(scope, obj_name): return s -def get_module(obj): - if inspect.ismodule(obj): - return obj - try: - obj = obj.__objclass__ - except AttributeError: - pass - - try: - imp_plz = obj.__module__ - except AttributeError: - # Unfortunately in some cases like `int` there's no __module__ - return builtins - else: - if imp_plz is None: - # Happens for example in `(_ for _ in []).send.__module__`. - return builtins - else: - try: - return __import__(imp_plz) - except ImportError: - # __module__ can be something arbitrary that doesn't exist. - return builtins - - -def _faked(grammar, module, obj, name): - # Crazy underscore actions to try to escape all the internal madness. - if module is None: - module = get_module(obj) - - faked_mod = _load_faked_module(grammar, module) - if faked_mod is None: - return None, None - - # Having the module as a `parser.python.tree.Module`, we need to scan - # for methods. - if name is None: - if inspect.isbuiltin(obj) or inspect.isclass(obj): - return _search_scope(faked_mod, obj.__name__), faked_mod - elif not inspect.isclass(obj): - # object is a method or descriptor - try: - objclass = obj.__objclass__ - except AttributeError: - return None, None - else: - cls = _search_scope(faked_mod, objclass.__name__) - if cls is None: - return None, None - return _search_scope(cls, obj.__name__), faked_mod - else: - if obj is module: - return _search_scope(faked_mod, name), faked_mod - else: - try: - cls_name = obj.__name__ - except AttributeError: - return None, None - cls = _search_scope(faked_mod, cls_name) - if cls is None: - return None, None - return _search_scope(cls, name), faked_mod - return None, None - - -def memoize_faked(obj): - """ - A typical memoize function that ignores issues with non hashable results. - """ - cache = obj.cache = {} - - def memoizer(*args, **kwargs): - key = (obj, args, frozenset(kwargs.items())) - try: - result = cache[key] - except (TypeError, ValueError): - return obj(*args, **kwargs) - except KeyError: - result = obj(*args, **kwargs) - if result is not None: - cache[key] = obj(*args, **kwargs) - return result - else: - return result - return memoizer - - -@memoize_faked -def _get_faked(grammar, module, obj, name=None): - result, fake_module = _faked(grammar, module, obj, name) - if result is None: - # We're not interested in classes. What we want is functions. - raise FakeDoesNotExist - elif result.type == 'classdef': - return result, fake_module - else: - # Set the docstr which was previously not set (faked modules don't - # contain it). - assert result.type == 'funcdef' - doc = '"""%s"""' % obj.__doc__ # TODO need escapes. - suite = result.children[-1] - string = tree.String(doc, (0, 0), '') - new_line = tree.Newline('\n', (0, 0)) - docstr_node = tree.PythonNode('simple_stmt', [string, new_line]) - suite.children.insert(1, docstr_node) - return result, fake_module - - -def get_faked(evaluator, module, obj, name=None, parent_context=None): - if parent_context and parent_context.tree_node is not None: +def get_faked_with_parent_context(parent_context, name): + if parent_context.tree_node is not None: # Try to search in already clearly defined stuff. found = _search_scope(parent_context.tree_node, name) if found is not None: return found - else: - raise FakeDoesNotExist + raise FakeDoesNotExist - faked, fake_module = _get_faked(evaluator.latest_grammar, module and module.obj, obj, name) - if module is not None: - module.get_used_names = fake_module.get_used_names - return faked - -def is_class_instance(obj): - """Like inspect.* methods.""" - try: - cls = obj.__class__ - except AttributeError: - return False - else: - return cls != type and not issubclass(cls, NOT_CLASS_TYPES) +def get_faked_module(evaluator, string_name): + module = _load_faked_module(evaluator, string_name) + if module is None: + raise FakeDoesNotExist + return module diff --git a/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym b/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym index 1225929c2cb7..46ec619fb420 100644 --- a/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym +++ b/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym @@ -201,10 +201,13 @@ class dict(): # has a strange docstr pass + def __getitem__(self, obj): + return self.__elements[obj] + def get(self, k, d=None): # TODO implement try: - #return self.__elements[k] + return self.__elements[k] pass except KeyError: return d diff --git a/pythonFiles/jedi/evaluate/compiled/getattr_static.py b/pythonFiles/jedi/evaluate/compiled/getattr_static.py index 9f8cd8a838cf..946ac09b6b06 100644 --- a/pythonFiles/jedi/evaluate/compiled/getattr_static.py +++ b/pythonFiles/jedi/evaluate/compiled/getattr_static.py @@ -10,6 +10,7 @@ _sentinel = object() + def _check_instance(obj, attr): instance_dict = {} try: @@ -28,6 +29,7 @@ def _check_class(klass, attr): pass return _sentinel + def _is_type(obj): try: _static_getmro(obj) @@ -87,7 +89,7 @@ class Foo(object): pass return getattr(klass, '__dict__', _sentinel) return _shadowed_dict_newstyle(klass) - class _OldStyleClass(): + class _OldStyleClass: pass _oldstyle_instance_type = type(_OldStyleClass()) @@ -122,7 +124,7 @@ def _safe_hasattr(obj, name): def _safe_is_data_descriptor(obj): - return (_safe_hasattr(obj, '__set__') or _safe_hasattr(obj, '__delete__')) + return _safe_hasattr(obj, '__set__') or _safe_hasattr(obj, '__delete__') def getattr_static(obj, attr, default=_sentinel): @@ -143,8 +145,7 @@ def getattr_static(obj, attr, default=_sentinel): if not _is_type(obj): klass = _get_type(obj) dict_attr = _shadowed_dict(klass) - if (dict_attr is _sentinel or - type(dict_attr) is types.MemberDescriptorType): + if (dict_attr is _sentinel or type(dict_attr) is types.MemberDescriptorType): instance_result = _check_instance(obj, attr) else: klass = obj diff --git a/pythonFiles/jedi/evaluate/compiled/mixed.py b/pythonFiles/jedi/evaluate/compiled/mixed.py index ac0f6dd614db..b8003502e3d7 100644 --- a/pythonFiles/jedi/evaluate/compiled/mixed.py +++ b/pythonFiles/jedi/evaluate/compiled/mixed.py @@ -5,6 +5,8 @@ import inspect import os +from jedi.parser_utils import get_cached_code_lines + from jedi import settings from jedi.evaluate import compiled from jedi.cache import underscore_memoization @@ -13,6 +15,8 @@ from jedi.evaluate.context import ModuleContext from jedi.evaluate.cache import evaluator_function_cache from jedi.evaluate.compiled.getattr_static import getattr_static +from jedi.evaluate.compiled.access import compiled_objects_cache +from jedi.evaluate.compiled.context import create_cached_compiled_object class MixedObject(object): @@ -37,7 +41,7 @@ def __init__(self, evaluator, parent_context, compiled_object, tree_context): self.parent_context = parent_context self.compiled_object = compiled_object self._context = tree_context - self.obj = compiled_object.obj + self.access_handle = compiled_object.access_handle # We have to overwrite everything that has to do with trailers, name # lookups and filters to make it possible to route name lookups towards @@ -49,7 +53,7 @@ def get_filters(self, *args, **kwargs): yield MixedObjectFilter(self.evaluator, self) def __repr__(self): - return '<%s: %s>' % (type(self).__name__, repr(self.obj)) + return '<%s: %s>' % (type(self).__name__, self.access_handle.get_repr()) def __getattr__(self, name): return getattr(self._context, name) @@ -64,7 +68,7 @@ def start_pos(self): contexts = list(self.infer()) if not contexts: # This means a start_pos that doesn't exist (compiled objects). - return (0, 0) + return 0, 0 return contexts[0].name.start_pos @start_pos.setter @@ -74,17 +78,11 @@ def start_pos(self, value): @underscore_memoization def infer(self): - obj = self.parent_context.obj - try: - # TODO use logic from compiled.CompiledObjectFilter - obj = getattr(obj, self.string_name) - except AttributeError: - # Happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None + access_handle = self.parent_context.access_handle + # TODO use logic from compiled.CompiledObjectFilter + access_handle = access_handle.getattr(self.string_name, default=None) return ContextSet( - _create(self._evaluator, obj, parent_context=self.parent_context) + _create(self._evaluator, access_handle, parent_context=self.parent_context) ) @property @@ -105,17 +103,17 @@ def __init__(self, evaluator, mixed_object, is_instance=False): @evaluator_function_cache() -def _load_module(evaluator, path, python_object): - module = evaluator.grammar.parse( +def _load_module(evaluator, path): + module_node = evaluator.grammar.parse( path=path, cache=True, diff_cache=True, cache_path=settings.cache_directory ).get_root_node() - python_module = inspect.getmodule(python_object) - - evaluator.modules[python_module.__name__] = module - return module + # python_module = inspect.getmodule(python_object) + # TODO we should actually make something like this possible. + #evaluator.modules[python_module.__name__] = module_node + return module_node def _get_object_to_check(python_object): @@ -135,39 +133,43 @@ def _get_object_to_check(python_object): raise TypeError # Prevents computation of `repr` within inspect. -def find_syntax_node_name(evaluator, python_object): +def _find_syntax_node_name(evaluator, access_handle): + # TODO accessing this is bad, but it probably doesn't matter that much, + # because we're working with interpreteters only here. + python_object = access_handle.access._obj try: python_object = _get_object_to_check(python_object) path = inspect.getsourcefile(python_object) except TypeError: # The type might not be known (e.g. class_with_dict.__weakref__) - return None, None + return None if path is None or not os.path.exists(path): # The path might not exist or be e.g. . - return None, None + return None - module = _load_module(evaluator, path, python_object) + module_node = _load_module(evaluator, path) if inspect.ismodule(python_object): # We don't need to check names for modules, because there's not really # a way to write a module in a module in Python (and also __name__ can # be something like ``email.utils``). - return module, path + code_lines = get_cached_code_lines(evaluator.grammar, path) + return module_node, module_node, path, code_lines try: name_str = python_object.__name__ except AttributeError: # Stuff like python_function.__code__. - return None, None + return None if name_str == '': - return None, None # It's too hard to find lambdas. + return None # It's too hard to find lambdas. # Doesn't always work (e.g. os.stat_result) try: - names = module.get_used_names()[name_str] + names = module_node.get_used_names()[name_str] except KeyError: - return None, None + return None names = [n for n in names if n.is_definition()] try: @@ -184,33 +186,40 @@ def find_syntax_node_name(evaluator, python_object): # There's a chance that the object is not available anymore, because # the code has changed in the background. if line_names: - return line_names[-1].parent, path + names = line_names + code_lines = get_cached_code_lines(evaluator.grammar, path) # It's really hard to actually get the right definition, here as a last # resort we just return the last one. This chance might lead to odd # completions at some points but will lead to mostly correct type # inference, because people tend to define a public name in a module only # once. - return names[-1].parent, path + return module_node, names[-1].parent, path, code_lines -@compiled.compiled_objects_cache('mixed_cache') -def _create(evaluator, obj, parent_context=None, *args): - tree_node, path = find_syntax_node_name(evaluator, obj) +@compiled_objects_cache('mixed_cache') +def _create(evaluator, access_handle, parent_context, *args): + compiled_object = create_cached_compiled_object( + evaluator, access_handle, parent_context=parent_context.compiled_object) - compiled_object = compiled.create( - evaluator, obj, parent_context=parent_context.compiled_object) - if tree_node is None: + result = _find_syntax_node_name(evaluator, access_handle) + if result is None: return compiled_object - module_node = tree_node.get_root_node() + module_node, tree_node, path, code_lines = result + if parent_context.tree_node.get_root_node() == module_node: module_context = parent_context.get_root_context() else: - module_context = ModuleContext(evaluator, module_node, path=path) + module_context = ModuleContext( + evaluator, module_node, + path=path, + code_lines=code_lines, + ) # TODO this __name__ is probably wrong. name = compiled_object.get_root_context().py__name__() - imports.add_module(evaluator, name, module_context) + if name is not None: + imports.add_module_to_cache(evaluator, name, module_context) tree_context = module_context.create_context( tree_node, @@ -218,7 +227,7 @@ def _create(evaluator, obj, parent_context=None, *args): node_is_object=True ) if tree_node.type == 'classdef': - if not inspect.isclass(obj): + if not access_handle.is_class(): # Is an instance, not a class. tree_context, = tree_context.execute_evaluated() @@ -228,4 +237,3 @@ def _create(evaluator, obj, parent_context=None, *args): compiled_object, tree_context=tree_context ) - diff --git a/pythonFiles/jedi/evaluate/compiled/subprocess/__init__.py b/pythonFiles/jedi/evaluate/compiled/subprocess/__init__.py new file mode 100644 index 000000000000..9cc8704a457e --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/subprocess/__init__.py @@ -0,0 +1,340 @@ +""" +Makes it possible to do the compiled analysis in a subprocess. This has two +goals: + +1. Making it safer - Segfaults and RuntimeErrors as well as stdout/stderr can + be ignored and dealt with. +2. Make it possible to handle different Python versions as well as virtualenvs. +""" + +import os +import sys +import subprocess +import socket +import errno +import weakref +import traceback +from functools import partial + +from jedi._compatibility import queue, is_py3, force_unicode, \ + pickle_dump, pickle_load, GeneralizedPopen +from jedi.cache import memoize_method +from jedi.evaluate.compiled.subprocess import functions +from jedi.evaluate.compiled.access import DirectObjectAccess, AccessPath, \ + SignatureParam +from jedi.api.exceptions import InternalError + +_subprocesses = {} + +_MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py') + + +def get_subprocess(executable): + try: + return _subprocesses[executable] + except KeyError: + sub = _subprocesses[executable] = _CompiledSubprocess(executable) + return sub + + +def _get_function(name): + return getattr(functions, name) + + +class _EvaluatorProcess(object): + def __init__(self, evaluator): + self._evaluator_weakref = weakref.ref(evaluator) + self._evaluator_id = id(evaluator) + self._handles = {} + + def get_or_create_access_handle(self, obj): + id_ = id(obj) + try: + return self.get_access_handle(id_) + except KeyError: + access = DirectObjectAccess(self._evaluator_weakref(), obj) + handle = AccessHandle(self, access, id_) + self.set_access_handle(handle) + return handle + + def get_access_handle(self, id_): + return self._handles[id_] + + def set_access_handle(self, handle): + self._handles[handle.id] = handle + + +class EvaluatorSameProcess(_EvaluatorProcess): + """ + Basically just an easy access to functions.py. It has the same API + as EvaluatorSubprocess and does the same thing without using a subprocess. + This is necessary for the Interpreter process. + """ + def __getattr__(self, name): + return partial(_get_function(name), self._evaluator_weakref()) + + +class EvaluatorSubprocess(_EvaluatorProcess): + def __init__(self, evaluator, compiled_subprocess): + super(EvaluatorSubprocess, self).__init__(evaluator) + self._used = False + self._compiled_subprocess = compiled_subprocess + + def __getattr__(self, name): + func = _get_function(name) + + def wrapper(*args, **kwargs): + self._used = True + + result = self._compiled_subprocess.run( + self._evaluator_weakref(), + func, + args=args, + kwargs=kwargs, + ) + # IMO it should be possible to create a hook in pickle.load to + # mess with the loaded objects. However it's extremely complicated + # to work around this so just do it with this call. ~ dave + return self._convert_access_handles(result) + + return wrapper + + def _convert_access_handles(self, obj): + if isinstance(obj, SignatureParam): + return SignatureParam(*self._convert_access_handles(tuple(obj))) + elif isinstance(obj, tuple): + return tuple(self._convert_access_handles(o) for o in obj) + elif isinstance(obj, list): + return [self._convert_access_handles(o) for o in obj] + elif isinstance(obj, AccessHandle): + try: + # Rewrite the access handle to one we're already having. + obj = self.get_access_handle(obj.id) + except KeyError: + obj.add_subprocess(self) + self.set_access_handle(obj) + elif isinstance(obj, AccessPath): + return AccessPath(self._convert_access_handles(obj.accesses)) + return obj + + def __del__(self): + if self._used: + self._compiled_subprocess.delete_evaluator(self._evaluator_id) + + +class _CompiledSubprocess(object): + _crashed = False + + def __init__(self, executable): + self._executable = executable + self._evaluator_deletion_queue = queue.deque() + + @property + @memoize_method + def _process(self): + parso_path = sys.modules['parso'].__file__ + args = ( + self._executable, + _MAIN_PATH, + os.path.dirname(os.path.dirname(parso_path)) + ) + return GeneralizedPopen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + def run(self, evaluator, function, args=(), kwargs={}): + # Delete old evaluators. + while True: + try: + evaluator_id = self._evaluator_deletion_queue.pop() + except IndexError: + break + else: + self._send(evaluator_id, None) + + assert callable(function) + return self._send(id(evaluator), function, args, kwargs) + + def get_sys_path(self): + return self._send(None, functions.get_sys_path, (), {}) + + def kill(self): + self._crashed = True + try: + subprocess = _subprocesses[self._executable] + except KeyError: + # Fine it was already removed from the cache. + pass + else: + # In the `!=` case there is already a new subprocess in place + # and we don't need to do anything here anymore. + if subprocess == self: + del _subprocesses[self._executable] + + self._process.kill() + self._process.wait() + + def _send(self, evaluator_id, function, args=(), kwargs={}): + if self._crashed: + raise InternalError("The subprocess %s has crashed." % self._executable) + + if not is_py3: + # Python 2 compatibility + kwargs = {force_unicode(key): value for key, value in kwargs.items()} + + data = evaluator_id, function, args, kwargs + try: + pickle_dump(data, self._process.stdin) + except (socket.error, IOError) as e: + # Once Python2 will be removed we can just use `BrokenPipeError`. + # Also, somehow in windows it returns EINVAL instead of EPIPE if + # the subprocess dies. + if e.errno not in (errno.EPIPE, errno.EINVAL): + # Not a broken pipe + raise + self.kill() + raise InternalError("The subprocess %s was killed. Maybe out of memory?" + % self._executable) + + try: + is_exception, traceback, result = pickle_load(self._process.stdout) + except EOFError: + self.kill() + raise InternalError("The subprocess %s has crashed." % self._executable) + + if is_exception: + # Replace the attribute error message with a the traceback. It's + # way more informative. + result.args = (traceback,) + raise result + return result + + def delete_evaluator(self, evaluator_id): + """ + Currently we are not deleting evalutors instantly. They only get + deleted once the subprocess is used again. It would probably a better + solution to move all of this into a thread. However, the memory usage + of a single evaluator shouldn't be that high. + """ + # With an argument - the evaluator gets deleted. + self._evaluator_deletion_queue.append(evaluator_id) + + +class Listener(object): + def __init__(self): + self._evaluators = {} + # TODO refactor so we don't need to process anymore just handle + # controlling. + self._process = _EvaluatorProcess(Listener) + + def _get_evaluator(self, function, evaluator_id): + from jedi.evaluate import Evaluator + + try: + evaluator = self._evaluators[evaluator_id] + except KeyError: + from jedi.api.environment import InterpreterEnvironment + evaluator = Evaluator( + # The project is not actually needed. Nothing should need to + # access it. + project=None, + environment=InterpreterEnvironment() + ) + self._evaluators[evaluator_id] = evaluator + return evaluator + + def _run(self, evaluator_id, function, args, kwargs): + if evaluator_id is None: + return function(*args, **kwargs) + elif function is None: + del self._evaluators[evaluator_id] + else: + evaluator = self._get_evaluator(function, evaluator_id) + + # Exchange all handles + args = list(args) + for i, arg in enumerate(args): + if isinstance(arg, AccessHandle): + args[i] = evaluator.compiled_subprocess.get_access_handle(arg.id) + for key, value in kwargs.items(): + if isinstance(value, AccessHandle): + kwargs[key] = evaluator.compiled_subprocess.get_access_handle(value.id) + + return function(evaluator, *args, **kwargs) + + def listen(self): + stdout = sys.stdout + # Mute stdout/stderr. Nobody should actually be able to write to those, + # because stdout is used for IPC and stderr will just be annoying if it + # leaks (on module imports). + sys.stdout = open(os.devnull, 'w') + sys.stderr = open(os.devnull, 'w') + stdin = sys.stdin + if sys.version_info[0] > 2: + stdout = stdout.buffer + stdin = stdin.buffer + + while True: + try: + payload = pickle_load(stdin) + except EOFError: + # It looks like the parent process closed. Don't make a big fuss + # here and just exit. + exit(1) + try: + result = False, None, self._run(*payload) + except Exception as e: + result = True, traceback.format_exc(), e + + pickle_dump(result, file=stdout) + + +class AccessHandle(object): + def __init__(self, subprocess, access, id_): + self.access = access + self._subprocess = subprocess + self.id = id_ + + def add_subprocess(self, subprocess): + self._subprocess = subprocess + + def __repr__(self): + try: + detail = self.access + except AttributeError: + detail = '#' + str(self.id) + return '<%s of %s>' % (self.__class__.__name__, detail) + + def __getstate__(self): + return self.id + + def __setstate__(self, state): + self.id = state + + def __getattr__(self, name): + if name in ('id', 'access') or name.startswith('_'): + raise AttributeError("Something went wrong with unpickling") + + #if not is_py3: print >> sys.stderr, name + #print('getattr', name, file=sys.stderr) + return partial(self._workaround, force_unicode(name)) + + def _workaround(self, name, *args, **kwargs): + """ + TODO Currently we're passing slice objects around. This should not + happen. They are also the only unhashable objects that we're passing + around. + """ + if args and isinstance(args[0], slice): + return self._subprocess.get_compiled_method_return(self.id, name, *args, **kwargs) + return self._cached_results(name, *args, **kwargs) + + @memoize_method + def _cached_results(self, name, *args, **kwargs): + #if type(self._subprocess) == EvaluatorSubprocess: + #print(name, args, kwargs, + #self._subprocess.get_compiled_method_return(self.id, name, *args, **kwargs) + #) + return self._subprocess.get_compiled_method_return(self.id, name, *args, **kwargs) diff --git a/pythonFiles/jedi/evaluate/compiled/subprocess/__main__.py b/pythonFiles/jedi/evaluate/compiled/subprocess/__main__.py new file mode 100644 index 000000000000..8a0148ce5040 --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/subprocess/__main__.py @@ -0,0 +1,49 @@ +import sys +import os + + +def _get_paths(): + # Get the path to jedi. + _d = os.path.dirname + _jedi_path = _d(_d(_d(_d(_d(__file__))))) + _parso_path = sys.argv[1] + # The paths are the directory that jedi and parso lie in. + return {'jedi': _jedi_path, 'parso': _parso_path} + + +# Remove the first entry, because it's simply a directory entry that equals +# this directory. +del sys.path[0] + +if sys.version_info > (3, 4): + from importlib.machinery import PathFinder + + class _ExactImporter(object): + def __init__(self, path_dct): + self._path_dct = path_dct + + def find_module(self, fullname, path=None): + if path is None and fullname in self._path_dct: + p = self._path_dct[fullname] + loader = PathFinder.find_module(fullname, path=[p]) + return loader + return None + + # Try to import jedi/parso. + sys.meta_path.insert(0, _ExactImporter(_get_paths())) + from jedi.evaluate.compiled import subprocess # NOQA + sys.meta_path.pop(0) +else: + import imp + + def load(name): + paths = list(_get_paths().values()) + fp, pathname, description = imp.find_module(name, paths) + return imp.load_module(name, fp, pathname, description) + + load('parso') + load('jedi') + from jedi.evaluate.compiled import subprocess # NOQA + +# And finally start the client. +subprocess.Listener().listen() diff --git a/pythonFiles/jedi/evaluate/compiled/subprocess/functions.py b/pythonFiles/jedi/evaluate/compiled/subprocess/functions.py new file mode 100644 index 000000000000..593ff91e8caf --- /dev/null +++ b/pythonFiles/jedi/evaluate/compiled/subprocess/functions.py @@ -0,0 +1,113 @@ +import sys +import os + +from jedi._compatibility import find_module, cast_path, force_unicode, \ + iter_modules, all_suffixes, print_to_stderr +from jedi.evaluate.compiled import access +from jedi import parser_utils + + +def get_sys_path(): + return list(map(cast_path, sys.path)) + + +def load_module(evaluator, **kwargs): + return access.load_module(evaluator, **kwargs) + + +def get_compiled_method_return(evaluator, id, attribute, *args, **kwargs): + handle = evaluator.compiled_subprocess.get_access_handle(id) + return getattr(handle.access, attribute)(*args, **kwargs) + + +def get_special_object(evaluator, identifier): + return access.get_special_object(evaluator, identifier) + + +def create_simple_object(evaluator, obj): + return access.create_access_path(evaluator, obj) + + +def get_module_info(evaluator, sys_path=None, full_name=None, **kwargs): + if sys_path is not None: + sys.path, temp = sys_path, sys.path + try: + module_file, module_path, is_pkg = find_module(full_name=full_name, **kwargs) + except ImportError: + return None, None, None + finally: + if sys_path is not None: + sys.path = temp + + code = None + if is_pkg: + # In this case, we don't have a file yet. Search for the + # __init__ file. + if module_path.endswith(('.zip', '.egg')): + code = module_file.loader.get_source(full_name) + else: + module_path = _get_init_path(module_path) + elif module_file: + if module_path.endswith(('.zip', '.egg')): + # Unfortunately we are reading unicode here already, not byes. + # It seems however hard to get bytes, because the zip importer + # logic just unpacks the zip file and returns a file descriptor + # that we cannot as easily access. Therefore we just read it as + # a string. + code = module_file.read() + else: + # Read the code with a binary file, because the binary file + # might not be proper unicode. This is handled by the parser + # wrapper. + with open(module_path, 'rb') as f: + code = f.read() + + module_file.close() + + return code, cast_path(module_path), is_pkg + + +def list_module_names(evaluator, search_path): + return [ + name + for module_loader, name, is_pkg in iter_modules(search_path) + ] + + +def get_builtin_module_names(evaluator): + return list(map(force_unicode, sys.builtin_module_names)) + + +def _test_raise_error(evaluator, exception_type): + """ + Raise an error to simulate certain problems for unit tests. + """ + raise exception_type + + +def _test_print(evaluator, stderr=None, stdout=None): + """ + Force some prints in the subprocesses. This exists for unit tests. + """ + if stderr is not None: + print_to_stderr(stderr) + sys.stderr.flush() + if stdout is not None: + print(stdout) + sys.stdout.flush() + + +def _get_init_path(directory_path): + """ + The __init__ file can be searched in a directory. If found return it, else + None. + """ + for suffix in all_suffixes(): + path = os.path.join(directory_path, '__init__' + suffix) + if os.path.exists(path): + return path + return None + + +def safe_literal_eval(evaluator, value): + return parser_utils.safe_literal_eval(value) diff --git a/pythonFiles/jedi/evaluate/context/asynchronous.py b/pythonFiles/jedi/evaluate/context/asynchronous.py new file mode 100644 index 000000000000..51e59a48261e --- /dev/null +++ b/pythonFiles/jedi/evaluate/context/asynchronous.py @@ -0,0 +1,38 @@ +from jedi.evaluate.filters import publish_method, BuiltinOverwrite +from jedi.evaluate.base_context import ContextSet + + +class AsyncBase(BuiltinOverwrite): + def __init__(self, evaluator, func_execution_context): + super(AsyncBase, self).__init__(evaluator) + self.func_execution_context = func_execution_context + + @property + def name(self): + return self.get_object().name + + def __repr__(self): + return "<%s of %s>" % (type(self).__name__, self.func_execution_context) + + +class Coroutine(AsyncBase): + special_object_identifier = u'COROUTINE' + + @publish_method('__await__') + def _await(self): + return ContextSet(CoroutineWrapper(self.evaluator, self.func_execution_context)) + + +class CoroutineWrapper(AsyncBase): + special_object_identifier = u'COROUTINE_WRAPPER' + + def py__stop_iteration_returns(self): + return self.func_execution_context.get_return_values() + + +class AsyncGenerator(AsyncBase): + """Handling of `yield` functions.""" + special_object_identifier = u'ASYNC_GENERATOR' + + def py__aiter__(self): + return self.func_execution_context.get_yield_lazy_contexts(is_async=True) diff --git a/pythonFiles/jedi/evaluate/context/function.py b/pythonFiles/jedi/evaluate/context/function.py index 0dba9c91d707..2bb3a9b88bc3 100644 --- a/pythonFiles/jedi/evaluate/context/function.py +++ b/pythonFiles/jedi/evaluate/context/function.py @@ -17,17 +17,20 @@ from jedi.evaluate.lazy_context import LazyKnownContexts, LazyKnownContext, \ LazyTreeContext from jedi.evaluate.context import iterable +from jedi.evaluate.context import asynchronous from jedi import parser_utils from jedi.evaluate.parser_cache import get_yield_exprs class LambdaName(AbstractNameDefinition): string_name = '' + api_type = u'function' def __init__(self, lambda_context): self._lambda_context = lambda_context self.parent_context = lambda_context.parent_context + @property def start_pos(self): return self._lambda_context.tree_node.start_pos @@ -39,7 +42,7 @@ class FunctionContext(use_metaclass(CachedMetaClass, TreeContext)): """ Needed because of decorators. Decorators are evaluated here. """ - api_type = 'function' + api_type = u'function' def __init__(self, evaluator, parent_context, funcdef): """ This should not be called directly """ @@ -63,11 +66,23 @@ def infer_function_execution(self, function_execution): """ Created to be used by inheritance. """ - yield_exprs = get_yield_exprs(self.evaluator, self.tree_node) - if yield_exprs: - return ContextSet(iterable.Generator(self.evaluator, function_execution)) + is_coroutine = self.tree_node.parent.type == 'async_stmt' + is_generator = bool(get_yield_exprs(self.evaluator, self.tree_node)) + + if is_coroutine: + if is_generator: + if self.evaluator.environment.version_info < (3, 6): + return NO_CONTEXTS + return ContextSet(asynchronous.AsyncGenerator(self.evaluator, function_execution)) + else: + if self.evaluator.environment.version_info < (3, 5): + return NO_CONTEXTS + return ContextSet(asynchronous.Coroutine(self.evaluator, function_execution)) else: - return function_execution.get_return_values() + if is_generator: + return ContextSet(iterable.Generator(self.evaluator, function_execution)) + else: + return function_execution.get_return_values() def get_function_execution(self, arguments=None): if arguments is None: @@ -83,9 +98,9 @@ def py__class__(self): # This differentiation is only necessary for Python2. Python3 does not # use a different method class. if isinstance(parser_utils.get_parent_scope(self.tree_node), tree.Class): - name = 'METHOD_CLASS' + name = u'METHOD_CLASS' else: - name = 'FUNCTION_CLASS' + name = u'FUNCTION_CLASS' return compiled.get_special_object(self.evaluator, name) @property @@ -122,7 +137,7 @@ def __init__(self, evaluator, parent_context, function_context, var_args): def get_return_values(self, check_yields=False): funcdef = self.tree_node if funcdef.type == 'lambdef': - return self.evaluator.eval_element(self, funcdef.children[-1]) + return self.eval_node(funcdef.children[-1]) if check_yields: context_set = NO_CONTEXTS @@ -140,13 +155,14 @@ def get_return_values(self, check_yields=False): if check_yields: context_set |= ContextSet.from_sets( lazy_context.infer() - for lazy_context in self._eval_yield(r) + for lazy_context in self._get_yield_lazy_context(r) ) else: try: children = r.children except AttributeError: - context_set |= ContextSet(compiled.create(self.evaluator, None)) + ctx = compiled.builtin_from_name(self.evaluator, u'None') + context_set |= ContextSet(ctx) else: context_set |= self.eval_node(children[1]) if check is flow_analysis.REACHABLE: @@ -154,10 +170,11 @@ def get_return_values(self, check_yields=False): break return context_set - def _eval_yield(self, yield_expr): + def _get_yield_lazy_context(self, yield_expr): if yield_expr.type == 'keyword': # `yield` just yields None. - yield LazyKnownContext(compiled.create(self.evaluator, None)) + ctx = compiled.builtin_from_name(self.evaluator, u'None') + yield LazyKnownContext(ctx) return node = yield_expr.children[1] @@ -169,7 +186,8 @@ def _eval_yield(self, yield_expr): yield LazyTreeContext(self, node) @recursion.execution_recursion_decorator(default=iter([])) - def get_yield_values(self): + def get_yield_lazy_contexts(self, is_async=False): + # TODO: if is_async, wrap yield statements in Awaitable/async_generator_asend for_parents = [(y, tree.search_ancestor(y, 'for_stmt', 'funcdef', 'while_stmt', 'if_stmt')) for y in get_yield_exprs(self.evaluator, self.tree_node)] @@ -202,7 +220,7 @@ def get_yield_values(self): if for_stmt is None: # No for_stmt, just normal yields. for yield_ in yields: - for result in self._eval_yield(yield_): + for result in self._get_yield_lazy_context(yield_): yield result else: input_node = for_stmt.get_testlist() @@ -213,7 +231,7 @@ def get_yield_values(self): dct = {str(for_stmt.children[1].value): lazy_context.infer()} with helpers.predefine_names(self, for_stmt, dct): for yield_in_same_for_stmt in yields: - for result in self._eval_yield(yield_in_same_for_stmt): + for result in self._get_yield_lazy_context(yield_in_same_for_stmt): yield result def get_filters(self, search_global, until_position=None, origin_scope=None): diff --git a/pythonFiles/jedi/evaluate/context/instance.py b/pythonFiles/jedi/evaluate/context/instance.py index 2c8d796c9c6d..def5e19a2da9 100644 --- a/pythonFiles/jedi/evaluate/context/instance.py +++ b/pythonFiles/jedi/evaluate/context/instance.py @@ -1,6 +1,5 @@ from abc import abstractproperty -from jedi._compatibility import is_py3 from jedi import debug from jedi.evaluate import compiled from jedi.evaluate import filters @@ -16,30 +15,34 @@ from jedi.parser_utils import get_parent_scope +class BaseInstanceFunctionExecution(FunctionExecutionContext): + def __init__(self, instance, *args, **kwargs): + self.instance = instance + super(BaseInstanceFunctionExecution, self).__init__( + instance.evaluator, *args, **kwargs) + -class InstanceFunctionExecution(FunctionExecutionContext): +class InstanceFunctionExecution(BaseInstanceFunctionExecution): def __init__(self, instance, parent_context, function_context, var_args): - self.instance = instance var_args = InstanceVarArgs(self, var_args) super(InstanceFunctionExecution, self).__init__( - instance.evaluator, parent_context, function_context, var_args) + instance, parent_context, function_context, var_args) -class AnonymousInstanceFunctionExecution(FunctionExecutionContext): +class AnonymousInstanceFunctionExecution(BaseInstanceFunctionExecution): function_execution_filter = filters.AnonymousInstanceFunctionExecutionFilter def __init__(self, instance, parent_context, function_context, var_args): - self.instance = instance super(AnonymousInstanceFunctionExecution, self).__init__( - instance.evaluator, parent_context, function_context, var_args) + instance, parent_context, function_context, var_args) class AbstractInstanceContext(Context): """ This class is used to evaluate instances. """ - api_type = 'instance' + api_type = u'instance' function_execution_cls = InstanceFunctionExecution def __init__(self, evaluator, parent_context, class_context, var_args): @@ -54,7 +57,7 @@ def is_class(self): @property def py__call__(self): - names = self.get_function_slot_names('__call__') + names = self.get_function_slot_names(u'__call__') if not names: # Means the Instance is not callable. raise AttributeError @@ -90,12 +93,12 @@ def execute_function_slots(self, names, *evaluated_args): def py__get__(self, obj): # Arguments in __get__ descriptors are obj, class. # `method` is the new parent of the array, don't know if that's good. - names = self.get_function_slot_names('__get__') + names = self.get_function_slot_names(u'__get__') if names: if isinstance(obj, AbstractInstanceContext): return self.execute_function_slots(names, obj, obj.class_context) else: - none_obj = compiled.create(self.evaluator, None) + none_obj = compiled.builtin_from_name(self.evaluator, u'None') return self.execute_function_slots(names, none_obj, obj) else: return ContextSet(self) @@ -104,14 +107,12 @@ def get_filters(self, search_global=None, until_position=None, origin_scope=None, include_self_names=True): if include_self_names: for cls in self.class_context.py__mro__(): - if isinstance(cls, compiled.CompiledObject): - if cls.tree_node is not None: - # In this case we're talking about a fake object, it - # doesn't make sense for normal compiled objects to - # search for self variables. - yield SelfNameFilter(self.evaluator, self, cls, origin_scope) - else: - yield SelfNameFilter(self.evaluator, self, cls, origin_scope) + if not isinstance(cls, compiled.CompiledObject) \ + or cls.tree_node is not None: + # In this case we're excluding compiled objects that are + # not fake objects. It doesn't make sense for normal + # compiled objects to search for self variables. + yield SelfAttributeFilter(self.evaluator, self, cls, origin_scope) for cls in self.class_context.py__mro__(): if isinstance(cls, compiled.CompiledObject): @@ -121,16 +122,16 @@ def get_filters(self, search_global=None, until_position=None, def py__getitem__(self, index): try: - names = self.get_function_slot_names('__getitem__') + names = self.get_function_slot_names(u'__getitem__') except KeyError: debug.warning('No __getitem__, cannot access the array.') return NO_CONTEXTS else: - index_obj = compiled.create(self.evaluator, index) + index_obj = compiled.create_simple_object(self.evaluator, index) return self.execute_function_slots(names, index_obj) def py__iter__(self): - iter_slot_names = self.get_function_slot_names('__iter__') + iter_slot_names = self.get_function_slot_names(u'__iter__') if not iter_slot_names: debug.warning('No __iter__ on %s.' % self) return @@ -138,7 +139,10 @@ def py__iter__(self): for generator in self.execute_function_slots(iter_slot_names): if isinstance(generator, AbstractInstanceContext): # `__next__` logic. - name = '__next__' if is_py3 else 'next' + if self.evaluator.environment.version_info.major == 2: + name = u'next' + else: + name = u'__next__' iter_slot_names = generator.get_function_slot_names(name) if iter_slot_names: yield LazyKnownContexts( @@ -166,8 +170,8 @@ def _create_init_execution(self, class_context, func_node): ) def create_init_executions(self): - for name in self.get_function_slot_names('__init__'): - if isinstance(name, LazyInstanceName): + for name in self.get_function_slot_names(u'__init__'): + if isinstance(name, SelfName): yield self._create_init_execution(name.class_context, name.tree_name.parent) @evaluator_method_cache() @@ -189,7 +193,7 @@ def create_instance_context(self, class_context, node): ) return bound_method.get_function_execution() elif scope.type == 'classdef': - class_context = ClassContext(self.evaluator, scope, parent_context) + class_context = ClassContext(self.evaluator, parent_context, scope) return class_context elif scope.type == 'comp_for': # Comprehensions currently don't have a special scope in Jedi. @@ -208,8 +212,10 @@ def __init__(self, *args, **kwargs): super(CompiledInstance, self).__init__(*args, **kwargs) # I don't think that dynamic append lookups should happen here. That # sounds more like something that should go to py__iter__. + self._original_var_args = self.var_args + if self.class_context.name.string_name in ['list', 'set'] \ - and self.parent_context.get_root_context() == self.evaluator.BUILTINS: + and self.parent_context.get_root_context() == self.evaluator.builtins_module: # compare the module path with the builtin name. self.var_args = iterable.get_dynamic_array_instance(self) @@ -223,6 +229,13 @@ def create_instance_context(self, class_context, node): else: return super(CompiledInstance, self).create_instance_context(class_context, node) + def get_first_non_keyword_argument_contexts(self): + key, lazy_context = next(self._original_var_args.unpack(), ('', None)) + if key is not None: + return NO_CONTEXTS + + return lazy_context.infer() + class TreeInstance(AbstractInstanceContext): def __init__(self, evaluator, parent_context, class_context, var_args): @@ -255,7 +268,8 @@ def __init__(self, evaluator, instance, parent_context, name): @iterator_to_context_set def infer(self): for result_context in super(CompiledInstanceName, self).infer(): - if isinstance(result_context, FunctionContext): + is_function = result_context.api_type == 'function' + if result_context.tree_node is not None and is_function: parent_context = result_context.parent_context while parent_context.is_class(): parent_context = parent_context.parent_context @@ -265,7 +279,7 @@ def infer(self): parent_context, result_context.tree_node ) else: - if result_context.api_type == 'function': + if is_function: yield CompiledBoundMethod(result_context) else: yield result_context @@ -306,7 +320,7 @@ def get_function_execution(self, arguments=None): class CompiledBoundMethod(compiled.CompiledObject): def __init__(self, func): super(CompiledBoundMethod, self).__init__( - func.evaluator, func.obj, func.parent_context, func.tree_node) + func.evaluator, func.access_handle, func.parent_context, func.tree_node) def get_param_names(self): return list(super(CompiledBoundMethod, self).get_param_names())[1:] @@ -317,7 +331,7 @@ def infer(self): return super(InstanceNameDefinition, self).infer() -class LazyInstanceName(filters.TreeNameDefinition): +class SelfName(filters.TreeNameDefinition): """ This name calculates the parent_context lazily. """ @@ -331,7 +345,7 @@ def parent_context(self): return self._instance.create_instance_context(self.class_context, self.tree_name) -class LazyInstanceClassName(LazyInstanceName): +class LazyInstanceClassName(SelfName): @iterator_to_context_set def infer(self): for result_context in super(LazyInstanceClassName, self).infer(): @@ -384,8 +398,11 @@ def _convert_names(self, names): return [self.name_class(self.context, self._class_context, name) for name in names] -class SelfNameFilter(InstanceClassFilter): - name_class = LazyInstanceName +class SelfAttributeFilter(InstanceClassFilter): + """ + This class basically filters all the use cases where `self.*` was assigned. + """ + name_class = SelfName def _filter(self, names): names = self._filter_self_names(names) diff --git a/pythonFiles/jedi/evaluate/context/iterable.py b/pythonFiles/jedi/evaluate/context/iterable.py index d0f468e43425..df012f50dd8a 100644 --- a/pythonFiles/jedi/evaluate/context/iterable.py +++ b/pythonFiles/jedi/evaluate/context/iterable.py @@ -22,74 +22,56 @@ """ from jedi import debug from jedi import settings +from jedi._compatibility import force_unicode, is_py3 +from jedi.cache import memoize_method from jedi.evaluate import compiled from jedi.evaluate import analysis from jedi.evaluate import recursion from jedi.evaluate.lazy_context import LazyKnownContext, LazyKnownContexts, \ LazyTreeContext -from jedi.evaluate.helpers import is_string, predefine_names, evaluate_call_of_leaf +from jedi.evaluate.helpers import get_int_or_none, is_string, \ + predefine_names, evaluate_call_of_leaf from jedi.evaluate.utils import safe_property from jedi.evaluate.utils import to_list from jedi.evaluate.cache import evaluator_method_cache -from jedi.evaluate.filters import ParserTreeFilter, has_builtin_methods, \ - register_builtin_method, SpecialMethodFilter +from jedi.evaluate.filters import ParserTreeFilter, BuiltinOverwrite, \ + publish_method from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS, Context, \ TreeContext, ContextualizedNode from jedi.parser_utils import get_comp_fors -class AbstractIterable(Context): - builtin_methods = {} - api_type = 'instance' +class IterableMixin(object): + def py__stop_iteration_returns(self): + return ContextSet(compiled.builtin_from_name(self.evaluator, u'None')) - def __init__(self, evaluator): - super(AbstractIterable, self).__init__(evaluator, evaluator.BUILTINS) - def get_filters(self, search_global, until_position=None, origin_scope=None): - raise NotImplementedError - - @property - def name(self): - return compiled.CompiledContextName(self, self.array_type) - - -@has_builtin_methods -class GeneratorMixin(object): +class GeneratorBase(BuiltinOverwrite, IterableMixin): array_type = None + special_object_identifier = u'GENERATOR_OBJECT' - @register_builtin_method('send') - @register_builtin_method('next', python_version_match=2) - @register_builtin_method('__next__', python_version_match=3) + @publish_method('send') + @publish_method('next', python_version_match=2) + @publish_method('__next__', python_version_match=3) def py__next__(self): - # TODO add TypeError if params are given. return ContextSet.from_sets(lazy_context.infer() for lazy_context in self.py__iter__()) - def get_filters(self, search_global, until_position=None, origin_scope=None): - gen_obj = compiled.get_special_object(self.evaluator, 'GENERATOR_OBJECT') - yield SpecialMethodFilter(self, self.builtin_methods, gen_obj) - for filter in gen_obj.get_filters(search_global): - yield filter - - def py__bool__(self): - return True - - def py__class__(self): - gen_obj = compiled.get_special_object(self.evaluator, 'GENERATOR_OBJECT') - return gen_obj.py__class__() - @property def name(self): return compiled.CompiledContextName(self, 'generator') -class Generator(GeneratorMixin, Context): +class Generator(GeneratorBase): """Handling of `yield` functions.""" def __init__(self, evaluator, func_execution_context): - super(Generator, self).__init__(evaluator, parent_context=evaluator.BUILTINS) + super(Generator, self).__init__(evaluator) self._func_execution_context = func_execution_context def py__iter__(self): - return self._func_execution_context.get_yield_values() + return self._func_execution_context.get_yield_lazy_contexts() + + def py__stop_iteration_returns(self): + return self._func_execution_context.get_return_values() def __repr__(self): return "<%s of %s>" % (type(self).__name__, self._func_execution_context) @@ -111,32 +93,33 @@ def get_filters(self, search_global, until_position=None, origin_scope=None): yield ParserTreeFilter(self.evaluator, self) -class Comprehension(AbstractIterable): - @staticmethod - def from_atom(evaluator, context, atom): - bracket = atom.children[0] - if bracket == '{': - if atom.children[1].children[1] == ':': - cls = DictComprehension - else: - cls = SetComprehension - elif bracket == '(': - cls = GeneratorComprehension - elif bracket == '[': - cls = ListComprehension - return cls(evaluator, context, atom) +def comprehension_from_atom(evaluator, context, atom): + bracket = atom.children[0] + if bracket == '{': + if atom.children[1].children[1] == ':': + cls = DictComprehension + else: + cls = SetComprehension + elif bracket == '(': + cls = GeneratorComprehension + elif bracket == '[': + cls = ListComprehension + return cls(evaluator, context, atom) + +class ComprehensionMixin(object): def __init__(self, evaluator, defining_context, atom): - super(Comprehension, self).__init__(evaluator) + super(ComprehensionMixin, self).__init__(evaluator) self._defining_context = defining_context self._atom = atom def _get_comprehension(self): + "return 'a for a in b'" # The atom contains a testlist_comp return self._atom.children[1] def _get_comp_for(self): - # The atom contains a testlist_comp + "return CompFor('for a in b')" return self._get_comprehension().children[1] def _eval_node(self, index=0): @@ -154,13 +137,17 @@ def _get_comp_for_context(self, parent_context, comp_for): def _nested(self, comp_fors, parent_context=None): comp_for = comp_fors[0] - input_node = comp_for.children[3] + + is_async = 'async' == comp_for.children[comp_for.children.index('for') - 1] + + input_node = comp_for.children[comp_for.children.index('in') + 1] parent_context = parent_context or self._defining_context input_types = parent_context.eval_node(input_node) + # TODO: simulate await if self.is_async cn = ContextualizedNode(parent_context, input_node) - iterated = input_types.iterate(cn) - exprlist = comp_for.children[1] + iterated = input_types.iterate(cn, is_async=is_async) + exprlist = comp_for.children[comp_for.children.index('for') + 1] for i, lazy_context in enumerate(iterated): types = lazy_context.infer() dct = unpack_tuple_to_dict(parent_context, types, exprlist) @@ -194,14 +181,18 @@ def __repr__(self): return "<%s of %s>" % (type(self).__name__, self._atom) -class ArrayMixin(object): - def get_filters(self, search_global, until_position=None, origin_scope=None): - # `array.type` is a string with the type, e.g. 'list'. +class Sequence(BuiltinOverwrite, IterableMixin): + api_type = u'instance' + + @property + def name(self): + return compiled.CompiledContextName(self, self.array_type) + + @memoize_method + def get_object(self): compiled_obj = compiled.builtin_from_name(self.evaluator, self.array_type) - yield SpecialMethodFilter(self, self.builtin_methods, compiled_obj) - for typ in compiled_obj.execute_evaluated(self): - for filter in typ.get_filters(): - yield filter + only_obj, = compiled_obj.execute_evaluated(self) + return only_obj def py__bool__(self): return None # We don't know the length, because of appends. @@ -211,7 +202,7 @@ def py__class__(self): @safe_property def parent(self): - return self.evaluator.BUILTINS + return self.evaluator.builtins_module def dict_values(self): return ContextSet.from_sets( @@ -220,8 +211,8 @@ def dict_values(self): ) -class ListComprehension(ArrayMixin, Comprehension): - array_type = 'list' +class ListComprehension(ComprehensionMixin, Sequence): + array_type = u'list' def py__getitem__(self, index): if isinstance(index, slice): @@ -231,13 +222,12 @@ def py__getitem__(self, index): return all_types[index].infer() -class SetComprehension(ArrayMixin, Comprehension): - array_type = 'set' +class SetComprehension(ComprehensionMixin, Sequence): + array_type = u'set' -@has_builtin_methods -class DictComprehension(ArrayMixin, Comprehension): - array_type = 'dict' +class DictComprehension(ComprehensionMixin, Sequence): + array_type = u'dict' def _get_comp_for(self): return self._get_comprehension().children[3] @@ -250,38 +240,38 @@ def py__getitem__(self, index): for keys, values in self._iterate(): for k in keys: if isinstance(k, compiled.CompiledObject): - if k.obj == index: + if k.get_safe_value(default=object()) == index: return values return self.dict_values() def dict_values(self): return ContextSet.from_sets(values for keys, values in self._iterate()) - @register_builtin_method('values') + @publish_method('values') def _imitate_values(self): lazy_context = LazyKnownContexts(self.dict_values()) - return ContextSet(FakeSequence(self.evaluator, 'list', [lazy_context])) + return ContextSet(FakeSequence(self.evaluator, u'list', [lazy_context])) - @register_builtin_method('items') + @publish_method('items') def _imitate_items(self): items = ContextSet.from_iterable( FakeSequence( - self.evaluator, 'tuple' + self.evaluator, u'tuple' (LazyKnownContexts(keys), LazyKnownContexts(values)) ) for keys, values in self._iterate() ) - return create_evaluated_sequence_set(self.evaluator, items, sequence_type='list') + return create_evaluated_sequence_set(self.evaluator, items, sequence_type=u'list') -class GeneratorComprehension(GeneratorMixin, Comprehension): +class GeneratorComprehension(ComprehensionMixin, GeneratorBase): pass -class SequenceLiteralContext(ArrayMixin, AbstractIterable): - mapping = {'(': 'tuple', - '[': 'list', - '{': 'set'} +class SequenceLiteralContext(Sequence): + mapping = {'(': u'tuple', + '[': u'list', + '{': u'set'} def __init__(self, evaluator, defining_context, atom): super(SequenceLiteralContext, self).__init__(evaluator) @@ -289,18 +279,19 @@ def __init__(self, evaluator, defining_context, atom): self._defining_context = defining_context if self.atom.type in ('testlist_star_expr', 'testlist'): - self.array_type = 'tuple' + self.array_type = u'tuple' else: self.array_type = SequenceLiteralContext.mapping[atom.children[0]] """The builtin name of the array (list, set, tuple or dict).""" def py__getitem__(self, index): """Here the index is an int/str. Raises IndexError/KeyError.""" - if self.array_type == 'dict': + if self.array_type == u'dict': + compiled_obj_index = compiled.create_simple_object(self.evaluator, index) for key, value in self._items(): for k in self._defining_context.eval_node(key): if isinstance(k, compiled.CompiledObject) \ - and index == k.obj: + and k.execute_operation(compiled_obj_index, u'==').get_safe_value(): return self._defining_context.eval_node(value) raise KeyError('No key found in dictionary %s.' % self) @@ -315,7 +306,7 @@ def py__iter__(self): While values returns the possible values for any array field, this function returns the value for a certain index. """ - if self.array_type == 'dict': + if self.array_type == u'dict': # Get keys. types = ContextSet() for k, _ in self._items(): @@ -333,7 +324,7 @@ def py__iter__(self): def _values(self): """Returns a list of a list of node.""" - if self.array_type == 'dict': + if self.array_type == u'dict': return ContextSet.from_sets(v for k, v in self._items()) else: return self._items() @@ -373,37 +364,36 @@ def exact_key_items(self): for key_node, value in self._items(): for key in self._defining_context.eval_node(key_node): if is_string(key): - yield key.obj, LazyTreeContext(self._defining_context, value) + yield key.get_safe_value(), LazyTreeContext(self._defining_context, value) def __repr__(self): return "<%s of %s>" % (self.__class__.__name__, self.atom) -@has_builtin_methods class DictLiteralContext(SequenceLiteralContext): - array_type = 'dict' + array_type = u'dict' def __init__(self, evaluator, defining_context, atom): super(SequenceLiteralContext, self).__init__(evaluator) self._defining_context = defining_context self.atom = atom - @register_builtin_method('values') + @publish_method('values') def _imitate_values(self): lazy_context = LazyKnownContexts(self.dict_values()) - return ContextSet(FakeSequence(self.evaluator, 'list', [lazy_context])) + return ContextSet(FakeSequence(self.evaluator, u'list', [lazy_context])) - @register_builtin_method('items') + @publish_method('items') def _imitate_items(self): lazy_contexts = [ LazyKnownContext(FakeSequence( - self.evaluator, 'tuple', + self.evaluator, u'tuple', (LazyTreeContext(self._defining_context, key_node), LazyTreeContext(self._defining_context, value_node)) )) for key_node, value_node in self._items() ] - return ContextSet(FakeSequence(self.evaluator, 'list', lazy_contexts)) + return ContextSet(FakeSequence(self.evaluator, u'list', lazy_contexts)) class _FakeArray(SequenceLiteralContext): @@ -437,16 +427,38 @@ def __repr__(self): class FakeDict(_FakeArray): def __init__(self, evaluator, dct): - super(FakeDict, self).__init__(evaluator, dct, 'dict') + super(FakeDict, self).__init__(evaluator, dct, u'dict') self._dct = dct def py__iter__(self): for key in self._dct: - yield LazyKnownContext(compiled.create(self.evaluator, key)) + yield LazyKnownContext(compiled.create_simple_object(self.evaluator, key)) def py__getitem__(self, index): + if is_py3 and self.evaluator.environment.version_info.major == 2: + # In Python 2 bytes and unicode compare. + if isinstance(index, bytes): + index_unicode = force_unicode(index) + try: + return self._dct[index_unicode].infer() + except KeyError: + pass + elif isinstance(index, str): + index_bytes = index.encode('utf-8') + try: + return self._dct[index_bytes].infer() + except KeyError: + pass + return self._dct[index].infer() + @publish_method('values') + def _values(self): + return ContextSet(FakeSequence( + self.evaluator, u'tuple', + [LazyKnownContexts(self.dict_values())] + )) + def dict_values(self): return ContextSet.from_sets(lazy_context.infer() for lazy_context in self._dct.values()) @@ -649,7 +661,7 @@ def py__iter__(self): for addition in additions: yield addition - def iterate(self, contextualized_node=None): + def iterate(self, contextualized_node=None, is_async=False): return self.py__iter__() @@ -657,7 +669,7 @@ class Slice(Context): def __init__(self, context, start, stop, step): super(Slice, self).__init__( context.evaluator, - parent_context=context.evaluator.BUILTINS + parent_context=context.evaluator.builtins_module ) self._context = context # all of them are either a Precedence or None. @@ -680,10 +692,9 @@ def get(element): # For simplicity, we want slices to be clear defined with just # one type. Otherwise we will return an empty slice object. raise IndexError - try: - return list(result)[0].obj - except AttributeError: - return None + + context, = result + return get_int_or_none(context) try: return slice(get(self._start), get(self._stop), get(self._step)) diff --git a/pythonFiles/jedi/evaluate/context/klass.py b/pythonFiles/jedi/evaluate/context/klass.py index b7d61d3e16bf..3157250161e2 100644 --- a/pythonFiles/jedi/evaluate/context/klass.py +++ b/pythonFiles/jedi/evaluate/context/klass.py @@ -89,7 +89,7 @@ class ClassContext(use_metaclass(CachedMetaClass, TreeContext)): This class is not only important to extend `tree.Class`, it is also a important for descriptors (if the descriptor methods are evaluated or not). """ - api_type = 'class' + api_type = u'class' def __init__(self, evaluator, parent_context, classdef): super(ClassContext, self).__init__(evaluator, parent_context=parent_context) @@ -136,17 +136,17 @@ def py__bases__(self): arglist = self.tree_node.get_super_arglist() if arglist: from jedi.evaluate import arguments - args = arguments.TreeArguments(self.evaluator, self, arglist) + args = arguments.TreeArguments(self.evaluator, self.parent_context, arglist) return [value for key, value in args.unpack() if key is None] else: - return [LazyKnownContext(compiled.create(self.evaluator, object))] + return [LazyKnownContext(compiled.builtin_from_name(self.evaluator, u'object'))] def py__call__(self, params): from jedi.evaluate.context import TreeInstance return ContextSet(TreeInstance(self.evaluator, self.parent_context, self, params)) def py__class__(self): - return compiled.create(self.evaluator, type) + return compiled.builtin_from_name(self.evaluator, u'type') def get_params(self): from jedi.evaluate.context import AnonymousInstance @@ -182,7 +182,7 @@ def get_function_slot_names(self, name): return [] def get_param_names(self): - for name in self.get_function_slot_names('__init__'): + for name in self.get_function_slot_names(u'__init__'): for context_ in name.infer(): try: method = context_.get_param_names diff --git a/pythonFiles/jedi/evaluate/context/module.py b/pythonFiles/jedi/evaluate/context/module.py index 5ba92cdb1c3e..8d4da11bc91a 100644 --- a/pythonFiles/jedi/evaluate/context/module.py +++ b/pythonFiles/jedi/evaluate/context/module.py @@ -1,14 +1,12 @@ -import pkgutil -import imp import re import os from parso import python_bytes_to_unicode -from jedi._compatibility import use_metaclass -from jedi.evaluate.cache import CachedMetaClass, evaluator_method_cache +from jedi.evaluate.cache import evaluator_method_cache +from jedi._compatibility import iter_modules, all_suffixes from jedi.evaluate.filters import GlobalNameFilter, ContextNameMixin, \ - AbstractNameDefinition, ParserTreeFilter, DictFilter + AbstractNameDefinition, ParserTreeFilter, DictFilter, MergedFilter from jedi.evaluate import compiled from jedi.evaluate.base_context import TreeContext from jedi.evaluate.imports import SubModuleName, infer_import @@ -18,14 +16,14 @@ class _ModuleAttributeName(AbstractNameDefinition): """ For module attributes like __file__, __str__ and so on. """ - api_type = 'instance' + api_type = u'instance' def __init__(self, parent_module, string_name): self.parent_context = parent_module self.string_name = string_name def infer(self): - return compiled.create(self.parent_context.evaluator, str).execute_evaluated() + return compiled.get_string_context_set(self.parent_context.evaluator) class ModuleName(ContextNameMixin, AbstractNameDefinition): @@ -40,23 +38,26 @@ def string_name(self): return self._name -class ModuleContext(use_metaclass(CachedMetaClass, TreeContext)): - api_type = 'module' +class ModuleContext(TreeContext): + api_type = u'module' parent_context = None - def __init__(self, evaluator, module_node, path): + def __init__(self, evaluator, module_node, path, code_lines): super(ModuleContext, self).__init__(evaluator, parent_context=None) self.tree_node = module_node self._path = path + self.code_lines = code_lines def get_filters(self, search_global, until_position=None, origin_scope=None): - yield ParserTreeFilter( - self.evaluator, - context=self, - until_position=until_position, - origin_scope=origin_scope + yield MergedFilter( + ParserTreeFilter( + self.evaluator, + context=self, + until_position=until_position, + origin_scope=origin_scope + ), + GlobalNameFilter(self, self.tree_node), ) - yield GlobalNameFilter(self, self.tree_node) yield DictFilter(self._sub_modules_dict()) yield DictFilter(self._module_attributes_dict()) for star_module in self.star_imports(): @@ -64,7 +65,7 @@ def get_filters(self, search_global, until_position=None, origin_scope=None): # I'm not sure if the star import cache is really that effective anymore # with all the other really fast import caches. Recheck. Also we would need - # to push the star imports into Evaluator.modules, if we reenable this. + # to push the star imports into Evaluator.module_cache, if we reenable this. @evaluator_method_cache([]) def star_imports(self): modules = [] @@ -93,7 +94,7 @@ def _string_name(self): sep = (re.escape(os.path.sep),) * 2 r = re.search(r'([^%s]*?)(%s__init__)?(\.py|\.so)?$' % sep, self._path) # Remove PEP 3149 names - return re.sub('\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) + return re.sub(r'\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) @property @evaluator_method_cache() @@ -105,7 +106,7 @@ def _get_init_directory(self): :return: The path to the directory of a package. None in case it's not a package. """ - for suffix, _, _ in imp.get_suffixes(): + for suffix in all_suffixes(): ending = '__init__' + suffix py__file__ = self.py__file__() if py__file__ is not None and py__file__.endswith(ending): @@ -114,7 +115,7 @@ def _get_init_directory(self): return None def py__name__(self): - for name, module in self.evaluator.modules.items(): + for name, module in self.evaluator.module_cache.iterate_modules_with_names(): if module == self and name != '': return name @@ -131,12 +132,12 @@ def py__file__(self): def py__package__(self): if self._get_init_directory() is None: - return re.sub(r'\.?[^\.]+$', '', self.py__name__()) + return re.sub(r'\.?[^.]+$', '', self.py__name__()) else: return self.py__name__() def _py__path__(self): - search_path = self.evaluator.project.sys_path + search_path = self.evaluator.get_sys_path() init_path = self.py__file__() if os.path.basename(init_path) == '__init__.py': with open(init_path, 'rb') as f: @@ -188,7 +189,7 @@ def _sub_modules_dict(self): path = self._path names = {} if path is not None and path.endswith(os.path.sep + '__init__.py'): - mods = pkgutil.iter_modules([os.path.dirname(path)]) + mods = iter_modules([os.path.dirname(path)]) for module_loader, name, is_pkg in mods: # It's obviously a relative import to the current module. names[name] = SubModuleName(self, name) @@ -203,11 +204,9 @@ def _sub_modules_dict(self): return names def py__class__(self): - return compiled.get_special_object(self.evaluator, 'MODULE_CLASS') + return compiled.get_special_object(self.evaluator, u'MODULE_CLASS') def __repr__(self): return "<%s: %s@%s-%s>" % ( self.__class__.__name__, self._string_name, self.tree_node.start_pos[0], self.tree_node.end_pos[0]) - - diff --git a/pythonFiles/jedi/evaluate/context/namespace.py b/pythonFiles/jedi/evaluate/context/namespace.py index e40c23a5e8ca..380d7c2270b9 100644 --- a/pythonFiles/jedi/evaluate/context/namespace.py +++ b/pythonFiles/jedi/evaluate/context/namespace.py @@ -1,11 +1,10 @@ import os from itertools import chain -from jedi._compatibility import use_metaclass -from jedi.evaluate.cache import evaluator_method_cache, CachedMetaClass +from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate import imports from jedi.evaluate.filters import DictFilter, AbstractNameDefinition -from jedi.evaluate.base_context import NO_CONTEXTS, TreeContext +from jedi.evaluate.base_context import TreeContext, ContextSet class ImplicitNSName(AbstractNameDefinition): @@ -14,27 +13,31 @@ class ImplicitNSName(AbstractNameDefinition): This object will prevent Jedi from raising exceptions """ def __init__(self, implicit_ns_context, string_name): - self.implicit_ns_context = implicit_ns_context + self.parent_context = implicit_ns_context self.string_name = string_name def infer(self): - return NO_CONTEXTS + return ContextSet(self.parent_context) def get_root_context(self): - return self.implicit_ns_context + return self.parent_context -class ImplicitNamespaceContext(use_metaclass(CachedMetaClass, TreeContext)): +class ImplicitNamespaceContext(TreeContext): """ Provides support for implicit namespace packages """ - api_type = 'module' + # Is a module like every other module, because if you import an empty + # folder foobar it will be available as an object: + # . + api_type = u'module' parent_context = None - def __init__(self, evaluator, fullname): + def __init__(self, evaluator, fullname, paths): super(ImplicitNamespaceContext, self).__init__(evaluator, parent_context=None) self.evaluator = evaluator - self.fullname = fullname + self._fullname = fullname + self.paths = paths def get_filters(self, search_global, until_position=None, origin_scope=None): yield DictFilter(self._sub_modules_dict()) @@ -51,7 +54,7 @@ def py__file__(self): def py__package__(self): """Return the fullname """ - return self.fullname + return self._fullname @property def py__path__(self): @@ -61,8 +64,7 @@ def py__path__(self): def _sub_modules_dict(self): names = {} - paths = self.paths - file_names = chain.from_iterable(os.listdir(path) for path in paths) + file_names = chain.from_iterable(os.listdir(path) for path in self.paths) mods = [ file_name.rpartition('.')[0] if '.' in file_name else file_name for file_name in file_names diff --git a/pythonFiles/jedi/evaluate/docstrings.py b/pythonFiles/jedi/evaluate/docstrings.py index f9c1141226e9..a927abd09028 100644 --- a/pythonFiles/jedi/evaluate/docstrings.py +++ b/pythonFiles/jedi/evaluate/docstrings.py @@ -18,7 +18,7 @@ import re from textwrap import dedent -from parso import parse +from parso import parse, ParserSyntaxError from jedi._compatibility import u from jedi.evaluate.utils import indent_block @@ -42,49 +42,59 @@ REST_ROLE_PATTERN = re.compile(r':[^`]+:`([^`]+)`') -try: - from numpydoc.docscrape import NumpyDocString -except ImportError: - def _search_param_in_numpydocstr(docstr, param_str): - return [] +_numpy_doc_string_cache = None - def _search_return_in_numpydocstr(docstr): - return [] -else: - def _search_param_in_numpydocstr(docstr, param_str): - """Search `docstr` (in numpydoc format) for type(-s) of `param_str`.""" - try: - # This is a non-public API. If it ever changes we should be - # prepared and return gracefully. - params = NumpyDocString(docstr)._parsed_data['Parameters'] - except (KeyError, AttributeError): - return [] - for p_name, p_type, p_descr in params: - if p_name == param_str: - m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type) - if m: - p_type = m.group(1) - return list(_expand_typestr(p_type)) + +def _get_numpy_doc_string_cls(): + global _numpy_doc_string_cache + try: + from numpydoc.docscrape import NumpyDocString + _numpy_doc_string_cache = NumpyDocString + except ImportError as e: + _numpy_doc_string_cache = e + if isinstance(_numpy_doc_string_cache, ImportError): + raise _numpy_doc_string_cache + return _numpy_doc_string_cache + + +def _search_param_in_numpydocstr(docstr, param_str): + """Search `docstr` (in numpydoc format) for type(-s) of `param_str`.""" + try: + # This is a non-public API. If it ever changes we should be + # prepared and return gracefully. + params = _get_numpy_doc_string_cls()(docstr)._parsed_data['Parameters'] + except (KeyError, AttributeError, ImportError): return [] + for p_name, p_type, p_descr in params: + if p_name == param_str: + m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type) + if m: + p_type = m.group(1) + return list(_expand_typestr(p_type)) + return [] - def _search_return_in_numpydocstr(docstr): - """ - Search `docstr` (in numpydoc format) for type(-s) of function returns. - """ - doc = NumpyDocString(docstr) - try: - # This is a non-public API. If it ever changes we should be - # prepared and return gracefully. - returns = doc._parsed_data['Returns'] - returns += doc._parsed_data['Yields'] - except (KeyError, AttributeError): - raise StopIteration - for r_name, r_type, r_descr in returns: - #Return names are optional and if so the type is in the name - if not r_type: - r_type = r_name - for type_ in _expand_typestr(r_type): - yield type_ + +def _search_return_in_numpydocstr(docstr): + """ + Search `docstr` (in numpydoc format) for type(-s) of function returns. + """ + try: + doc = _get_numpy_doc_string_cls()(docstr) + except ImportError: + return + try: + # This is a non-public API. If it ever changes we should be + # prepared and return gracefully. + returns = doc._parsed_data['Returns'] + returns += doc._parsed_data['Yields'] + except (KeyError, AttributeError): + return + for r_name, r_type, r_descr in returns: + # Return names are optional and if so the type is in the name + if not r_type: + r_type = r_name + for type_ in _expand_typestr(r_type): + yield type_ def _expand_typestr(type_str): @@ -145,8 +155,7 @@ def _search_param_in_docstr(docstr, param_str): if match: return [_strip_rst_role(match.group(1))] - return (_search_param_in_numpydocstr(docstr, param_str) or - []) + return _search_param_in_numpydocstr(docstr, param_str) def _strip_rst_role(type_str): @@ -179,7 +188,7 @@ def pseudo_docstring_stuff(): Need this docstring so that if the below part is not valid Python this is still a function. ''' - {0} + {} """)) if string is None: return [] @@ -193,7 +202,10 @@ def pseudo_docstring_stuff(): # will be impossible to use `...` (Ellipsis) as a token. Docstring types # don't need to conform with the current grammar. grammar = module_context.evaluator.latest_grammar - module = grammar.parse(code.format(indent_block(string))) + try: + module = grammar.parse(code.format(indent_block(string)), error_recovery=False) + except ParserSyntaxError: + return [] try: funcdef = next(module.iter_funcdefs()) # First pick suite, then simple_stmt and then the node, @@ -243,7 +255,7 @@ def _execute_array_values(evaluator, array): for typ in lazy_context.infer() ) values.append(LazyKnownContexts(objects)) - return set([FakeSequence(evaluator, array.array_type, values)]) + return {FakeSequence(evaluator, array.array_type, values)} else: return array.execute_evaluated() diff --git a/pythonFiles/jedi/evaluate/dynamic.py b/pythonFiles/jedi/evaluate/dynamic.py index 7d05000dc9d5..9e8d57144bdb 100644 --- a/pythonFiles/jedi/evaluate/dynamic.py +++ b/pythonFiles/jedi/evaluate/dynamic.py @@ -73,24 +73,33 @@ def search_params(evaluator, execution_context, funcdef): # you will see the slowdown, especially in 3.6. return create_default_params(execution_context, funcdef) - debug.dbg('Dynamic param search in %s.', funcdef.name.value, color='MAGENTA') - - module_context = execution_context.get_root_context() - function_executions = _search_function_executions( - evaluator, - module_context, - funcdef - ) - if function_executions: - zipped_params = zip(*list( - function_execution.get_params() - for function_execution in function_executions - )) - params = [MergedExecutedParams(executed_params) for executed_params in zipped_params] - # Evaluate the ExecutedParams to types. + if funcdef.type == 'lambdef': + string_name = _get_lambda_name(funcdef) + if string_name is None: + return create_default_params(execution_context, funcdef) else: - return create_default_params(execution_context, funcdef) - debug.dbg('Dynamic param result finished', color='MAGENTA') + string_name = funcdef.name.value + debug.dbg('Dynamic param search in %s.', string_name, color='MAGENTA') + + try: + module_context = execution_context.get_root_context() + function_executions = _search_function_executions( + evaluator, + module_context, + funcdef, + string_name=string_name, + ) + if function_executions: + zipped_params = zip(*list( + function_execution.get_params() + for function_execution in function_executions + )) + params = [MergedExecutedParams(executed_params) for executed_params in zipped_params] + # Evaluate the ExecutedParams to types. + else: + return create_default_params(execution_context, funcdef) + finally: + debug.dbg('Dynamic param result finished', color='MAGENTA') return params finally: evaluator.dynamic_params_depth -= 1 @@ -98,25 +107,24 @@ def search_params(evaluator, execution_context, funcdef): @evaluator_function_cache(default=None) @to_list -def _search_function_executions(evaluator, module_context, funcdef): +def _search_function_executions(evaluator, module_context, funcdef, string_name): """ Returns a list of param names. """ - func_string_name = funcdef.name.value compare_node = funcdef - if func_string_name == '__init__': + if string_name == '__init__': cls = get_parent_scope(funcdef) if isinstance(cls, tree.Class): - func_string_name = cls.name.value + string_name = cls.name.value compare_node = cls found_executions = False i = 0 for for_mod_context in imports.get_modules_containing_name( - evaluator, [module_context], func_string_name): + evaluator, [module_context], string_name): if not isinstance(module_context, ModuleContext): return - for name, trailer in _get_possible_nodes(for_mod_context, func_string_name): + for name, trailer in _get_possible_nodes(for_mod_context, string_name): i += 1 # This is a simple way to stop Jedi's dynamic param recursion @@ -137,6 +145,18 @@ def _search_function_executions(evaluator, module_context, funcdef): return +def _get_lambda_name(node): + stmt = node.parent + if stmt.type == 'expr_stmt': + first_operator = next(stmt.yield_operators(), None) + if first_operator == '=': + first = stmt.children[0] + if first.type == 'name': + return first.value + + return None + + def _get_possible_nodes(module_context, func_string_name): try: names = module_context.tree_node.get_used_names()[func_string_name] @@ -156,11 +176,9 @@ def _check_name_for_execution(evaluator, context, compare_node, name, trailer): def create_func_excs(): arglist = trailer.children[1] if arglist == ')': - arglist = () + arglist = None args = TreeArguments(evaluator, context, arglist, trailer) - if value_node.type == 'funcdef': - yield value.get_function_execution(args) - else: + if value_node.type == 'classdef': created_instance = instance.TreeInstance( evaluator, value.parent_context, @@ -169,6 +187,8 @@ def create_func_excs(): ) for execution in created_instance.create_init_executions(): yield execution + else: + yield value.get_function_execution(args) for value in evaluator.goto_definitions(context, name): value_node = value.tree_node diff --git a/pythonFiles/jedi/evaluate/filters.py b/pythonFiles/jedi/evaluate/filters.py index 35dff9dace65..4294f2a60004 100644 --- a/pythonFiles/jedi/evaluate/filters.py +++ b/pythonFiles/jedi/evaluate/filters.py @@ -6,7 +6,8 @@ from parso.tree import search_ancestor -from jedi._compatibility import is_py3 +from jedi._compatibility import use_metaclass, Parameter +from jedi.cache import memoize_method from jedi.evaluate import flow_analysis from jedi.evaluate.base_context import ContextSet, Context from jedi.parser_utils import get_parent_scope @@ -27,7 +28,7 @@ def infer(self): def goto(self): # Typically names are already definitions and therefore a goto on that # name will always result on itself. - return set([self]) + return {self} def get_root_context(self): return self.parent_context.get_root_context() @@ -43,6 +44,9 @@ def execute(self, arguments): def execute_evaluated(self, *args, **kwargs): return self.infer().execute_evaluated(*args, **kwargs) + def is_import(self): + return False + @property def api_type(self): return self.parent_context.api_type @@ -56,6 +60,10 @@ def __init__(self, parent_context, tree_name): def goto(self): return self.parent_context.evaluator.goto(self.parent_context, self.tree_name) + def is_import(self): + imp = search_ancestor(self.tree_name, 'import_from', 'import_name') + return imp is not None + @property def string_name(self): return self.tree_name.value @@ -108,12 +116,28 @@ def api_type(self): class ParamName(AbstractTreeName): - api_type = 'param' + api_type = u'param' def __init__(self, parent_context, tree_name): self.parent_context = parent_context self.tree_name = tree_name + def get_kind(self): + tree_param = search_ancestor(self.tree_name, 'param') + if tree_param.star_count == 1: # *args + return Parameter.VAR_POSITIONAL + if tree_param.star_count == 2: # **kwargs + return Parameter.VAR_KEYWORD + + parent = tree_param.parent + for p in parent.children: + if p.type == 'param': + if p.star_count: + return Parameter.KEYWORD_ONLY + if p == tree_param: + break + return Parameter.POSITIONAL_OR_KEYWORD + def infer(self): return self.get_param().infer() @@ -163,7 +187,7 @@ def __init__(self, context, parser_scope): def get(self, name): try: - names = self._used_names[str(name)] + names = self._used_names[name] except KeyError: return [] @@ -213,7 +237,10 @@ def _is_name_reachable(self, name): def _check_flows(self, names): for name in sorted(names, key=lambda name: name.start_pos, reverse=True): check = flow_analysis.reachability_check( - self._node_context, self._parser_scope, name, self._origin_scope + context=self._node_context, + context_scope=self._parser_scope, + node=name, + origin_scope=self._origin_scope ) if check is not flow_analysis.UNREACHABLE: yield name @@ -266,22 +293,42 @@ def __init__(self, dct): def get(self, name): try: - value = self._convert(name, self._dct[str(name)]) + value = self._convert(name, self._dct[name]) except KeyError: return [] - - return list(self._filter([value])) + else: + return list(self._filter([value])) def values(self): - return self._filter(self._convert(*item) for item in self._dct.items()) + def yielder(): + for item in self._dct.items(): + try: + yield self._convert(*item) + except KeyError: + pass + return self._filter(yielder()) def _convert(self, name, value): return value +class MergedFilter(object): + def __init__(self, *filters): + self._filters = filters + + def get(self, name): + return [n for filter in self._filters for n in filter.get(name)] + + def values(self): + return [n for filter in self._filters for n in filter.values()] + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, ', '.join(str(f) for f in self._filters)) + + class _BuiltinMappedMethod(Context): """``Generator.__next__`` ``dict.values`` methods and so on.""" - api_type = 'function' + api_type = u'function' def __init__(self, builtin_context, method, builtin_func): super(_BuiltinMappedMethod, self).__init__( @@ -292,6 +339,7 @@ def __init__(self, builtin_context, method, builtin_func): self._builtin_func = builtin_func def py__call__(self, params): + # TODO add TypeError if params are given/or not correct. return self._method(self.parent_context) def __getattr__(self, name): @@ -304,21 +352,33 @@ class SpecialMethodFilter(DictFilter): classes like Generator (for __next__, etc). """ class SpecialMethodName(AbstractNameDefinition): - api_type = 'function' + api_type = u'function' + + def __init__(self, parent_context, string_name, value, builtin_context): + callable_, python_version = value + if python_version is not None and \ + python_version != parent_context.evaluator.environment.version_info.major: + raise KeyError - def __init__(self, parent_context, string_name, callable_, builtin_context): self.parent_context = parent_context self.string_name = string_name self._callable = callable_ self._builtin_context = builtin_context def infer(self): - filter = next(self._builtin_context.get_filters()) - # We can take the first index, because on builtin methods there's - # always only going to be one name. The same is true for the - # inferred values. - builtin_func = next(iter(filter.get(self.string_name)[0].infer())) - return ContextSet(_BuiltinMappedMethod(self.parent_context, self._callable, builtin_func)) + for filter in self._builtin_context.get_filters(): + # We can take the first index, because on builtin methods there's + # always only going to be one name. The same is true for the + # inferred values. + for name in filter.get(self.string_name): + builtin_func = next(iter(name.infer())) + break + else: + continue + break + return ContextSet( + _BuiltinMappedMethod(self.parent_context, self._callable, builtin_func) + ) def __init__(self, context, dct, builtin_context): super(SpecialMethodFilter, self).__init__(dct) @@ -335,34 +395,58 @@ def _convert(self, name, value): return self.SpecialMethodName(self.context, name, value, self._builtin_context) -def has_builtin_methods(cls): - base_dct = {} - # Need to care properly about inheritance. Builtin Methods should not get - # lost, just because they are not mentioned in a class. - for base_cls in reversed(cls.__bases__): - try: - base_dct.update(base_cls.builtin_methods) - except AttributeError: - pass +class _OverwriteMeta(type): + def __init__(cls, name, bases, dct): + super(_OverwriteMeta, cls).__init__(name, bases, dct) - cls.builtin_methods = base_dct - for func in cls.__dict__.values(): - try: - cls.builtin_methods.update(func.registered_builtin_methods) - except AttributeError: - pass - return cls - - -def register_builtin_method(method_name, python_version_match=None): - def wrapper(func): - if python_version_match and python_version_match != 2 + int(is_py3): - # Some functions do only apply to certain versions. - return func - dct = func.__dict__.setdefault('registered_builtin_methods', {}) - dct[method_name] = func + base_dct = {} + for base_cls in reversed(cls.__bases__): + try: + base_dct.update(base_cls.overwritten_methods) + except AttributeError: + pass + + for func in cls.__dict__.values(): + try: + base_dct.update(func.registered_overwritten_methods) + except AttributeError: + pass + cls.overwritten_methods = base_dct + + +class AbstractObjectOverwrite(use_metaclass(_OverwriteMeta, object)): + def get_object(self): + raise NotImplementedError + + def get_filters(self, search_global, *args, **kwargs): + yield SpecialMethodFilter(self, self.overwritten_methods, self.get_object()) + + for filter in self.get_object().get_filters(search_global): + yield filter + + +class BuiltinOverwrite(Context, AbstractObjectOverwrite): + special_object_identifier = None + + def __init__(self, evaluator): + super(BuiltinOverwrite, self).__init__(evaluator, evaluator.builtins_module) + + @memoize_method + def get_object(self): + from jedi.evaluate import compiled + assert self.special_object_identifier + return compiled.get_special_object(self.evaluator, self.special_object_identifier) + + def py__class__(self): + return self.get_object().py__class__() + + +def publish_method(method_name, python_version_match=None): + def decorator(func): + dct = func.__dict__.setdefault('registered_overwritten_methods', {}) + dct[method_name] = func, python_version_match return func - return wrapper + return decorator def get_global_filters(evaluator, context, until_position, origin_scope): @@ -379,40 +463,37 @@ def get_global_filters(evaluator, context, until_position, origin_scope): ... def func(): ... y = None ... ''')) - >>> module_node = script._get_module_node() + >>> module_node = script._module_node >>> scope = next(module_node.iter_funcdefs()) >>> scope >>> context = script._get_module().create_context(scope) >>> filters = list(get_global_filters(context.evaluator, context, (4, 0), None)) - First we get the names names from the function scope. + First we get the names from the function scope. - >>> no_unicode_pprint(filters[0]) - > + >>> no_unicode_pprint(filters[0]) #doctest: +ELLIPSIS + MergedFilter(, ) >>> sorted(str(n) for n in filters[0].values()) ['', ''] - >>> filters[0]._until_position + >>> filters[0]._filters[0]._until_position (4, 0) + >>> filters[0]._filters[1]._until_position Then it yields the names from one level "lower". In this example, this is - the module scope. As a side note, you can see, that the position in the - filter is now None, because typically the whole module is loaded before the - function is called. + the module scope (including globals). + As a side note, you can see, that the position in the filter is None on the + globals filter, because there the whole module is searched. - >>> filters[1].values() # global names -> there are none in our example. - [] - >>> list(filters[2].values()) # package modules -> Also empty. + >>> list(filters[1].values()) # package modules -> Also empty. [] - >>> sorted(name.string_name for name in filters[3].values()) # Module attributes + >>> sorted(name.string_name for name in filters[2].values()) # Module attributes ['__doc__', '__file__', '__name__', '__package__'] - >>> print(filters[1]._until_position) - None Finally, it yields the builtin filter, if `include_builtin` is true (default). - >>> filters[4].values() #doctest: +ELLIPSIS + >>> filters[3].values() #doctest: +ELLIPSIS [, ...] """ from jedi.evaluate.context.function import FunctionExecutionContext @@ -430,5 +511,5 @@ def get_global_filters(evaluator, context, until_position, origin_scope): context = context.parent_context # Add builtins to the global scope. - for filter in evaluator.BUILTINS.get_filters(search_global=True): + for filter in evaluator.builtins_module.get_filters(search_global=True): yield filter diff --git a/pythonFiles/jedi/evaluate/finder.py b/pythonFiles/jedi/evaluate/finder.py index 96032ae9b792..5e7043f79600 100644 --- a/pythonFiles/jedi/evaluate/finder.py +++ b/pythonFiles/jedi/evaluate/finder.py @@ -56,7 +56,10 @@ def find(self, filters, attribute_lookup): names = self.filter_name(filters) if self._found_predefined_types is not None and names: check = flow_analysis.reachability_check( - self._context, self._context.tree_node, self._name) + context=self._context, + context_scope=self._context.tree_node, + node=self._name, + ) if check is flow_analysis.UNREACHABLE: return ContextSet() return self._found_predefined_types @@ -92,7 +95,26 @@ def _get_origin_scope(self): def get_filters(self, search_global=False): origin_scope = self._get_origin_scope() if search_global: - return get_global_filters(self._evaluator, self._context, self._position, origin_scope) + position = self._position + + # For functions and classes the defaults don't belong to the + # function and get evaluated in the context before the function. So + # make sure to exclude the function/class name. + if origin_scope is not None: + ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef', 'lambdef') + lambdef = None + if ancestor == 'lambdef': + # For lambdas it's even more complicated since parts will + # be evaluated later. + lambdef = ancestor + ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef') + if ancestor is not None: + colon = ancestor.children[-2] + if position < colon.start_pos: + if lambdef is None or position < lambdef.children[-2].start_pos: + position = ancestor.start_pos + + return get_global_filters(self._evaluator, self._context, position, origin_scope) else: return self._context.get_filters(search_global, self._position, origin_scope=origin_scope) @@ -102,8 +124,7 @@ def filter_name(self, filters): ``filters``), until a name fits. """ names = [] - if self._context.predefined_names: - # TODO is this ok? node might not always be a tree.Name + if self._context.predefined_names and isinstance(self._name, tree.Name): node = self._name while node is not None and not is_scope(node): node = node.parent @@ -133,14 +154,14 @@ def filter_name(self, filters): continue break - debug.dbg('finder.filter_name "%s" in (%s): %s@%s', self._string_name, - self._context, names, self._position) + debug.dbg('finder.filter_name %s in (%s): %s@%s', + self._string_name, self._context, names, self._position) return list(names) def _check_getattr(self, inst): """Checks for both __getattr__ and __getattribute__ methods""" # str is important, because it shouldn't be `Name`! - name = compiled.create(self._evaluator, self._string_name) + name = compiled.create_simple_object(self._evaluator, self._string_name) # This is a little bit special. `__getattribute__` is in Python # executed before `__getattr__`. But: I know no use case, where @@ -149,8 +170,8 @@ def _check_getattr(self, inst): # We are inversing this, because a hand-crafted `__getattribute__` # could still call another hand-crafted `__getattr__`, but not the # other way around. - names = (inst.get_function_slot_names('__getattr__') or - inst.get_function_slot_names('__getattribute__')) + names = (inst.get_function_slot_names(u'__getattr__') or + inst.get_function_slot_names(u'__getattribute__')) return inst.execute_function_slots(names, name) def _names_to_types(self, names, attribute_lookup): @@ -248,8 +269,7 @@ def _check_isinstance_type(context, element, search_name): context_set = ContextSet() for cls_or_tup in lazy_context_cls.infer(): - if isinstance(cls_or_tup, iterable.AbstractIterable) and \ - cls_or_tup.array_type == 'tuple': + if isinstance(cls_or_tup, iterable.Sequence) and cls_or_tup.array_type == 'tuple': for lazy_context in cls_or_tup.py__iter__(): for context in lazy_context.infer(): context_set |= context.execute_evaluated() diff --git a/pythonFiles/jedi/evaluate/flow_analysis.py b/pythonFiles/jedi/evaluate/flow_analysis.py index 670b7a71934c..946f5a2ecaf0 100644 --- a/pythonFiles/jedi/evaluate/flow_analysis.py +++ b/pythonFiles/jedi/evaluate/flow_analysis.py @@ -1,4 +1,5 @@ from jedi.parser_utils import get_flow_branch_keyword, is_scope, get_parent_scope +from jedi.evaluate.recursion import execution_allowed class Status(object): @@ -104,9 +105,13 @@ def _break_check(context, context_scope, flow_scope, node): def _check_if(context, node): - types = context.eval_node(node) - values = set(x.py__bool__() for x in types) - if len(values) == 1: - return Status.lookup_table[values.pop()] - else: - return UNSURE + with execution_allowed(context.evaluator, node) as allowed: + if not allowed: + return UNSURE + + types = context.eval_node(node) + values = set(x.py__bool__() for x in types) + if len(values) == 1: + return Status.lookup_table[values.pop()] + else: + return UNSURE diff --git a/pythonFiles/jedi/evaluate/helpers.py b/pythonFiles/jedi/evaluate/helpers.py index 3b21e01bda9e..c6226cde2b70 100644 --- a/pythonFiles/jedi/evaluate/helpers.py +++ b/pythonFiles/jedi/evaluate/helpers.py @@ -176,8 +176,6 @@ def get_module_names(module, all_scopes): @contextmanager def predefine_names(context, flow_scope, dct): predefined = context.predefined_names - if flow_scope in predefined: - raise NotImplementedError('Why does this happen?') predefined[flow_scope] = dct try: yield @@ -190,12 +188,27 @@ def is_compiled(context): def is_string(context): - return is_compiled(context) and isinstance(context.obj, (str, unicode)) + if context.evaluator.environment.version_info.major == 2: + str_classes = (unicode, bytes) + else: + str_classes = (unicode,) + return is_compiled(context) and isinstance(context.get_safe_value(default=None), str_classes) def is_literal(context): return is_number(context) or is_string(context) +def _get_safe_value_or_none(context, accept): + if is_compiled(context): + value = context.get_safe_value(default=None) + if isinstance(value, accept): + return value + + +def get_int_or_none(context): + return _get_safe_value_or_none(context, int) + + def is_number(context): - return is_compiled(context) and isinstance(context.obj, (int, float)) + return _get_safe_value_or_none(context, (int, float)) is not None diff --git a/pythonFiles/jedi/evaluate/imports.py b/pythonFiles/jedi/evaluate/imports.py index ecf656b1a676..bcd3bdc74a4d 100644 --- a/pythonFiles/jedi/evaluate/imports.py +++ b/pythonFiles/jedi/evaluate/imports.py @@ -9,31 +9,48 @@ correct implementation is delegated to _compatibility. This module also supports import autocompletion, which means to complete -statements like ``from datetim`` (curser at the end would return ``datetime``). +statements like ``from datetim`` (cursor at the end would return ``datetime``). """ -import imp import os -import pkgutil -import sys from parso.python import tree from parso.tree import search_ancestor -from parso.cache import parser_cache from parso import python_bytes_to_unicode -from jedi._compatibility import find_module, unicode, ImplicitNSInfo +from jedi._compatibility import unicode, ImplicitNSInfo, force_unicode from jedi import debug from jedi import settings +from jedi.parser_utils import get_cached_code_lines from jedi.evaluate import sys_path from jedi.evaluate import helpers from jedi.evaluate import compiled from jedi.evaluate import analysis -from jedi.evaluate.utils import unite +from jedi.evaluate.utils import unite, dotted_from_fs_path from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate.filters import AbstractNameDefinition from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS +class ModuleCache(object): + def __init__(self): + self._path_cache = {} + self._name_cache = {} + + def add(self, module, name): + path = module.py__file__() + self._path_cache[path] = module + self._name_cache[name] = module + + def iterate_modules_with_names(self): + return self._name_cache.items() + + def get(self, name): + return self._name_cache[name] + + def get_from_path(self, path): + return self._path_cache[path] + + # This memoization is needed, because otherwise we will infinitely loop on # certain imports. @evaluator_method_cache(default=NO_CONTEXTS) @@ -130,20 +147,13 @@ def __repr__(self): def _add_error(context, name, message=None): # Should be a name, not a string! + if message is None: + name_str = str(name.value) if isinstance(name, tree.Name) else name + message = 'No module named ' + name_str if hasattr(name, 'parent'): analysis.add(context, 'import-error', name, message) - - -def get_init_path(directory_path): - """ - The __init__ file can be searched in a directory. If found return it, else - None. - """ - for suffix, _, _ in imp.get_suffixes(): - path = os.path.join(directory_path, '__init__' + suffix) - if os.path.exists(path): - return path - return None + else: + debug.warning('ImportError without origin: ' + message) class ImportName(AbstractNameDefinition): @@ -204,7 +214,7 @@ def __init__(self, evaluator, import_path, module_context, level=0): if level: base = module_context.py__package__().split('.') - if base == ['']: + if base == [''] or base == ['__main__']: base = [] if level > len(base): path = module_context.py__file__() @@ -226,10 +236,11 @@ def __init__(self, evaluator, import_path, module_context, level=0): else: import_path.insert(0, dir_name) else: - _add_error(module_context, import_path[-1]) + _add_error( + module_context, import_path[-1], + message='Attempted relative import beyond top-level package.' + ) import_path = [] - # TODO add import error. - debug.warning('Attempted relative import beyond top-level package.') # If no path is defined in the module we have no ideas where we # are in the file system. Therefore we cannot know what to do. # In this case we just let the path there and ignore that it's @@ -248,27 +259,19 @@ def str_import_path(self): """Returns the import path as pure strings instead of `Name`.""" return tuple( name.value if isinstance(name, tree.Name) else name - for name in self.import_path) + for name in self.import_path + ) def sys_path_with_modifications(self): - in_path = [] - sys_path_mod = self._evaluator.project.sys_path \ + sys_path_mod = self._evaluator.get_sys_path() \ + sys_path.check_sys_path_modifications(self.module_context) - if self.file_path is not None: - # If you edit e.g. gunicorn, there will be imports like this: - # `from gunicorn import something`. But gunicorn is not in the - # sys.path. Therefore look if gunicorn is a parent directory, #56. - if self.import_path: # TODO is this check really needed? - for path in sys_path.traverse_parents(self.file_path): - if os.path.basename(path) == self.str_import_path[0]: - in_path.append(os.path.dirname(path)) - - # Since we know nothing about the call location of the sys.path, - # it's a possibility that the current directory is the origin of - # the Python execution. - sys_path_mod.insert(0, os.path.dirname(self.file_path)) - - return in_path + sys_path_mod + + if self.import_path and self.file_path is not None \ + and self._evaluator.environment.version_info.major == 2: + # Python2 uses an old strange way of importing relative imports. + sys_path_mod.append(force_unicode(os.path.dirname(self.file_path))) + + return sys_path_mod def follow(self): if not self.import_path: @@ -280,7 +283,7 @@ def _do_import(self, import_path, sys_path): This method is very similar to importlib's `_gcd_import`. """ import_parts = [ - i.value if isinstance(i, tree.Name) else i + force_unicode(i.value if isinstance(i, tree.Name) else i) for i in import_path ] @@ -298,7 +301,7 @@ def _do_import(self, import_path, sys_path): module_name = '.'.join(import_parts) try: - return ContextSet(self._evaluator.modules[module_name]) + return ContextSet(self._evaluator.module_cache.get(module_name)) except KeyError: pass @@ -332,62 +335,43 @@ def _do_import(self, import_path, sys_path): for path in paths: # At the moment we are only using one path. So this is # not important to be correct. - try: - if not isinstance(path, list): - path = [path] - module_file, module_path, is_pkg = \ - find_module(import_parts[-1], path, fullname=module_name) + 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 + ) + if module_path is not None: break - except ImportError: - module_path = None - if module_path is None: + else: _add_error(self.module_context, import_path[-1]) return NO_CONTEXTS else: - parent_module = None - try: - debug.dbg('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. - sys.path, temp = sys_path, sys.path - try: - module_file, module_path, is_pkg = \ - find_module(import_parts[-1], fullname=module_name) - finally: - sys.path = temp - except ImportError: + debug.dbg('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, + ) + if module_path is None: # The module is not a package. _add_error(self.module_context, import_path[-1]) return NO_CONTEXTS - code = None - if is_pkg: - # In this case, we don't have a file yet. Search for the - # __init__ file. - if module_path.endswith(('.zip', '.egg')): - code = module_file.loader.get_source(module_name) - else: - module_path = get_init_path(module_path) - elif module_file: - code = module_file.read() - module_file.close() - - if isinstance(module_path, ImplicitNSInfo): - from jedi.evaluate.context.namespace import ImplicitNamespaceContext - fullname, paths = module_path.name, module_path.paths - module = ImplicitNamespaceContext(self._evaluator, fullname=fullname) - module.paths = paths - elif module_file is None and not module_path.endswith(('.py', '.zip', '.egg')): - module = compiled.load_module(self._evaluator, module_path) - else: - module = _load_module(self._evaluator, module_path, code, sys_path, parent_module) + module = _load_module( + self._evaluator, module_path, code, sys_path, + module_name=module_name, + safe_module_name=True, + ) if module is None: # The file might raise an ImportError e.g. and therefore not be # importable. return NO_CONTEXTS - self._evaluator.modules[module_name] = module return ContextSet(module) def _generate_name(self, name, in_module=None): @@ -401,15 +385,17 @@ def _get_module_names(self, search_path=None, in_module=None): Get the names of all modules in the search_path. This means file names and not names defined in the files. """ + sub = self._evaluator.compiled_subprocess names = [] # add builtin module names if search_path is None and in_module is None: - names += [self._generate_name(name) for name in sys.builtin_module_names] + names += [self._generate_name(name) for name in sub.get_builtin_module_names()] if search_path is None: search_path = self.sys_path_with_modifications() - for module_loader, name, is_pkg in pkgutil.iter_modules(search_path): + + for name in sub.list_module_names(search_path): names.append(self._generate_name(name, in_module=in_module)) return names @@ -448,7 +434,7 @@ def completion_names(self, evaluator, only_modules=False): # implicit namespace packages elif isinstance(context, ImplicitNamespaceContext): paths = context.paths - names += self._get_module_names(paths) + names += self._get_module_names(paths, in_module=context) if only_modules: # In the case of an import like `from x.` we don't need to @@ -476,38 +462,65 @@ def completion_names(self, evaluator, only_modules=False): return names -def _load_module(evaluator, path=None, code=None, sys_path=None, parent_module=None): - if sys_path is None: - sys_path = evaluator.project.sys_path +def _load_module(evaluator, path=None, code=None, sys_path=None, + module_name=None, safe_module_name=False): + try: + return evaluator.module_cache.get(module_name) + except KeyError: + pass + try: + return evaluator.module_cache.get_from_path(path) + except KeyError: + pass - dotted_path = path and compiled.dotted_from_fs_path(path, sys_path) - if path is not None and path.endswith(('.py', '.zip', '.egg')) \ - and dotted_path not in settings.auto_import_modules: + if isinstance(path, ImplicitNSInfo): + from jedi.evaluate.context.namespace import ImplicitNamespaceContext + module = ImplicitNamespaceContext( + evaluator, + fullname=path.name, + paths=path.paths, + ) + else: + if sys_path is None: + sys_path = evaluator.get_sys_path() + + dotted_path = path and dotted_from_fs_path(path, sys_path) + if path is not None and path.endswith(('.py', '.zip', '.egg')) \ + and dotted_path not in settings.auto_import_modules: + + module_node = evaluator.parse( + code=code, path=path, cache=True, diff_cache=True, + cache_path=settings.cache_directory) + + from jedi.evaluate.context import ModuleContext + module = ModuleContext( + evaluator, module_node, + path=path, + code_lines=get_cached_code_lines(evaluator.grammar, path), + ) + else: + module = compiled.load_module(evaluator, path=path, sys_path=sys_path) - module_node = evaluator.grammar.parse( - code=code, path=path, cache=True, diff_cache=True, - cache_path=settings.cache_directory) + if module is not None and module_name is not None: + add_module_to_cache(evaluator, module_name, module, safe=safe_module_name) - from jedi.evaluate.context import ModuleContext - return ModuleContext(evaluator, module_node, path=path) - else: - return compiled.load_module(evaluator, path) + return module -def add_module(evaluator, module_name, module): - if '.' not in module_name: +def add_module_to_cache(evaluator, module_name, module, safe=False): + if not safe and '.' not in module_name: # We cannot add paths with dots, because that would collide with # the sepatator dots for nested packages. Therefore we return # `__main__` in ModuleWrapper.py__name__(), which is similar to # Python behavior. - evaluator.modules[module_name] = module + return + evaluator.module_cache.add(module, module_name) def get_modules_containing_name(evaluator, modules, name): """ Search a name in the directories of modules. """ - from jedi.evaluate.context import ModuleContext def check_directories(paths): for p in paths: if p is not None: @@ -519,28 +532,16 @@ def check_directories(paths): if file_name.endswith('.py'): yield path - def check_python_file(path): - try: - # TODO I don't think we should use the cache here?! - node_cache_item = parser_cache[evaluator.grammar._hashed][path] - except KeyError: - try: - return check_fs(path) - except IOError: - return None - else: - module_node = node_cache_item.node - return ModuleContext(evaluator, module_node, path=path) - def check_fs(path): with open(path, 'rb') as f: code = python_bytes_to_unicode(f.read(), errors='replace') if name in code: - module = _load_module(evaluator, path, code) - - module_name = sys_path.dotted_path_in_sys_path(evaluator.project.sys_path, path) - if module_name is not None: - add_module(evaluator, module_name, module) + e_sys_path = evaluator.get_sys_path() + module_name = sys_path.dotted_path_in_sys_path(e_sys_path, path) + module = _load_module( + evaluator, path, code, + sys_path=e_sys_path, module_name=module_name + ) return module # skip non python modules @@ -565,6 +566,6 @@ def check_fs(path): # Sort here to make issues less random. for p in sorted(paths): # make testing easier, sort it - same results on every interpreter - m = check_python_file(p) + m = check_fs(p) if m is not None and not isinstance(m, compiled.CompiledObject): yield m diff --git a/pythonFiles/jedi/evaluate/param.py b/pythonFiles/jedi/evaluate/param.py index a46394ce8a81..1445ef0c8ff6 100644 --- a/pythonFiles/jedi/evaluate/param.py +++ b/pythonFiles/jedi/evaluate/param.py @@ -97,7 +97,7 @@ def get_params(execution_context, var_args): var_arg_iterator.push_back((key, argument)) break lazy_context_list.append(argument) - seq = iterable.FakeSequence(execution_context.evaluator, 'tuple', lazy_context_list) + seq = iterable.FakeSequence(execution_context.evaluator, u'tuple', lazy_context_list) result_arg = LazyKnownContext(seq) elif param.star_count == 2: # **kwargs param @@ -176,7 +176,7 @@ def _error_argument_count(funcdef, actual_count): def _create_default_param(execution_context, param): if param.star_count == 1: result_arg = LazyKnownContext( - iterable.FakeSequence(execution_context.evaluator, 'tuple', []) + iterable.FakeSequence(execution_context.evaluator, u'tuple', []) ) elif param.star_count == 2: result_arg = LazyKnownContext( @@ -192,4 +192,3 @@ def _create_default_param(execution_context, param): def create_default_params(execution_context, funcdef): return [_create_default_param(execution_context, p) for p in funcdef.get_params()] - diff --git a/pythonFiles/jedi/evaluate/pep0484.py b/pythonFiles/jedi/evaluate/pep0484.py index 820f112c54e0..f23943e1a8a5 100644 --- a/pythonFiles/jedi/evaluate/pep0484.py +++ b/pythonFiles/jedi/evaluate/pep0484.py @@ -22,16 +22,17 @@ import os import re -from parso import ParserSyntaxError +from parso import ParserSyntaxError, parse, split_lines from parso.python import tree +from jedi._compatibility import unicode, force_unicode from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate import compiled from jedi.evaluate.base_context import NO_CONTEXTS, ContextSet from jedi.evaluate.lazy_context import LazyTreeContext from jedi.evaluate.context import ModuleContext +from jedi.evaluate.helpers import is_string from jedi import debug -from jedi import _compatibility from jedi import parser_utils @@ -41,17 +42,23 @@ def _evaluate_for_annotation(context, annotation, index=None): If index is not None, the annotation is expected to be a tuple and we're interested in that index """ - if annotation is not None: - context_set = context.eval_node(_fix_forward_reference(context, annotation)) - if index is not None: - context_set = context_set.filter( - lambda context: context.array_type == 'tuple' \ - and len(list(context.py__iter__())) >= index - ).py__getitem__(index) - return context_set.execute_evaluated() - else: + context_set = context.eval_node(_fix_forward_reference(context, annotation)) + return context_set.execute_evaluated() + + +def _evaluate_annotation_string(context, string, index=None): + node = _get_forward_reference_node(context, string) + if node is None: return NO_CONTEXTS + context_set = context.eval_node(node) + if index is not None: + context_set = context_set.filter( + lambda context: context.array_type == u'tuple' + and len(list(context.py__iter__())) >= index + ).py__getitem__(index) + return context_set.execute_evaluated() + def _fix_forward_reference(context, node): evaled_nodes = context.eval_node(node) @@ -59,30 +66,111 @@ def _fix_forward_reference(context, node): debug.warning("Eval'ed typing index %s should lead to 1 object, " " not %s" % (node, evaled_nodes)) return node - evaled_node = list(evaled_nodes)[0] - if isinstance(evaled_node, compiled.CompiledObject) and \ - isinstance(evaled_node.obj, str): - try: - new_node = context.evaluator.grammar.parse( - _compatibility.unicode(evaled_node.obj), - start_symbol='eval_input', - error_recovery=False - ) - except ParserSyntaxError: - debug.warning('Annotation not parsed: %s' % evaled_node.obj) - return node - else: - module = node.get_root_node() - parser_utils.move(new_node, module.end_pos[0]) - new_node.parent = context.tree_node - return new_node + + evaled_context = list(evaled_nodes)[0] + if is_string(evaled_context): + result = _get_forward_reference_node(context, evaled_context.get_safe_value()) + if result is not None: + return result + + return node + + +def _get_forward_reference_node(context, string): + try: + new_node = context.evaluator.grammar.parse( + force_unicode(string), + start_symbol='eval_input', + error_recovery=False + ) + except ParserSyntaxError: + debug.warning('Annotation not parsed: %s' % string) + return None else: - return node + module = context.tree_node.get_root_node() + parser_utils.move(new_node, module.end_pos[0]) + new_node.parent = context.tree_node + return new_node + + +def _split_comment_param_declaration(decl_text): + """ + Split decl_text on commas, but group generic expressions + together. + + For example, given "foo, Bar[baz, biz]" we return + ['foo', 'Bar[baz, biz]']. + + """ + try: + node = parse(decl_text, error_recovery=False).children[0] + except ParserSyntaxError: + debug.warning('Comment annotation is not valid Python: %s' % decl_text) + return [] + + if node.type == 'name': + return [node.get_code().strip()] + + params = [] + try: + children = node.children + except AttributeError: + return [] + else: + for child in children: + if child.type in ['name', 'atom_expr', 'power']: + params.append(child.get_code().strip()) + + return params @evaluator_method_cache() def infer_param(execution_context, param): + """ + Infers the type of a function parameter, using type annotations. + """ annotation = param.annotation + if annotation is None: + # If no Python 3-style annotation, look for a Python 2-style comment + # annotation. + # Identify parameters to function in the same sequence as they would + # appear in a type comment. + all_params = [child for child in param.parent.children + if child.type == 'param'] + + node = param.parent.parent + comment = parser_utils.get_following_comment_same_line(node) + if comment is None: + return NO_CONTEXTS + + match = re.match(r"^#\s*type:\s*\(([^#]*)\)\s*->", comment) + if not match: + return NO_CONTEXTS + params_comments = _split_comment_param_declaration(match.group(1)) + + # Find the specific param being investigated + index = all_params.index(param) + # If the number of parameters doesn't match length of type comment, + # ignore first parameter (assume it's self). + if len(params_comments) != len(all_params): + debug.warning( + "Comments length != Params length %s %s", + params_comments, all_params + ) + from jedi.evaluate.context.instance import BaseInstanceFunctionExecution + if isinstance(execution_context, BaseInstanceFunctionExecution): + if index == 0: + # Assume it's self, which is already handled + return NO_CONTEXTS + index -= 1 + if index >= len(params_comments): + return NO_CONTEXTS + + param_comment = params_comments[index] + return _evaluate_annotation_string( + execution_context.get_root_context(), + param_comment + ) module_context = execution_context.get_root_context() return _evaluate_for_annotation(module_context, annotation) @@ -102,12 +190,33 @@ def py__annotations__(funcdef): @evaluator_method_cache() def infer_return_types(function_context): + """ + Infers the type of a function's return value, + according to type annotations. + """ annotation = py__annotations__(function_context.tree_node).get("return", None) + if annotation is None: + # If there is no Python 3-type annotation, look for a Python 2-type annotation + node = function_context.tree_node + comment = parser_utils.get_following_comment_same_line(node) + if comment is None: + return NO_CONTEXTS + + match = re.match(r"^#\s*type:\s*\([^#]*\)\s*->\s*([^#]*)", comment) + if not match: + return NO_CONTEXTS + + return _evaluate_annotation_string( + function_context.get_root_context(), + match.group(1).strip() + ) + module_context = function_context.get_root_context() return _evaluate_for_annotation(module_context, annotation) _typing_module = None +_typing_module_code_lines = None def _get_typing_replacement_module(grammar): @@ -115,14 +224,15 @@ def _get_typing_replacement_module(grammar): The idea is to return our jedi replacement for the PEP-0484 typing module as discussed at https://github.com/davidhalter/jedi/issues/663 """ - global _typing_module + global _typing_module, _typing_module_code_lines if _typing_module is None: typing_path = \ os.path.abspath(os.path.join(__file__, "../jedi_typing.py")) with open(typing_path) as f: - code = _compatibility.unicode(f.read()) + code = unicode(f.read()) _typing_module = grammar.parse(code) - return _typing_module + _typing_module_code_lines = split_lines(code, keepends=True) + return _typing_module, _typing_module_code_lines def py__getitem__(context, typ, node): @@ -152,10 +262,12 @@ def py__getitem__(context, typ, node): # check for the instance typing._Optional (Python 3.6). return context.eval_node(nodes[0]) + module_node, code_lines = _get_typing_replacement_module(context.evaluator.latest_grammar) typing = ModuleContext( context.evaluator, - module_node=_get_typing_replacement_module(context.evaluator.latest_grammar), - path=None + module_node=module_node, + path=None, + code_lines=code_lines, ) factories = typing.py__getattribute__("factory") assert len(factories) == 1 @@ -167,12 +279,12 @@ def py__getitem__(context, typ, node): if isinstance(child, tree.Class)) if type_name not in valid_classnames: return None - compiled_classname = compiled.create(context.evaluator, type_name) + compiled_classname = compiled.create_simple_object(context.evaluator, type_name) from jedi.evaluate.context.iterable import FakeSequence args = FakeSequence( context.evaluator, - "tuple", + u'tuple', [LazyTreeContext(context, n) for n in nodes] ) @@ -213,10 +325,6 @@ def _find_type_from_comment_hint(context, node, varlist, name): if comment is None: return [] match = re.match(r"^#\s*type:\s*([^#]*)", comment) - if not match: + if match is None: return [] - annotation = tree.String( - repr(str(match.group(1).strip())), - node.start_pos) - annotation.parent = node.parent - return _evaluate_for_annotation(context, annotation, index) + return _evaluate_annotation_string(context, match.group(1).strip(), index) diff --git a/pythonFiles/jedi/evaluate/project.py b/pythonFiles/jedi/evaluate/project.py deleted file mode 100644 index b90f0f0c3be8..000000000000 --- a/pythonFiles/jedi/evaluate/project.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import sys - -from jedi.evaluate.sys_path import get_venv_path, detect_additional_paths -from jedi.cache import underscore_memoization - - -class Project(object): - def __init__(self, sys_path=None): - if sys_path is not None: - self._sys_path = sys_path - - venv = os.getenv('VIRTUAL_ENV') - if venv: - sys_path = get_venv_path(venv) - - if sys_path is None: - sys_path = sys.path - - base_sys_path = list(sys_path) - try: - base_sys_path.remove('') - except ValueError: - pass - - self._base_sys_path = base_sys_path - - def add_script_path(self, script_path): - self._script_path = script_path - - def add_evaluator(self, evaluator): - self._evaluator = evaluator - - @property - @underscore_memoization - def sys_path(self): - if self._script_path is None: - return self._base_sys_path - - return self._base_sys_path + detect_additional_paths(self._evaluator, self._script_path) diff --git a/pythonFiles/jedi/evaluate/recursion.py b/pythonFiles/jedi/evaluate/recursion.py index e2f34a4a9a06..5be3f8be4d29 100644 --- a/pythonFiles/jedi/evaluate/recursion.py +++ b/pythonFiles/jedi/evaluate/recursion.py @@ -49,6 +49,7 @@ A function may not be executed more than this number of times recursively. """ + class RecursionDetector(object): def __init__(self): self.pushed_nodes = [] @@ -67,9 +68,11 @@ def execution_allowed(evaluator, node): node.start_pos) yield False else: - pushed_nodes.append(node) - yield True - pushed_nodes.pop() + try: + pushed_nodes.append(node) + yield True + finally: + pushed_nodes.pop() def execution_recursion_decorator(default=NO_CONTEXTS): @@ -113,7 +116,7 @@ def push_execution(self, execution): self._parent_execution_funcs.append(funcdef) module = execution.get_root_context() - if module == self._evaluator.BUILTINS: + if module == self._evaluator.builtins_module: # We have control over builtins so we know they are not recursing # like crazy. Therefore we just let them execute always, because # they usually just help a lot with getting good results. diff --git a/pythonFiles/jedi/evaluate/site.py b/pythonFiles/jedi/evaluate/site.py deleted file mode 100644 index bf884faefaaf..000000000000 --- a/pythonFiles/jedi/evaluate/site.py +++ /dev/null @@ -1,110 +0,0 @@ -"""An adapted copy of relevant site-packages functionality from Python stdlib. - -This file contains some functions related to handling site-packages in Python -with jedi-specific modifications: - -- the functions operate on sys_path argument rather than global sys.path - -- in .pth files "import ..." lines that allow execution of arbitrary code are - skipped to prevent code injection into jedi interpreter - -""" - -# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -# 2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved - -from __future__ import print_function - -import sys -import os - - -def makepath(*paths): - dir = os.path.join(*paths) - try: - dir = os.path.abspath(dir) - except OSError: - pass - return dir, os.path.normcase(dir) - - -def _init_pathinfo(sys_path): - """Return a set containing all existing directory entries from sys_path""" - d = set() - for dir in sys_path: - try: - if os.path.isdir(dir): - dir, dircase = makepath(dir) - d.add(dircase) - except TypeError: - continue - return d - - -def addpackage(sys_path, sitedir, name, known_paths): - """Process a .pth file within the site-packages directory: - For each line in the file, either combine it with sitedir to a path - and add that to known_paths, or execute it if it starts with 'import '. - """ - if known_paths is None: - known_paths = _init_pathinfo(sys_path) - reset = 1 - else: - reset = 0 - fullname = os.path.join(sitedir, name) - try: - f = open(fullname, "r") - except OSError: - return - with f: - for n, line in enumerate(f): - if line.startswith("#"): - continue - try: - if line.startswith(("import ", "import\t")): - # Change by immerrr: don't evaluate import lines to prevent - # code injection into jedi through pth files. - # - # exec(line) - continue - line = line.rstrip() - dir, dircase = makepath(sitedir, line) - if not dircase in known_paths and os.path.exists(dir): - sys_path.append(dir) - known_paths.add(dircase) - except Exception: - print("Error processing line {:d} of {}:\n".format(n+1, fullname), - file=sys.stderr) - import traceback - for record in traceback.format_exception(*sys.exc_info()): - for line in record.splitlines(): - print(' '+line, file=sys.stderr) - print("\nRemainder of file ignored", file=sys.stderr) - break - if reset: - known_paths = None - return known_paths - - -def addsitedir(sys_path, sitedir, known_paths=None): - """Add 'sitedir' argument to sys_path if missing and handle .pth files in - 'sitedir'""" - if known_paths is None: - known_paths = _init_pathinfo(sys_path) - reset = 1 - else: - reset = 0 - sitedir, sitedircase = makepath(sitedir) - if not sitedircase in known_paths: - sys_path.append(sitedir) # Add path component - known_paths.add(sitedircase) - try: - names = os.listdir(sitedir) - except OSError: - return - names = [name for name in names if name.endswith(".pth")] - for name in sorted(names): - addpackage(sys_path, sitedir, name, known_paths) - if reset: - known_paths = None - return known_paths diff --git a/pythonFiles/jedi/evaluate/stdlib.py b/pythonFiles/jedi/evaluate/stdlib.py index 259d692733db..06296d938183 100644 --- a/pythonFiles/jedi/evaluate/stdlib.py +++ b/pythonFiles/jedi/evaluate/stdlib.py @@ -9,9 +9,11 @@ the standard library. The usual way to understand the standard library is the compiled module that returns the types for C-builtins. """ -import collections import re +import parso + +from jedi._compatibility import force_unicode from jedi import debug from jedi.evaluate.arguments import ValuesArguments from jedi.evaluate import analysis @@ -57,7 +59,7 @@ def execute(evaluator, obj, arguments): except AttributeError: pass else: - if obj.parent_context == evaluator.BUILTINS: + if obj.parent_context == evaluator.builtins_module: module_name = 'builtins' elif isinstance(obj.parent_context, ModuleContext): module_name = obj.parent_context.name.string_name @@ -107,10 +109,11 @@ def argument_clinic(string, want_obj=False, want_context=False, want_arguments=F def f(func): def wrapper(evaluator, obj, arguments): debug.dbg('builtin start %s' % obj, color='MAGENTA') + result = NO_CONTEXTS try: lst = list(arguments.eval_argument_clinic(clinic_args)) except ValueError: - return NO_CONTEXTS + pass else: kwargs = {} if want_context: @@ -119,9 +122,10 @@ def wrapper(evaluator, obj, arguments): kwargs['obj'] = obj if want_arguments: kwargs['arguments'] = arguments - return func(evaluator, *lst, **kwargs) + result = func(evaluator, *lst, **kwargs) finally: - debug.dbg('builtin end', color='MAGENTA') + debug.dbg('builtin end: %s', result, color='MAGENTA') + return result return wrapper return f @@ -133,7 +137,7 @@ def builtins_next(evaluator, iterators, defaults): TODO this function is currently not used. It's a stab at implementing next in a different way than fake objects. This would be a bit more flexible. """ - if evaluator.python_version[0] == 2: + if evaluator.environment.version_info.major == 2: name = 'next' else: name = '__next__' @@ -157,7 +161,7 @@ def builtins_getattr(evaluator, objects, names, defaults=None): for obj in objects: for name in names: if is_string(name): - return obj.py__getattribute__(name.obj) + return obj.py__getattribute__(force_unicode(name.get_safe_value())) else: debug.warning('getattr called without str') continue @@ -207,22 +211,24 @@ def builtins_reversed(evaluator, sequences, obj, arguments): # necessary, because `reversed` is a function and autocompletion # would fail in certain cases like `reversed(x).__iter__` if we # just returned the result directly. - seq = iterable.FakeSequence(evaluator, 'list', rev) + seq = iterable.FakeSequence(evaluator, u'list', rev) arguments = ValuesArguments([ContextSet(seq)]) - return ContextSet(CompiledInstance(evaluator, evaluator.BUILTINS, obj, arguments)) + return ContextSet(CompiledInstance(evaluator, evaluator.builtins_module, obj, arguments)) @argument_clinic('obj, type, /', want_arguments=True) def builtins_isinstance(evaluator, objects, types, arguments): bool_results = set() for o in objects: + cls = o.py__class__() try: - mro_func = o.py__class__().py__mro__ + mro_func = cls.py__mro__ except AttributeError: # This is temporary. Everything should have a class attribute in # Python?! Maybe we'll leave it here, because some numpy objects or # whatever might not. - return ContextSet(compiled.create(True), compiled.create(False)) + bool_results = set([True, False]) + break mro = mro_func() @@ -230,7 +236,7 @@ def builtins_isinstance(evaluator, objects, types, arguments): if cls_or_tup.is_class(): bool_results.add(cls_or_tup in mro) elif cls_or_tup.name.string_name == 'tuple' \ - and cls_or_tup.get_root_context() == evaluator.BUILTINS: + and cls_or_tup.get_root_context() == evaluator.builtins_module: # Check for tuples. classes = ContextSet.from_sets( lazy_context.infer() @@ -246,7 +252,10 @@ def builtins_isinstance(evaluator, objects, types, arguments): 'not %s.' % cls_or_tup analysis.add(lazy_context._context, 'type-error-isinstance', node, message) - return ContextSet.from_iterable(compiled.create(evaluator, x) for x in bool_results) + return ContextSet.from_iterable( + compiled.builtin_from_name(evaluator, force_unicode(str(b))) + for b in bool_results + ) def collections_namedtuple(evaluator, obj, arguments): @@ -256,45 +265,54 @@ def collections_namedtuple(evaluator, obj, arguments): 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'): + collections_context = obj.parent_context + _class_template_set = collections_context.py__getattribute__(u'_class_template') + if not _class_template_set: + # Namedtuples are not supported on Python 2.6, early 2.7, because the + # _class_template variable is not defined, there. return NO_CONTEXTS # Process arguments # TODO here we only use one of the types, we should use all. - name = list(_follow_param(evaluator, arguments, 0))[0].obj + # TODO this is buggy, doesn't need to be a string + name = list(_follow_param(evaluator, arguments, 0))[0].get_safe_value() _fields = list(_follow_param(evaluator, arguments, 1))[0] if isinstance(_fields, compiled.CompiledObject): - fields = _fields.obj.replace(',', ' ').split() - elif isinstance(_fields, iterable.AbstractIterable): + fields = _fields.get_safe_value().replace(',', ' ').split() + elif isinstance(_fields, iterable.Sequence): fields = [ - v.obj + v.get_safe_value() for lazy_context in _fields.py__iter__() - for v in lazy_context.infer() if hasattr(v, 'obj') + for v in lazy_context.infer() if is_string(v) ] else: return NO_CONTEXTS - base = collections._class_template + def get_var(name): + x, = collections_context.py__getattribute__(name) + return x.get_safe_value() + + base = next(iter(_class_template_set)).get_safe_value() base += _NAMEDTUPLE_INIT - # Build source - source = base.format( + # Build source code + code = base.format( typename=name, field_names=tuple(fields), num_fields=len(fields), - arg_list = repr(tuple(fields)).replace("'", "")[1:-1], - 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) + arg_list=repr(tuple(fields)).replace("u'", "").replace("'", "")[1:-1], + repr_fmt=', '.join(get_var(u'_repr_template').format(name=name) for name in fields), + field_defs='\n'.join(get_var(u'_field_template').format(index=index, name=name) for index, name in enumerate(fields)) ) - # Parse source - module = evaluator.grammar.parse(source) + # Parse source code + module = evaluator.grammar.parse(code) generated_class = next(module.iter_classdefs()) - parent_context = ModuleContext(evaluator, module, '') + parent_context = ModuleContext( + evaluator, module, None, + code_lines=parso.split_lines(code, keepends=True), + ) return ContextSet(ClassContext(evaluator, parent_context, generated_class)) diff --git a/pythonFiles/jedi/evaluate/syntax_tree.py b/pythonFiles/jedi/evaluate/syntax_tree.py index 1d847a4960de..4efe845d476d 100644 --- a/pythonFiles/jedi/evaluate/syntax_tree.py +++ b/pythonFiles/jedi/evaluate/syntax_tree.py @@ -2,10 +2,10 @@ Functions evaluating the syntax tree. """ import copy -import operator as op from parso.python import tree +from jedi._compatibility import force_unicode, unicode from jedi import debug from jedi import parser_utils from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS, ContextualizedNode, \ @@ -17,11 +17,13 @@ from jedi.evaluate import analysis from jedi.evaluate import imports from jedi.evaluate import arguments +from jedi.evaluate.pep0484 import _evaluate_for_annotation from jedi.evaluate.context import ClassContext, FunctionContext from jedi.evaluate.context import iterable from jedi.evaluate.context import TreeInstance, CompiledInstance from jedi.evaluate.finder import NameFinder from jedi.evaluate.helpers import is_string, is_literal, is_number, is_compiled +from jedi.evaluate.compiled.access import COMPARISON_OPERATORS def _limit_context_infers(func): @@ -48,13 +50,25 @@ def wrapper(context, *args, **kwargs): return wrapper +def _py__stop_iteration_returns(generators): + results = ContextSet() + for generator in generators: + try: + method = generator.py__stop_iteration_returns + except AttributeError: + debug.warning('%s is not actually a generator', generator) + else: + results |= method() + return results + + @debug.increase_indent @_limit_context_infers def eval_node(context, element): - debug.dbg('eval_element %s@%s', element, element.start_pos) + debug.dbg('eval_node %s@%s', element, element.start_pos) evaluator = context.evaluator typ = element.type - if typ in ('name', 'number', 'string', 'atom'): + if typ in ('name', 'number', 'string', 'atom', 'strings'): return eval_atom(context, element) elif typ == 'keyword': # For False/True/None @@ -68,22 +82,33 @@ def eval_node(context, element): return eval_expr_stmt(context, element) elif typ in ('power', 'atom_expr'): first_child = element.children[0] - if not (first_child.type == 'keyword' and first_child.value == 'await'): - context_set = eval_atom(context, first_child) - for trailer in element.children[1:]: - if trailer == '**': # has a power operation. - right = evaluator.eval_element(context, element.children[2]) - context_set = _eval_comparison( - evaluator, - context, - context_set, - trailer, - right - ) - break - context_set = eval_trailer(context, context_set, trailer) - return context_set - return NO_CONTEXTS + children = element.children[1:] + had_await = False + if first_child.type == 'keyword' and first_child.value == 'await': + had_await = True + first_child = children.pop(0) + + context_set = eval_atom(context, first_child) + for trailer in children: + if trailer == '**': # has a power operation. + right = context.eval_node(children[1]) + context_set = _eval_comparison( + evaluator, + context, + context_set, + trailer, + right + ) + break + context_set = eval_trailer(context, context_set, trailer) + + if had_await: + await_context_set = context_set.py__getattribute__(u"__await__") + if not await_context_set: + debug.warning('Tried to run py__await__ on context %s', context) + context_set = ContextSet() + return _py__stop_iteration_returns(await_context_set.execute_evaluated()) + return context_set elif typ in ('testlist_star_expr', 'testlist',): # The implicit tuple in statements. return ContextSet(iterable.SequenceLiteralContext(evaluator, context, element)) @@ -100,8 +125,10 @@ def eval_node(context, element): # Must be an ellipsis, other operators are not evaluated. # In Python 2 ellipsis is coded as three single dot tokens, not # as one token 3 dot token. - assert element.value in ('.', '...') - return ContextSet(compiled.create(evaluator, Ellipsis)) + if element.value not in ('.', '...'): + origin = element.parent + raise AssertionError("unhandled operator %s in %s " % (repr(element.value), origin)) + return ContextSet(compiled.builtin_from_name(evaluator, u'Ellipsis')) elif typ == 'dotted_name': context_set = eval_atom(context, element.children[0]) for next_name in element.children[2::2]: @@ -112,6 +139,15 @@ def eval_node(context, element): return eval_node(context, element.children[0]) elif typ == 'annassign': return pep0484._evaluate_for_annotation(context, element.children[1]) + elif typ == 'yield_expr': + if len(element.children) and element.children[1].type == 'yield_arg': + # Implies that it's a yield from. + element = element.children[1].children[1] + generators = context.eval_node(element) + return _py__stop_iteration_returns(generators) + + # Generator.send() is not implemented. + return NO_CONTEXTS else: return eval_or_test(context, element) @@ -119,7 +155,7 @@ def eval_node(context, element): def eval_trailer(context, base_contexts, trailer): trailer_op, node = trailer.children[:2] if node == ')': # `arglist` is optional. - node = () + node = None if trailer_op == '[': trailer_op, node, _ = trailer.children @@ -148,7 +184,7 @@ def eval_trailer(context, base_contexts, trailer): name_or_str=node ) else: - assert trailer_op == '(' + assert trailer_op == '(', 'trailer_op is actually %s' % trailer_op args = arguments.TreeArguments(context.evaluator, context, node, trailer) return base_contexts.execute(args) @@ -173,19 +209,19 @@ def eval_atom(context, atom): ) elif isinstance(atom, tree.Literal): - string = parser_utils.safe_literal_eval(atom.value) - return ContextSet(compiled.create(context.evaluator, string)) + string = context.evaluator.compiled_subprocess.safe_literal_eval(atom.value) + return ContextSet(compiled.create_simple_object(context.evaluator, string)) + elif atom.type == 'strings': + # Will be multiple string. + context_set = eval_atom(context, atom.children[0]) + for string in atom.children[1:]: + right = eval_atom(context, string) + context_set = _eval_comparison(context.evaluator, context, context_set, u'+', right) + return context_set else: c = atom.children - if c[0].type == 'string': - # Will be one string. - context_set = eval_atom(context, c[0]) - for string in c[1:]: - right = eval_atom(context, string) - context_set = _eval_comparison(context.evaluator, context, context_set, '+', right) - return context_set # Parentheses without commas are not tuples. - elif c[0] == '(' and not len(c) == 2 \ + if c[0] == '(' and not len(c) == 2 \ and not(c[1].type == 'testlist_comp' and len(c[1].children) > 1): return context.eval_node(c[1]) @@ -203,7 +239,9 @@ def eval_atom(context, atom): pass if comp_for.type == 'comp_for': - return ContextSet(iterable.Comprehension.from_atom(context.evaluator, context, atom)) + return ContextSet(iterable.comprehension_from_atom( + context.evaluator, context, atom + )) # It's a dict/list/tuple literal. array_node = c[1] @@ -221,7 +259,21 @@ def eval_atom(context, atom): @_limit_context_infers def eval_expr_stmt(context, stmt, seek_name=None): with recursion.execution_allowed(context.evaluator, stmt) as allowed: - if allowed or context.get_root_context() == context.evaluator.BUILTINS: + # Here we allow list/set to recurse under certain conditions. To make + # it possible to resolve stuff like list(set(list(x))), this is + # necessary. + if not allowed and context.get_root_context() == context.evaluator.builtins_module: + try: + instance = context.instance + except AttributeError: + pass + else: + if instance.name.string_name in ('list', 'set'): + c = instance.get_first_non_keyword_argument_contexts() + if instance not in c: + allowed = True + + if allowed: return _eval_expr_stmt(context, stmt, seek_name) return NO_CONTEXTS @@ -286,16 +338,16 @@ def eval_or_test(context, or_test): # handle lazy evaluation of and/or here. if operator in ('and', 'or'): left_bools = set(left.py__bool__() for left in types) - if left_bools == set([True]): + if left_bools == {True}: if operator == 'and': types = context.eval_node(right) - elif left_bools == set([False]): + elif left_bools == {False}: if operator != 'and': types = context.eval_node(right) # Otherwise continue, because of uncertainty. else: types = _eval_comparison(context.evaluator, context, types, operator, - context.eval_node(right)) + context.eval_node(right)) debug.dbg('eval_or_test types %s', types) return types @@ -308,29 +360,16 @@ def eval_factor(context_set, operator): for context in context_set: if operator == '-': if is_number(context): - yield compiled.create(context.evaluator, -context.obj) + yield context.negate() elif operator == 'not': value = context.py__bool__() if value is None: # Uncertainty. return - yield compiled.create(context.evaluator, not value) + yield compiled.create_simple_object(context.evaluator, not value) else: yield context -# Maps Python syntax to the operator module. -COMPARISON_OPERATORS = { - '==': op.eq, - '!=': op.ne, - 'is': op.is_, - 'is not': op.is_not, - '<': op.lt, - '<=': op.le, - '>': op.gt, - '>=': op.ge, -} - - def _literals_to_types(evaluator, result): # Changes literals ('a', 1, 1.0, etc) to its type instances (str(), # int(), float(), etc). @@ -366,49 +405,59 @@ def _eval_comparison(evaluator, context, left_contexts, operator, right_contexts def _is_tuple(context): - return isinstance(context, iterable.AbstractIterable) and context.array_type == 'tuple' + return isinstance(context, iterable.Sequence) and context.array_type == 'tuple' def _is_list(context): - return isinstance(context, iterable.AbstractIterable) and context.array_type == 'list' + return isinstance(context, iterable.Sequence) and context.array_type == 'list' + + +def _bool_to_context(evaluator, bool_): + return compiled.builtin_from_name(evaluator, force_unicode(str(bool_))) def _eval_comparison_part(evaluator, context, left, operator, right): l_is_num = is_number(left) r_is_num = is_number(right) - if operator == '*': + if isinstance(operator, unicode): + str_operator = operator + else: + str_operator = force_unicode(str(operator.value)) + + if str_operator == '*': # for iterables, ignore * operations - if isinstance(left, iterable.AbstractIterable) or is_string(left): + if isinstance(left, iterable.Sequence) or is_string(left): return ContextSet(left) - elif isinstance(right, iterable.AbstractIterable) or is_string(right): + elif isinstance(right, iterable.Sequence) or is_string(right): return ContextSet(right) - elif operator == '+': + elif str_operator == '+': if l_is_num and r_is_num or is_string(left) and is_string(right): - return ContextSet(compiled.create(evaluator, left.obj + right.obj)) + return ContextSet(left.execute_operation(right, str_operator)) elif _is_tuple(left) and _is_tuple(right) or _is_list(left) and _is_list(right): return ContextSet(iterable.MergedArray(evaluator, (left, right))) - elif operator == '-': + elif str_operator == '-': if l_is_num and r_is_num: - return ContextSet(compiled.create(evaluator, left.obj - right.obj)) - elif operator == '%': + return ContextSet(left.execute_operation(right, str_operator)) + elif str_operator == '%': # With strings and numbers the left type typically remains. Except for # `int() % float()`. return ContextSet(left) - elif operator in COMPARISON_OPERATORS: - operation = COMPARISON_OPERATORS[operator] + elif str_operator in COMPARISON_OPERATORS: if is_compiled(left) and is_compiled(right): # Possible, because the return is not an option. Just compare. - left = left.obj - right = right.obj - - try: - result = operation(left, right) - except TypeError: - # Could be True or False. - return ContextSet(compiled.create(evaluator, True), compiled.create(evaluator, False)) + try: + return ContextSet(left.execute_operation(right, str_operator)) + except TypeError: + # Could be True or False. + pass else: - return ContextSet(compiled.create(evaluator, result)) - elif operator == 'in': + if str_operator in ('is', '!=', '==', 'is not'): + operation = COMPARISON_OPERATORS[str_operator] + bool_ = operation(left, right) + return ContextSet(_bool_to_context(evaluator, bool_)) + + return ContextSet(_bool_to_context(evaluator, True), _bool_to_context(evaluator, False)) + elif str_operator == 'in': return NO_CONTEXTS def check(obj): @@ -417,7 +466,7 @@ def check(obj): obj.name.string_name in ('int', 'float') # Static analysis, one is a number, the other one is not. - if operator in ('+', '-') and l_is_num != r_is_num \ + if str_operator in ('+', '-') and l_is_num != r_is_num \ and not (check(left) or check(right)): message = "TypeError: unsupported operand type(s) for +: %s and %s" analysis.add(context, 'type-error-operation', operator, @@ -442,6 +491,22 @@ def _remove_statements(evaluator, context, stmt, name): def tree_name_to_contexts(evaluator, context, tree_name): + + context_set = ContextSet() + module_node = context.get_root_context().tree_node + if module_node is not None: + names = module_node.get_used_names().get(tree_name.value, []) + for name in names: + expr_stmt = name.parent + + correct_scope = parser_utils.get_parent_scope(name) == context.tree_node + + if expr_stmt.type == "expr_stmt" and expr_stmt.children[1].type == "annassign" and correct_scope: + context_set |= _evaluate_for_annotation(context, expr_stmt.children[1].children[1]) + + if context_set: + return context_set + types = [] node = tree_name.get_definition(import_name_always=True) if node is None: @@ -455,7 +520,7 @@ def tree_name_to_contexts(evaluator, context, tree_name): filters = [next(filters)] return finder.find(filters, attribute_lookup=False) elif node.type not in ('import_from', 'import_name'): - raise ValueError("Should not happen.") + raise ValueError("Should not happen. type: %s", node.type) typ = node.type if typ == 'for_stmt': @@ -472,14 +537,18 @@ def tree_name_to_contexts(evaluator, context, tree_name): types = context.predefined_names[node][tree_name.value] except KeyError: cn = ContextualizedNode(context, node.children[3]) - for_types = iterate_contexts(cn.infer(), cn) + for_types = iterate_contexts( + cn.infer(), + contextualized_node=cn, + is_async=node.parent.type == 'async_stmt', + ) c_node = ContextualizedName(context, tree_name) types = check_tuple_assignments(evaluator, c_node, for_types) elif typ == 'expr_stmt': types = _remove_statements(evaluator, context, node, tree_name) elif typ == 'with_stmt': context_managers = context.eval_node(node.get_test_node_from_name(tree_name)) - enter_methods = context_managers.py__getattribute__('__enter__') + enter_methods = context_managers.py__getattribute__(u'__enter__') return enter_methods.execute_evaluated() elif typ in ('import_from', 'import_name'): types = imports.infer_import(context, tree_name) @@ -492,7 +561,7 @@ def tree_name_to_contexts(evaluator, context, tree_name): exceptions = context.eval_node(tree_name.get_previous_sibling().get_previous_sibling()) types = exceptions.execute_evaluated() else: - raise ValueError("Should not happen.") + raise ValueError("Should not happen. type: %s" % typ) return types @@ -583,6 +652,8 @@ def eval_subscript_list(evaluator, context, index): result += [None] * (3 - len(result)) return ContextSet(iterable.Slice(context, *result)) + elif index.type == 'subscriptlist': + return NO_CONTEXTS # No slices return context.eval_node(index) diff --git a/pythonFiles/jedi/evaluate/sys_path.py b/pythonFiles/jedi/evaluate/sys_path.py index 82e5e9df9ceb..d765a6653c02 100644 --- a/pythonFiles/jedi/evaluate/sys_path.py +++ b/pythonFiles/jedi/evaluate/sys_path.py @@ -1,85 +1,27 @@ -import glob import os -import sys -import imp -from jedi.evaluate.site import addsitedir -from jedi._compatibility import unicode +from jedi._compatibility import unicode, force_unicode, all_suffixes from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate.base_context import ContextualizedNode from jedi.evaluate.helpers import is_string +from jedi.common.utils import traverse_parents +from jedi.parser_utils import get_cached_code_lines from jedi import settings from jedi import debug -from jedi.evaluate.utils import ignored - - -def get_venv_path(venv): - """Get sys.path for specified virtual environment.""" - sys_path = _get_venv_path_dirs(venv) - with ignored(ValueError): - sys_path.remove('') - sys_path = _get_sys_path_with_egglinks(sys_path) - # As of now, get_venv_path_dirs does not scan built-in pythonpath and - # user-local site-packages, let's approximate them using path from Jedi - # interpreter. - return sys_path + sys.path - - -def _get_sys_path_with_egglinks(sys_path): - """Find all paths including those referenced by egg-links. - - Egg-link-referenced directories are inserted into path immediately before - the directory on which their links were found. Such directories are not - taken into consideration by normal import mechanism, but they are traversed - when doing pkg_resources.require. - """ - result = [] - for p in sys_path: - # pkg_resources does not define a specific order for egg-link files - # using os.listdir to enumerate them, we're sorting them to have - # reproducible tests. - for egg_link in sorted(glob.glob(os.path.join(p, '*.egg-link'))): - with open(egg_link) as fd: - for line in fd: - line = line.strip() - if line: - result.append(os.path.join(p, line)) - # pkg_resources package only interprets the first - # non-empty line in egg-link files. - break - result.append(p) - return result - - -def _get_venv_path_dirs(venv): - """Get sys.path for venv without starting up the interpreter.""" - venv = os.path.abspath(venv) - sitedir = _get_venv_sitepackages(venv) - sys_path = [] - addsitedir(sys_path, sitedir) - return sys_path - - -def _get_venv_sitepackages(venv): - if os.name == 'nt': - p = os.path.join(venv, 'lib', 'site-packages') - else: - p = os.path.join(venv, 'lib', 'python%d.%d' % sys.version_info[:2], - 'site-packages') - return p def _abs_path(module_context, path): - module_path = module_context.py__file__() if os.path.isabs(path): return path + module_path = module_context.py__file__() if module_path is None: # In this case we have no idea where we actually are in the file # system. return None base_dir = os.path.dirname(module_path) + path = force_unicode(path) return os.path.abspath(os.path.join(base_dir, path)) @@ -87,7 +29,7 @@ def _paths_from_assignment(module_context, expr_stmt): """ Extracts the assigned strings from an assignment that looks as follows:: - >>> sys.path[0:0] = ['module/path', 'another/module/path'] + sys.path[0:0] = ['module/path', 'another/module/path'] This function is in general pretty tolerant (and therefore 'buggy'). However, it's not a big issue usually to add more paths to Jedi's sys_path, @@ -121,7 +63,7 @@ def _paths_from_assignment(module_context, expr_stmt): for lazy_context in cn.infer().iterate(cn): for context in lazy_context.infer(): if is_string(context): - abs_path = _abs_path(module_context, context.obj) + abs_path = _abs_path(module_context, context.get_safe_value()) if abs_path is not None: yield abs_path @@ -144,7 +86,7 @@ def _paths_from_list_modifications(module_context, trailer1, trailer2): for context in module_context.create_context(arg).eval_node(arg): if is_string(context): - abs_path = _abs_path(module_context, context.obj) + abs_path = _abs_path(module_context, context.get_safe_value()) if abs_path is not None: yield abs_path @@ -187,24 +129,19 @@ def get_sys_path_powers(names): return added -def sys_path_with_modifications(evaluator, module_context): - return evaluator.project.sys_path + check_sys_path_modifications(module_context) - - -def detect_additional_paths(evaluator, script_path): - django_paths = _detect_django_path(script_path) +def discover_buildout_paths(evaluator, script_path): buildout_script_paths = set() for buildout_script_path in _get_buildout_script_paths(script_path): for path in _get_paths_from_buildout_script(evaluator, buildout_script_path): buildout_script_paths.add(path) - return django_paths + list(buildout_script_paths) + return buildout_script_paths def _get_paths_from_buildout_script(evaluator, buildout_script_path): try: - module_node = evaluator.grammar.parse( + module_node = evaluator.parse( path=buildout_script_path, cache=True, cache_path=settings.cache_directory @@ -214,20 +151,14 @@ def _get_paths_from_buildout_script(evaluator, buildout_script_path): return from jedi.evaluate.context import ModuleContext - module = ModuleContext(evaluator, module_node, buildout_script_path) + module = ModuleContext( + evaluator, module_node, buildout_script_path, + code_lines=get_cached_code_lines(evaluator.grammar, buildout_script_path), + ) for path in check_sys_path_modifications(module): yield path -def traverse_parents(path): - while True: - new = os.path.dirname(path) - if new == path: - return - path = new - yield path - - def _get_parent_dir_with_file(path, filename): for parent in traverse_parents(path): if os.path.isfile(os.path.join(parent, filename)): @@ -235,47 +166,34 @@ def _get_parent_dir_with_file(path, filename): return None -def _detect_django_path(module_path): - """ Detects the path of the very well known Django library (if used) """ - result = [] - - for parent in traverse_parents(module_path): - with ignored(IOError): - with open(parent + os.path.sep + 'manage.py'): - debug.dbg('Found django path: %s', module_path) - result.append(parent) - return result - - -def _get_buildout_script_paths(module_path): +def _get_buildout_script_paths(search_path): """ if there is a 'buildout.cfg' file in one of the parent directories of the given module it will return a list of all files in the buildout bin directory that look like python files. - :param module_path: absolute path to the module. - :type module_path: str + :param search_path: absolute path to the module. + :type search_path: str """ - project_root = _get_parent_dir_with_file(module_path, 'buildout.cfg') + project_root = _get_parent_dir_with_file(search_path, 'buildout.cfg') if not project_root: - return [] + return bin_path = os.path.join(project_root, 'bin') if not os.path.exists(bin_path): - return [] - extra_module_paths = [] + return + for filename in os.listdir(bin_path): try: filepath = os.path.join(bin_path, filename) with open(filepath, 'r') as f: firstline = f.readline() if firstline.startswith('#!') and 'python' in firstline: - extra_module_paths.append(filepath) + yield filepath except (UnicodeDecodeError, IOError) as e: - # Probably a binary file; permission error or race cond. because file got deleted - # ignore + # Probably a binary file; permission error or race cond. because + # file got deleted. Ignore it. debug.warning(unicode(e)) continue - return extra_module_paths def dotted_path_in_sys_path(sys_path, module_path): @@ -283,7 +201,7 @@ def dotted_path_in_sys_path(sys_path, module_path): Returns the dotted path inside a sys.path. """ # First remove the suffix. - for suffix, _, _ in imp.get_suffixes(): + for suffix in all_suffixes(): if module_path.endswith(suffix): module_path = module_path[:-len(suffix)] break diff --git a/pythonFiles/jedi/evaluate/utils.py b/pythonFiles/jedi/evaluate/utils.py index 7fc1c246de0d..e00e477441de 100644 --- a/pythonFiles/jedi/evaluate/utils.py +++ b/pythonFiles/jedi/evaluate/utils.py @@ -2,10 +2,19 @@ import sys import contextlib import functools +import re +import os from jedi._compatibility import reraise +_sep = os.path.sep +if os.path.altsep is not None: + _sep += os.path.altsep +_path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) +del _sep + + def to_list(func): def wrapper(*args, **kwargs): return list(func(*args, **kwargs)) @@ -108,3 +117,38 @@ def indent_block(text, indention=' '): text = text[:-1] lines = text.split('\n') return '\n'.join(map(lambda s: indention + s, lines)) + temp + + +def dotted_from_fs_path(fs_path, sys_path): + """ + Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e. + compares the path with sys.path and then returns the dotted_path. If the + path is not in the sys.path, just returns None. + """ + if os.path.basename(fs_path).startswith('__init__.'): + # We are calculating the path. __init__ files are not interesting. + fs_path = os.path.dirname(fs_path) + + # prefer + # - UNIX + # /path/to/pythonX.Y/lib-dynload + # /path/to/pythonX.Y/site-packages + # - Windows + # C:\path\to\DLLs + # C:\path\to\Lib\site-packages + # over + # - UNIX + # /path/to/pythonX.Y + # - Windows + # C:\path\to\Lib + path = '' + for s in sys_path: + if (fs_path.startswith(s) and len(path) < len(s)): + path = s + + # - Window + # X:\path\to\lib-dynload/datetime.pyd => datetime + module_path = fs_path[len(path):].lstrip(os.path.sep).lstrip('/') + # - Window + # Replace like X:\path\to\something/foo/bar.py + return _path_re.sub('', module_path).replace(os.path.sep, '.').replace('/', '.') diff --git a/pythonFiles/jedi/parser_utils.py b/pythonFiles/jedi/parser_utils.py index 59c6408ea1c6..e630265314e4 100644 --- a/pythonFiles/jedi/parser_utils.py +++ b/pythonFiles/jedi/parser_utils.py @@ -1,14 +1,15 @@ import textwrap from inspect import cleandoc -from jedi._compatibility import literal_eval, is_py3 from parso.python import tree +from parso.cache import parser_cache -_EXECUTE_NODES = set([ - 'funcdef', 'classdef', 'import_from', 'import_name', 'test', 'or_test', - 'and_test', 'not_test', 'comparison', 'expr', 'xor_expr', 'and_expr', - 'shift_expr', 'arith_expr', 'atom_expr', 'term', 'factor', 'power', 'atom' -]) +from jedi._compatibility import literal_eval, force_unicode + +_EXECUTE_NODES = {'funcdef', 'classdef', 'import_from', 'import_name', 'test', + 'or_test', 'and_test', 'not_test', 'comparison', 'expr', + 'xor_expr', 'and_expr', 'shift_expr', 'arith_expr', + 'atom_expr', 'term', 'factor', 'power', 'atom'} _FLOW_KEYWORDS = ( 'try', 'except', 'finally', 'else', 'if', 'elif', 'with', 'for', 'while' @@ -112,10 +113,7 @@ def clean_scope_docstring(scope_node): cleaned = cleandoc(safe_literal_eval(node.value)) # Since we want the docstr output to be always unicode, just # force it. - if is_py3 or isinstance(cleaned, unicode): - return cleaned - else: - return unicode(cleaned, 'UTF-8', 'replace') + return force_unicode(cleaned) return '' @@ -205,6 +203,9 @@ def get_following_comment_same_line(node): whitespace = node.children[5].get_first_leaf().prefix elif node.type == 'with_stmt': whitespace = node.children[3].get_first_leaf().prefix + elif node.type == 'funcdef': + # actually on the next line + whitespace = node.children[4].get_first_leaf().get_next_leaf().prefix else: whitespace = node.get_last_leaf().get_next_leaf().prefix except AttributeError: @@ -239,3 +240,11 @@ def get_parent_scope(node, include_flows=False): break scope = scope.parent return scope + + +def get_cached_code_lines(grammar, path): + """ + Basically access the cached code lines in parso. This is not the nicest way + to do this, but we avoid splitting all the lines again. + """ + return parser_cache[grammar._hashed][path].lines diff --git a/pythonFiles/jedi/refactoring.py b/pythonFiles/jedi/refactoring.py index ee938427fcee..6c1d74d1bdb8 100644 --- a/pythonFiles/jedi/refactoring.py +++ b/pythonFiles/jedi/refactoring.py @@ -1,11 +1,14 @@ """ +THIS is not in active development, please check +https://github.com/davidhalter/jedi/issues/667 first before editing. + Introduce some basic refactoring functions to |jedi|. This module is still in a very early development stage and needs much testing and improvement. .. warning:: I won't do too much here, but if anyone wants to step in, please do. Refactoring is none of my priorities -It uses the |jedi| `API `_ and supports currently the +It uses the |jedi| `API `_ and supports currently the following functions (sometimes bug-prone): - rename @@ -50,9 +53,8 @@ def diff(self): def rename(script, new_name): """ The `args` / `kwargs` params are the same as in `api.Script`. - :param operation: The refactoring operation to execute. - :type operation: str - :type source: str + :param new_name: The new name of the script. + :param script: The source Script object. :return: list of changed lines/changed files """ return Refactoring(_rename(script.usages(), new_name)) @@ -105,11 +107,12 @@ def extract(script, new_name): user_stmt = script._parser.user_stmt() - # TODO care for multiline extracts + # TODO care for multi-line extracts dct = {} if user_stmt: pos = script._pos line_index = pos[0] - 1 + # Be careful here. 'array_for_pos' does not exist in 'helpers'. arr, index = helpers.array_for_pos(user_stmt, pos) if arr is not None: start_pos = arr[index].start_pos @@ -120,7 +123,7 @@ def extract(script, new_name): start_line = new_lines[start_pos[0] - 1] text = start_line[start_pos[1]:e] for l in range(start_pos[0], end_pos[0] - 1): - text += '\n' + l + text += '\n' + str(l) if e is None: end_line = new_lines[end_pos[0] - 1] text += '\n' + end_line[:end_pos[1]] @@ -140,7 +143,7 @@ def extract(script, new_name): new_lines[start_pos[0] - 1] = start_line new_lines[start_pos[0]:end_pos[0] - 1] = [] - # add parentheses in multiline case + # add parentheses in multi-line case open_brackets = ['(', '[', '{'] close_brackets = [')', ']', '}'] if '\n' in text and not (text[0] in open_brackets and text[-1] == @@ -172,7 +175,7 @@ def inline(script): inlines = sorted(inlines, key=lambda x: (x.module_path, x.line, x.column), reverse=True) expression_list = stmt.expression_list() - # don't allow multiline refactorings for now. + # don't allow multi-line refactorings for now. assert stmt.start_pos[0] == stmt.end_pos[0] index = stmt.start_pos[0] - 1 diff --git a/pythonFiles/jedi/utils.py b/pythonFiles/jedi/utils.py index 177524c50168..0f42e7d55858 100644 --- a/pythonFiles/jedi/utils.py +++ b/pythonFiles/jedi/utils.py @@ -89,12 +89,13 @@ def complete(self, text, state): lines = split_lines(text) position = (len(lines), len(lines[-1])) name = get_on_completion_name( - interpreter._get_module_node(), + interpreter._module_node, lines, position ) before = text[:len(text) - len(name)] completions = interpreter.completions() + logging.debug("REPL completions: %s", completions) except: logging.error("REPL Completion error:\n" + traceback.format_exc()) raise @@ -108,6 +109,11 @@ def complete(self, text, state): return None try: + # Need to import this one as well to make sure it's executed before + # this code. This didn't use to be an issue until 3.3. Starting with + # 3.4 this is different, it always overwrites the completer if it's not + # already imported here. + import rlcompleter import readline except ImportError: print("Jedi: Module readline not available.") diff --git a/pythonFiles/parso/__init__.py b/pythonFiles/parso/__init__.py index f0a0fc4f5015..c4cce53ea690 100644 --- a/pythonFiles/parso/__init__.py +++ b/pythonFiles/parso/__init__.py @@ -43,7 +43,7 @@ from parso.utils import split_lines, python_bytes_to_unicode -__version__ = '0.1.1' +__version__ = '0.2.0' def parse(code=None, **kwargs): diff --git a/pythonFiles/parso/_compatibility.py b/pythonFiles/parso/_compatibility.py index 9ddf23dc6786..db411eebf981 100644 --- a/pythonFiles/parso/_compatibility.py +++ b/pythonFiles/parso/_compatibility.py @@ -36,7 +36,7 @@ def use_metaclass(meta, *bases): def u(string): """Cast to unicode DAMMIT! Written because Python2 repr always implicitly casts to a string, so we - have to cast back to a unicode (and we now that we always deal with valid + have to cast back to a unicode (and we know that we always deal with valid unicode, because we check that in the beginning). """ if py_version >= 30: diff --git a/pythonFiles/parso/grammar.py b/pythonFiles/parso/grammar.py index 2cf26d77fb27..c825b5554c0e 100644 --- a/pythonFiles/parso/grammar.py +++ b/pythonFiles/parso/grammar.py @@ -12,7 +12,6 @@ from parso.python.parser import Parser as PythonParser from parso.python.errors import ErrorFinderConfig from parso.python import pep8 -from parso.python import fstring _loaded_grammars = {} @@ -73,7 +72,7 @@ def parse(self, code=None, **kwargs): :py:class:`parso.python.tree.Module`. """ if 'start_pos' in kwargs: - raise TypeError("parse() got an unexpected keyworda argument.") + raise TypeError("parse() got an unexpected keyword argument.") return self._parse(code=code, **kwargs) def _parse(self, code=None, error_recovery=True, path=None, @@ -186,7 +185,6 @@ def _get_normalizer_issues(self, node, normalizer_config=None): normalizer.walk(node) return normalizer.issues - def __repr__(self): labels = self._pgen_grammar.number2symbol.values() txt = ' '.join(list(labels)[:3]) + ' ...' @@ -215,34 +213,6 @@ def _tokenize(self, code): return tokenize(code, self.version_info) -class PythonFStringGrammar(Grammar): - _token_namespace = fstring.TokenNamespace - _start_symbol = 'fstring' - - def __init__(self): - super(PythonFStringGrammar, self).__init__( - text=fstring.GRAMMAR, - tokenizer=fstring.tokenize, - parser=fstring.Parser - ) - - def parse(self, code, **kwargs): - return self._parse(code, **kwargs) - - def _parse(self, code, error_recovery=True, start_pos=(1, 0)): - tokens = self._tokenizer(code, start_pos=start_pos) - p = self._parser( - self._pgen_grammar, - error_recovery=error_recovery, - start_symbol=self._start_symbol, - ) - return p.parse(tokens=tokens) - - def parse_leaf(self, leaf, error_recovery=True): - code = leaf._get_payload() - return self.parse(code, error_recovery=True, start_pos=leaf.start_pos) - - def load_grammar(**kwargs): """ Loads a :py:class:`parso.Grammar`. The default version is the current Python @@ -273,10 +243,6 @@ def load_grammar(language='python', version=None): except FileNotFoundError: message = "Python version %s is currently not supported." % version raise NotImplementedError(message) - elif language == 'python-f-string': - if version is not None: - raise NotImplementedError("Currently different versions are not supported.") - return PythonFStringGrammar() else: raise NotImplementedError("No support for language %s." % language) diff --git a/pythonFiles/parso/pgen2/pgen.py b/pythonFiles/parso/pgen2/pgen.py index 10ef6ffd1532..a3e39fa5fe74 100644 --- a/pythonFiles/parso/pgen2/pgen.py +++ b/pythonFiles/parso/pgen2/pgen.py @@ -28,6 +28,7 @@ def make_grammar(self): c = grammar.Grammar(self._bnf_text) names = list(self.dfas.keys()) names.sort() + # TODO do we still need this? names.remove(self.startsymbol) names.insert(0, self.startsymbol) for name in names: @@ -316,8 +317,8 @@ def _parse_atom(self): def _expect(self, type): if self.type != type: - self._raise_error("expected %s, got %s(%s)", - type, self.type, self.value) + self._raise_error("expected %s(%s), got %s(%s)", + type, token.tok_name[type], self.type, self.value) value = self.value self._gettoken() return value diff --git a/pythonFiles/parso/python/diff.py b/pythonFiles/parso/python/diff.py index c2e44fd3cb21..96c6e5f2ca41 100644 --- a/pythonFiles/parso/python/diff.py +++ b/pythonFiles/parso/python/diff.py @@ -133,7 +133,7 @@ def update(self, old_lines, new_lines): LOG.debug('diff: line_lengths old: %s, new: %s' % (len(old_lines), line_length)) for operation, i1, i2, j1, j2 in opcodes: - LOG.debug('diff %s old[%s:%s] new[%s:%s]', + LOG.debug('diff code[%s] old[%s:%s] new[%s:%s]', operation, i1 + 1, i2, j1 + 1, j2) if j2 == line_length and new_lines[-1] == '': @@ -454,7 +454,7 @@ def _remove_endmarker(self, tree_nodes): self._last_prefix = '' if is_endmarker: try: - separation = last_leaf.prefix.rindex('\n') + separation = last_leaf.prefix.rindex('\n') + 1 except ValueError: pass else: @@ -462,7 +462,7 @@ def _remove_endmarker(self, tree_nodes): # That is not relevant if parentheses were opened. Always parse # until the end of a line. last_leaf.prefix, self._last_prefix = \ - last_leaf.prefix[:separation + 1], last_leaf.prefix[separation + 1:] + last_leaf.prefix[:separation], last_leaf.prefix[separation:] first_leaf = tree_nodes[0].get_first_leaf() first_leaf.prefix = self.prefix + first_leaf.prefix @@ -472,7 +472,6 @@ def _remove_endmarker(self, tree_nodes): self.prefix = last_leaf.prefix tree_nodes = tree_nodes[:-1] - return tree_nodes def copy_nodes(self, tree_nodes, until_line, line_offset): @@ -492,6 +491,13 @@ def _copy_nodes(self, tos, nodes, until_line, line_offset): new_tos = tos for node in nodes: if node.type == 'endmarker': + # We basically removed the endmarker, but we are not allowed to + # remove the newline at the end of the line, otherwise it's + # going to be missing. + try: + self.prefix = node.prefix[:node.prefix.rindex('\n') + 1] + except ValueError: + pass # Endmarkers just distort all the checks below. Remove them. break diff --git a/pythonFiles/parso/python/errors.py b/pythonFiles/parso/python/errors.py index 65296568b54c..cfb8380ea743 100644 --- a/pythonFiles/parso/python/errors.py +++ b/pythonFiles/parso/python/errors.py @@ -563,7 +563,8 @@ def is_issue(self, leaf): and self._normalizer.version == (3, 5): self.add_issue(self.get_node(leaf), message=self.message_async_yield) -@ErrorFinder.register_rule(type='atom') + +@ErrorFinder.register_rule(type='strings') class _BytesAndStringMix(SyntaxRule): # e.g. 's' b'' message = "cannot mix bytes and nonbytes literals" @@ -744,7 +745,12 @@ def is_issue(self, node): @ErrorFinder.register_rule(type='arglist') class _ArglistRule(SyntaxRule): - message = "Generator expression must be parenthesized if not sole argument" + @property + def message(self): + if self._normalizer.version < (3, 7): + return "Generator expression must be parenthesized if not sole argument" + else: + return "Generator expression must be parenthesized" def is_issue(self, node): first_arg = node.children[0] @@ -837,101 +843,36 @@ def is_issue(self, try_stmt): self.add_issue(default_except, message=self.message) -@ErrorFinder.register_rule(type='string') +@ErrorFinder.register_rule(type='fstring') class _FStringRule(SyntaxRule): _fstring_grammar = None - message_empty = "f-string: empty expression not allowed" # f'{}' - message_single_closing = "f-string: single '}' is not allowed" # f'}' message_nested = "f-string: expressions nested too deeply" - message_backslash = "f-string expression part cannot include a backslash" # f'{"\"}' or f'{"\\"}' - message_comment = "f-string expression part cannot include '#'" # f'{#}' - message_unterminated_string = "f-string: unterminated string" # f'{"}' message_conversion = "f-string: invalid conversion character: expected 's', 'r', or 'a'" - message_incomplete = "f-string: expecting '}'" # f'{' - message_syntax = "invalid syntax" - @classmethod - def _load_grammar(cls): - import parso + def _check_format_spec(self, format_spec, depth): + self._check_fstring_contents(format_spec.children[1:], depth) - if cls._fstring_grammar is None: - cls._fstring_grammar = parso.load_grammar(language='python-f-string') - return cls._fstring_grammar + def _check_fstring_expr(self, fstring_expr, depth): + if depth >= 2: + self.add_issue(fstring_expr, message=self.message_nested) - def is_issue(self, fstring): - if 'f' not in fstring.string_prefix.lower(): - return + conversion = fstring_expr.children[2] + if conversion.type == 'fstring_conversion': + name = conversion.children[1] + if name.value not in ('s', 'r', 'a'): + self.add_issue(name, message=self.message_conversion) - parsed = self._load_grammar().parse_leaf(fstring) - for child in parsed.children: - if child.type == 'expression': - self._check_expression(child) - elif child.type == 'error_node': - next_ = child.get_next_leaf() - if next_.type == 'error_leaf' and next_.original_type == 'unterminated_string': - self.add_issue(next_, message=self.message_unterminated_string) - # At this point nothing more is comming except the error - # leaf that we've already checked here. - break - self.add_issue(child, message=self.message_incomplete) - elif child.type == 'error_leaf': - self.add_issue(child, message=self.message_single_closing) - - def _check_python_expr(self, python_expr): - value = python_expr.value - if '\\' in value: - self.add_issue(python_expr, message=self.message_backslash) - return - if '#' in value: - self.add_issue(python_expr, message=self.message_comment) - return - if re.match('\s*$', value) is not None: - self.add_issue(python_expr, message=self.message_empty) - return - - # This is now nested parsing. We parsed the fstring and now - # we're parsing Python again. - try: - # CPython has a bit of a special ways to parse Python code within - # f-strings. It wraps the code in brackets to make sure that - # whitespace doesn't make problems (indentation/newlines). - # Just use that algorithm as well here and adapt start positions. - start_pos = python_expr.start_pos - start_pos = start_pos[0], start_pos[1] - 1 - eval_input = self._normalizer.grammar._parse( - '(%s)' % value, - start_symbol='eval_input', - start_pos=start_pos, - error_recovery=False - ) - except ParserSyntaxError as e: - self.add_issue(e.error_leaf, message=self.message_syntax) - return + format_spec = fstring_expr.children[-2] + if format_spec.type == 'fstring_format_spec': + self._check_format_spec(format_spec, depth + 1) - issues = self._normalizer.grammar.iter_errors(eval_input) - self._normalizer.issues += issues - - def _check_format_spec(self, format_spec): - for expression in format_spec.children[1:]: - nested_format_spec = expression.children[-2] - if nested_format_spec.type == 'format_spec': - if len(nested_format_spec.children) > 1: - self.add_issue( - nested_format_spec.children[1], - message=self.message_nested - ) - - self._check_expression(expression) + def is_issue(self, fstring): + self._check_fstring_contents(fstring.children[1:-1]) - def _check_expression(self, expression): - for c in expression.children: - if c.type == 'python_expr': - self._check_python_expr(c) - elif c.type == 'conversion': - if c.value not in ('s', 'r', 'a'): - self.add_issue(c, message=self.message_conversion) - elif c.type == 'format_spec': - self._check_format_spec(c) + def _check_fstring_contents(self, children, depth=0): + for fstring_content in children: + if fstring_content.type == 'fstring_expr': + self._check_fstring_expr(fstring_content, depth) class _CheckAssignmentRule(SyntaxRule): @@ -944,7 +885,7 @@ def _check_assignment(self, node, is_deletion=False): first, second = node.children[:2] error = _get_comprehension_type(node) if error is None: - if second.type in ('dictorsetmaker', 'string'): + if second.type == 'dictorsetmaker': error = 'literal' elif first in ('(', '['): if second.type == 'yield_expr': @@ -963,7 +904,7 @@ def _check_assignment(self, node, is_deletion=False): error = 'Ellipsis' elif type_ == 'comparison': error = 'comparison' - elif type_ in ('string', 'number'): + elif type_ in ('string', 'number', 'strings'): error = 'literal' elif type_ == 'yield_expr': # This one seems to be a slightly different warning in Python. diff --git a/pythonFiles/parso/python/fstring.py b/pythonFiles/parso/python/fstring.py deleted file mode 100644 index a8fe7b452df5..000000000000 --- a/pythonFiles/parso/python/fstring.py +++ /dev/null @@ -1,211 +0,0 @@ -import re - -from itertools import count -from parso.utils import PythonVersionInfo -from parso.utils import split_lines -from parso.python.tokenize import Token -from parso import parser -from parso.tree import TypedLeaf, ErrorNode, ErrorLeaf - -version36 = PythonVersionInfo(3, 6) - - -class TokenNamespace: - _c = count() - LBRACE = next(_c) - RBRACE = next(_c) - ENDMARKER = next(_c) - COLON = next(_c) - CONVERSION = next(_c) - PYTHON_EXPR = next(_c) - EXCLAMATION_MARK = next(_c) - UNTERMINATED_STRING = next(_c) - - token_map = dict((v, k) for k, v in locals().items() if not k.startswith('_')) - - @classmethod - def generate_token_id(cls, string): - if string == '{': - return cls.LBRACE - elif string == '}': - return cls.RBRACE - elif string == '!': - return cls.EXCLAMATION_MARK - elif string == ':': - return cls.COLON - return getattr(cls, string) - - -GRAMMAR = """ -fstring: expression* ENDMARKER -format_spec: ':' expression* -expression: '{' PYTHON_EXPR [ '!' CONVERSION ] [ format_spec ] '}' -""" - -_prefix = r'((?:[^{}]+)*)' -_expr = _prefix + r'(\{|\}|$)' -_in_expr = r'([^{}\[\]:"\'!]*)(.?)' -# There's only one conversion character allowed. But the rules have to be -# checked later anyway, so allow more here. This makes error recovery nicer. -_conversion = r'([^={}:]*)(.?)' - -_compiled_expr = re.compile(_expr) -_compiled_in_expr = re.compile(_in_expr) -_compiled_conversion = re.compile(_conversion) - - -def tokenize(code, start_pos=(1, 0)): - def add_to_pos(string): - lines = split_lines(string) - l = len(lines[-1]) - if len(lines) > 1: - start_pos[0] += len(lines) - 1 - start_pos[1] = l - else: - start_pos[1] += l - - def tok(value, type=None, prefix=''): - if type is None: - type = TokenNamespace.generate_token_id(value) - - add_to_pos(prefix) - token = Token(type, value, tuple(start_pos), prefix) - add_to_pos(value) - return token - - start = 0 - recursion_level = 0 - added_prefix = '' - start_pos = list(start_pos) - while True: - match = _compiled_expr.match(code, start) - prefix = added_prefix + match.group(1) - found = match.group(2) - start = match.end() - if not found: - # We're at the end. - break - - if found == '}': - if recursion_level == 0 and len(code) > start and code[start] == '}': - # This is a }} escape. - added_prefix = prefix + '}}' - start += 1 - continue - - recursion_level = max(0, recursion_level - 1) - yield tok(found, prefix=prefix) - added_prefix = '' - else: - assert found == '{' - if recursion_level == 0 and len(code) > start and code[start] == '{': - # This is a {{ escape. - added_prefix = prefix + '{{' - start += 1 - continue - - recursion_level += 1 - yield tok(found, prefix=prefix) - added_prefix = '' - - expression = '' - squared_count = 0 - curly_count = 0 - while True: - expr_match = _compiled_in_expr.match(code, start) - expression += expr_match.group(1) - found = expr_match.group(2) - start = expr_match.end() - - if found == '{': - curly_count += 1 - expression += found - elif found == '}' and curly_count > 0: - curly_count -= 1 - expression += found - elif found == '[': - squared_count += 1 - expression += found - elif found == ']': - # Use a max function here, because the Python code might - # just have syntax errors. - squared_count = max(0, squared_count - 1) - expression += found - elif found == ':' and (squared_count or curly_count): - expression += found - elif found in ('"', "'"): - search = found - if len(code) > start + 1 and \ - code[start] == found == code[start+1]: - search *= 3 - start += 2 - - index = code.find(search, start) - if index == -1: - yield tok(expression, type=TokenNamespace.PYTHON_EXPR) - yield tok( - found + code[start:], - type=TokenNamespace.UNTERMINATED_STRING, - ) - start = len(code) - break - expression += found + code[start:index+1] - start = index + 1 - elif found == '!' and len(code) > start and code[start] == '=': - # This is a python `!=` and not a conversion. - expression += found - else: - yield tok(expression, type=TokenNamespace.PYTHON_EXPR) - if found: - yield tok(found) - break - - if found == '!': - conversion_match = _compiled_conversion.match(code, start) - found = conversion_match.group(2) - start = conversion_match.end() - yield tok(conversion_match.group(1), type=TokenNamespace.CONVERSION) - if found: - yield tok(found) - if found == '}': - recursion_level -= 1 - - # We don't need to handle everything after ':', because that is - # basically new tokens. - - yield tok('', type=TokenNamespace.ENDMARKER, prefix=prefix) - - -class Parser(parser.BaseParser): - def parse(self, tokens): - node = super(Parser, self).parse(tokens) - if isinstance(node, self.default_leaf): # Is an endmarker. - # If there's no curly braces we get back a non-module. We always - # want an fstring. - node = self.default_node('fstring', [node]) - - return node - - def convert_leaf(self, pgen_grammar, type, value, prefix, start_pos): - # TODO this is so ugly. - leaf_type = TokenNamespace.token_map[type].lower() - return TypedLeaf(leaf_type, value, start_pos, prefix) - - def error_recovery(self, pgen_grammar, stack, arcs, typ, value, start_pos, prefix, - add_token_callback): - if not self._error_recovery: - return super(Parser, self).error_recovery( - pgen_grammar, stack, arcs, typ, value, start_pos, prefix, - add_token_callback - ) - - token_type = TokenNamespace.token_map[typ].lower() - if len(stack) == 1: - error_leaf = ErrorLeaf(token_type, value, start_pos, prefix) - stack[0][2][1].append(error_leaf) - else: - dfa, state, (type_, nodes) = stack[1] - stack[0][2][1].append(ErrorNode(nodes)) - stack[1:] = [] - - add_token_callback(typ, value, start_pos, prefix) diff --git a/pythonFiles/parso/python/grammar26.txt b/pythonFiles/parso/python/grammar26.txt index b972a41d6a4a..d9cede2e9da9 100644 --- a/pythonFiles/parso/python/grammar26.txt +++ b/pythonFiles/parso/python/grammar26.txt @@ -119,7 +119,8 @@ atom: ('(' [yield_expr|testlist_comp] ')' | '[' [listmaker] ']' | '{' [dictorsetmaker] '}' | '`' testlist1 '`' | - NAME | NUMBER | STRING+) + NAME | NUMBER | strings) +strings: STRING+ listmaker: test ( list_for | (',' test)* [','] ) # Dave: Renamed testlist_gexpr to testlist_comp, because in 2.7+ this is the # default. It's more consistent like this. diff --git a/pythonFiles/parso/python/grammar27.txt b/pythonFiles/parso/python/grammar27.txt index 4c3f33da32d5..359f12b43e1f 100644 --- a/pythonFiles/parso/python/grammar27.txt +++ b/pythonFiles/parso/python/grammar27.txt @@ -104,7 +104,8 @@ atom: ('(' [yield_expr|testlist_comp] ')' | '[' [listmaker] ']' | '{' [dictorsetmaker] '}' | '`' testlist1 '`' | - NAME | NUMBER | STRING+) + NAME | NUMBER | strings) +strings: STRING+ listmaker: test ( list_for | (',' test)* [','] ) testlist_comp: test ( comp_for | (',' test)* [','] ) lambdef: 'lambda' [varargslist] ':' test diff --git a/pythonFiles/parso/python/grammar33.txt b/pythonFiles/parso/python/grammar33.txt index d7aaffd60e14..3a5580926797 100644 --- a/pythonFiles/parso/python/grammar33.txt +++ b/pythonFiles/parso/python/grammar33.txt @@ -103,7 +103,8 @@ power: atom trailer* ['**' factor] atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') +strings: STRING+ testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] diff --git a/pythonFiles/parso/python/grammar34.txt b/pythonFiles/parso/python/grammar34.txt index 05c3181627db..324bba18753d 100644 --- a/pythonFiles/parso/python/grammar34.txt +++ b/pythonFiles/parso/python/grammar34.txt @@ -103,7 +103,8 @@ power: atom trailer* ['**' factor] atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') +strings: STRING+ testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] diff --git a/pythonFiles/parso/python/grammar35.txt b/pythonFiles/parso/python/grammar35.txt index c38217f3f97f..5868b8f7031a 100644 --- a/pythonFiles/parso/python/grammar35.txt +++ b/pythonFiles/parso/python/grammar35.txt @@ -110,7 +110,8 @@ atom_expr: ['await'] atom trailer* atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') +strings: STRING+ testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] diff --git a/pythonFiles/parso/python/grammar36.txt b/pythonFiles/parso/python/grammar36.txt index e76147e9e4fc..b82c1fec1145 100644 --- a/pythonFiles/parso/python/grammar36.txt +++ b/pythonFiles/parso/python/grammar36.txt @@ -108,7 +108,7 @@ atom_expr: ['await'] atom trailer* atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] @@ -148,3 +148,10 @@ encoding_decl: NAME yield_expr: 'yield' [yield_arg] yield_arg: 'from' test | testlist + +strings: (STRING | fstring)+ +fstring: FSTRING_START fstring_content* FSTRING_END +fstring_content: FSTRING_STRING | fstring_expr +fstring_conversion: '!' NAME +fstring_expr: '{' testlist_comp [ fstring_conversion ] [ fstring_format_spec ] '}' +fstring_format_spec: ':' fstring_content* diff --git a/pythonFiles/parso/python/grammar37.txt b/pythonFiles/parso/python/grammar37.txt index e76147e9e4fc..7d112f79852b 100644 --- a/pythonFiles/parso/python/grammar37.txt +++ b/pythonFiles/parso/python/grammar37.txt @@ -108,7 +108,7 @@ atom_expr: ['await'] atom trailer* atom: ('(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']' | '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') + NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False') testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] @@ -148,3 +148,10 @@ encoding_decl: NAME yield_expr: 'yield' [yield_arg] yield_arg: 'from' test | testlist + +strings: (STRING | fstring)+ +fstring: FSTRING_START fstring_content* FSTRING_END +fstring_content: FSTRING_STRING | fstring_expr +fstring_conversion: '!' NAME +fstring_expr: '{' testlist [ fstring_conversion ] [ fstring_format_spec ] '}' +fstring_format_spec: ':' fstring_content* diff --git a/pythonFiles/parso/python/parser.py b/pythonFiles/parso/python/parser.py index 1897f53e8d6f..7cdf987ab365 100644 --- a/pythonFiles/parso/python/parser.py +++ b/pythonFiles/parso/python/parser.py @@ -1,6 +1,7 @@ from parso.python import tree from parso.python.token import (DEDENT, INDENT, ENDMARKER, NEWLINE, NUMBER, - STRING, tok_name, NAME) + STRING, tok_name, NAME, FSTRING_STRING, + FSTRING_START, FSTRING_END) from parso.parser import BaseParser from parso.pgen2.parse import token_to_ilabel @@ -50,6 +51,17 @@ class structure of different scopes. } default_node = tree.PythonNode + # Names/Keywords are handled separately + _leaf_map = { + STRING: tree.String, + NUMBER: tree.Number, + NEWLINE: tree.Newline, + ENDMARKER: tree.EndMarker, + FSTRING_STRING: tree.FStringString, + FSTRING_START: tree.FStringStart, + FSTRING_END: tree.FStringEnd, + } + def __init__(self, pgen_grammar, error_recovery=True, start_symbol='file_input'): super(Parser, self).__init__(pgen_grammar, start_symbol, error_recovery=error_recovery) @@ -121,16 +133,8 @@ def convert_leaf(self, pgen_grammar, type, value, prefix, start_pos): return tree.Keyword(value, start_pos, prefix) else: return tree.Name(value, start_pos, prefix) - elif type == STRING: - return tree.String(value, start_pos, prefix) - elif type == NUMBER: - return tree.Number(value, start_pos, prefix) - elif type == NEWLINE: - return tree.Newline(value, start_pos, prefix) - elif type == ENDMARKER: - return tree.EndMarker(value, start_pos, prefix) - else: - return tree.Operator(value, start_pos, prefix) + + return self._leaf_map.get(type, tree.Operator)(value, start_pos, prefix) def error_recovery(self, pgen_grammar, stack, arcs, typ, value, start_pos, prefix, add_token_callback): diff --git a/pythonFiles/parso/python/token.py b/pythonFiles/parso/python/token.py index fb590a5f28c6..dd849b01daa7 100644 --- a/pythonFiles/parso/python/token.py +++ b/pythonFiles/parso/python/token.py @@ -32,6 +32,14 @@ ERROR_DEDENT = next(_counter) tok_name[ERROR_DEDENT] = 'ERROR_DEDENT' +FSTRING_START = next(_counter) +tok_name[FSTRING_START] = 'FSTRING_START' +FSTRING_END = next(_counter) +tok_name[FSTRING_END] = 'FSTRING_END' +FSTRING_STRING = next(_counter) +tok_name[FSTRING_STRING] = 'FSTRING_STRING' +EXCLAMATION = next(_counter) +tok_name[EXCLAMATION] = 'EXCLAMATION' # Map from operator to number (since tokenize doesn't do this) @@ -84,6 +92,7 @@ //= DOUBLESLASHEQUAL -> RARROW ... ELLIPSIS +! EXCLAMATION """ opmap = {} diff --git a/pythonFiles/parso/python/tokenize.py b/pythonFiles/parso/python/tokenize.py index ecd2437f5ebb..31f081d9b804 100644 --- a/pythonFiles/parso/python/tokenize.py +++ b/pythonFiles/parso/python/tokenize.py @@ -20,14 +20,15 @@ from parso.python.token import (tok_name, ENDMARKER, STRING, NUMBER, opmap, NAME, ERRORTOKEN, NEWLINE, INDENT, DEDENT, - ERROR_DEDENT) + ERROR_DEDENT, FSTRING_STRING, FSTRING_START, + FSTRING_END) from parso._compatibility import py_version from parso.utils import split_lines TokenCollection = namedtuple( 'TokenCollection', - 'pseudo_token single_quoted triple_quoted endpats always_break_tokens', + 'pseudo_token single_quoted triple_quoted endpats fstring_pattern_map always_break_tokens', ) BOM_UTF8_STRING = BOM_UTF8.decode('utf-8') @@ -52,32 +53,35 @@ def group(*choices, **kwargs): return start + '|'.join(choices) + ')' -def any(*choices): - return group(*choices) + '*' - - def maybe(*choices): return group(*choices) + '?' # Return the empty string, plus all of the valid string prefixes. -def _all_string_prefixes(version_info): +def _all_string_prefixes(version_info, include_fstring=False, only_fstring=False): def different_case_versions(prefix): for s in _itertools.product(*[(c, c.upper()) for c in prefix]): yield ''.join(s) # The valid string prefixes. Only contain the lower case versions, # and don't contain any permuations (include 'fr', but not # 'rf'). The various permutations will be generated. - _valid_string_prefixes = ['b', 'r', 'u'] + valid_string_prefixes = ['b', 'r', 'u'] if version_info >= (3, 0): - _valid_string_prefixes.append('br') + valid_string_prefixes.append('br') - if version_info >= (3, 6): - _valid_string_prefixes += ['f', 'fr'] + result = set(['']) + if version_info >= (3, 6) and include_fstring: + f = ['f', 'fr'] + if only_fstring: + valid_string_prefixes = f + result = set() + else: + valid_string_prefixes += f + elif only_fstring: + return set() # if we add binary f-strings, add: ['fb', 'fbr'] - result = set(['']) - for prefix in _valid_string_prefixes: + for prefix in valid_string_prefixes: for t in _itertools.permutations(prefix): # create a list with upper and lower versions of each # character @@ -102,6 +106,10 @@ def _get_token_collection(version_info): return result +fstring_string_single_line = _compile(r'(?:[^{}\r\n]+|\{\{|\}\})+') +fstring_string_multi_line = _compile(r'(?:[^{}]+|\{\{|\}\})+') + + def _create_token_collection(version_info): # Note: we use unicode matching for names ("\w") but ascii matching for # number literals. @@ -141,6 +149,9 @@ def _create_token_collection(version_info): # StringPrefix can be the empty string (making it optional). possible_prefixes = _all_string_prefixes(version_info) StringPrefix = group(*possible_prefixes) + StringPrefixWithF = group(*_all_string_prefixes(version_info, include_fstring=True)) + fstring_prefixes = _all_string_prefixes(version_info, include_fstring=True, only_fstring=True) + FStringStart = group(*fstring_prefixes) # Tail end of ' string. Single = r"[^'\\]*(?:\\.[^'\\]*)*'" @@ -150,14 +161,14 @@ def _create_token_collection(version_info): Single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''" # Tail end of """ string. Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""' - Triple = group(StringPrefix + "'''", StringPrefix + '"""') + Triple = group(StringPrefixWithF + "'''", StringPrefixWithF + '"""') # Because of leftmost-then-longest match semantics, be sure to put the # longest operators first (e.g., if = came before ==, == would get # recognized as two instances of =). - Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"!=", + Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"//=?", r"->", - r"[+\-*/%&@`|^=<>]=?", + r"[+\-*/%&@`|^!=<>]=?", r"~") Bracket = '[][(){}]' @@ -174,7 +185,12 @@ def _create_token_collection(version_info): group("'", r'\\\r?\n'), StringPrefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*' + group('"', r'\\\r?\n')) - PseudoExtras = group(r'\\\r?\n|\Z', Comment, Triple) + pseudo_extra_pool = [Comment, Triple] + all_quotes = '"', "'", '"""', "'''" + if fstring_prefixes: + pseudo_extra_pool.append(FStringStart + group(*all_quotes)) + + PseudoExtras = group(r'\\\r?\n|\Z', *pseudo_extra_pool) PseudoToken = group(Whitespace, capture=True) + \ group(PseudoExtras, Number, Funny, ContStr, Name, capture=True) @@ -192,18 +208,24 @@ def _create_token_collection(version_info): # including the opening quotes. single_quoted = set() triple_quoted = set() + fstring_pattern_map = {} for t in possible_prefixes: - for p in (t + '"', t + "'"): - single_quoted.add(p) - for p in (t + '"""', t + "'''"): - triple_quoted.add(p) + for quote in '"', "'": + single_quoted.add(t + quote) + + for quote in '"""', "'''": + triple_quoted.add(t + quote) + + for t in fstring_prefixes: + for quote in all_quotes: + fstring_pattern_map[t + quote] = quote ALWAYS_BREAK_TOKENS = (';', 'import', 'class', 'def', 'try', 'except', 'finally', 'while', 'with', 'return') pseudo_token_compiled = _compile(PseudoToken) return TokenCollection( pseudo_token_compiled, single_quoted, triple_quoted, endpats, - ALWAYS_BREAK_TOKENS + fstring_pattern_map, ALWAYS_BREAK_TOKENS ) @@ -226,12 +248,104 @@ def __repr__(self): self._replace(type=self._get_type_name())) +class FStringNode(object): + def __init__(self, quote): + self.quote = quote + self.parentheses_count = 0 + self.previous_lines = '' + self.last_string_start_pos = None + # In the syntax there can be multiple format_spec's nested: + # {x:{y:3}} + self.format_spec_count = 0 + + def open_parentheses(self, character): + self.parentheses_count += 1 + + def close_parentheses(self, character): + self.parentheses_count -= 1 + + def allow_multiline(self): + return len(self.quote) == 3 + + def is_in_expr(self): + return (self.parentheses_count - self.format_spec_count) > 0 + + +def _check_fstring_ending(fstring_stack, token, from_start=False): + fstring_end = float('inf') + fstring_index = None + for i, node in enumerate(fstring_stack): + if from_start: + if token.startswith(node.quote): + fstring_index = i + fstring_end = len(node.quote) + else: + continue + else: + try: + end = token.index(node.quote) + except ValueError: + pass + else: + if fstring_index is None or end < fstring_end: + fstring_index = i + fstring_end = end + return fstring_index, fstring_end + + +def _find_fstring_string(fstring_stack, line, lnum, pos): + tos = fstring_stack[-1] + if tos.is_in_expr(): + return '', pos + else: + new_pos = pos + allow_multiline = tos.allow_multiline() + if allow_multiline: + match = fstring_string_multi_line.match(line, pos) + else: + match = fstring_string_single_line.match(line, pos) + if match is None: + string = tos.previous_lines + else: + if not tos.previous_lines: + tos.last_string_start_pos = (lnum, pos) + + string = match.group(0) + for fstring_stack_node in fstring_stack: + try: + string = string[:string.index(fstring_stack_node.quote)] + except ValueError: + pass # The string was not found. + + new_pos += len(string) + if allow_multiline and string.endswith('\n'): + tos.previous_lines += string + string = '' + else: + string = tos.previous_lines + string + + return string, new_pos + + def tokenize(code, version_info, start_pos=(1, 0)): """Generate tokens from a the source code (string).""" lines = split_lines(code, keepends=True) return tokenize_lines(lines, version_info, start_pos=start_pos) +def _print_tokens(func): + """ + A small helper function to help debug the tokenize_lines function. + """ + def wrapper(*args, **kwargs): + for token in func(*args, **kwargs): + print(token) + yield token + + return wrapper + + +# @_print_tokens def tokenize_lines(lines, version_info, start_pos=(1, 0)): """ A heavily modified Python standard library tokenizer. @@ -240,7 +354,7 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): token. This idea comes from lib2to3. The prefix contains all information that is irrelevant for the parser like newlines in parentheses or comments. """ - pseudo_token, single_quoted, triple_quoted, endpats, always_break_tokens, = \ + pseudo_token, single_quoted, triple_quoted, endpats, fstring_pattern_map, always_break_tokens, = \ _get_token_collection(version_info) paren_level = 0 # count parentheses indents = [0] @@ -257,6 +371,7 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): additional_prefix = '' first = True lnum = start_pos[0] - 1 + fstring_stack = [] for line in lines: # loop over lines in stream lnum += 1 pos = 0 @@ -287,6 +402,37 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): continue while pos < max: + if fstring_stack: + string, pos = _find_fstring_string(fstring_stack, line, lnum, pos) + if string: + yield PythonToken( + FSTRING_STRING, string, + fstring_stack[-1].last_string_start_pos, + # Never has a prefix because it can start anywhere and + # include whitespace. + prefix='' + ) + fstring_stack[-1].previous_lines = '' + continue + + if pos == max: + break + + rest = line[pos:] + fstring_index, end = _check_fstring_ending(fstring_stack, rest, from_start=True) + + if fstring_index is not None: + yield PythonToken( + FSTRING_END, + fstring_stack[fstring_index].quote, + (lnum, pos), + prefix=additional_prefix, + ) + additional_prefix = '' + del fstring_stack[fstring_index:] + pos += end + continue + pseudomatch = pseudo_token.match(line, pos) if not pseudomatch: # scan for tokens txt = line[pos:] @@ -311,10 +457,11 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): if new_line and initial not in '\r\n#': new_line = False - if paren_level == 0: + if paren_level == 0 and not fstring_stack: i = 0 while line[i] == '\f': i += 1 + # TODO don't we need to change spos as well? start -= 1 if start > indents[-1]: yield PythonToken(INDENT, '', spos, '') @@ -326,11 +473,33 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): yield PythonToken(DEDENT, '', spos, '') indents.pop() + if fstring_stack: + fstring_index, end = _check_fstring_ending(fstring_stack, token) + if fstring_index is not None: + if end != 0: + yield PythonToken(ERRORTOKEN, token[:end], spos, prefix) + prefix = '' + + yield PythonToken( + FSTRING_END, + fstring_stack[fstring_index].quote, + (lnum, spos[1] + 1), + prefix=prefix + ) + del fstring_stack[fstring_index:] + pos -= len(token) - end + continue + if (initial in numchars or # ordinary number (initial == '.' and token != '.' and token != '...')): yield PythonToken(NUMBER, token, spos, prefix) elif initial in '\r\n': - if not new_line and paren_level == 0: + if any(not f.allow_multiline() for f in fstring_stack): + # Would use fstring_stack.clear, but that's not available + # in Python 2. + fstring_stack[:] = [] + + if not new_line and paren_level == 0 and not fstring_stack: yield PythonToken(NEWLINE, token, spos, prefix) else: additional_prefix = prefix + token @@ -362,8 +531,12 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): break else: # ordinary string yield PythonToken(STRING, token, spos, prefix) + elif token in fstring_pattern_map: # The start of an fstring. + fstring_stack.append(FStringNode(fstring_pattern_map[token])) + yield PythonToken(FSTRING_START, token, spos, prefix) elif is_identifier(initial): # ordinary name if token in always_break_tokens: + fstring_stack[:] = [] paren_level = 0 while True: indent = indents.pop() @@ -378,9 +551,18 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0)): break else: if token in '([{': - paren_level += 1 + if fstring_stack: + fstring_stack[-1].open_parentheses(token) + else: + paren_level += 1 elif token in ')]}': - paren_level -= 1 + if fstring_stack: + fstring_stack[-1].close_parentheses(token) + else: + paren_level -= 1 + elif token == ':' and fstring_stack \ + and fstring_stack[-1].parentheses_count == 1: + fstring_stack[-1].format_spec_count += 1 try: # This check is needed in any case to check if it's a valid diff --git a/pythonFiles/parso/python/tree.py b/pythonFiles/parso/python/tree.py index eb977800a607..e2bf010bdff0 100644 --- a/pythonFiles/parso/python/tree.py +++ b/pythonFiles/parso/python/tree.py @@ -262,6 +262,33 @@ def _get_payload(self): return match.group(2)[:-len(match.group(1))] +class FStringString(Leaf): + """ + f-strings contain f-string expressions and normal python strings. These are + the string parts of f-strings. + """ + type = 'fstring_string' + __slots__ = () + + +class FStringStart(Leaf): + """ + f-strings contain f-string expressions and normal python strings. These are + the string parts of f-strings. + """ + type = 'fstring_start' + __slots__ = () + + +class FStringEnd(Leaf): + """ + f-strings contain f-string expressions and normal python strings. These are + the string parts of f-strings. + """ + type = 'fstring_end' + __slots__ = () + + class _StringComparisonMixin(object): def __eq__(self, other): """ diff --git a/pythonFiles/parso/tree.py b/pythonFiles/parso/tree.py index 72a14945b0f6..5316795be57c 100644 --- a/pythonFiles/parso/tree.py +++ b/pythonFiles/parso/tree.py @@ -55,7 +55,6 @@ def get_previous_sibling(self): Returns the node immediately preceding this node in this parent's children list. If this node does not have a previous sibling, it is None. - None. """ # Can't use index(); we need to test by identity for i, child in enumerate(self.parent.children): @@ -339,7 +338,7 @@ def __repr__(self): class ErrorNode(BaseNode): """ - A node that containes valid nodes/leaves that we're follow by a token that + A node that contains valid nodes/leaves that we're follow by a token that was invalid. This basically means that the leaf after this node is where Python would mark a syntax error. """ diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index b88b82ce65bf..62d75c0ec0ba 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, workspace } from 'vscode'; import { isTestExecution } from './constants'; import { - IAutoCompeteSettings, + IAutoCompleteSettings, IFormattingSettings, ILintingSettings, IPythonSettings, @@ -35,7 +35,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { public devOptions: string[] = []; public linting?: ILintingSettings; public formatting?: IFormattingSettings; - public autoComplete?: IAutoCompeteSettings; + public autoComplete?: IAutoCompleteSettings; public unitTest?: IUnitTestSettings; public terminal?: ITerminalSettings; public sortImports?: ISortImportSettings; @@ -219,9 +219,9 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; + const autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; if (this.autoComplete) { - Object.assign(this.autoComplete, autoCompleteSettings); + Object.assign(this.autoComplete, autoCompleteSettings); } else { this.autoComplete = autoCompleteSettings; } @@ -229,7 +229,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.autoComplete = this.autoComplete ? this.autoComplete : { extraPaths: [], addBrackets: false, - preloadModules: [] + preloadModules: [], + showAdvancedMembers: false }; // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion diff --git a/src/client/common/types.ts b/src/client/common/types.ts index f64617178288..5e16a4557786 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -105,7 +105,7 @@ export interface IPythonSettings { readonly linting?: ILintingSettings; readonly formatting?: IFormattingSettings; readonly unitTest?: IUnitTestSettings; - readonly autoComplete?: IAutoCompeteSettings; + readonly autoComplete?: IAutoCompleteSettings; readonly terminal?: ITerminalSettings; readonly sortImports?: ISortImportSettings; readonly workspaceSymbols?: IWorkspaceSymbolSettings; @@ -194,10 +194,11 @@ export interface IFormattingSettings { yapfPath: string; readonly yapfArgs: string[]; } -export interface IAutoCompeteSettings { +export interface IAutoCompleteSettings { readonly addBrackets: boolean; readonly extraPaths: string[]; readonly preloadModules: string[]; + readonly showAdvancedMembers: boolean; } export interface IWorkspaceSymbolSettings { readonly enabled: boolean; @@ -212,6 +213,9 @@ export interface ITerminalSettings { readonly launchArgs: string[]; readonly activateEnvironment: boolean; } +export interface IPythonAnalysisEngineSettings { + readonly showAdvancedMembers: boolean; +} export const IConfigurationService = Symbol('IConfigurationService'); diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index f0136585786f..7a8e47b62b1c 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -7,8 +7,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import * as pidusage from 'pidusage'; import { setInterval } from 'timers'; -import { Uri } from 'vscode'; -import * as vscode from 'vscode'; +import { CancellationToken, CancellationTokenSource, CompletionItemKind, Disposable, SymbolKind, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { debounce, swallowExceptions } from '../common/decorators'; import '../common/extensions'; @@ -22,96 +21,96 @@ import * as logger from './../common/logger'; const IS_WINDOWS = /^win/.test(process.platform); -const pythonVSCodeTypeMappings = new Map(); -pythonVSCodeTypeMappings.set('none', vscode.CompletionItemKind.Value); -pythonVSCodeTypeMappings.set('type', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('tuple', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dict', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dictionary', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('function', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('lambda', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('generator', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('class', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('instance', vscode.CompletionItemKind.Reference); -pythonVSCodeTypeMappings.set('method', vscode.CompletionItemKind.Method); -pythonVSCodeTypeMappings.set('builtin', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('builtinfunction', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('module', vscode.CompletionItemKind.Module); -pythonVSCodeTypeMappings.set('file', vscode.CompletionItemKind.File); -pythonVSCodeTypeMappings.set('xrange', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('slice', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('traceback', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('frame', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('buffer', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dictproxy', vscode.CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('funcdef', vscode.CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('property', vscode.CompletionItemKind.Property); -pythonVSCodeTypeMappings.set('import', vscode.CompletionItemKind.Module); -pythonVSCodeTypeMappings.set('keyword', vscode.CompletionItemKind.Keyword); -pythonVSCodeTypeMappings.set('constant', vscode.CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('variable', vscode.CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('value', vscode.CompletionItemKind.Value); -pythonVSCodeTypeMappings.set('param', vscode.CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('statement', vscode.CompletionItemKind.Keyword); - -const pythonVSCodeSymbolMappings = new Map(); -pythonVSCodeSymbolMappings.set('none', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('type', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('tuple', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('dict', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('dictionary', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('function', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('lambda', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('generator', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('class', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('instance', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('method', vscode.SymbolKind.Method); -pythonVSCodeSymbolMappings.set('builtin', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('builtinfunction', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('module', vscode.SymbolKind.Module); -pythonVSCodeSymbolMappings.set('file', vscode.SymbolKind.File); -pythonVSCodeSymbolMappings.set('xrange', vscode.SymbolKind.Array); -pythonVSCodeSymbolMappings.set('slice', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('traceback', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('frame', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('buffer', vscode.SymbolKind.Array); -pythonVSCodeSymbolMappings.set('dictproxy', vscode.SymbolKind.Class); -pythonVSCodeSymbolMappings.set('funcdef', vscode.SymbolKind.Function); -pythonVSCodeSymbolMappings.set('property', vscode.SymbolKind.Property); -pythonVSCodeSymbolMappings.set('import', vscode.SymbolKind.Module); -pythonVSCodeSymbolMappings.set('keyword', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('constant', vscode.SymbolKind.Constant); -pythonVSCodeSymbolMappings.set('variable', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('value', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('param', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('statement', vscode.SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('boolean', vscode.SymbolKind.Boolean); -pythonVSCodeSymbolMappings.set('int', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('longlean', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('float', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('complex', vscode.SymbolKind.Number); -pythonVSCodeSymbolMappings.set('string', vscode.SymbolKind.String); -pythonVSCodeSymbolMappings.set('unicode', vscode.SymbolKind.String); -pythonVSCodeSymbolMappings.set('list', vscode.SymbolKind.Array); - -function getMappedVSCodeType(pythonType: string): vscode.CompletionItemKind { +const pythonVSCodeTypeMappings = new Map(); +pythonVSCodeTypeMappings.set('none', CompletionItemKind.Value); +pythonVSCodeTypeMappings.set('type', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('tuple', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dict', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dictionary', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('function', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('lambda', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('generator', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('class', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('instance', CompletionItemKind.Reference); +pythonVSCodeTypeMappings.set('method', CompletionItemKind.Method); +pythonVSCodeTypeMappings.set('builtin', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('builtinfunction', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('module', CompletionItemKind.Module); +pythonVSCodeTypeMappings.set('file', CompletionItemKind.File); +pythonVSCodeTypeMappings.set('xrange', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('slice', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('traceback', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('frame', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('buffer', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dictproxy', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('funcdef', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('property', CompletionItemKind.Property); +pythonVSCodeTypeMappings.set('import', CompletionItemKind.Module); +pythonVSCodeTypeMappings.set('keyword', CompletionItemKind.Keyword); +pythonVSCodeTypeMappings.set('constant', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('variable', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('value', CompletionItemKind.Value); +pythonVSCodeTypeMappings.set('param', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('statement', CompletionItemKind.Keyword); + +const pythonVSCodeSymbolMappings = new Map(); +pythonVSCodeSymbolMappings.set('none', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('type', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('tuple', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('dict', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('dictionary', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('function', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('lambda', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('generator', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('class', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('instance', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('method', SymbolKind.Method); +pythonVSCodeSymbolMappings.set('builtin', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('builtinfunction', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('module', SymbolKind.Module); +pythonVSCodeSymbolMappings.set('file', SymbolKind.File); +pythonVSCodeSymbolMappings.set('xrange', SymbolKind.Array); +pythonVSCodeSymbolMappings.set('slice', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('traceback', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('frame', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('buffer', SymbolKind.Array); +pythonVSCodeSymbolMappings.set('dictproxy', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('funcdef', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('property', SymbolKind.Property); +pythonVSCodeSymbolMappings.set('import', SymbolKind.Module); +pythonVSCodeSymbolMappings.set('keyword', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('constant', SymbolKind.Constant); +pythonVSCodeSymbolMappings.set('variable', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('value', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('param', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('statement', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('boolean', SymbolKind.Boolean); +pythonVSCodeSymbolMappings.set('int', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('longlean', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('float', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('complex', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('string', SymbolKind.String); +pythonVSCodeSymbolMappings.set('unicode', SymbolKind.String); +pythonVSCodeSymbolMappings.set('list', SymbolKind.Array); + +function getMappedVSCodeType(pythonType: string): CompletionItemKind { if (pythonVSCodeTypeMappings.has(pythonType)) { const value = pythonVSCodeTypeMappings.get(pythonType); if (value) { return value; } } - return vscode.CompletionItemKind.Keyword; + return CompletionItemKind.Keyword; } -function getMappedVSCodeSymbol(pythonType: string): vscode.SymbolKind { +function getMappedVSCodeSymbol(pythonType: string): SymbolKind { if (pythonVSCodeSymbolMappings.has(pythonType)) { const value = pythonVSCodeSymbolMappings.get(pythonType); if (value) { return value; } } - return vscode.SymbolKind.Variable; + return SymbolKind.Variable; } export enum CommandType { @@ -131,7 +130,7 @@ commandNames.set(CommandType.Hover, 'tooltip'); commandNames.set(CommandType.Usages, 'usages'); commandNames.set(CommandType.Symbols, 'names'); -export class JediProxy implements vscode.Disposable { +export class JediProxy implements Disposable { private proc?: ChildProcess; private pythonSettings: PythonSettings; private cmdId: number = 0; @@ -151,7 +150,7 @@ export class JediProxy implements vscode.Disposable { public constructor(private extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) { this.workspacePath = workspacePath; - this.pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspacePath)); + this.pythonSettings = PythonSettings.getInstance(Uri.file(workspacePath)); this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; this.logger = serviceContainer.get(ILogger); this.pythonSettings.on('change', () => this.pythonSettingsChangeHandler()); @@ -315,7 +314,8 @@ export class JediProxy implements vscode.Disposable { args.push('custom'); args.push(this.pythonSettings.jediPath); } - if (Array.isArray(this.pythonSettings.autoComplete.preloadModules) && + if (this.pythonSettings.autoComplete && + Array.isArray(this.pythonSettings.autoComplete.preloadModules) && this.pythonSettings.autoComplete.preloadModules.length > 0) { const modules = this.pythonSettings.autoComplete.preloadModules.filter(m => m.trim().length > 0).join(','); args.push(modules); @@ -636,7 +636,8 @@ export class JediProxy implements vscode.Disposable { } private getConfig() { // Add support for paths relative to workspace. - const extraPaths = this.pythonSettings.autoComplete.extraPaths.map(extraPath => { + const extraPaths = this.pythonSettings.autoComplete ? + this.pythonSettings.autoComplete.extraPaths.map(extraPath => { if (path.isAbsolute(extraPath)) { return extraPath; } @@ -644,7 +645,7 @@ export class JediProxy implements vscode.Disposable { return ''; } return path.join(this.workspacePath, extraPath); - }); + }) : []; // Always add workspace path into extra paths. if (typeof this.workspacePath === 'string') { @@ -686,7 +687,7 @@ export interface ICommand { interface IExecutionCommand extends ICommand { id: number; deferred?: Deferred; - token: vscode.CancellationToken; + token: CancellationToken; delay?: number; } @@ -739,9 +740,9 @@ export interface IReference { } export interface IAutoCompleteItem { - type: vscode.CompletionItemKind; - rawType: vscode.CompletionItemKind; - kind: vscode.SymbolKind; + type: CompletionItemKind; + rawType: CompletionItemKind; + kind: SymbolKind; text: string; description: string; raw_docstring: string; @@ -755,8 +756,8 @@ export interface IDefinitionRange { } export interface IDefinition { rawType: string; - type: vscode.CompletionItemKind; - kind: vscode.SymbolKind; + type: CompletionItemKind; + kind: SymbolKind; text: string; fileName: string; container: string; @@ -764,22 +765,22 @@ export interface IDefinition { } export interface IHoverItem { - kind: vscode.SymbolKind; + kind: SymbolKind; text: string; description: string; docstring: string; signature: string; } -export class JediProxyHandler implements vscode.Disposable { - private commandCancellationTokenSources: Map; +export class JediProxyHandler implements Disposable { + private commandCancellationTokenSources: Map; public get JediProxy(): JediProxy { return this.jediProxy; } public constructor(private jediProxy: JediProxy) { - this.commandCancellationTokenSources = new Map(); + this.commandCancellationTokenSources = new Map(); } public dispose() { @@ -788,7 +789,7 @@ export class JediProxyHandler implements vscode.Dispos } } - public sendCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { + public sendCommand(cmd: ICommand, token?: CancellationToken): Promise { const executionCmd = >cmd; executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); @@ -799,7 +800,7 @@ export class JediProxyHandler implements vscode.Dispos } } - const cancellation = new vscode.CancellationTokenSource(); + const cancellation = new CancellationTokenSource(); this.commandCancellationTokenSources.set(cmd.command, cancellation); executionCmd.token = cancellation.token; @@ -810,7 +811,7 @@ export class JediProxyHandler implements vscode.Dispos }); } - public sendCommandNonCancellableCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { + public sendCommandNonCancellableCommand(cmd: ICommand, token?: CancellationToken): Promise { const executionCmd = >cmd; executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); if (token) { diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 12dad261c39b..cf1014296519 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -1,8 +1,15 @@ 'use strict'; import { EOL } from 'os'; -import * as vscode from 'vscode'; -import { CancellationToken, Position, SignatureHelp, TextDocument } from 'vscode'; +import { + CancellationToken, + ParameterInformation, + Position, + SignatureHelp, + SignatureHelpProvider, + SignatureInformation, + TextDocument +} from 'vscode'; import { JediFactory } from '../languageServices/jediProxyFactory'; import { captureTelemetry } from '../telemetry'; import { SIGNATURE } from '../telemetry/constants'; @@ -45,9 +52,9 @@ function extractParamDocString(paramName: string, docString: string): string { return paramDocString.trim(); } -export class PythonSignatureProvider implements vscode.SignatureHelpProvider { +export class PythonSignatureProvider implements SignatureHelpProvider { public constructor(private jediFactory: JediFactory) { } - private static parseData(data: proxy.IArgumentsResult): vscode.SignatureHelp { + private static parseData(data: proxy.IArgumentsResult): SignatureHelp { if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { const signature = new SignatureHelp(); signature.activeSignature = 0; @@ -60,29 +67,36 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { // Some functions do not come with parameter docs let label: string; let documentation: string; - const validParamInfo = def.params && def.params.length > 0 && def.docstring.startsWith(`${def.name}(`); + const validParamInfo = def.params && def.params.length > 0 && def.docstring && def.docstring.startsWith(`${def.name}(`); if (validParamInfo) { const docLines = def.docstring.splitLines(); label = docLines.shift().trim(); documentation = docLines.join(EOL).trim(); } else { - label = def.description; - documentation = def.docstring; + if (def.params && def.params.length > 0) { + label = `${def.name}(${def.params.map(p => p.name).join(', ')})`; + documentation = def.docstring; + } else { + label = def.description; + documentation = def.docstring; + } } - const sig = { + // tslint:disable-next-line:no-object-literal-type-assertion + const sig = { label, documentation, parameters: [] }; - if (validParamInfo) { + if (def.params && def.params.length) { sig.parameters = def.params.map(arg => { if (arg.docstring.length === 0) { arg.docstring = extractParamDocString(arg.name, def.docstring); } - return { + // tslint:disable-next-line:no-object-literal-type-assertion + return { documentation: arg.docstring.length > 0 ? arg.docstring : arg.description, label: arg.name.trim() }; diff --git a/src/test/definitions/hover.ptvs.test.ts b/src/test/definitions/hover.ptvs.test.ts index 8c3b981ca4bb..d2a456efd4bd 100644 --- a/src/test/definitions/hover.ptvs.test.ts +++ b/src/test/definitions/hover.ptvs.test.ts @@ -49,10 +49,15 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); assert.equal(def[0].contents.length, 1, 'Invalid content items'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 2, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'obj.method1: method method1 of one.Class1 objects', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'This is method1', 'function signature line #2 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'obj.method1:', + 'method method1 of one.Class1 objects', + '```html', + 'This is method1', + '```' + ]; + verifySignatureLines(actual, expected); }); test('Across files', async () => { @@ -61,10 +66,15 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,0', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 2, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'two.ct().fun: method fun of two.ct objects', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'This is fun', 'function signature line #2 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'two.ct().fun:', + 'method fun of two.ct objects', + '```html', + 'This is fun', + '```' + ]; + verifySignatureLines(actual, expected); }); test('With Unicode Characters', async () => { @@ -73,13 +83,18 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,0', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 5, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'Foo.bar: def four.Foo.bar()', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), '说明 - keep this line, it works', 'function signature line #2 is incorrect'); - assert.equal(lines[2].trim(), 'delete following line, it works', 'function signature line #3 is incorrect'); - assert.equal(lines[3].trim(), '如果存在需要等待审批或正在执行的任务,将不刷新页面', 'function signature line #4 is incorrect'); - assert.equal(lines[4].trim(), 'declared in Foo', 'function signature line #5 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'Foo.bar:', + 'four.Foo.bar() -> bool', + '```html', + '说明 - keep this line, it works', + 'delete following line, it works', + '如果存在需要等待审批或正在执行的任务,将不刷新页面', + '```', + 'declared in Foo' + ]; + verifySignatureLines(actual, expected); }); test('Across files with Unicode Characters', async () => { @@ -88,11 +103,16 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,0', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 3, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'four.showMessage: def four.showMessage()', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.', 'function signature line #2 is incorrect'); - assert.equal(lines[2].trim(), 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', 'function signature line #3 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'four.showMessage:', + 'four.showMessage()', + '```html', + 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.', + 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', + '```' + ]; + verifySignatureLines(actual, expected); }); test('Nothing for keywords (class)', async () => { @@ -111,10 +131,22 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,7', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 9, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'misc.Random: class misc.Random(_random.Random)', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'Random number generator base class used by bound module functions.', 'function signature line #2 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'misc.Random:', + 'class misc.Random(_random.Random)', + 'Random number generator base class used by bound module functions.', + '```html', + 'Used to instantiate instances of Random to get generators that don\'t', + 'share state.', + 'Class Random can also be subclassed if you want to use a different basic', + 'generator of your own devising: in that case, override the following', + 'methods: random(), seed(), getstate(), and setstate().', + 'Optionally, implement a getrandbits() method so that randrange()', + 'can cover arbitrarily large ranges.', + '```' + ]; + verifySignatureLines(actual, expected); }); test('Highlight Method', async () => { @@ -123,10 +155,13 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,0', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 2, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'rnd2.randint: method randint of misc.Random objects -> int', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'Return random integer in range [a, b], including both end points.', 'function signature line #2 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'rnd2.randint:', + 'method randint of misc.Random objects -> int', + 'Return random integer in range [a, b], including both end points.' + ]; + verifySignatureLines(actual, expected); }); test('Highlight Function', async () => { @@ -135,11 +170,14 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,6', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 3, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'math.acos: built-in function acos(x)', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'acos(x)', 'function signature line #2 is incorrect'); - assert.equal(lines[2].trim(), 'Return the arc cosine (measured in radians) of x.', 'function signature line #3 is incorrect'); + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'math.acos:', + 'built-in function acos(x)', + 'acos(x)', + 'Return the arc cosine (measured in radians) of x.' + ]; + verifySignatureLines(actual, expected); }); test('Highlight Multiline Method Signature', async () => { @@ -148,11 +186,16 @@ suite('Hover Definition (Analysis Engine)', () => { assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,4', 'Start position is incorrect'); assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); - const lines = normalizeMarkedString(def[0].contents[0]).splitLines(); - assert.equal(lines.length, 3, 'incorrect number of lines'); - assert.equal(lines[0].trim(), 'misc.Thread: class misc.Thread(_Verbose)', 'function signature line #1 is incorrect'); - assert.equal(lines[1].trim(), 'A class that represents a thread of control.', 'function signature line #2 is incorrect'); - + const actual = normalizeMarkedString(def[0].contents[0]).splitLines(); + const expected = [ + 'misc.Thread:', + 'class misc.Thread(_Verbose)', + 'A class that represents a thread of control.', + '```html', + 'This class can be safely subclassed in a limited fashion.', + '```' + ]; + verifySignatureLines(actual, expected); }); test('Variable', async () => { @@ -181,4 +224,11 @@ suite('Hover Definition (Analysis Engine)', () => { assert.fail(contents, '', '\'Return a capitalized version of S/Return a copy of the string S with only its first character\' message missing', 'compare'); } }); + + function verifySignatureLines(actual: string[], expected: string[]) { + assert.equal(actual.length, expected.length, 'incorrect number of lines'); + for (let i = 0; i < actual.length; i += 1) { + assert.equal(actual[i].trim(), expected[i], `signature line ${i + 1} is incorrect`); + } + } }); diff --git a/src/test/signature/signature.jedi.test.ts b/src/test/signature/signature.jedi.test.ts index 1ab80cce964d..0d3a0b5ed90b 100644 --- a/src/test/signature/signature.jedi.test.ts +++ b/src/test/signature/signature.jedi.test.ts @@ -74,13 +74,15 @@ suite('Signatures (Jedi)', () => { new SignatureHelpResult(0, 3, 0, 0, null), new SignatureHelpResult(0, 4, 0, 0, null), new SignatureHelpResult(0, 5, 0, 0, null), - new SignatureHelpResult(0, 6, 1, 0, 'start'), - new SignatureHelpResult(0, 7, 1, 0, 'start'), - new SignatureHelpResult(0, 8, 1, 1, 'stop'), - new SignatureHelpResult(0, 9, 1, 1, 'stop'), - new SignatureHelpResult(0, 10, 1, 1, 'stop'), - new SignatureHelpResult(0, 11, 1, 2, 'step'), - new SignatureHelpResult(1, 0, 1, 2, 'step') + new SignatureHelpResult(0, 6, 1, 0, 'stop'), + new SignatureHelpResult(0, 7, 1, 0, 'stop') + // new SignatureHelpResult(0, 6, 1, 0, 'start'), + // new SignatureHelpResult(0, 7, 1, 0, 'start'), + // new SignatureHelpResult(0, 8, 1, 1, 'stop'), + // new SignatureHelpResult(0, 9, 1, 1, 'stop'), + // new SignatureHelpResult(0, 10, 1, 1, 'stop'), + // new SignatureHelpResult(0, 11, 1, 2, 'step'), + // new SignatureHelpResult(1, 0, 1, 2, 'step') ]; const document = await openDocument(path.join(autoCompPath, 'basicSig.py')); diff --git a/src/test/signature/signature.ptvs.test.ts b/src/test/signature/signature.ptvs.test.ts index 68720e33cde1..ad8e58508342 100644 --- a/src/test/signature/signature.ptvs.test.ts +++ b/src/test/signature/signature.ptvs.test.ts @@ -74,14 +74,13 @@ suite('Signatures (Analysis Engine)', () => { new SignatureHelpResult(0, 3, 1, -1, null), new SignatureHelpResult(0, 4, 1, -1, null), new SignatureHelpResult(0, 5, 1, -1, null), - new SignatureHelpResult(0, 6, 1, 0, 'stop'), - new SignatureHelpResult(0, 7, 1, 0, 'stop') - // https://github.com/Microsoft/PTVS/issues/3869 - // new SignatureHelpResult(0, 8, 1, 1, 'stop'), - // new SignatureHelpResult(0, 9, 1, 1, 'stop'), - // new SignatureHelpResult(0, 10, 1, 1, 'stop'), - // new SignatureHelpResult(0, 11, 1, 2, 'step'), - // new SignatureHelpResult(1, 0, 1, 2, 'step') + new SignatureHelpResult(0, 6, 1, 0, 'start'), + new SignatureHelpResult(0, 7, 1, 0, 'start'), + new SignatureHelpResult(0, 8, 1, 1, 'stop'), + new SignatureHelpResult(0, 9, 1, 1, 'stop'), + new SignatureHelpResult(0, 10, 1, 1, 'stop'), + new SignatureHelpResult(0, 11, 1, 2, 'step'), + new SignatureHelpResult(1, 0, 1, 2, 'step') ]; const document = await openDocument(path.join(autoCompPath, 'basicSig.py'));