Repository: spyder-ide/spyder
Pull request: PR: Catch error when checking for updates (Update Manager)
<!--- 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 --->

Spyder catches the exception caused by certificate related errors. When this exception occurs, an informative message is displayed

![PreviewOSError](https://github.com/user-attachments/assets/aaed806c-5dd8-4b48-80d1-622d020cffed)


### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #22834


### 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:
Error when checking for updates
 ## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

I'm just pulling in my libraries when this happens

### Traceback
```python-traceback
Traceback (most recent call last):
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\spyder\plugins\updatemanager\workers.py", line 166, in start
    page = requests.get(url, headers=headers)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\adapters.py", line 639, in send
    self.cert_verify(conn, request.url, verify, cert)
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\adapters.py", line 328, in cert_verify
    raise OSError(
OSError: Could not find a suitable TLS CA certificate bundle, invalid path: %beatwoo%\ca.crt
Could not find a suitable TLS CA certificate bundle, invalid path: %beatwoo%\ca.crt
Traceback (most recent call last):
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\spyder\plugins\updatemanager\workers.py", line 166, in start
    page = requests.get(url, headers=headers)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\adapters.py", line 639, in send
    self.cert_verify(conn, request.url, verify, cert)
  File "C:\Users\beatwoo\AppData\Roaming\Python\Python312\site-packages\requests\adapters.py", line 328, in cert_verify
    raise OSError(
OSError: Could not find a suitable TLS CA certificate bundle, invalid path: %beatwoo%\ca.crt
```

## Versions

* Spyder version: 6.0.1  (pip)
* Python version: 3.12.4 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.11
* Operating System: Windows-10-10.0.19045-SP0

### Dependencies

```
# Mandatory:
aiohttp >=3.9.3                      :  3.10.9 (OK)
asyncssh >=2.14.0,<3.0.0             :  2.17.0 (OK)
atomicwrites >=1.2.0                 :  1.4.1 (OK)
chardet >=2.0.0                      :  3.0.4 (OK)
cloudpickle >=0.5.0                  :  3.0.0 (OK)
cookiecutter >=1.6.0                 :  2.6.0 (OK)
diff_match_patch >=20181111          :  20230430 (OK)
github >=2.3.0                       :  2.4.0 (OK)
importlib_metadata >=4.6.0           :  8.5.0 (OK)
intervaltree >=3.0.2                 :  3.1.0 (OK)
IPython >=8.13.0,<9.0.0,!=8.17.1     :  8.28.0 (OK)
jedi >=0.17.2,<0.20.0                :  0.19.1 (OK)
jellyfish >=0.7                      :  1.1.0 (OK)
jsonschema >=3.2.0                   :  4.23.0 (OK)
keyring >=17.0.0                     :  25.4.1 (OK)
nbconvert >=4.0                      :  7.16.4 (OK)
numpydoc >=0.6.0                     :  1.8.0 (OK)
parso >=0.7.0,<0.9.0                 :  0.8.4 (OK)
pexpect >=4.4.0                      :  4.9.0 (OK)
pickleshare >=0.4                    :  0.7.5 (OK)
psutil >=5.3                         :  6.0.0 (OK)
pygments >=2.0                       :  2.18.0 (OK)
pylint >=3.1,<4                      :  3.3.1 (OK)
pylint_venv >=3.0.2                  :  3.0.3 (OK)
pyls_spyder >=0.4.0                  :  0.4.0 (OK)
pylsp >=1.12.0,<1.13.0               :  1.12.0 (OK)
pylsp_black >=2.0.0,<3.0.0           :  2.0.0 (OK)
pyuca >=1.2                          :  1.2 (OK)
qdarkstyle >=3.2.0,<3.3.0            :  3.2.3 (OK)
qstylizer >=0.2.2                    :  0.2.3 (OK)
qtawesome >=1.3.1,<1.4.0             :  1.3.1 (OK)
qtconsole >=5.6.0,<5.7.0             :  5.6.0 (OK)
qtpy >=2.4.0                         :  2.4.1 (OK)
rtree >=0.9.7                        :  1.3.0 (OK)
setuptools >=49.6.0                  :  75.1.0 (OK)
sphinx >=0.6.6                       :  8.0.2 (OK)
spyder_kernels >=3.0.0,<3.1.0        :  3.0.0 (OK)
superqt >=0.6.2,<1.0.0               :  0.6.7 (OK)
textdistance >=4.2.0                 :  4.6.2 (OK)
three_merge >=0.1.1                  :  0.1.1 (OK)
watchdog >=0.10.3                    :  5.0.3 (OK)
yarl >=1.9.4                         :  1.14.0 (OK)
zmq >=24.0.0                         :  26.2.0 (OK)

# Optional:
cython >=0.21                        :  None (NOK)
matplotlib >=3.0.0                   :  3.9.2 (OK)
numpy >=1.7                          :  1.26.4 (OK)
pandas >=1.1.1                       :  2.2.3 (OK)
scipy >=0.17.0                       :  1.14.1 (OK)
sympy >=0.7.3                        :  None (NOK)

# Spyder plugins:
spyder_notebook.notebookplugin 0.6.1 :  0.6.1 (OK)
```

### Environment

<details><summary>Environment</summary>

```
Package                       Version
----------------------------- --------------
aiohappyeyeballs              2.4.3
aiohttp                       3.10.9
aiosignal                     1.3.1
alabaster                     1.0.0
annotated-types               0.7.0
anyio                         4.6.0
appdirs                       1.4.4
argon2-cffi                   23.1.0
argon2-cffi-bindings          21.2.0
arrow                         1.3.0
asn1crypto                    1.5.1
astor                         0.8.1
astroid                       3.3.5
asttokens                     2.4.1
async-lru                     2.0.4
asyncssh                      2.17.0
atomicwrites                  1.4.1
attrs                         24.2.0
autopep8                      2.0.4
azure-common                  1.1.28
azure-core                    1.31.0
azure-identity                1.15.0
azure-keyvault-secrets        4.7.0
babel                         2.16.0
backoff                       2.2.1
beautifulsoup4                4.12.3
binaryornot                   0.4.4
black                         24.10.0
bleach                        6.1.0
cachetools                    5.5.0
certifi                       2024.8.30
cffi                          1.17.1
chardet                       3.0.4
charset-normalizer            3.3.2
click                         8.1.7
cloudpickle                   3.0.0
colorama                      0.4.6
comm                          0.2.2
contourpy                     1.3.0
cookiecutter                  2.6.0
cryptography                  43.0.1
cycler                        0.12.1
DateTime                      5.5
dateutils                     0.6.12
debugpy                       1.8.6
decorator                     5.1.1
defusedxml                    0.7.1
Deprecated                    1.2.14
deprecation                   2.1.0
diff-match-patch              20230430
dill                          0.3.9
docstring-to-markdown         0.15
docutils                      0.21.2
duckdb                        1.1.1
eikon                         1.1.18
et-xmlfile                    1.1.0
excelReader                   0.0.1
executing                     2.1.0
fastjsonschema                2.20.0
filelock                      3.16.1
fiscalyear                    0.4.0
flake8                        7.1.1
fonttools                     4.54.1
fqdn                          1.5.1
frozenlist                    1.4.1
greenlet                      3.1.1
gw_dsl_parser                 0.1.49.1
h11                           0.14.0
h2                            4.1.0
hpack                         4.0.0
httpcore                      1.0.6
httpx                         0.27.2
hyperframe                    6.0.1
idna                          2.10
imagesize                     1.4.1
importlib_metadata            8.5.0
importlib_resources           6.4.5
inflection                    0.5.1
intervaltree                  3.1.0
ipykernel                     6.29.5
ipylab                        1.0.0
ipython                       8.28.0
ipywidgets                    8.1.5
isodate                       0.7.0
isoduration                   20.11.0
isort                         5.13.2
jaraco.classes                3.4.0
jaraco.context                6.0.1
jaraco.functools              4.1.0
jedi                          0.19.1
jellyfish                     1.1.0
Jinja2                        3.1.4
joblib                        1.4.2
json5                         0.9.25
jsonpointer                   3.0.0
jsonschema                    4.23.0
jsonschema-specifications     2023.12.1
jupyter                       1.1.1
jupyter_client                8.6.3
jupyter-console               6.6.3
jupyter_core                  5.7.2
jupyter-events                0.10.0
jupyter-lsp                   2.2.5
jupyter_server                2.14.2
jupyter_server_terminals      0.5.3
jupyterlab                    4.2.5
jupyterlab_pygments           0.3.0
jupyterlab_server             2.27.3
jupyterlab_widgets            3.0.13
kanaries_track                0.0.5
keyring                       25.4.1
kiwisolver                    1.4.7
markdown-it-py                3.0.0
MarkupSafe                    3.0.0
matplotlib                    3.9.2
matplotlib-inline             0.1.7
mccabe                        0.7.0
mdurl                         0.1.2
mistune                       3.0.2
monotonic                     1.6
more-itertools                10.5.0
msal                          1.26.0
msal-extensions               1.1.0
multidict                     6.1.0
mypy-extensions               1.0.0
nbclient                      0.10.0
nbconvert                     7.16.4
nbformat                      5.10.4
nest-asyncio                  1.6.0
notebook                      7.2.2
notebook_shim                 0.2.4
numpy                         1.26.4
numpydoc                      1.8.0
openpyxl                      3.1.5
overrides                     7.7.0
packaging                     24.1
pandas                        2.2.3
pandocfilters                 1.5.1
parso                         0.8.4
pathspec                      0.12.1
patsy                         0.5.6
pexpect                       4.9.0
pickleshare                   0.7.5
pillow                        10.4.0
pip                           24.3.1
platformdirs                  4.3.6
plotly                        5.24.1
pluggy                        1.5.0
portalocker                   2.10.1
prometheus_client             0.21.0
prompt_toolkit                3.0.48
propcache                     0.2.0
psutil                        6.0.0
ptyprocess                    0.7.0
pure_eval                     0.2.3
pyarrow                       17.0.0
pycodestyle                   2.12.1
pycparser                     2.22
pydantic                      2.9.2
pydantic_core                 2.23.4
pydocstyle                    6.3.0
pyflakes                      3.2.0
PyGithub                      2.4.0
Pygments                      2.18.0
pygwalker                     0.4.9.10
PyJWT                         2.9.0
pylint                        3.3.1
pylint-venv                   3.0.3
pyls-spyder                   0.4.0
PyNaCl                        1.5.0
pyOpenSSL                     24.2.1
pyparsing                     3.1.4
PyQt5                         5.15.11
PyQt5-Qt5                     5.15.2
PyQt5_sip                     12.15.0
PyQtWebEngine                 5.15.7
PyQtWebEngine-Qt5             5.15.2
python-dateutil               2.9.0.post0
python-decouple               3.8
python-json-logger            2.0.7
python-lsp-black              2.0.0
python-lsp-jsonrpc            1.1.2
python-lsp-server             1.12.0
python-slugify                8.0.4
pytoolconfig                  1.3.1
pytz                          2024.2
pyuca                         1.2
pywin32                       307
pywin32-ctypes                0.2.3
pywinpty                      2.0.13
PyYAML                        6.0.2
pyzmq                         26.2.0
QDarkStyle                    3.2.3
qstylizer                     0.2.3
QtAwesome                     1.3.1
qtconsole                     5.6.0
QtPy                          2.4.1
quickjs                       1.19.4
referencing                   0.35.1
requests                      2.32.3
rfc3339-validator             0.1.4
rfc3986                       1.5.0
rfc3986-validator             0.1.1
rich                          13.9.2
rope                          1.13.0
rpds-py                       0.20.0
Rtree                         1.3.0
schedule                      1.2.2
scikit-learn                  1.5.2
scipy                         1.14.1
seaborn                       0.13.2
segment-analytics-python      2.2.3
Send2Trash                    1.8.3
setuptools                    75.1.0
six                           1.16.0
sniffio                       1.3.1
snowballstemmer               2.2.0
snowflake-connector-python    3.12.2
sortedcontainers              2.4.0
soupsieve                     2.6
Sphinx                        8.0.2
sphinxcontrib-applehelp       2.0.0
sphinxcontrib-devhelp         2.0.0
sphinxcontrib-htmlhelp        2.1.0
sphinxcontrib-jsmath          1.0.1
sphinxcontrib-qthelp          2.0.0
sphinxcontrib-serializinghtml 2.0.0
spyder                        6.0.1
spyder-kernels                3.0.0
spyder-notebook               0.6.1
SQLAlchemy                    2.0.35
sqlglot                       25.24.5
stack-data                    0.6.3
statsmodels                   0.14.4
superqt                       0.6.7
tabulate                      0.9.0
tap-toolset                   0.2.15
tenacity                      9.0.0
terminado                     0.18.1
text-unidecode                1.3
textdistance                  4.6.3
threadpoolctl                 3.5.0
three-merge                   0.1.1
tinycss2                      1.3.0
tomli                         2.0.2
tomlkit                       0.13.2
tornado                       6.4.1
traitlets                     5.14.3
types-python-dateutil         2.9.0.20241003
typing_extensions             4.12.2
tzdata                        2024.2
ujson                         5.10.0
uri-template                  1.3.0
urllib3                       2.2.3
wasmtime                      25.0.0
watchdog                      5.0.3
wcwidth                       0.2.13
webcolors                     24.8.0
webencodings                  0.5.1
websocket-client              1.8.0
whatthepatch                  1.0.6
widgetsnbextension            4.0.13
wrapt                         1.16.0
yapf                          0.40.2
yarl                          1.14.0
zipp                          3.20.2
zope.interface                7.0.3

```
</details>

<!---   *** BEFORE SUBMITTING: PASTE CLIPBOARD HERE TO COMPLETE YOUR REPORT ***   ---!>

Hints from issue discussion:
Hey @elwoodataine, thanks for reporting. It seems Spyder is unable to check if an update is available because some security certificates are not available in your system.

There's nothing we can do on our side about it other than catching the error to not show it to users, which we'll do in a later version. To prevent this problem in the meantime, you need to update Spyder by opening a system terminal (i.e. cmd.exe) and running there

    pip install -U spyder
After validating this with @dalthviz , it appears that the problem is related to the SSL certificate configuration on the system rather than an issue with Spyder itself.

To address this, the following approach is proposed:

**Capturing the Exception:**
Spyder will capture the specific exception triggered by certificate-related errors.

**Informing the User:**
When this exception occurs, Spyder will display a clear and informative message indicating that the issue is related to the certificate configuration.

@ccordoba12 , what do you think about this possible solution?

Commit message: add comment

Patch to review:
diff --git a/spyder/plugins/updatemanager/widgets/update.py b/spyder/plugins/updatemanager/widgets/update.py
index 3cc7b7f7020..36b6f04975e 100644
--- a/spyder/plugins/updatemanager/widgets/update.py
+++ b/spyder/plugins/updatemanager/widgets/update.py
@@ -228,6 +228,7 @@ def _process_check_update(self):
         # Get results from worker
         update_available = self.update_worker.update_available
         error_msg = self.update_worker.error
+        checkbox = self.update_worker.checkbox
 
         # Always set status, regardless of error, DEV, or startup
         self.set_status(PENDING if update_available else NO_STATUS)
@@ -243,7 +244,7 @@ def _process_check_update(self):
             # Do not alert the user to anything
             pass
         elif error_msg is not None:
-            error_messagebox(self, error_msg)
+            error_messagebox(self, error_msg, checkbox)
         elif update_available:
             self.start_update()
         else:
@@ -532,10 +533,11 @@ def update_progress(self, progress, total):
         self._progress_bar.setValue(progress)
 
 
-def error_messagebox(parent, error_msg):
-    box = UpdateMessageBox(
-        icon=QMessageBox.Warning, text=error_msg, parent=parent
-    )
+def error_messagebox(parent, error_msg, checkbox=False):
+    # Use a message box with a checkbox for enabling update checks,
+    # or a standard one otherwise.
+    box_class = UpdateMessageCheckBox if checkbox else UpdateMessageBox
+    box = box_class(icon=QMessageBox.Warning, text=error_msg, parent=parent)
     box.setWindowTitle(_("Spyder update error"))
     box.setStandardButtons(QMessageBox.Ok)
     box.setDefaultButton(QMessageBox.Ok)
diff --git a/spyder/plugins/updatemanager/workers.py b/spyder/plugins/updatemanager/workers.py
index c0781b7d4ef..787643455d8 100644
--- a/spyder/plugins/updatemanager/workers.py
+++ b/spyder/plugins/updatemanager/workers.py
@@ -51,6 +51,11 @@
     '<br><br>Please contact your network administrator for assistance.'
 )
 
+OS_ERROR_MSG = _(
+    'An error occurred while checking for Spyder updates, possibly related to'
+    ' system configuration or file access. <br><br>Please verify your system '
+    'settings, including file paths and permissions.'
+)
 
 def _rate_limits(page):
     """Log rate limits for GitHub.com"""
@@ -190,6 +195,7 @@ def __init__(self, stable_only):
         self.latest_release = None
         self.update_available = False
         self.error = None
+        self.checkbox = False
         self.channel = None
 
     def _check_update_available(
@@ -294,6 +300,10 @@ def start(self):
         except HTTPError as err:
             error_msg = HTTP_ERROR_MSG.format(status_code=page.status_code)
             logger.warning(err, exc_info=err)
+        except OSError as err:
+            error_msg = OS_ERROR_MSG
+            self.checkbox = True
+            logger.warning(err, exc_info=err)
         except Exception as err:
             # Send untracked errors to our error reporter
             error_data = dict(


Code review comments:
diff_hunk: @@ -51,6 +51,11 @@
     '<br><br>Please contact your network administrator for assistance.'
 )
 
+OS_ERROR_MSG = _(
+    'An error occurred while checking for Spyder updates, possibly related to'
+    ' system configuration or file access. <br><br>Please verify your system '
+    'settings, including file paths and permissions.'
path: spyder/plugins/updatemanager/workers.py
text: @user1:
```suggestion
    "An error occurred while checking for Spyder updates, possibly related to "
    "your operating system configuration or file access.<br><br>If you're not "
    "sure what to do about it, you can disable checking for updates below. "
    "<br><br>The error was:<br><br><tt>{error}</tt>"
```

- It's not easy to understand what you should do, as a user, to solve the problem with the current message.
- Make it more explicit that users can disable checking for updates if they don't know what to do.
- Show the error to users to in case they know how to fix it.

diff_hunk: @@ -294,6 +300,10 @@ def start(self):
         except HTTPError as err:
             error_msg = HTTP_ERROR_MSG.format(status_code=page.status_code)
             logger.warning(err, exc_info=err)
+        except OSError as err:
+            error_msg = OS_ERROR_MSG
path: spyder/plugins/updatemanager/workers.py
text: @user1:
```suggestion
            error_msg = OS_ERROR_MSG.format(error=err)
```

To go with my suggestion above.