Repository: spyder-ide/spyder
Pull request: PR : Add validation for theme color changes (Preferences)
<!--- Make sure to read the Contributing Guidelines:                   --->
<!--- https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md --->
<!--- and follow PEP 8, PEP 257 and Spyder's code style:               --->
<!--- https://github.com/spyder-ide/spyder/wiki/Dev:-Coding-Style      --->

## Description of Changes



<!--- Explain what you've done and why --->

Add validation for theme color changes. A message box was added to display invalid colors
Preview:

![PreviewColorEscheme](https://github.com/user-attachments/assets/83df809a-a17e-4258-85a9-3ebc0205e978)

### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #22970 


### Affirmation

By submitting this Pull Request or typing my (user)name below,
I affirm the [Developer Certificate of Origin](https://developercertificate.org)
with respect to all commits and content included in this PR,
and understand I am releasing the same under Spyder's MIT (Expat) license.

<!--- TYPE YOUR USER/NAME AFTER THE FOLLOWING: --->
I certify the above statement is true and correct:
@jsbautista 
<!--- Thanks for your help making Spyder better for everyone! --->


Problem statement:
Custom syntax highlighting theme has been removed / Spyder crashes when a custom syntax highlighting theme has as color an invalid hex value
Hi

I noticed that my personal (dark) theme was recently removed (maybe after an update) ; twice, i tried to re-create my theme:

1. new values don't change anything
2. when spyder is closed and reopened, the window immediatly closes when it appears
3. when using the anaconda prompt to launch spyder, no message is displayed

I needed to reset and return to default settings to use Spyder

Paul

* Spyder version: 6.0.2  (conda)
* Python version: 3.11.10 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.10
* Operating System: Windows-10-10.0.19045-SP0

Hints from issue discussion:
Hey @paul18frr, thanks for reporting. You said:

> I noticed that my personal (dark) theme was recently removed (maybe after an update)

That probably happened when you updated from Spyder 5 to 6 because we had to reset several default theme colors.

> twice, i tried to re-create my theme:
>
> 1. new values don't change anything
> 2. when spyder is closed and reopened, the window immediatly closes when it appears
> 3. when using the anaconda prompt to launch spyder, no message is displayed

So, when after you set your own theme, Spyder crashes at startup?
> So, when after you set your own theme, Spyder crashes at startup?

Yep
@dalthviz, could you check if you're able to reproduce this problem on Windows? Thanks!
@paul18frr could it be possible for you to run spyder in debug mode and check if any output related with the error is registered? For that, use an Anaconda Prompt with the env you have Spyder installed activated and try launching with something like `spyder --debug-info verbose`. Also, what values are you using for your custom theme? How are you setting such values? Could it be possible for you to create a GIF/video showing the behavior you are experiencing?

Any further info in order to reproduce and better understand this is greatly appreciate! Let us know!
Well, I cannot explain what I did wrong yesterday, because it works now => i've probably implemented wrong values (when typing, sometimes an extra character appeared like a bullet or a spade one during my trials :-) ... I may forgot to remove one ?!? ).

Here after my theme which is no more no less than the original Spyder theme i'm using for years (actually since I started to play with the IDE :-)

So sorry for the post

![myTheme](https://github.com/user-attachments/assets/594f052b-2d10-4b46-aae8-a6808936979a)



> i've probably implemented wrong values (when typing, sometimes an extra character appeared like a bullet or a spade one during my trials :-) 

This is very important information! I guess if any of the colors entered by users in their custom color scheme is not a valid hex color, then Spyder would crash in the way you described.

@dalthviz, could you check if you're able to reproduce that on your side?

> So sorry for the post

No worries! I think you revealed to us a critical error that we really need to fix.
Thank you for the new info @paul18frr ! Adding a `!` to one of the colors of an scheme, I was able to reproduce crashing Spyder after it is relaunched. Checking, from a dev version I was able to get the following traceback:

```python-traceback
Traceback (most recent call last):
  File "E:\Acer\Documentos\Spyder\spyder\bootstrap.py", line 214, in <module>
    start.main()
  File "E:\Acer\Documentos\Spyder\spyder\spyder\app\start.py", line 285, in main
    mainwindow.main(options, args)
  File "E:\Acer\Documentos\Spyder\spyder\spyder\app\mainwindow.py", line 1425, in main
    mainwindow = create_window(MainWindow, app, splash, options, args)
  File "E:\Acer\Documentos\Spyder\spyder\spyder\app\utils.py", line 370, in create_window
    main.post_visible_setup()
  File "E:\Acer\Documentos\Spyder\spyder\spyder\app\mainwindow.py", line 861, in post_visible_setup
    plugin.on_mainwindow_visible()
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\plugin.py", line 562, in on_mainwindow_visible
    self.create_new_client(give_focus=False)
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\plugin.py", line 687, in create_new_client
    self.get_widget().create_new_client(
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\widgets\main_widget.py", line 1696, in create_new_client
    client = ClientWidget(
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\widgets\client.py", line 148, in __init__
    self.shellwidget = ShellWidget(
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\widgets\shell.py", line 167, in __init__
    super().__init__(*args, **kw)
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\widgets\debugging.py", line 206, in __init__
    super(DebuggingWidget, self).__init__(*args, **kwargs)
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\widgets\debugging.py", line 85, in __init__
    super(DebuggingHistoryWidget, self).__init__(*args, **kwargs)
  File "e:\acer\documentos\spyder\spyder\external-deps\qtconsole\qtconsole\rich_jupyter_widget.py", line 71, in __init__
    super().__init__(*args, **kw)
  File "e:\acer\documentos\spyder\spyder\external-deps\qtconsole\qtconsole\jupyter_widget.py", line 129, in __init__
    self._syntax_style_changed()
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\widgets\shell.py", line 1439, in _syntax_style_changed
    self._highlighter._style = create_style_class(self.syntax_style)
  File "E:\Acer\Documentos\Spyder\spyder\spyder\plugins\ipythonconsole\utils\style.py", line 171, in create_style_class
    class StyleClass(Style):
  File "C:\Users\dalth\anaconda3\envs\spyder-dev\lib\site-packages\pygments\style.py", line 122, in __new__
    ndef[0] = colorformat(styledef)
  File "C:\Users\dalth\anaconda3\envs\spyder-dev\lib\site-packages\pygments\style.py", line 79, in colorformat
    assert False, f"wrong color format {text!r}"
AssertionError: wrong color format '#b0e686!'
```

Commit message: fix message

Patch to review:
diff --git a/spyder/plugins/appearance/widgets.py b/spyder/plugins/appearance/widgets.py
index f4828f35ec4..64d205d4164 100644
--- a/spyder/plugins/appearance/widgets.py
+++ b/spyder/plugins/appearance/widgets.py
@@ -8,7 +8,9 @@
 
 from qtpy.QtCore import Qt
 from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QGridLayout, QGroupBox,
-                            QHBoxLayout, QVBoxLayout, QWidget)
+                            QHBoxLayout, QVBoxLayout, QWidget, QMessageBox)
+
+import re
 
 from spyder.api.translations import _
 from spyder.api.widgets.dialogs import SpyderDialogButtonBox
@@ -42,7 +44,7 @@ def __init__(self, parent=None, stack=None):
         self.setLayout(layout)
 
         # Signals
-        bbox.accepted.connect(self.accept)
+        bbox.accepted.connect(self.validate_colors)
         bbox.accepted.connect(self.get_edited_color_scheme)
         bbox.rejected.connect(self.reject)
 
@@ -60,6 +62,41 @@ def get_scheme_name(self):
         """
         return self.scheme_name_textbox[self.last_used_scheme].text()
 
+    def validate_colors(self):
+        """
+        Validate the current color scheme and display a message box listing 
+        any invalid colors.
+        """
+        invalid_colors = {}
+        scheme_name = self.last_used_scheme
+        pattern = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$"
+        for key in self.widgets[scheme_name]:
+            items = self.widgets[scheme_name][key]
+
+            if(not bool(re.match(pattern, items[0].text()))):
+                    invalid_colors[key] = items[0].text()
+
+        if invalid_colors:
+            message = _("The following properties have invalid colors:\n\n")
+            for property_name, color in invalid_colors.items():
+                name = syntaxhighlighters.COLOR_SCHEME_KEYS[property_name]
+                clean_name = name[:-1].replace("<br>","")
+                message += _(
+                    "The property {} has an invalid color: {}.\n"
+                    ).format(clean_name, color)
+            title = _('Error getting colors.')
+            self.msgbox = QMessageBox(
+                        QMessageBox.Warning,
+                        title,
+                        message,
+                        QMessageBox.Ok,
+                        self
+                    )
+            self.msgbox.exec_()         
+        else:
+            self.accept()
+
+
     def get_edited_color_scheme(self):
         """
         Get the values of the last edited color scheme to be used in an instant


Code review comments:
diff_hunk: @@ -60,6 +62,41 @@ def get_scheme_name(self):
         """
         return self.scheme_name_textbox[self.last_used_scheme].text()
 
+    def validate_colors(self):
+        """
+        Validate the current color scheme and display a message box listing 
+        any invalid colors.
+        """
+        invalid_colors = {}
+        scheme_name = self.last_used_scheme
+        pattern = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$"
+        for key in self.widgets[scheme_name]:
+            items = self.widgets[scheme_name][key]
+
+            if(not bool(re.match(pattern, items[0].text()))):
+                    invalid_colors[key] = items[0].text()
+
+        if invalid_colors:
+            message = _("The following properties have invalid colors:\n\n")
+            for property_name, color in invalid_colors.items():
+                name = syntaxhighlighters.COLOR_SCHEME_KEYS[property_name]
+                clean_name = name[:-1].replace("<br>","")
+                message += _(
+                    "The property {} has an invalid color: {}.\n"
+                    ).format(clean_name, color)
+            title = _('Error getting colors.')
+            self.msgbox = QMessageBox(
+                        QMessageBox.Warning,
+                        title,
+                        message,
+                        QMessageBox.Ok,
+                        self
+                    )
+            self.msgbox.exec_()         
path: spyder/plugins/appearance/widgets.py
text: @user1:
```suggestion
            msgbox.exec_()         
```

To go with my suggestion above.

diff_hunk: @@ -8,7 +8,9 @@
 
 from qtpy.QtCore import Qt
path: spyder/plugins/appearance/widgets.py
text: @user1:
```suggestion
import re

from qtpy.QtCore import Qt
```

`re` is a standard library module, so it needs to be placed before third-party ones.

diff_hunk: @@ -60,6 +62,41 @@ def get_scheme_name(self):
         """
         return self.scheme_name_textbox[self.last_used_scheme].text()
 
+    def validate_colors(self):
+        """
+        Validate the current color scheme and display a message box listing 
+        any invalid colors.
+        """
+        invalid_colors = {}
+        scheme_name = self.last_used_scheme
+        pattern = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$"
+        for key in self.widgets[scheme_name]:
+            items = self.widgets[scheme_name][key]
+
+            if(not bool(re.match(pattern, items[0].text()))):
path: spyder/plugins/appearance/widgets.py
text: @user1:
```suggestion
            if not bool(re.match(pattern, items[0].text())):
```

Remove unnecessary extra parenthesis.

diff_hunk: @@ -60,6 +62,41 @@ def get_scheme_name(self):
         """
         return self.scheme_name_textbox[self.last_used_scheme].text()
 
+    def validate_colors(self):
+        """
+        Validate the current color scheme and display a message box listing 
+        any invalid colors.
+        """
+        invalid_colors = {}
+        scheme_name = self.last_used_scheme
+        pattern = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$"
+        for key in self.widgets[scheme_name]:
+            items = self.widgets[scheme_name][key]
+
+            if(not bool(re.match(pattern, items[0].text()))):
+                    invalid_colors[key] = items[0].text()
+
+        if invalid_colors:
+            message = _("The following properties have invalid colors:\n\n")
+            for property_name, color in invalid_colors.items():
+                name = syntaxhighlighters.COLOR_SCHEME_KEYS[property_name]
+                clean_name = name[:-1].replace("<br>","")
+                message += _(
+                    "The property {} has an invalid color: {}.\n"
+                    ).format(clean_name, color)
+            title = _('Error getting colors.')
+            self.msgbox = QMessageBox(
+                        QMessageBox.Warning,
+                        title,
+                        message,
+                        QMessageBox.Ok,
+                        self
+                    )
path: spyder/plugins/appearance/widgets.py
text: @user1:
```suggestion

            msgbox = QMessageBox(
                QMessageBox.Warning,
                _('Error setting colors'),
                message,
                QMessageBox.Ok,
                self
            )
```

- No need of an additional variable for the title.
- Fix title name (the error happens when we're setting the colors, not getting them).
- No need to add this message as a class attribute (i.e. no `self.msgbox`)
- Add empty line between the previous `for` and this for clarity.

diff_hunk: @@ -60,6 +62,41 @@ def get_scheme_name(self):
         """
         return self.scheme_name_textbox[self.last_used_scheme].text()
 
+    def validate_colors(self):
+        """
+        Validate the current color scheme and display a message box listing 
+        any invalid colors.
+        """
+        invalid_colors = {}
+        scheme_name = self.last_used_scheme
+        pattern = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$"
+        for key in self.widgets[scheme_name]:
+            items = self.widgets[scheme_name][key]
+
+            if(not bool(re.match(pattern, items[0].text()))):
+                    invalid_colors[key] = items[0].text()
+
+        if invalid_colors:
+            message = _("The following properties have invalid colors:\n\n")
+            for property_name, color in invalid_colors.items():
+                name = syntaxhighlighters.COLOR_SCHEME_KEYS[property_name]
+                clean_name = name[:-1].replace("<br>","")
+                message += _(
+                    "The property {} has an invalid color: {}.\n"
+                    ).format(clean_name, color)
path: spyder/plugins/appearance/widgets.py
text: @user1:
```suggestion
                message += _(
                    "The property <b>{}</b> has an invalid color: {}\n"
                ).format(clean_name, color)
```

- Highlight property name in bold.
- Remove dot at the end because I don't think it's necessary.
- Fix code style.