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: Use the Python interpreter set in Preferences to run tests #174

Merged
merged 27 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e8d6dd1
feat: allow user to pick a different python for running tests
stevetracvc Apr 16, 2022
e54857c
feat: validate chosen python executable
stevetracvc Apr 17, 2022
fa4a74c
fix: try spyder get_translation import, then fall back to gettext
stevetracvc Apr 20, 2022
1152930
fix: relative import of ZmqStreamWriter
stevetracvc Apr 23, 2022
b060203
fix: config dialog test fix
stevetracvc Apr 23, 2022
7ae8b3b
fix: more issues with zmq
stevetracvc Apr 23, 2022
af7bb84
new tests for config dialog
stevetracvc Apr 25, 2022
5b58ef6
removed text translation code
stevetracvc Apr 25, 2022
0411d39
fix: store pyexec in config file
stevetracvc Apr 25, 2022
c70b410
Use Python interpreter from Spyder preferences by default
jitseniesen Apr 28, 2022
6dda2b8
fix: store empty string in project config if no pyexec chosen
stevetracvc May 1, 2022
97a22f5
moved on_interpreter_config_change to UnitTestWidget
stevetracvc May 15, 2022
466798d
removed manual pyexec config option
stevetracvc May 15, 2022
8509737
upgraded to spyder 5.3.1 for get_conf
stevetracvc May 31, 2022
e2ab7a2
fixed testing issues
stevetracvc Jun 1, 2022
d349c7c
another test skipped
stevetracvc Jun 1, 2022
9ba80c6
skipping another test
stevetracvc Jun 1, 2022
c07ea16
attempt newer pytest-qt for testing
stevetracvc Jun 2, 2022
01b73c9
skipping all qtbot tests
stevetracvc Jun 2, 2022
c2bbe2b
attempt to fix segfaults on linux tests
stevetracvc Jun 2, 2022
c7abea1
Revert "skipping all qtbot tests"
stevetracvc Jun 2, 2022
323617b
Revert "attempt newer pytest-qt for testing"
stevetracvc Jun 2, 2022
9b05ea6
Revert "skipping another test"
stevetracvc Jun 2, 2022
f9ea870
Revert "another test skipped"
stevetracvc Jun 2, 2022
c4bb682
fixed segfaults, no longer need to skip qtbot tests
stevetracvc Jun 2, 2022
c3102c1
bumped pytest requirements to version 5
stevetracvc Jun 2, 2022
6642de0
fixed some comments
stevetracvc Jun 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spyder_unittest/backend/pytestrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ def create_argument_list(self):
pyfile = os.path.join(os.path.dirname(__file__), 'pytestworker.py')
return [pyfile, str(self.reader.port)]

def start(self, config, pythonpath):
def start(self, config, executable, pythonpath):
"""Start process which will run the unit test suite."""
self.config = config
self.reader = ZmqStreamReader()
self.reader.sig_received.connect(self.process_output)
RunnerBase.start(self, config, pythonpath)
RunnerBase.start(self, config, executable, pythonpath)

def process_output(self, output):
"""
Expand Down
22 changes: 10 additions & 12 deletions spyder_unittest/backend/pytestworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@
# Third party imports
import pytest

# Local imports
from spyder.config.base import get_translation
from spyder_unittest.backend.zmqstream import ZmqStreamWriter

# Local imports, needs to not be absolute otherwise it will fail if trying
stevetracvc marked this conversation as resolved.
Show resolved Hide resolved
# to execute in a different env with only spyder-kernel installed
try:
_ = get_translation('spyder_unittest')
except KeyError: # pragma: no cover
import gettext
_ = gettext.gettext

# this line is needed for the pytests to succeed
stevetracvc marked this conversation as resolved.
Show resolved Hide resolved
from .zmqstream import ZmqStreamWriter
except:
# this line is needed for the plugin to work
from zmqstream import ZmqStreamWriter

class FileStub():
"""Stub for ZmqStreamWriter which instead writes to a file."""
Expand Down Expand Up @@ -104,8 +102,8 @@ def pytest_runtest_logreport(self, report):
self.was_skipped = True
if hasattr(report, 'wasxfail'):
self.was_xfail = True
self.longrepr.append(report.wasxfail if report.wasxfail else _(
'WAS EXPECTED TO FAIL'))
self.longrepr.append(report.wasxfail if report.wasxfail else
'WAS EXPECTED TO FAIL')
self.sections = report.sections # already accumulated over phases
if report.longrepr:
first_msg_idx = len(self.longrepr)
Expand All @@ -120,7 +118,7 @@ def pytest_runtest_logreport(self, report):
if report.outcome == 'failed' and report.when in (
'setup', 'teardown'):
self.longrepr[first_msg_idx] = '{} {}: {}'.format(
_('ERROR at'), report.when, self.longrepr[first_msg_idx])
'ERROR at', report.when, self.longrepr[first_msg_idx])

def pytest_runtest_logfinish(self, nodeid, location):
"""Called by pytest when the entire test is completed."""
Expand Down
7 changes: 4 additions & 3 deletions spyder_unittest/backend/runnerbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from qtpy.QtCore import (QObject, QProcess, QProcessEnvironment, QTextCodec,
Signal)
from spyder.py3compat import to_text_string
from spyder.utils.misc import get_python_executable

try:
from importlib.util import find_spec as find_spec_or_loader
Expand Down Expand Up @@ -189,7 +188,7 @@ def _prepare_process(self, config, pythonpath):
process.setProcessEnvironment(env)
return process

def start(self, config, pythonpath):
def start(self, config, executable, pythonpath):
"""
Start process which will run the unit test suite.

Expand All @@ -203,6 +202,8 @@ def start(self, config, pythonpath):
----------
config : TestConfig
Unit test configuration.
executable : str
Path to Python executable (may be overridden by `config`)
pythonpath : list of str
List of directories to be added to the Python path

Expand All @@ -212,7 +213,7 @@ def start(self, config, pythonpath):
If process failed to start.
"""
self.process = self._prepare_process(config, pythonpath)
executable = get_python_executable()
executable = config.pyexec or executable
p_args = self.create_argument_list()
try:
os.remove(self.resultfilename)
Expand Down
7 changes: 4 additions & 3 deletions spyder_unittest/backend/tests/test_pytestrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
# Standard library imports
import os
import os.path as osp
import sys

# Third party imports
import pytest
from qtpy.QtCore import QByteArray

# Local imports
from spyder_unittest.backend.pytestrunner import (PyTestRunner,
Expand Down Expand Up @@ -58,12 +58,13 @@ def test_pytestrunner_start(monkeypatch):

runner = PyTestRunner(None, 'results')
config = Config()
runner.start(config, ['pythondir'])
runner.start(config, sys.executable, ['pythondir'])
assert runner.config is config
assert runner.reader is mock_reader
runner.reader.sig_received.connect.assert_called_once_with(
runner.process_output)
MockRunnerBase.start.assert_called_once_with(runner, config, ['pythondir'])
MockRunnerBase.start.assert_called_once_with(
runner, config, sys.executable, ['pythondir'])


def test_pytestrunner_process_output_with_collected(qtbot):
Expand Down
8 changes: 2 additions & 6 deletions spyder_unittest/backend/tests/test_runnerbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,13 @@ def test_runnerbase_start(monkeypatch):
monkeypatch.setattr('spyder_unittest.backend.runnerbase.os.remove',
mock_remove)

monkeypatch.setattr(
'spyder_unittest.backend.runnerbase.get_python_executable',
lambda: 'python')

runner = RunnerBase(None, 'results')
runner._prepare_process = lambda c, p: mock_process
runner.create_argument_list = lambda: ['arg1', 'arg2']
config = Config('pytest', 'wdir')
mock_process.waitForStarted = lambda: False
with pytest.raises(RuntimeError):
runner.start(config, ['pythondir'])
runner.start(config, 'python_exec', ['pythondir'])

mock_process.start.assert_called_once_with('python', ['arg1', 'arg2'])
mock_process.start.assert_called_once_with('python_exec', ['arg1', 'arg2'])
mock_remove.assert_called_once_with('results')
28 changes: 25 additions & 3 deletions spyder_unittest/unittestplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
import os.path as osp

# Third party imports
from spyder.api.config.decorators import on_conf_change
from spyder.api.plugins import Plugins, SpyderDockablePlugin
from spyder.api.plugin_registration.decorators import on_plugin_available
from spyder.config.base import get_translation
from spyder.config.gui import is_dark_interface
from spyder.plugins.mainmenu.api import ApplicationMenus
from spyder.py3compat import PY2, getcwd
from spyder.utils.misc import get_python_executable

# Local imports
from spyder_unittest.widgets.configdialog import Config
Expand All @@ -38,8 +40,9 @@ class UnitTestPlugin(SpyderDockablePlugin):
WIDGET_CLASS = UnitTestWidget

CONF_SECTION = NAME
CONF_DEFAULTS = [(CONF_SECTION, {'framework': '', 'wdir': ''})]
CONF_NAMEMAP = {CONF_SECTION: [(CONF_SECTION, ['framework', 'wdir'])]}
CONF_DEFAULTS = [(CONF_SECTION, {'framework': '', 'wdir': '', 'pyexec': ''})]
CONF_NAMEMAP = {CONF_SECTION: [(CONF_SECTION,
['framework', 'wdir', 'pyexec'])]}
CONF_FILE = True
CONF_VERSION = '0.1.0'

Expand Down Expand Up @@ -183,6 +186,22 @@ def on_working_directory_available(self):
self.update_default_wdir)
self.update_default_wdir()

@on_conf_change(section='main_interpreter',
option=['default', 'custom_interpreter'])
def on_interpreter_config_change(self, option, value):
"""
Handle changes of interpreter configuration.

Retrieve the Python interpreter in the Spyder preferences and
communicate this to the unittest widget.
"""
if self.get_conf(section='main_interpreter', option='default'):
executable = get_python_executable()
else:
executable = self.get_conf(section='main_interpreter',
option='custom_interpreter')
self.get_widget().python_executable = executable
stevetracvc marked this conversation as resolved.
Show resolved Hide resolved

# --- UnitTestPlugin methods ----------------------------------------------

def update_pythonpath(self):
Expand Down Expand Up @@ -263,7 +282,8 @@ def load_config(self):

new_config = Config(
framework=project.get_option('framework', self.CONF_SECTION),
wdir=project.get_option('wdir', self.CONF_SECTION))
wdir=project.get_option('wdir', self.CONF_SECTION),
pyexec=project.get_option('pyexec', self.CONF_SECTION))
if not widget.config_is_valid(new_config):
new_config = None
widget.set_config_without_emit(new_config)
Expand All @@ -283,6 +303,8 @@ def save_config(self, test_config):
project.set_option('framework', test_config.framework,
self.CONF_SECTION)
project.set_option('wdir', test_config.wdir, self.CONF_SECTION)
project.set_option('pyexec', test_config.pyexec or '',
self.CONF_SECTION)

def goto_in_editor(self, filename, lineno):
"""
Expand Down
46 changes: 40 additions & 6 deletions spyder_unittest/widgets/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@
import os.path as osp

# Third party imports
from qtpy.compat import getexistingdirectory
from qtpy.compat import getexistingdirectory, getopenfilename
from qtpy.QtCore import Slot
from qtpy.QtWidgets import (QApplication, QComboBox, QDialog, QDialogButtonBox,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QVBoxLayout)
from spyder.config.base import get_translation
from spyder.py3compat import getcwd, to_text_string
from spyder.utils import icon_manager as ima
from spyder.utils import icon_manager as ima, programs
from spyder.utils.misc import getcwd_or_home

try:
_ = get_translation('spyder_unittest')
except KeyError:
import gettext
_ = gettext.gettext

Config = namedtuple('Config', ['framework', 'wdir'])
Config.__new__.__defaults__ = (None, '')
Config = namedtuple('Config', ['framework', 'wdir', 'pyexec'])
Config.__new__.__defaults__ = (None, '', None)


class ConfigDialog(QDialog):
Expand Down Expand Up @@ -93,6 +94,22 @@ def __init__(self, frameworks, config, parent=None):

layout.addSpacing(20)

pyexec_label = QLabel(_('Use the following Python interpreter:'))
stevetracvc marked this conversation as resolved.
Show resolved Hide resolved
layout.addWidget(pyexec_label)
pyexec_layout = QHBoxLayout()
self.pyexec_lineedit = QLineEdit(self)
pyexec_layout.addWidget(self.pyexec_lineedit)
self.pyexec_button = QPushButton(ima.icon('DirOpenIcon'), '', self)
self.pyexec_button.setToolTip(_("Select interpreter"))
self.pyexec_button.clicked.connect(
lambda: self.select_file(self.pyexec_lineedit,
"Python (python*)"
))
pyexec_layout.addWidget(self.pyexec_button)
layout.addLayout(pyexec_layout)

layout.addSpacing(20)

self.buttons = QDialogButtonBox(QDialogButtonBox.Ok |
QDialogButtonBox.Cancel)
layout.addWidget(self.buttons)
Expand All @@ -110,6 +127,7 @@ def __init__(self, frameworks, config, parent=None):
if index != -1:
self.framework_combobox.setCurrentIndex(index)
self.wdir_lineedit.setText(config.wdir)
self.pyexec_lineedit.setText(config.pyexec)

@Slot(int)
def framework_changed(self, index):
Expand All @@ -127,6 +145,18 @@ def select_directory(self):
if directory:
self.wdir_lineedit.setText(directory)

def select_file(self, edit, filters=None):
"""Select File"""
basedir = osp.dirname(to_text_string(edit.text()))
if not osp.isdir(basedir):
basedir = getcwd_or_home()
if filters is None:
filters = _("All files (*)")
title = _("Select file")
filename, _selfilter = getopenfilename(self, title, basedir, filters)
if filename and programs.is_python_interpreter(filename):
edit.setText(filename)

def get_config(self):
"""
Return the test configuration specified by the user.
Expand All @@ -139,7 +169,11 @@ def get_config(self):
framework = self.framework_combobox.currentText()
if framework == '':
framework = None
return Config(framework=framework, wdir=self.wdir_lineedit.text())
pyexec = self.pyexec_lineedit.text()
if pyexec == '':
pyexec = None
return Config(framework=framework, wdir=self.wdir_lineedit.text(),
pyexec=pyexec)


def ask_for_config(frameworks, config, parent=None):
Expand All @@ -158,5 +192,5 @@ def ask_for_config(frameworks, config, parent=None):
if __name__ == '__main__':
app = QApplication([])
frameworks = ['nose', 'pytest', 'unittest']
config = Config(framework=None, wdir=getcwd())
config = Config(framework=None, wdir=getcwd(), pyexec=None)
print(ask_for_config(frameworks, config))
22 changes: 21 additions & 1 deletion spyder_unittest/widgets/tests/test_configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

# Standard library imports
import os
import sys

# Third party imports
from qtpy.QtWidgets import QDialogButtonBox
Expand Down Expand Up @@ -43,7 +44,7 @@ def is_installed(cls):


def default_config():
return Config(framework=None, wdir=os.getcwd())
return Config(framework=None, wdir=os.getcwd(), pyexec=None)


def test_configdialog_uses_frameworks(qtbot):
Expand Down Expand Up @@ -117,3 +118,22 @@ def test_configdialog_wdir_button(qtbot, monkeypatch):
lambda parent, caption, basedir: wdir)
configdialog.wdir_button.click()
assert configdialog.get_config().wdir == wdir


def test_configdialog_pyexec_lineedit(qtbot):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
pyexec = sys.executable
configdialog.pyexec_lineedit.setText(pyexec)
assert configdialog.get_config().pyexec == pyexec


def test_configdialog_pyexec_button(qtbot, monkeypatch):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
pyexec = sys.executable
monkeypatch.setattr(
'spyder_unittest.widgets.configdialog.getopenfilename',
lambda parent, caption, basedir, filters: (pyexec, None))
configdialog.pyexec_button.click()
assert configdialog.get_config().pyexec == pyexec
5 changes: 4 additions & 1 deletion spyder_unittest/widgets/unittestgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class UnitTestWidget(PluginMainWidget):
pre_test_hook : function returning bool or None
If set, contains function to run before running tests; abort the test
run if hook returns False.
python_executable : str
Path to Python executable for running tests.
pythonpath : list of str
Directories to be added to the Python path when running tests.
testrunner : TestRunner or None
Expand Down Expand Up @@ -110,6 +112,7 @@ def __init__(self, name, plugin, parent):

self.config = None
self.pythonpath = None
self.python_executable = sys.executable
self.default_wdir = None
self.pre_test_hook = None
self.testrunner = None
Expand Down Expand Up @@ -342,7 +345,7 @@ def run_tests(self, config=None):
self.testrunner.sig_stop.connect(self.tests_stopped)

try:
self.testrunner.start(config, pythonpath)
self.testrunner.start(config, self.python_executable, pythonpath)
except RuntimeError:
QMessageBox.critical(self,
_("Error"), _("Process failed to start"))
Expand Down