diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 2b6b3932..e6eabe43 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -322,13 +322,13 @@ 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. """ - import pickle class NewUnpickler(pickle._Unpickler): dispatch = dict(pickle._Unpickler.dispatch) @@ -381,3 +381,17 @@ if sys.version_info[:2] == (3, 3): pickle.Unpickler = NewUnpickler pickle.load = load pickle.loads = loads + + +_PICKLE_PROTOCOL = 2 + + +def pickle_load(file): + if is_py3: + return pickle.load(file, encoding='bytes') + else: + return pickle.load(file) + + +def pickle_dump(data, file): + pickle.dump(data, file, protocol=_PICKLE_PROTOCOL) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index d8e3f3f8..5b0d52b8 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -98,7 +98,10 @@ class InterpreterEnvironment(_BaseEnvironment): def _get_virtual_env_from_var(): var = os.environ.get('VIRTUAL_ENV') - if var is not None and var != sys.prefix: + if var is not None: + if var == sys.prefix: + return DefaultEnvironment() + try: return create_environment(var) except InvalidPythonEnvironment: @@ -179,6 +182,11 @@ def create_environment(path): return Environment(path, _get_executable_path(path, safe=False)) +def from_executable(executable): + path = os.path.dirname(os.path.dirname(executable)) + return Environment(path, executable) + + def _get_executable_path(path, safe=True): """ Returns None if it's not actually a virtual env. diff --git a/jedi/api/exceptions.py b/jedi/api/exceptions.py index 33cca2a4..99cebdb7 100644 --- a/jedi/api/exceptions.py +++ b/jedi/api/exceptions.py @@ -1,2 +1,10 @@ -class InternalError(Exception): +class _JediError(Exception): + pass + + +class InternalError(_JediError): + pass + + +class WrongVersion(_JediError): pass diff --git a/jedi/api/project.py b/jedi/api/project.py new file mode 100644 index 00000000..772eb313 --- /dev/null +++ b/jedi/api/project.py @@ -0,0 +1,111 @@ +import os +import json + +from jedi._compatibility import FileNotFoundError +from jedi.api.environment import DefaultEnvironment, \ + get_default_environment, from_executable +from jedi.api.exceptions import WrongVersion + +_CONFIG_FOLDER = '.jedi' +_CONTAINS_POTENTIAL_PROJECT = 'setup.py', '.git', '.hg', 'MANIFEST.in' + +_SERIALIZER_VERSION = 1 + + +class Project(object): + _serializer_ignore_attributes = ('_environment',) + _environment = None + _executable = 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) + if self._executable is not None: + self._environment = from_executable(self._executable) + 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. + """ + def py2_comp(path, environment=None, sys_path=None): + self._path = path + if isinstance(environment, DefaultEnvironment): + self._environment = environment + self._executable = environment._executable + + self._sys_path = sys_path + + py2_comp(path, **kwargs) + + def _get_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() + + return environment.get_sys_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_default_environment() + + return self._environment + + def __setstate__(self, state): + self.__dict__.update(state) + + +def _is_potential_project(path): + for name in _CONTAINS_POTENTIAL_PROJECT: + if os.path.exists(os.path.join(path)): + return True + return False + + +def get_default_project(): + previous = None + curdir = dir = os.path.realpath(os.curdir()) + probable_path = None + while dir != previous: + try: + return Project.load(dir) + except FileNotFoundError: + pass + + if probable_path is None and _is_potential_project(dir): + probable_path = dir + + previous = dir + dir = os.path.dirname(dir) + else: + if probable_path is not None: + # TODO search for setup.py etc + return Project(probable_path) + return Project(curdir) diff --git a/jedi/evaluate/compiled/subprocess/__init__.py b/jedi/evaluate/compiled/subprocess/__init__.py index 20367702..0bc4c450 100644 --- a/jedi/evaluate/compiled/subprocess/__init__.py +++ b/jedi/evaluate/compiled/subprocess/__init__.py @@ -13,18 +13,15 @@ import subprocess import socket import errno import weakref -import pickle from functools import partial -from jedi._compatibility import queue, is_py3, force_unicode +from jedi._compatibility import queue, is_py3, force_unicode, pickle_dump, pickle_load 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 - _subprocesses = {} _MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py') @@ -38,13 +35,6 @@ def get_subprocess(executable): return sub -def _pickle_load(file): - if is_py3: - return pickle.load(file, encoding='bytes') - else: - return pickle.load(file) - - def _get_function(name): return getattr(functions, name) @@ -193,7 +183,7 @@ class _CompiledSubprocess(object): 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) + pickle_dump(data, self._process.stdin) try: self._process.stdin.flush() except socket.error as e: @@ -205,7 +195,7 @@ class _CompiledSubprocess(object): raise InternalError("The subprocess was killed. Maybe out of memory?") try: - is_exception, result = _pickle_load(self._process.stdout) + is_exception, result = pickle_load(self._process.stdout) except EOFError: self.kill() raise InternalError("The subprocess crashed.") @@ -274,7 +264,7 @@ class Listener(object): while True: try: - payload = _pickle_load(stdin) + payload = pickle_load(stdin) except EOFError: # It looks like the parent process closed. Don't make a big fuss # here and just exit. @@ -286,7 +276,7 @@ class Listener(object): #print_to_stderr(traceback.format_exc()) result = True, e - pickle.dump(result, file=stdout, protocol=_PICKLE_PROTOCOL) + pickle_dump(result, file=stdout) stdout.flush()