1
0
forked from VimPlug/jedi

Improve Environment

It only takes `executable` and gets all the information from the
subprocess directly.

Fixes https://github.com/davidhalter/jedi/issues/1107.
This commit is contained in:
Daniel Hahler
2018-04-27 10:05:46 +02:00
committed by Dave Halter
parent f6bc166ea7
commit 2fc91ceb64
3 changed files with 69 additions and 74 deletions

View File

@@ -3,14 +3,12 @@ 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. static analysis. The Python binary in that environment is going to be executed.
""" """
import os import os
import re
import sys import sys
import hashlib import hashlib
import filecmp import filecmp
from subprocess import PIPE
from collections import namedtuple from collections import namedtuple
from jedi._compatibility import GeneralizedPopen, which from jedi._compatibility import highest_pickle_protocol, which
from jedi.cache import memoize_method, time_cache from jedi.cache import memoize_method, time_cache
from jedi.evaluate.compiled.subprocess import get_subprocess, \ from jedi.evaluate.compiled.subprocess import get_subprocess, \
EvaluatorSameProcess, EvaluatorSubprocess EvaluatorSameProcess, EvaluatorSubprocess
@@ -46,49 +44,52 @@ class _BaseEnvironment(object):
return self._hash return self._hash
def _get_info():
return (
sys.executable,
sys.prefix,
sys.version_info[:3],
)
class Environment(_BaseEnvironment): class Environment(_BaseEnvironment):
""" """
This class is supposed to be created by internal Jedi architecture. You This class is supposed to be created by internal Jedi architecture. You
should not create it directly. Please use create_environment or the other should not create it directly. Please use create_environment or the other
functions instead. It is then returned by that function. functions instead. It is then returned by that function.
""" """
def __init__(self, path, executable): def __init__(self, executable):
self.path = os.path.abspath(path) try:
""" self._subprocess = get_subprocess(executable)
The path to an environment, matches ``sys.prefix``. info = self._subprocess._send(None, _get_info)
""" except Exception as exc:
self.executable = os.path.abspath(executable) raise InvalidPythonEnvironment(
"Could not get version information for %r: %r" % (
executable,
exc))
self.executable = info[0]
""" """
The Python executable, matches ``sys.executable``. The Python executable, matches ``sys.executable``.
""" """
self.version_info = self._get_version() self.path = info[1]
"""
The path to an environment, matches ``sys.prefix``.
"""
self.version_info = _VersionInfo(*info[2])
""" """
Like ``sys.version_info``. A tuple to show the current Environment's Like ``sys.version_info``. A tuple to show the current Environment's
Python version. Python version.
""" """
def _get_version(self): # Adjust pickle protocol according to host and client version.
try: self._subprocess._pickle_protocol = highest_pickle_protocol([
process = GeneralizedPopen([self.executable, '--version'], stdout=PIPE, stderr=PIPE) sys.version_info, self.version_info])
stdout, stderr = process.communicate()
retcode = process.poll()
if retcode:
raise InvalidPythonEnvironment(
"Exited with %d (stdout=%r, stderr=%r)" % (
retcode, stdout, stderr))
except OSError as exc:
raise InvalidPythonEnvironment(
"Could not get version information: %r" % exc)
# Until Python 3.4 wthe version string is part of stderr, after that # py2 sends bytes via pickle apparently?!
# stdout. if self.version_info.major == 2:
output = stdout + stderr self.executable = self.executable.decode()
match = re.match(br'Python (\d+)\.(\d+)\.(\d+)', output) self.path = self.path.decode()
if match is None:
raise InvalidPythonEnvironment("--version not working")
return _VersionInfo(*[int(m) for m in match.groups()])
def __repr__(self): def __repr__(self):
version = '.'.join(str(i) for i in self.version_info) version = '.'.join(str(i) for i in self.version_info)
@@ -98,7 +99,7 @@ class Environment(_BaseEnvironment):
return EvaluatorSubprocess(evaluator, self._get_subprocess()) return EvaluatorSubprocess(evaluator, self._get_subprocess())
def _get_subprocess(self): def _get_subprocess(self):
return get_subprocess(self.executable, self.version_info) return get_subprocess(self.executable)
@memoize_method @memoize_method
def get_sys_path(self): def get_sys_path(self):
@@ -118,10 +119,9 @@ class Environment(_BaseEnvironment):
class SameEnvironment(Environment): class SameEnvironment(Environment):
def __init__(self): def __init__(self):
super(SameEnvironment, self).__init__(sys.prefix, sys.executable) self.executable = sys.executable
self.path = sys.prefix
def _get_version(self): self.version_info = _VersionInfo(*sys.version_info[:3])
return _VersionInfo(*sys.version_info[:3])
class InterpreterEnvironment(_BaseEnvironment): class InterpreterEnvironment(_BaseEnvironment):
@@ -222,7 +222,7 @@ def find_virtualenvs(paths=None, **kwargs):
try: try:
executable = _get_executable_path(path, safe=safe) executable = _get_executable_path(path, safe=safe)
yield Environment(path, executable) yield Environment(executable)
except InvalidPythonEnvironment: except InvalidPythonEnvironment:
pass pass
@@ -246,23 +246,6 @@ def find_system_environments():
pass 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 # TODO: this function should probably return a list of environments since
# multiple Python installations can be found on a system for the same version. # multiple Python installations can be found on a system for the same version.
def get_system_environment(version): def get_system_environment(version):
@@ -277,17 +260,17 @@ def get_system_environment(version):
if exe: if exe:
if exe == sys.executable: if exe == sys.executable:
return SameEnvironment() return SameEnvironment()
return Environment(_get_python_prefix(exe), exe) return Environment(exe)
if os.name == 'nt': if os.name == 'nt':
for prefix, exe in _get_executables_from_windows_registry(version): for exe in _get_executables_from_windows_registry(version):
return Environment(prefix, exe) return Environment(exe)
raise InvalidPythonEnvironment("Cannot find executable python%s." % version) raise InvalidPythonEnvironment("Cannot find executable python%s." % version)
def create_environment(path, safe=True): def create_environment(path, safe=True):
""" """
Make it possible to manually create an environment by specifying a Make it possible to manually create an Environment object by specifying a
Virtualenv path or an executable path. Virtualenv path or an executable path.
:raises: :exc:`.InvalidPythonEnvironment` :raises: :exc:`.InvalidPythonEnvironment`
@@ -295,8 +278,8 @@ def create_environment(path, safe=True):
""" """
if os.path.isfile(path): if os.path.isfile(path):
_assert_safe(path, safe) _assert_safe(path, safe)
return Environment(_get_python_prefix(path), path) return Environment(path)
return Environment(path, _get_executable_path(path, safe=safe)) return Environment(_get_executable_path(path, safe=safe))
def _get_executable_path(path, safe=True): def _get_executable_path(path, safe=True):
@@ -318,9 +301,9 @@ def _get_executable_path(path, safe=True):
def _get_executables_from_windows_registry(version): def _get_executables_from_windows_registry(version):
# The winreg module is named _winreg on Python 2. # The winreg module is named _winreg on Python 2.
try: try:
import winreg import winreg
except ImportError: except ImportError:
import _winreg as winreg import _winreg as winreg
# TODO: support Python Anaconda. # TODO: support Python Anaconda.
sub_keys = [ sub_keys = [
@@ -337,7 +320,7 @@ def _get_executables_from_windows_registry(version):
prefix = winreg.QueryValueEx(key, '')[0] prefix = winreg.QueryValueEx(key, '')[0]
exe = os.path.join(prefix, 'python.exe') exe = os.path.join(prefix, 'python.exe')
if os.path.isfile(exe): if os.path.isfile(exe):
yield prefix, exe yield exe
except WindowsError: except WindowsError:
pass pass

View File

@@ -17,7 +17,7 @@ import traceback
from functools import partial 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, highest_pickle_protocol, GeneralizedPopen pickle_dump, pickle_load, GeneralizedPopen
from jedi.cache import memoize_method from jedi.cache import memoize_method
from jedi.evaluate.compiled.subprocess import functions from jedi.evaluate.compiled.subprocess import functions
from jedi.evaluate.compiled.access import DirectObjectAccess, AccessPath, \ from jedi.evaluate.compiled.access import DirectObjectAccess, AccessPath, \
@@ -29,12 +29,11 @@ _subprocesses = {}
_MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py') _MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py')
def get_subprocess(executable, version): def get_subprocess(executable):
try: try:
return _subprocesses[executable] return _subprocesses[executable]
except KeyError: except KeyError:
sub = _subprocesses[executable] = _CompiledSubprocess(executable, sub = _subprocesses[executable] = _CompiledSubprocess(executable)
version)
return sub return sub
@@ -125,12 +124,21 @@ class EvaluatorSubprocess(_EvaluatorProcess):
class _CompiledSubprocess(object): class _CompiledSubprocess(object):
_crashed = False _crashed = False
# Start with 2, gets set after _get_info.
_pickle_protocol = 2
def __init__(self, executable, version): def __init__(self, executable):
self._executable = executable self._executable = executable
self._evaluator_deletion_queue = queue.deque() self._evaluator_deletion_queue = queue.deque()
self._pickle_protocol = highest_pickle_protocol([sys.version_info,
version]) def __repr__(self):
pid = os.getpid()
return '<_CompiledSubprocess _executable=%r, _pickle_protocol=%r, _crashed=%r, pid=%r>' % (
self._executable,
self._pickle_protocol,
self._crashed,
pid,
)
@property @property
@memoize_method @memoize_method
@@ -140,7 +148,7 @@ class _CompiledSubprocess(object):
self._executable, self._executable,
_MAIN_PATH, _MAIN_PATH,
os.path.dirname(os.path.dirname(parso_path)), os.path.dirname(os.path.dirname(parso_path)),
str(self._pickle_protocol) '.'.join(str(x) for x in sys.version_info[:3]),
) )
return GeneralizedPopen( return GeneralizedPopen(
args, args,

View File

@@ -1,5 +1,5 @@
import sys
import os import os
import sys
def _get_paths(): def _get_paths():
@@ -45,7 +45,11 @@ else:
load('jedi') load('jedi')
from jedi.evaluate.compiled import subprocess # NOQA from jedi.evaluate.compiled import subprocess # NOQA
from jedi._compatibility import highest_pickle_protocol # noqa: E402
# Retrieve the pickle protocol. # Retrieve the pickle protocol.
pickle_protocol = int(sys.argv[2]) host_sys_version = [int(x) for x in sys.argv[2].split('.')]
pickle_protocol = highest_pickle_protocol([sys.version_info, host_sys_version])
# And finally start the client. # And finally start the client.
subprocess.Listener(pickle_protocol).listen() subprocess.Listener(pickle_protocol=pickle_protocol).listen()