Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Send Spyder python path to introspection plugins #4962

Merged
merged 12 commits into from
Aug 31, 2017
97 changes: 97 additions & 0 deletions spyder/plugins/tests/test_editor_introspection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
#
"""Tests for the Editor plugin."""

# Third party imports
import pytest
import os
import os.path as osp
from flaky import flaky

try:
from unittest.mock import Mock
except ImportError:
from mock import Mock # Python 2

from qtpy.QtWidgets import QWidget, QApplication
from qtpy.QtCore import Qt

from spyder.utils.introspection.jedi_plugin import JEDI_010
from spyder.utils.qthelpers import qapplication

# Location of this file
LOCATION = osp.realpath(osp.join(os.getcwd(), osp.dirname(__file__)))


@pytest.fixture
def setup_editor(qtbot, monkeypatch):
"""Set up the Editor plugin."""
app = qapplication()
os.environ['SPY_TEST_USE_INTROSPECTION'] = 'True'
monkeypatch.setattr('spyder.dependencies', Mock())
from spyder.plugins.editor import Editor

monkeypatch.setattr('spyder.plugins.editor.add_actions', Mock())

class MainMock(QWidget):
def __getattr__(self, attr):
if attr.endswith('actions'):
return []
else:
return Mock()

def get_spyder_pythonpath(*args):
return []

editor = Editor(MainMock())
qtbot.addWidget(editor)
editor.show()
editor.new(fname="test.py", text="")
editor.introspector.set_editor_widget(editor.editorstacks[0])

yield editor, qtbot
# teardown
os.environ['SPY_TEST_USE_INTROSPECTION'] = 'False'
editor.introspector.plugin_manager.close()


@pytest.mark.skipif(os.environ.get('CI', None) is not None,
reason="This test fails too much in the CI :(")
@pytest.mark.skipif(not JEDI_010,
reason="This feature is only supported in jedy >= 0.10")
def test_introspection(setup_editor):
"""Validate changing path in introspection plugins."""
editor, qtbot = setup_editor
code_editor = editor.get_focus_widget()
completion = code_editor.completion_widget

# Set cursor to start
code_editor.go_to_line(1)

# Complete fr --> from
qtbot.keyClicks(code_editor, 'fr')
qtbot.wait(5000)

# press tab and get completions
with qtbot.waitSignal(completion.sig_show_completions,
timeout=5000) as sig:
qtbot.keyPress(code_editor, Qt.Key_Tab)
assert "from" in sig.args[0]

# enter should accept first completion
qtbot.keyPress(completion, Qt.Key_Enter, delay=1000)
assert code_editor.toPlainText() == 'from\n'

# Modify PYTHONPATH
editor.introspector.change_extra_path([LOCATION])
qtbot.wait(10000)

# Type 'from test' and try to get completion
with qtbot.waitSignal(completion.sig_show_completions,
timeout=10000) as sig:
qtbot.keyClicks(code_editor, ' test_')
qtbot.keyPress(code_editor, Qt.Key_Tab)
assert "test_editor_introspection" in sig.args[0]
11 changes: 9 additions & 2 deletions spyder/utils/introspection/jedi_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
except ImportError:
jedi = None

JEDI_010 = programs.is_module_installed('jedi', '>=0.10.0')


class JediPlugin(IntrospectionPlugin):
"""
Expand Down Expand Up @@ -172,8 +174,13 @@ def get_jedi_object(self, func_name, info, use_filename=True):
filename = None

try:
script = jedi.Script(info['source_code'], info['line_num'],
info['column'], filename)
if JEDI_010:
script = jedi.api.Script(info['source_code'], info['line_num'],
info['column'], filename,
sys_path=info['sys_path'])
else:
script = jedi.api.Script(info['source_code'], info['line_num'],
info['column'], filename)
func = getattr(script, func_name)
val = func()
except Exception as e:
Expand Down
20 changes: 13 additions & 7 deletions spyder/utils/introspection/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import print_function
from collections import OrderedDict
import time
import sys

# Third party imports
from qtpy.QtCore import QObject, QTimer, Signal
Expand Down Expand Up @@ -43,13 +44,13 @@ class PluginManager(QObject):

introspection_complete = Signal(object)

def __init__(self, executable, extra_path=None):
def __init__(self, executable):

super(PluginManager, self).__init__()
plugins = OrderedDict()
for name in PLUGINS:
try:
plugin = PluginClient(name, executable, extra_path=extra_path)
plugin = PluginClient(name, executable)
plugin.run()
except Exception as e:
debug_print('Introspection Plugin Failed: %s' % name)
Expand Down Expand Up @@ -163,13 +164,16 @@ class IntrospectionManager(QObject):
send_to_help = Signal(str, str, str, str, bool)
edit_goto = Signal(str, int, str)

def __init__(self, executable=None, extra_path=None):
def __init__(self, executable=None, extra_path=[]):
super(IntrospectionManager, self).__init__()
self.editor_widget = None
self.pending = None
self.sys_path = sys.path[:]
self.extra_path = extra_path
if self.extra_path:
self.sys_path.extend(extra_path)
self.executable = executable
self.plugin_manager = PluginManager(executable, extra_path)
self.plugin_manager = PluginManager(executable)
self.plugin_manager.introspection_complete.connect(
self._introspection_complete)

Expand All @@ -178,14 +182,15 @@ def change_executable(self, executable):
self._restart_plugin()

def change_extra_path(self, extra_path):
"""Change extra_path and update sys_path."""
if extra_path != self.extra_path:
self.extra_path = extra_path
self._restart_plugin()
self.sys_path = sys.path[:]
self.sys_path.extend(extra_path)

def _restart_plugin(self):
self.plugin_manager.close()
self.plugin_manager = PluginManager(self.executable,
extra_path=self.extra_path)
self.plugin_manager = PluginManager(self.executable)
self.plugin_manager.introspection_complete.connect(
self._introspection_complete)

Expand All @@ -204,6 +209,7 @@ def _get_code_info(self, name, position=None, **kwargs):
kwargs['editor'] = editor
kwargs['finfo'] = finfo
kwargs['editor_widget'] = self.editor_widget
kwargs['sys_path'] = self.sys_path

return CodeInfo(name, finfo.get_source_code(), position,
finfo.filename, editor.is_python_like, in_comment_or_string,
Expand Down
19 changes: 5 additions & 14 deletions spyder/utils/introspection/plugin_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ class AsyncClient(QObject):
received = Signal(object)

def __init__(self, target, executable=None, name=None,
extra_args=None, libs=None, cwd=None, env=None,
extra_path=None):
extra_args=None, libs=None, cwd=None, env=None):
super(AsyncClient, self).__init__()
self.executable = executable or sys.executable
self.extra_args = extra_args
Expand All @@ -51,7 +50,6 @@ def __init__(self, target, executable=None, name=None,
self.libs = libs
self.cwd = cwd
self.env = env
self.extra_path = extra_path
self.is_initialized = False
self.closing = False
self.notifier = None
Expand Down Expand Up @@ -90,13 +88,6 @@ def run(self):
python_path = osp.pathsep.join([python_path, path])
except ImportError:
pass
if self.extra_path:
try:
python_path = osp.pathsep.join([python_path] +
self.extra_path)
except Exception as e:
debug_print("Error when adding extra_path to plugin env")
debug_print(e)
env.append("PYTHONPATH=%s" % python_path)
if self.env:
env.update(self.env)
Expand Down Expand Up @@ -211,10 +202,10 @@ class PluginClient(AsyncClient):
def __init__(self, plugin_name, executable=None, env=None,
extra_path=None):
cwd = os.path.dirname(__file__)
super(PluginClient, self).__init__('plugin_server.py',
executable=executable, cwd=cwd, env=env,
extra_args=[plugin_name], libs=[plugin_name],
extra_path=extra_path)
super(PluginClient, self).__init__(
'plugin_server.py',
executable=executable, cwd=cwd, env=env,
extra_args=[plugin_name], libs=[plugin_name])
self.name = plugin_name


Expand Down
47 changes: 29 additions & 18 deletions spyder/utils/introspection/rope_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def get_completions(self, info):
source_code = info['source_code']
offset = info['position']

# Set python path into rope project
self.project.prefs.set('python_path', info['sys_path'])

# Prevent Rope from returning import completions because
# it can't handle them. Only Jedi can do it!
lines = sourcecode.split_source(source_code[:offset])
Expand All @@ -89,12 +92,13 @@ def get_completions(self, info):
and not ';' in last_line:
return []

if PY2:
filename = filename.encode('utf-8')
else:
#TODO: test if this is working without any further change in
# Python 3 with a user account containing unicode characters
pass
if filename is not None:
if PY2:
filename = filename.encode('utf-8')
else:
# TODO: test if this is working without any further change in
# Python 3 with a user account containing unicode characters
pass
try:
resource = rope.base.libutils.path_to_resource(self.project,
filename)
Expand Down Expand Up @@ -124,12 +128,16 @@ def get_info(self, info):
source_code = info['source_code']
offset = info['position']

if PY2:
filename = filename.encode('utf-8')
else:
#TODO: test if this is working without any further change in
# Python 3 with a user account containing unicode characters
pass
# Set python path into rope project
self.project.prefs.set('python_path', info['sys_path'])

if filename is not None:
if PY2:
filename = filename.encode('utf-8')
else:
# TODO: test if this is working without any further change in
# Python 3 with a user account containing unicode characters
pass
try:
resource = rope.base.libutils.path_to_resource(self.project,
filename)
Expand Down Expand Up @@ -215,12 +223,15 @@ def get_definition(self, info):
source_code = info['source_code']
offset = info['position']

if PY2:
filename = filename.encode('utf-8')
else:
#TODO: test if this is working without any further change in
# Python 3 with a user account containing unicode characters
pass
# Set python path into rope project
self.project.prefs.set('python_path', info['sys_path'])
if filename is not None:
if PY2:
filename = filename.encode('utf-8')
else:
# TODO: test if this is working without any further change in
# Python 3 with a user account containing unicode characters
pass
try:
resource = rope.base.libutils.path_to_resource(self.project,
filename)
Expand Down
12 changes: 12 additions & 0 deletions spyder/utils/introspection/tests/test_jedi_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from textwrap import dedent

import pytest
import os
import os.path as osp

from spyder.utils.introspection.manager import CodeInfo
from spyder.utils.introspection import jedi_plugin
Expand All @@ -27,6 +29,8 @@
except ImportError:
matplotlib = None

LOCATION = osp.realpath(osp.join(os.getcwd(), osp.dirname(__file__)))

p = jedi_plugin.JediPlugin()
p.load_plugin()

Expand Down Expand Up @@ -98,5 +102,13 @@ def test_matplotlib_fig_returns():
assert ('add_axes', 'function') in completions


def test_completions_custom_path():
source_code = dedent('import test_')
completions = p.get_completions(CodeInfo('completions', source_code,
len(source_code),
sys_path=[LOCATION]))
assert ('test_jedi_plugin', 'module') in completions


if __name__ == '__main__':
pytest.main()
47 changes: 47 additions & 0 deletions spyder/utils/introspection/tests/test_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
#
"""
Tests for manager.py
"""

# Standard library imports
import sys

# Test library imports
import pytest

# Local imports
from spyder.utils.introspection.manager import IntrospectionManager


@pytest.fixture
def introspector_manager():
"""Create a basic instrospection manager."""
introspector = IntrospectionManager()

return introspector


def test_introspector_manager_extra_path(introspector_manager):
"""Test adding of extra path.

Extra path is used for adding spyder_path to plugin clients.
"""
introspector = introspector_manager
extra_path = ['/some/dummy/path']

assert set(introspector.sys_path) == set(sys.path)

# Add extra path
introspector.change_extra_path(extra_path)
assert set(sys.path).issubset(set(introspector.sys_path))
assert set(extra_path).issubset(set(introspector.sys_path))

# Remove extra path
introspector.change_extra_path([])
print(introspector.sys_path)
assert set(sys.path).issubset(set(introspector.sys_path))
assert not set(extra_path).issubset(set(introspector.sys_path))
Loading