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: Improve the "Create new project" dialog (Projects) #16847

Merged
merged 7 commits into from
Nov 17, 2021
18 changes: 7 additions & 11 deletions spyder/plugins/projects/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,17 @@
"""

# Standard library imports
import os
import os.path as osp
from collections import OrderedDict

# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.translations import get_translation
from spyder.config.base import get_project_config_folder
from spyder.plugins.projects.utils.config import (ProjectMultiConfig,
PROJECT_NAME_MAP,
PROJECT_DEFAULTS,
PROJECT_CONF_VERSION,
WORKSPACE, CODESTYLE,
ENCODING, VCS)

WORKSPACE)

# Localization
_ = get_translation("spyder")
Expand All @@ -43,7 +39,7 @@ def __init__(self, root_path, parent_plugin=None):
self.root_path = root_path
self.open_project_files = []
self.open_non_project_files = []
path = os.path.join(root_path, get_project_config_folder(), 'config')
path = osp.join(root_path, get_project_config_folder(), 'config')
self.config = ProjectMultiConfig(
PROJECT_NAME_MAP,
path=path,
Expand Down Expand Up @@ -72,9 +68,9 @@ def set_recent_files(self, recent_files):
"""Set a list of files opened by the project."""
processed_recent_files = []
for recent_file in recent_files:
if os.path.isfile(recent_file):
if osp.isfile(recent_file):
try:
relative_recent_file = os.path.relpath(
relative_recent_file = osp.relpath(
recent_file, self.root_path)
processed_recent_files.append(relative_recent_file)
except ValueError:
Expand All @@ -95,11 +91,11 @@ def get_recent_files(self):
else:
recent_files = self.get_option("recent_files", default=[])

recent_files = [recent_file if os.path.isabs(recent_file)
else os.path.join(self.root_path, recent_file)
recent_files = [recent_file if osp.isabs(recent_file)
else osp.join(self.root_path, recent_file)
for recent_file in recent_files]
for recent_file in recent_files[:]:
if not os.path.isfile(recent_file):
if not osp.isfile(recent_file):
recent_files.remove(recent_file)

return list(OrderedDict.fromkeys(recent_files))
Expand Down
108 changes: 66 additions & 42 deletions spyder/plugins/projects/widgets/projectdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
# -----------------------------------------------------------------------------
"""Project creation dialog."""

from __future__ import print_function

# Standard library imports
import errno
import os
import os.path as osp
import sys
import tempfile
Expand All @@ -27,7 +24,6 @@
from spyder.config.base import _, get_home_dir
from spyder.utils.icon_manager import ima
from spyder.utils.qthelpers import create_toolbutton
from spyder.py3compat import to_text_string


def is_writable(path):
Expand Down Expand Up @@ -69,40 +65,40 @@ def __init__(self, parent, project_types):
self.setWindowFlags(
self.windowFlags() & ~Qt.WindowContextHelpButtonHint)

# Variables
current_python_version = '.'.join(
[to_text_string(sys.version_info[0]),
to_text_string(sys.version_info[1])])
python_versions = ['2.7', '3.4', '3.5']
if current_python_version not in python_versions:
python_versions.append(current_python_version)
python_versions = sorted(python_versions)

self.project_name = None
self.location = get_home_dir()

# Widgets
projects_url = "http://docs.spyder-ide.org/current/panes/projects.html"
self.description_label = QLabel(
_(f"Select a new or existing directory to create a new Spyder "
f"project in it. To learn more about projects, take a look at "
f"our <a href=\"{projects_url}\">documentation</a>.")
)
self.description_label.setOpenExternalLinks(True)
self.description_label.setWordWrap(True)

self.groupbox = QGroupBox()
self.radio_new_dir = QRadioButton(_("New directory"))
self.radio_from_dir = QRadioButton(_("Existing directory"))

self.label_project_name = QLabel(_('Project name'))
self.label_location = QLabel(_('Location'))
self.label_project_type = QLabel(_('Project type'))
self.label_python_version = QLabel(_('Python version'))

self.text_project_name = QLineEdit()
self.text_location = QLineEdit(get_home_dir())
self.combo_project_type = QComboBox()
self.combo_python_version = QComboBox()

self.label_information = QLabel("")
self.label_information.hide()

self.button_select_location = create_toolbutton(
self,
triggered=self.select_location,
icon=ima.icon('DirOpenIcon'),
tip=_("Select directory"))
tip=_("Select directory")
)
self.button_cancel = QPushButton(_('Cancel'))
self.button_create = QPushButton(_('Create'))

Expand All @@ -111,7 +107,6 @@ def __init__(self, parent, project_types):
self.bbox.addButton(self.button_create, QDialogButtonBox.ActionRole)

# Widget setup
self.combo_python_version.addItems(python_versions)
self.radio_new_dir.setChecked(True)
self.text_location.setEnabled(True)
self.text_location.setReadOnly(True)
Expand All @@ -122,18 +117,14 @@ def __init__(self, parent, project_types):
in project_types.items()]:
self.combo_project_type.addItem(name, id_)

self.combo_python_version.setCurrentIndex(
python_versions.index(current_python_version))
self.setWindowTitle(_('Create new project'))
self.setFixedWidth(500)
self.label_python_version.setVisible(False)
self.combo_python_version.setVisible(False)

# Layouts
layout_top = QHBoxLayout()
layout_top.addWidget(self.radio_new_dir)
layout_top.addSpacing(15)
layout_top.addWidget(self.radio_from_dir)
layout_top.addStretch(1)
layout_top.addSpacing(200)
self.groupbox.setLayout(layout_top)

layout_grid = QGridLayout()
Expand All @@ -144,17 +135,17 @@ def __init__(self, parent, project_types):
layout_grid.addWidget(self.button_select_location, 1, 2)
layout_grid.addWidget(self.label_project_type, 2, 0)
layout_grid.addWidget(self.combo_project_type, 2, 1, 1, 2)
layout_grid.addWidget(self.label_python_version, 3, 0)
layout_grid.addWidget(self.combo_python_version, 3, 1, 1, 2)
layout_grid.addWidget(self.label_information, 4, 0, 1, 3)
layout_grid.addWidget(self.label_information, 3, 0, 1, 3)

layout = QVBoxLayout()
layout.addWidget(self.description_label)
layout.addSpacing(3)
layout.addWidget(self.groupbox)
layout.addSpacing(10)
layout.addSpacing(8)
layout.addLayout(layout_grid)
layout.addStretch()
layout.addSpacing(20)
layout.addSpacing(8)
layout.addWidget(self.bbox)
layout.setSizeConstraint(layout.SetFixedSize)

self.setLayout(layout)

Expand All @@ -175,36 +166,70 @@ def select_location(self):
)
)

if location:
if location and location != '.':
if is_writable(location):
self.location = location
self.text_project_name.setText(osp.basename(location))
self.update_location()

def update_location(self, text=''):
"""Update text of location."""
self.text_project_name.setEnabled(self.radio_new_dir.isChecked())
"""Update text of location and validate it."""
msg = ''
path_validation = False
path = self.location
name = self.text_project_name.text().strip()

# Setup
self.text_project_name.setEnabled(self.radio_new_dir.isChecked())
self.label_information.setText('')
self.label_information.hide()

if name and self.radio_new_dir.isChecked():
# Allow to create projects only on new directories.
path = osp.join(self.location, name)
self.button_create.setDisabled(os.path.isdir(path))
path_validation = not osp.isdir(path)
if not path_validation:
msg = _("This directory already exists!")
elif self.radio_from_dir.isChecked():
self.button_create.setEnabled(True)
path = self.location
else:
self.button_create.setEnabled(False)
# Allow to create projects in current directories that are not
# Spyder projects.
path = self.location
path_validation = not osp.isdir(osp.join(path, '.spyproject'))
if not path_validation:
msg = _("This directory is already a Spyder project!")

# Set path in text_location
self.text_location.setText(path)

# Validate name with the method from the currently selected project
# Validate project name with the method from the currently selected
# project.
project_type_id = self.combo_project_type.currentData()
validate_func = self._project_types[project_type_id].validate_name
validated, msg = validate_func(path, name)
msg = "" if validated else msg
self.label_information.setText(msg)
project_name_validation, project_msg = validate_func(path, name)
if not project_name_validation:
if msg:
msg = msg + '\n\n' + project_msg
else:
msg = project_msg

# Set message
if msg:
self.label_information.show()
self.label_information.setText('\n' + msg)

# Allow to create project if validation was successful
validated = path_validation and project_name_validation
self.button_create.setEnabled(validated)

# Set default state of buttons according to validation
# Fixes spyder-ide/spyder#16745
if validated:
self.button_create.setDefault(True)
self.button_create.setAutoDefault(True)
else:
self.button_cancel.setDefault(True)
self.button_cancel.setAutoDefault(True)

def create_project(self):
"""Create project."""
self.project_data = {
Expand Down Expand Up @@ -234,7 +259,6 @@ def get_name():
def validate_name(path, name):
return False, "BOOM!"


app = qapplication()
dlg = ProjectDialog(None, {"empty": MockProjectType})
dlg.show()
Expand Down
57 changes: 51 additions & 6 deletions spyder/plugins/projects/widgets/tests/test_projectdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,10 @@ def projects_dialog(qtbot):
"""Set up ProjectDialog."""
dlg = ProjectDialog(None, {'Empty project': EmptyProject})
qtbot.addWidget(dlg)
dlg.show()
return dlg


def test_project_dialog(projects_dialog):
"""Run project dialog."""
projects_dialog.show()
assert projects_dialog


@pytest.mark.skipif(os.name != 'nt', reason="Specific to Windows platform")
def test_projectdialog_location(monkeypatch):
"""Test that select_location normalizes delimiters and updates the path."""
Expand Down Expand Up @@ -71,5 +66,55 @@ def test_projectdialog_location(monkeypatch):
assert dlg.location == r"c:\a_a_1\Bbbb\2345\d-6D"


def test_directory_validations(projects_dialog, monkeypatch, tmpdir):
"""
Test that we perform appropiate validations before allowing users to
create a project in a directory.
"""
dlg = projects_dialog

# Assert button_create is disabled by default
assert not dlg.button_create.isEnabled()
assert not dlg.button_create.isDefault()

# Set location to tmpdir root
dlg.location = str(tmpdir)
dlg.text_location.setText(str(tmpdir))

# Check that we don't allow to create projects in existing directories when
# 'New directory' is selected.
dlg.radio_new_dir.click()
tmpdir.mkdir('foo')
dlg.text_project_name.setText('foo')
assert not dlg.button_create.isEnabled()
assert not dlg.button_create.isDefault()
assert dlg.label_information.text() == '\nThis directory already exists!'

# Selecting 'Existing directory' should allow to create a project there
dlg.radio_from_dir.click()
assert dlg.button_create.isEnabled()
assert dlg.button_create.isDefault()
assert dlg.label_information.text() == ''

# Create a Spyder project
folder = tmpdir.mkdir('bar')
folder.mkdir('.spyproject')

# Mock selecting a directory
mock_getexistingdirectory = Mock()
monkeypatch.setattr('spyder.plugins.projects.widgets.projectdialog' +
'.getexistingdirectory', mock_getexistingdirectory)
mock_getexistingdirectory.return_value = str(folder)

# Check that we don't allow to create projects in existing directories when
# 'Existing directory' is selected and there's already a Spyder project
# there.
dlg.select_location()
assert not dlg.button_create.isEnabled()
assert not dlg.button_create.isDefault()
msg = '\nThis directory is already a Spyder project!'
assert dlg.label_information.text() == msg


if __name__ == "__main__":
pytest.main()