diff --git a/jedi/__init__.py b/jedi/__init__.py index 739fe4c3..dee5ae54 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -42,3 +42,4 @@ from jedi.api import Script, Interpreter, set_debug_function, \ preload_module, names from jedi import settings from jedi.api.environment import find_virtualenvs, find_python_environments +from jedi.api.exceptions import InternalError diff --git a/jedi/api/exceptions.py b/jedi/api/exceptions.py new file mode 100644 index 00000000..33cca2a4 --- /dev/null +++ b/jedi/api/exceptions.py @@ -0,0 +1,2 @@ +class InternalError(Exception): + pass diff --git a/jedi/evaluate/compiled/subprocess/__init__.py b/jedi/evaluate/compiled/subprocess/__init__.py index 4c251076..9e4f3d05 100644 --- a/jedi/evaluate/compiled/subprocess/__init__.py +++ b/jedi/evaluate/compiled/subprocess/__init__.py @@ -19,6 +19,7 @@ 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 _PICKLE_PROTOCOL = 2 @@ -141,25 +142,6 @@ class _Subprocess(object): # stderr=subprocess.PIPE ) - def _send(self, evaluator_id, function, args=(), kwargs={}): - if not is_py3: - # Python 2 compatibility - kwargs = {force_unicode(key): value for key, value in kwargs.items()} - - data = evaluator_id, function, args, kwargs - pickle.dump(data, self._process.stdin, protocol=_PICKLE_PROTOCOL) - self._process.stdin.flush() - is_exception, result = _pickle_load(self._process.stdout) - if is_exception: - raise result - return result - - def terminate(self): - self._process.terminate() - - def kill(self): - self._process.kill() - class _CompiledSubprocess(_Subprocess): def __init__(self, executable): @@ -170,6 +152,7 @@ class _CompiledSubprocess(_Subprocess): os.path.dirname(os.path.dirname(parso_path)) ) ) + self._executable = executable self._evaluator_deletion_queue = queue.deque() def run(self, evaluator, function, args=(), kwargs={}): @@ -188,6 +171,38 @@ class _CompiledSubprocess(_Subprocess): def get_sys_path(self): return self._send(None, functions.get_sys_path, (), {}) + def kill(self): + 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() + + def _send(self, evaluator_id, function, args=(), kwargs={}): + if not is_py3: + # Python 2 compatibility + kwargs = {force_unicode(key): value for key, value in kwargs.items()} + + data = evaluator_id, function, args, kwargs + pickle.dump(data, self._process.stdin, protocol=_PICKLE_PROTOCOL) + self._process.stdin.flush() + try: + is_exception, result = _pickle_load(self._process.stdout) + except EOFError: + self.kill() + raise InternalError("The subprocess crashed.") + + if is_exception: + raise result + return result + def delete_evaluator(self, evaluator_id): """ Currently we are not deleting evalutors instantly. They only get diff --git a/jedi/evaluate/compiled/subprocess/functions.py b/jedi/evaluate/compiled/subprocess/functions.py index 598fe321..543e7708 100644 --- a/jedi/evaluate/compiled/subprocess/functions.py +++ b/jedi/evaluate/compiled/subprocess/functions.py @@ -66,6 +66,13 @@ 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 _get_init_path(directory_path): """ The __init__ file can be searched in a directory. If found return it, else diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index 63300cf5..6bf2094b 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -1,5 +1,6 @@ import pytest +import jedi from jedi._compatibility import py_version from jedi.api.environment import Environment, get_default_environment, \ InvalidPythonEnvironment, find_python_environments @@ -45,3 +46,12 @@ def test_load_module(evaluator): assert access_handle.get_api_type() == 'module' with pytest.raises(AttributeError): access_handle.py__mro__() + + +def test_error_in_environment(evaluator, Script): + # Provoke an error to show how Jedi can recover from it. + with pytest.raises(jedi.InternalError): + evaluator.compiled_subprocess._test_raise_error(KeyboardInterrupt) + # Jedi should still work. + def_, = Script('str').goto_definitions() + assert def_.name == 'str'