From e7f649be4b64fe8f0bcf39683484d191c4f4ca27 Mon Sep 17 00:00:00 2001 From: goanpeca Date: Mon, 1 Jun 2020 21:08:11 -0500 Subject: [PATCH] Add cookiecutter GUI generation and handling --- .github/scripts/install.sh | 2 +- CONTRIBUTING.md | 2 +- binder/environment.yml | 1 + requirements/conda.txt | 1 + setup.py | 1 + spyder/dependencies.py | 7 +- spyder/plugins/projects/utils/cookie.py | 56 +++ .../plugins/projects/utils/tests/__init__.py | 9 + .../projects/utils/tests/test_cookie.py | 137 ++++++ .../plugins/projects/widgets/qcookiecutter.py | 421 ++++++++++++++++++ .../widgets/tests/test_qcookiecutter.py | 154 +++++++ .../pylint/tests/test_pylint_config_dialog.py | 2 +- 12 files changed, 789 insertions(+), 4 deletions(-) create mode 100644 spyder/plugins/projects/utils/cookie.py create mode 100644 spyder/plugins/projects/utils/tests/__init__.py create mode 100644 spyder/plugins/projects/utils/tests/test_cookie.py create mode 100644 spyder/plugins/projects/widgets/qcookiecutter.py create mode 100644 spyder/plugins/projects/widgets/tests/test_qcookiecutter.py diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 7f0872ea05e..a733c025ff6 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -14,7 +14,7 @@ if [ "$USE_CONDA" = "true" ]; then fi # Install main dependencies - conda install python=$PYTHON_VERSION --file requirements/conda.txt -q -y + conda install python=$PYTHON_VERSION --file requirements/conda.txt -q -y -c spyder-ide/label/alpha # Install test ones conda install python=$PYTHON_VERSION --file requirements/tests.txt -c spyder-ide -q -y diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b38be4b4b21..62b9771b833 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ $ workon spyder-dev After you have created your development environment, you need to install Spyder's necessary dependencies. The easiest way to do so (with Anaconda) is ```bash -$ conda install -c spyder-ide --file requirements/conda.txt +$ conda install -c spyder-ide/label/alpha --file requirements/conda.txt ``` This installs all Spyder's dependencies into the environment. diff --git a/binder/environment.yml b/binder/environment.yml index 0f65f6007b4..e5238ba1dc3 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -11,6 +11,7 @@ dependencies: - atomicwrites >=1.2.0 - chardet >=2.0.0 - cloudpickle >=0.5.0 +- cookiecutter >=1.6.0 - diff-match-patch >=20181111 - intervaltree - ipython >=4.0 diff --git a/requirements/conda.txt b/requirements/conda.txt index 6492a8693d6..cf2a77a2259 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -6,6 +6,7 @@ applaunchservices >=0.1.7 atomicwrites >=1.2.0 chardet >=2.0.0 cloudpickle >=0.5.0 +cookiecutter >=1.6.0 diff-match-patch >=20181111 intervaltree IPython >=4.0 diff --git a/setup.py b/setup.py index 911b5880d4b..696bbf98b77 100644 --- a/setup.py +++ b/setup.py @@ -205,6 +205,7 @@ def run(self): 'atomicwrites>=1.2.0', 'chardet>=2.0.0', 'cloudpickle>=0.5.0', + 'cookiecutter>=1.6.0', 'diff-match-patch>=20181111', 'intervaltree', 'ipython>=4.0', diff --git a/spyder/dependencies.py b/spyder/dependencies.py index 5fc8b89da82..a2a3244938d 100644 --- a/spyder/dependencies.py +++ b/spyder/dependencies.py @@ -33,6 +33,7 @@ ATOMICWRITES_REQVER = '>=1.2.0' CHARDET_REQVER = '>=2.0.0' CLOUDPICKLE_REQVER = '>=0.5.0' +COOKIECUTTER_REQVER = '>=1.6.0' DIFF_MATCH_PATCH_REQVER = '>=20181111' INTERVALTREE_REQVER = None IPYTHON_REQVER = ">=4.0;<6.0" if PY2 else ">=4.0" @@ -94,6 +95,10 @@ 'package_name': "cloudpickle", 'features': _("Handle communications between kernel and frontend"), 'required_version': CLOUDPICKLE_REQVER}, + {'modname': "cookiecutter", + 'package_name': "cookiecutter", + 'features': _("Create projects from cookiecutter templates"), + 'required_version': COOKIECUTTER_REQVER}, {'modname': "diff_match_patch", 'package_name': "diff-match-patch", 'features': _("Compute text file diff changes during edition"), @@ -199,7 +204,7 @@ {'modname': "watchdog", 'package_name': "watchdog", 'features': _("Watch file changes on project directories"), - 'required_version': WATCHDOG_REQVER} + 'required_version': WATCHDOG_REQVER}, ] diff --git a/spyder/plugins/projects/utils/cookie.py b/spyder/plugins/projects/utils/cookie.py new file mode 100644 index 00000000000..5d7719b84e0 --- /dev/null +++ b/spyder/plugins/projects/utils/cookie.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Cookiecutter utilities. +""" + +import json +import os + +from cookiecutter.main import cookiecutter + + +def generate_cookiecutter_project(cookiecutter_path, output_path, + extra_content=None): + """ + Generate a cookicutter project programmatically. + """ + status = True + try: + result = cookiecutter( + cookiecutter_path, + output_dir=output_path, + overwrite_if_exists=True, + extra_context=extra_content, + no_input=True, + ) + except Exception as err: + result = err + status = False + + return status, result + + +def load_cookiecutter_project(project_path): + """ + Load a cookicutter options and pre-hook script. + """ + options = None + pre_gen_code = None + cookiepath = os.path.join(project_path, "cookiecutter.json") + pre_gen_path = os.path.join(project_path, "hooks", "pre_gen_project.py") + + if os.path.isdir(project_path): + if os.path.isfile(cookiepath): + with open(cookiepath, 'r') as fh: + options = json.loads(fh.read()) + + if os.path.isfile(pre_gen_path): + with open(pre_gen_path, 'r') as fh: + pre_gen_code = fh.read() + + return options, pre_gen_code diff --git a/spyder/plugins/projects/utils/tests/__init__.py b/spyder/plugins/projects/utils/tests/__init__.py new file mode 100644 index 00000000000..c649e2a0ea3 --- /dev/null +++ b/spyder/plugins/projects/utils/tests/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see LICENSE.txt for details) +# ----------------------------------------------------------------------------- + +"""Tests.""" diff --git a/spyder/plugins/projects/utils/tests/test_cookie.py b/spyder/plugins/projects/utils/tests/test_cookie.py new file mode 100644 index 00000000000..68e709f5f43 --- /dev/null +++ b/spyder/plugins/projects/utils/tests/test_cookie.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# + +""" +Tests for qcookiecutter widget. +""" + +# Standard library imports +import json +import os +import shutil +import tempfile + +# Third party imports +import pytest + +# Local imports +from spyder.plugins.projects.utils.cookie import ( + generate_cookiecutter_project, load_cookiecutter_project) + + +def test_load_cookiecutter_project_config(): + settings = { + "opt_1": "value", + "opt_2": "{{ cookiecutter.opt_1 }}", + } + temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter') + temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json') + + with open(temp_cookie_path, 'w') as fh: + fh.write(json.dumps(settings, sort_keys=True)) + + sets, pre_gen_code = load_cookiecutter_project(temp_path) + assert settings == sets + assert pre_gen_code is None + + shutil.rmtree(temp_path) + + +def test_load_cookiecutter_project_hooks(): + settings = { + "opt_1": "value", + "opt_2": "{{ cookiecutter.opt_1 }}", + } + pre_gen_code = "import sys\n\nprint('test!')\nsys.exit(1)\n" + temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter') + temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json') + temp_hooks_path = os.path.join(temp_path, 'hooks') + temp_hooks_pre_path = os.path.join(temp_hooks_path, 'pre_gen_project.py') + os.makedirs(temp_hooks_path) + + with open(temp_cookie_path, 'w') as fh: + fh.write(json.dumps(settings, sort_keys=True)) + + with open(temp_hooks_pre_path, 'w') as fh: + fh.write(pre_gen_code) + + sets, pre_gen_code = load_cookiecutter_project(temp_path) + assert settings == sets + assert pre_gen_code == pre_gen_code + + shutil.rmtree(temp_path) + + +def test_generate_cookiecutter_project_defaults(): + settings = { + "repo_name": "value", + } + temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter') + temp_path_created = tempfile.mkdtemp(suffix='-created-project') + temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json') + temp_project_path = os.path.join(temp_path, '{{cookiecutter.repo_name}}') + os.makedirs(temp_project_path) + + with open(temp_cookie_path, 'w') as fh: + fh.write(json.dumps(settings, sort_keys=True)) + + status, result = generate_cookiecutter_project( + temp_path, + temp_path_created, + ) + assert "value" in result + assert status is True + shutil.rmtree(temp_path) + + +def test_generate_cookiecutter_project_extra_content(): + settings = { + "repo_name": "value", + } + temp_path = tempfile.mkdtemp(suffix='-some-cookiecutter') + temp_path_created = tempfile.mkdtemp(suffix='-created-project') + temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json') + temp_project_path = os.path.join(temp_path, '{{cookiecutter.repo_name}}') + os.makedirs(temp_project_path) + + with open(temp_cookie_path, 'w') as fh: + fh.write(json.dumps(settings, sort_keys=True)) + + status, result = generate_cookiecutter_project( + temp_path, + temp_path_created, + {"repo_name": "boom"}, + ) + assert "boom" in result + assert status is True + shutil.rmtree(temp_path) + + +def test_generate_cookiecutter_project_exception(): + settings = { + "repo_name": "value", + } + temp_path = tempfile.mkdtemp(suffix='-some-invalid-cookiecutter') + temp_path_created = tempfile.mkdtemp(suffix='-created-project') + temp_cookie_path = os.path.join(temp_path, 'cookiecutter.json') + temp_project_path = os.path.join( + temp_path, + '{{cookiecutter.not_foun_variable}}', + ) + os.makedirs(temp_project_path) + + with open(temp_cookie_path, 'w') as fh: + fh.write(json.dumps(settings, sort_keys=True)) + + status, __ = generate_cookiecutter_project( + temp_path, + temp_path_created, + ) + assert status is False + + +if __name__ == "__main__": + pytest.main() diff --git a/spyder/plugins/projects/widgets/qcookiecutter.py b/spyder/plugins/projects/widgets/qcookiecutter.py new file mode 100644 index 00000000000..887e16c06ee --- /dev/null +++ b/spyder/plugins/projects/widgets/qcookiecutter.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © Spyder Project Contributors +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +# ----------------------------------------------------------------------------- + +""" +Cookiecutter widget. +""" + +import sys +import tempfile +from collections import OrderedDict + +from jinja2 import Template +from qtpy import QtCore +from qtpy import QtWidgets + + +class Namespace: + """ + Namespace to provide a holder for attributes when rendering a template. + """ + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +class CookiecutterDialog(QtWidgets.QDialog): + """ + QDialog to display cookiecutter.json options. + + cookiecutter_settings: dict + A cookiecutter.json settings content. + pre_gen_code: str + The code of the pregeneration script. + """ + + sig_validated = QtCore.Signal(int, str) + """ + This signal is emited after validation has been executed. + + It provides the process exit code and the output captured. + """ + + def __init__(self, parent, cookiecutter_settings=None, pre_gen_code=None): + super().__init__(parent) + + self._widget = CookiecutterWidget( + self, cookiecutter_settings, + pre_gen_code + ) + self._info_label = QtWidgets.QLabel() + self._validate_button = QtWidgets.QPushButton("Validate") + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self._widget) + layout.addWidget(self._info_label) + layout.addWidget(self._validate_button) + self.setLayout(layout) + + # Signals + self._validate_button.clicked.connect(self.validate) + self._widget.sig_validated.connect(self._set_message) + self._widget.sig_validated.connect(self.sig_validated) + + def _set_message(self, exit_code, message): + if exit_code != 0: + self._info_label.setText(message) + + def setup(self, cookiecutter_settings): + """ + Setup the widget using options. + """ + self._widget.setup(cookiecutter_settings) + + def set_pre_gen_code(self, pre_gen_code): + """ + Set the cookiecutter pregeneration code. + """ + self._widget.set_pre_gen_code(pre_gen_code) + + def validate(self): + """ + Run, pre generation script and provide information on finished. + """ + self._widget.validate() + + def get_values(self): + """ + Return all entered and generated values. + """ + return self._widget.get_values() + + +class CookiecutterWidget(QtWidgets.QWidget): + """ + QWidget to display cookiecutter.json options. + + cookiecutter_settings: dict + A cookiecutter.json settings content. + pre_gen_code: str + The code of the pregeneration script. + """ + + sig_validated = QtCore.Signal(int, str) + """ + This signal is emited after validation has been executed. + + It provides the process exit code and the output captured. + """ + + def __init__(self, parent, cookiecutter_settings=None, pre_gen_code=None): + super().__init__(parent) + + # Attributes + self._parent = parent + self._cookiecutter_settings = cookiecutter_settings + self._pre_gen_code = pre_gen_code + self._widgets = OrderedDict() + self._defined_settings = OrderedDict() + self._rendered_settings = OrderedDict() + self._process = None + self._tempfile = tempfile.mkstemp(suffix=".py")[-1] + + # Cookiecutter special variables + self._extensions = None + self._copy_without_render = None + self._new_lines = None + self._private_vars = None + self._rendered_private_var = None + + # Layout + self._form_layout = QtWidgets.QFormLayout() + self._form_layout.setFieldGrowthPolicy( + self._form_layout.AllNonFixedFieldsGrow) + self.setLayout(self._form_layout) + + # --- Helpers + # ------------------------------------------------------------------------ + def _check_jinja_options(self): + """ + Check which values are Jinja2 expressions. + """ + if self._cookiecutter_settings: + # https://cookiecutter.readthedocs.io/en/latest/advanced/template_extensions.html + self._extensions = self._cookiecutter_settings.pop("_extensions", + []) + + # https://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html + self._copy_without_render = self._cookiecutter_settings.pop( + "_copy_without_render", []) + + # https://cookiecutter.readthedocs.io/en/latest/advanced/new_line_characters.html + self._new_lines = self._cookiecutter_settings.pop("_new_lines", "") + + for setting, value in self._cookiecutter_settings.items(): + # Treat everything like a list for convenience + if isinstance(value, dict): + # https://cookiecutter.readthedocs.io/en/latest/advanced/dict_variables.html + list_values = list(value.keys()) + elif not isinstance(value, list): + list_values = [value] + else: + list_values = value + + are_rendered_values = [] + if list_values and value: + for list_value in list_values: + template = Template(list_value) + rendered_value = template.render( + cookiecutter=Namespace( + **self._cookiecutter_settings)) + + are_rendered_values.append( + list_value != rendered_value) + + if any(are_rendered_values): + self._rendered_settings[setting] = value + else: + self._defined_settings[setting] = value + + def _is_jinja(self, setting): + """ + Check if option contains jinja2 code. + """ + return setting in self._rendered_settings + + def _parse_bool_text(self, text): + """ + Convert a text value into a boolean. + """ + value = None + if text.lower() in ["n", "no", "false"]: + value = False + elif text.lower() in ["y", "yes", "true"]: + value = True + + return value + + def _create_textbox(self, setting, label, default=None): + """ + Create a textbox field. + """ + if default is not None and len(default) > 30: + box = QtWidgets.QTextEdit(parent=self) + box.setText = box.setPlainText + box.text = box.toPlainText + else: + box = QtWidgets.QLineEdit(parent=self) + + box.setting = setting + if default is not None: + box.setText(default) + box.textChanged.connect(lambda x=None: self.render()) + + box.get_value = lambda: box.text() + box.set_value = lambda text: box.setText(text) + + return box + + def _create_checkbox(self, setting, label, default=None): + """ + Create a checkbox field. + """ + box = QtWidgets.QCheckBox(parent=self) + box.setting = setting + if default is not None: + new_default = self._parse_bool_text(default) + box.setChecked(new_default) + + def _get_value(): + bool_to_values = { + self._parse_bool_text(default): default, + not self._parse_bool_text(default): "other-value-" + default + } + return bool_to_values[box.isChecked()] + + box.get_value = _get_value + + return box + + def _create_combobox(self, setting, label, choices, default=None): + """ + Create a combobox field. + """ + box = QtWidgets.QComboBox(parent=self) + if isinstance(choices, dict): + temp = OrderedDict() + for choice, choice_value in choices.items(): + box.addItem(choice, {choice: choice_value}) + else: + for choice in choices: + box.addItem(choice, choice) + + box.setting = setting + box.get_value = lambda: box.currentData() + + return box + + def _create_field(self, setting, value): + """ + Create a form field. + """ + label = " ".join(setting.split("_")).capitalize() + if isinstance(value, (list, dict)): + # https://cookiecutter.readthedocs.io/en/latest/advanced/choice_variables.html + widget = self._create_combobox(setting, label, value) + elif isinstance(value, str): + if value.lower() in ["y", "yes", "true", "n", "no", "false"]: + widget = self._create_checkbox(setting, label, default=value) + else: + default = None if self._is_jinja(setting) else value + widget = self._create_textbox(setting, label, default=default) + else: + raise Exception( + "Cookiecutter option '{}'cannot be processed".format(setting)) + + self._widgets[setting] = (label, widget) + + return label, widget + + def _on_process_finished(self): + """ + Process output of valiation script. + """ + if self._process is not None: + out = bytes(self._process.readAllStandardOutput()).decode() + error = bytes(self._process.readAllStandardError()).decode() + message = "" + if out: + message += out + + if error: + message += error + + message = message.replace("\r\n", " ") + message = message.replace("\n", " ") + self.sig_validated.emit(self._process.exitCode(), message) + + # --- API + # ------------------------------------------------------------------------ + def setup(self, cookiecutter_settings): + """ + Setup the widget using options. + """ + self._cookiecutter_settings = cookiecutter_settings + self._check_jinja_options() + + for setting, value in self._cookiecutter_settings.items(): + if not setting.startswith(("__", "_")): + label, widget = self._create_field(setting, value) + self._form_layout.addRow(label, widget) + + self.render() + + def set_pre_gen_code(self, pre_gen_code): + """ + Set the cookiecutter pregeneration code. + """ + self._pre_gen_code = pre_gen_code + + def render(self): + """ + Render text that contains Jinja2 expressions and set their values. + """ + cookiecutter_settings = self.get_values() + for setting, value in self._rendered_settings.items(): + if not setting.startswith(("__", "_")): + template = Template(value) + val = template.render( + cookiecutter=Namespace(**cookiecutter_settings)) + __, widget = self._widgets[setting] + widget.set_value(val) + + def get_values(self): + """ + Return all entered and generated values. + """ + cookiecutter_settings = cs = OrderedDict() + if self._cookiecutter_settings: + for setting, value in self._cookiecutter_settings.items(): + if setting.startswith(("__", "_")): + cookiecutter_settings[setting] = value + else: + __, widget = self._widgets[setting] + cookiecutter_settings[setting] = widget.get_value() + + # Cookiecutter special variables + cookiecutter_settings["_extensions"] = self._extensions + cookiecutter_settings["_copy_without_render"] = ( + self._copy_without_render) + cookiecutter_settings["_new_lines"] = self._new_lines + + return cookiecutter_settings + + def validate(self): + """ + Run, pre generation script and provide information on finished. + """ + if self._pre_gen_code is not None: + cookiecutter_settings = self.get_values() + template = Template(self._pre_gen_code) + val = template.render( + cookiecutter=Namespace(**cookiecutter_settings)) + + with open(self._tempfile, "w") as fh: + fh.write(val) + + if self._process is not None: + self._process.terminate() + + self._process = QtCore.QProcess() + self._process.setProgram(sys.executable) + self._process.setArguments([self._tempfile]) + self._process.finished.connect(self._on_process_finished) + self._process.start() + + +if __name__ == "__main__": + from spyder.utils.qthelpers import qapplication + + app = qapplication() + dlg = CookiecutterDialog(parent=None) + dlg.setup( + { + "list_option": ["1", "2", "3"], + "checkbox_option": "y", + "checkbox_option_2": "false", + "fixed_option": "goanpeca", + "rendered_option": "{{ cookiecutter.fixed_option|upper }}", + "dict_option": { + "png": { + "name": "Portable Network Graphic", + "library": "libpng", + "apps": [ + "GIMP" + ] + }, + "bmp": { + "name": "Bitmap", + "library": "libbmp", + "apps": [ + "Paint", + "GIMP" + ] + } + }, + "_private": "{{ cookiecutter.fixed_option }}", + "__private_rendered": "{{ cookiecutter.fixed_option }}", + } + ) + dlg.set_pre_gen_code(''' +import sys +print("HELP!") # spyder: test-skip +sys.exit(10)''') + dlg.show() + sys.exit(app.exec_()) diff --git a/spyder/plugins/projects/widgets/tests/test_qcookiecutter.py b/spyder/plugins/projects/widgets/tests/test_qcookiecutter.py new file mode 100644 index 00000000000..1f2b136b355 --- /dev/null +++ b/spyder/plugins/projects/widgets/tests/test_qcookiecutter.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# + +""" +Tests for qcookiecutter widget. +""" + +# Standard library imports +import os +from unittest.mock import Mock + +# Third party imports +import pytest + +# Local imports +from spyder.plugins.projects.widgets.qcookiecutter import CookiecutterWidget + + +@pytest.fixture +def coookie_widget(qtbot): + """Set up CookieCutter Widget.""" + widget = CookiecutterWidget(None) + qtbot.addWidget(widget) + return widget + + +def test_cookiecutter_widget_empty(coookie_widget): + assert len(coookie_widget._widgets) == 0 + assert len(coookie_widget.get_values()) == 3 + + coookie_widget.setup({}) + assert len(coookie_widget._widgets) == 0 + assert len(coookie_widget.get_values()) == 3 + + +@pytest.mark.parametrize("option,value", [ + ("opt", "y"), + ("opt", "yes"), + ("opt", "true"), + ("opt", "YES"), + ("opt", "True"), +]) +def test_cookiecutter_widget_checkbox_yes(coookie_widget, option, value): + coookie_widget.setup({option: value}) + label, widget = coookie_widget._widgets[option] + assert len(coookie_widget._widgets) == 1 + assert label == option.capitalize() + assert widget.isChecked() + assert widget.get_value() == value + + +@pytest.mark.parametrize("option,value", [ + ("opt", "n"), + ("opt", "no"), + ("opt", "false"), + ("opt", "NO"), + ("opt", "False"), +]) +def test_cookiecutter_widget_checkbox_no(coookie_widget, option, value): + coookie_widget.setup({option: value}) + label, widget = coookie_widget._widgets[option] + assert len(coookie_widget._widgets) == 1 + assert label == option.capitalize() + assert not widget.isChecked() + assert widget.get_value() == value + + +@pytest.mark.parametrize("option,value", [ + ("opt", ["1", "2", "3"]), +]) +def test_cookiecutter_widget_list(coookie_widget, option, value): + coookie_widget.setup({option: value}) + label, widget = coookie_widget._widgets[option] + assert len(coookie_widget._widgets) == 1 + assert label == option.capitalize() + assert widget.get_value() == value[0] + + +@pytest.mark.parametrize("option,value", [ + ("opt", {"1": [1, 2], "2": [3, 4]}), +]) +def test_cookiecutter_widget_dict(coookie_widget, option, value): + coookie_widget.setup({option: value}) + label, widget = coookie_widget._widgets[option] + assert len(coookie_widget._widgets) == 1 + assert label == option.capitalize() + assert widget.get_value() == {"1": value["1"]} + + +@pytest.mark.parametrize("option,value", [ + ("_nope", "nothing"), + ("__nope_2", "nothing"), +]) +def test_cookiecutter_widget_private_variables(coookie_widget, option, value): + coookie_widget.setup({option: value}) + assert len(coookie_widget._widgets) == 0 + assert len(coookie_widget.get_values()) == 4 + + +def test_cookiecutter_widget_render(coookie_widget): + coookie_widget.setup({ + "opt_1": "test", + "opt_2": "{{ cookiecutter.opt_1 }}", + }) + ows = coookie_widget._widgets + assert ows["opt_2"][1].get_value() == ows["opt_1"][1].get_value() + + +def test_cookiecutter_widget_no_render(coookie_widget): + coookie_widget.setup({ + "opt_1": "test", + "opt_2": "{{ cookiecutter.opt_1 }}", + "_opt_3": "{{ cookiecutter.opt_1 }}", + "__opt_4": "{{ cookiecutter.opt_1 }}", + }) + ows = coookie_widget.get_values() + assert ows["_opt_3"] == ows["_opt_3"] + assert ows["__opt_4"] == ows["__opt_4"] + + +def test_cookiecutter_widget_validate_passes(qtbot, coookie_widget): + coookie_widget.setup({ + "opt_1": "test", + }) + coookie_widget.set_pre_gen_code(''' +import sys +sys.exit(0) +''') + with qtbot.waitSignal(coookie_widget.sig_validated) as blocker: + coookie_widget.validate() + + assert blocker.args == [0, ""] + + +def test_cookiecutter_widget_validate_fails(qtbot, coookie_widget): + coookie_widget.setup({ + "opt_1": "test", + }) + coookie_widget.set_pre_gen_code(''' +import sys +print('ERROR!') # spyder: test-skip +sys.exit(1) +''') + with qtbot.waitSignal(coookie_widget.sig_validated) as blocker: + coookie_widget.validate() + + assert blocker.args == [1, "ERROR! "] + + +if __name__ == "__main__": + pytest.main() diff --git a/spyder/plugins/pylint/tests/test_pylint_config_dialog.py b/spyder/plugins/pylint/tests/test_pylint_config_dialog.py index 15018f9b579..b9b8c8e1e7a 100644 --- a/spyder/plugins/pylint/tests/test_pylint_config_dialog.py +++ b/spyder/plugins/pylint/tests/test_pylint_config_dialog.py @@ -24,7 +24,7 @@ def __init__(self): super().__init__(None) self.editor = Mock() self.editor.sig_editor_focus_changed = self.sig_editor_focus_changed - self.projects = MagicMock() + self.projects = Mock() @pytest.mark.parametrize(