Repository: spyder-ide/spyder

Issue:
Spyder update not checking in conda-forge
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [x] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [x] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [x] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [x] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [x] Tried basic troubleshooting (if a bug/error)
    * [x] Restarted Spyder
    * [x] Reset preferences with ``spyder --reset``
    * [x] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [x] Tried the other applicable steps from the Troubleshooting Guide
* [x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description

For Spyder running in a conda environment, Spyder checks Anaconda's channel for updates, regardless of channel configuration set by conda's .condarc file because the channel's URL is hard-coded into Spyder.  Not only will this produce inaccurate results if one is using [Spyder installed from a channel such as conda-forge](https://anaconda.org/conda-forge/spyder), which may have different versions of Spyder available for install than the Anaconda channel, but may also be putting them in violation of the [Anaconda TOS](https://legal.anaconda.com/policies/en/?name=terms-of-service#terms-of-service) for usage of the Anaconda channel if they are using Spyder in a large business (> 200 employees, as defined in the TOS) setting.  The problem is specifically [here](https://github.com/spyder-ide/spyder/blob/a894eb65778777aec915d1d95fe8e564e177cf62/spyder/workers/updates.py#L86).  This should not be hard-coded and, should instead, check for Spyder updates in the users's configured conda channels.

### What steps reproduce the problem?

1. Start monitoring network activity for domains accessed
2. Install Spyder from conda-forge or any non-anaconda conda channel in an environment in which neither "anaconda" nor "defaults" are in the channels key of the applicable .condarc files
3. Open Spyder and check for updates

### What is the expected output? What do you see instead?

Spyder checks for updates in Anaconda channel regardless of channel settings in .condarc file.  It should, instead, check for Spyder updates in the channels configured in the user's [.condarc file(s)](https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html)

### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 5.4.5
* Python version:  3.11.5
* Qt version: 5.15.8
* PyQt version: 5.15.9
* Operating System name/version: Windows 11

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
# Mandatory:
atomicwrites >=1.2.0                                                        :  1.4.1 (OK)
chardet >=2.0.0                                                             :  5.2.0 (OK)
cloudpickle >=0.5.0                                                         :  2.2.1 (OK)
cookiecutter >=1.6.0                                                        :  2.3.0 (OK)
diff_match_patch >=20181111                                                 :  20230430 (OK)
intervaltree >=3.0.2                                                        :  3.1.0 (OK)
IPython >=7.31.1,<9.0.0,!=8.8.0,!=8.9.0,!=8.10.0,!=8.11.0,!=8.12.0,!=8.12.1 :  8.15.0 (OK)
jedi >=0.17.2,<0.19.0                                                       :  0.18.2 (OK)
jellyfish >=0.7                                                             :  1.0.0 (OK)
jsonschema >=3.2.0                                                          :  4.19.0 (OK)
keyring >=17.0.0                                                            :  24.2.0 (OK)
nbconvert >=4.0                                                             :  7.8.0 (OK)
numpydoc >=0.6.0                                                            :  1.5.0 (OK)
paramiko >=2.4.0                                                            :  3.3.1 (OK)
parso >=0.7.0,<0.9.0                                                        :  0.8.3 (OK)
pexpect >=4.4.0                                                             :  4.8.0 (OK)
pickleshare >=0.4                                                           :  0.7.5 (OK)
psutil >=5.3                                                                :  5.9.5 (OK)
pygments >=2.0                                                              :  2.16.1 (OK)
pylint >=2.5.0,<3.0                                                         :  2.17.5 (OK)
pylint_venv >=3.0.2                                                         :  3.0.2 (OK)
pyls_spyder >=0.4.0                                                         :  0.4.0 (OK)
pylsp >=1.7.4,<1.8.0                                                        :  1.7.4 (OK)
pylsp_black >=1.2.0,<3.0.0                                                  :  1.3.0 (OK)
qdarkstyle >=3.0.2,<3.2.0                                                   :  3.1 (OK)
qstylizer >=0.2.2                                                           :  0.2.2 (OK)
qtawesome >=1.2.1                                                           :  1.2.3 (OK)
qtconsole >=5.4.2,<5.5.0                                                    :  5.4.4 (OK)
qtpy >=2.1.0                                                                :  2.4.0 (OK)
rtree >=0.9.7                                                               :  1.0.1 (OK)
setuptools >=49.6.0                                                         :  68.2.2 (OK)
sphinx >=0.6.6                                                              :  7.2.6 (OK)
spyder_kernels >=2.4.4,<2.5.0                                               :  2.4.4 (OK)
textdistance >=4.2.0                                                        :  4.5.0 (OK)
three_merge >=0.1.1                                                         :  0.1.1 (OK)
watchdog >=0.10.3                                                           :  3.0.0 (OK)
zmq >=22.1.0                                                                :  25.1.1 (OK)

# Optional:
cython >=0.21                                                               :  3.0.2 (OK)
matplotlib >=3.0.0                                                          :  3.8.0 (OK)
numpy >=1.7                                                                 :  1.26.0 (OK)
pandas >=1.1.1                                                              :  2.1.0 (OK)
scipy >=0.17.0                                                              :  1.11.2 (OK)
sympy >=0.7.3                                                               :  1.12 (OK)

# Spyder plugins:
spyder_notebook.notebookplugin 0.5.1                                        :  0.5.1 (OK)

```



Discussion:
Hey @norweeg, thanks for bringing this up. I think it's a valid request and something we haven't address so far because our number of downloads from Conda-forge seems rather low compared to defaults.

But we'll fix it in our next version (5.5.0), to be released next month.
> Hey @norweeg, thanks for bringing this up. I think it's a valid request and something we haven't address so far because our number of downloads from Conda-forge seems rather low compared to defaults.
> 
> But we'll fix it in our next version (5.5.0), to be released next month.

Thanks, @ccordoba12!  By the way, you have significantly more (~5x) the installs of spyder from the conda-forge channel than from anaconda channel according to the channel statistics for the spyder package on anaconda.org. (conda-forge has 1852405 where as anaconda only has 373940).  Conda-forge also has a newer version of spyder than the Anaconda channel (5.4.5 vs 5.4.3 respectively)

![image](https://github.com/spyder-ide/spyder/assets/5969754/8f845348-e694-41b7-992e-7ef317484711)

That's right, but the `anaconda` channel in anaconda.org is not the same as the same as the `defaults` channel. In `defaults` we get ~150k downloads per month last time I checked (and the numbers above are total number of downloads).

Resolving patch:
diff --git a/spyder/plugins/application/container.py b/spyder/plugins/application/container.py
--- a/spyder/plugins/application/container.py
+++ b/spyder/plugins/application/container.py
@@ -23,7 +23,7 @@
 from qtpy.QtWidgets import QAction, QMessageBox, QPushButton
 
 # Local imports
-from spyder import __docs_url__, __forum_url__, __trouble_url__, __version__
+from spyder import __docs_url__, __forum_url__, __trouble_url__
 from spyder import dependencies
 from spyder.api.translations import _
 from spyder.api.widgets.main_container import PluginMainContainer
@@ -33,7 +33,7 @@
                                 running_in_mac_app)
 from spyder.plugins.application.widgets.status import ApplicationUpdateStatus
 from spyder.plugins.console.api import ConsoleActions
-from spyder.utils.conda import is_anaconda_pkg
+from spyder.utils.conda import is_anaconda_pkg, get_spyder_conda_channel
 from spyder.utils.environ import UserEnvDialog
 from spyder.utils.qthelpers import start_file, DialogManager
 from spyder.widgets.about import AboutDialog
@@ -283,15 +283,13 @@ def _check_updates_ready(self):
         box.set_checked(self.get_conf(option))
 
         header = _(
-            "<b>Spyder {} is available!</b> "
-            "<i>(you&nbsp;have&nbsp;{})</i><br><br>"
-        ).format(latest_release, __version__)
+            "<h3>Spyder {} is available!</h3><br>"
+        ).format(latest_release)
 
         if error_msg is not None:
             box.setText(error_msg)
             box.set_check_visible(False)
-            box.exec_()
-
+            box.show()
         elif update_available:
             # Update using our installers
             if (
@@ -341,31 +339,64 @@ def _check_updates_ready(self):
                 box.setStandardButtons(QMessageBox.Ok)
                 box.setDefaultButton(QMessageBox.Ok)
 
-                msg = _("")
+                msg = ""
                 if not box.result():
                     msg += header
 
-                terminal = _("terminal")
                 if os.name == "nt":
                     if is_anaconda():
-                        terminal = "Anaconda prompt"
+                        msg += _("Run the following commands in the Anaconda "
+                                 "prompt to update manually:<br><br>")
                     else:
-                        terminal = _("cmd prompt")
-                msg += _("Run the following commands in the {} to update "
-                         "manually:<br><br>").format(terminal)
+                        msg += _("Run the following commands in a cmd prompt "
+                                 "to update manually:<br><br>")
+                else:
+                    msg += _("Run the following commands in a terminal to "
+                             "update manually:<br><br>")
 
                 if is_anaconda():
-                    if is_anaconda_pkg():
-                        msg += _("<code>conda update anaconda</code><br>")
-                    msg += _("<code>conda install spyder={}"
-                             "</code><br><br>").format(latest_release)
-                    msg += _("<b>Important note:</b> Since you installed "
-                             "Spyder with Anaconda, please <b>don't</b> use "
-                             "<code>pip</code> to update it as that will "
-                             "break your installation.")
+                    channel, __ = get_spyder_conda_channel()
+                    is_pypi = channel == 'pypi'
+
+                    if is_anaconda_pkg() and not is_pypi:
+                        msg += "<code>conda update anaconda</code><br>"
+
+                    if is_pypi:
+                        dont_mix_pip_conda_video = (
+                            "https://youtu.be/Ul79ihg41Rs"
+                        )
+
+                        msg += (
+                            "<code>pip install --upgrade spyder</code>"
+                            "<br><br><br>"
+                        )
+
+                        msg += _(
+                            "<b>Important note:</b> You installed Spyder with "
+                            "pip in a Conda environment, which is not a good "
+                            "idea. See <a href=\"{}\">our video</a> for more "
+                            "details about it."
+                        ).format(dont_mix_pip_conda_video)
+                    else:
+                        if channel == 'pkgs/main':
+                            channel = ''
+                        else:
+                            channel = f'-c {channel}'
+
+                        msg += (
+                            f"<code>conda install {channel} "
+                            f"spyder={latest_release}"
+                            f"</code><br><br><br>"
+                        )
+
+                        msg += _(
+                            "<b>Important note:</b> Since you installed "
+                            "Spyder with Anaconda, please don't use pip "
+                            "to update it as that will break your "
+                            "installation."
+                        )
                 else:
-                    msg += _("<code>pip install --upgrade spyder"
-                             "</code>")
+                    msg += "<code>pip install --upgrade spyder</code><br>"
 
                 msg += _(
                     "<br><br>For more information, visit our "
@@ -373,11 +404,10 @@ def _check_updates_ready(self):
                 ).format(url_i)
 
                 box.setText(msg)
-                box.exec_()
-
+                box.show()
         elif feedback:
             box.setText(_("Spyder is up to date."))
-            box.exec_()
+            box.show()
             if self.application_update_status:
                 self.application_update_status.set_no_status()
         else:
diff --git a/spyder/utils/conda.py b/spyder/utils/conda.py
--- a/spyder/utils/conda.py
+++ b/spyder/utils/conda.py
@@ -169,7 +169,35 @@ def is_anaconda_pkg(prefix=sys.prefix):
     """Detect if the anaconda meta package is installed."""
     if is_conda_env(prefix):
         conda_meta = osp.join(prefix, "conda-meta")
-        if glob("anaconda-[0-9]*.json", root_dir=conda_meta):
+        if glob(f"{conda_meta}{os.sep}anaconda-[0-9]*.json"):
             return True
 
     return False
+
+
+def get_spyder_conda_channel():
+    """Get the conda channel from which Spyder was installed."""
+    conda = find_conda()
+
+    if conda is None:
+        return None
+
+    env = get_conda_env_path(sys.executable)
+    cmdstr = ' '.join([conda, 'list', 'spyder', '--json', '--prefix', env])
+
+    try:
+        out, __ = run_shell_command(cmdstr, env={}).communicate()
+        out = out.decode()
+        out = json.loads(out)
+    except Exception:
+        return None
+
+    for package_info in out:
+        if package_info["name"] == 'spyder':
+            channel = package_info["channel"]
+            channel_url = package_info["base_url"]
+
+    if "<develop>" in channel_url:
+        channel_url = None
+
+    return channel, channel_url
diff --git a/spyder/workers/updates.py b/spyder/workers/updates.py
--- a/spyder/workers/updates.py
+++ b/spyder/workers/updates.py
@@ -9,10 +9,9 @@
 import logging
 import os
 import os.path as osp
-import re
 import ssl
-import sys
 import tempfile
+import traceback
 from urllib.request import urlopen, urlretrieve
 from urllib.error import URLError, HTTPError
 
@@ -21,9 +20,11 @@
 
 # Local imports
 from spyder import __version__
-from spyder.config.base import _, is_stable_version
+from spyder.config.base import (
+    _, is_stable_version, is_pynsist, running_in_mac_app)
 from spyder.py3compat import is_text_string
 from spyder.config.utils import is_anaconda
+from spyder.utils.conda import get_spyder_conda_channel
 from spyder.utils.programs import check_version, is_module_installed
 
 # Logger setup
@@ -58,14 +59,16 @@ def __init__(self, parent, startup, version="", releases=None):
             self.version = version
 
     def check_update_available(self):
-        """Checks if there is an update available.
+        """
+        Check if there is an update available.
 
         It takes as parameters the current version of Spyder and a list of
         valid cleaned releases in chronological order.
         Example: ['2.3.2', '2.3.3' ...] or with github ['2.3.4', '2.3.3' ...]
         """
-        # Don't perform any check for development versions
-        if 'dev' in self.version:
+        # Don't perform any check for development versions or we were unable to
+        # detect releases.
+        if 'dev' in self.version or not self.releases:
             return (False, self.latest_release)
 
         # Filter releases
@@ -81,22 +84,27 @@ def check_update_available(self):
                 latest_release)
 
     def start(self):
-        """Main method of the WorkerUpdates worker"""
-        if is_anaconda():
-            self.url = 'https://repo.anaconda.com/pkgs/main'
-            if os.name == 'nt':
-                self.url += '/win-64/repodata.json'
-            elif sys.platform == 'darwin':
-                self.url += '/osx-64/repodata.json'
-            else:
-                self.url += '/linux-64/repodata.json'
-        else:
-            self.url = ('https://api.github.com/repos/'
-                        'spyder-ide/spyder/releases')
+        """Main method of the worker."""
         self.update_available = False
         self.latest_release = __version__
 
         error_msg = None
+        pypi_url = "https://pypi.org/pypi/spyder/json"
+
+        if is_pynsist() or running_in_mac_app():
+            self.url = ('https://api.github.com/repos/'
+                        'spyder-ide/spyder/releases')
+        elif is_anaconda():
+            channel, channel_url = get_spyder_conda_channel()
+
+            if channel_url is None:
+                return
+            elif channel == "pypi":
+                self.url = pypi_url
+            else:
+                self.url = channel_url + '/channeldata.json'
+        else:
+            self.url = pypi_url
 
         try:
             if hasattr(ssl, '_create_unverified_context'):
@@ -107,39 +115,48 @@ def start(self):
                 page = urlopen(self.url, context=context)
             else:
                 page = urlopen(self.url)
-            try:
-                data = page.read()
-
-                # Needed step for python3 compatibility
-                if not is_text_string(data):
-                    data = data.decode()
-                data = json.loads(data)
-
-                if is_anaconda():
-                    if self.releases is None:
-                        self.releases = []
-                        for item in data['packages']:
-                            if ('spyder' in item and
-                                    not re.search(r'spyder-[a-zA-Z]', item)):
-                                self.releases.append(item.split('-')[1])
-                    result = self.check_update_available()
-                else:
-                    if self.releases is None:
-                        self.releases = [item['tag_name'].replace('v', '')
-                                         for item in data]
-                        self.releases = list(reversed(self.releases))
-
-                result = self.check_update_available()
-                self.update_available, self.latest_release = result
-            except Exception:
-                error_msg = _('Unable to retrieve information.')
-        except HTTPError:
-            error_msg = _('Unable to retrieve information.')
+
+            data = page.read()
+
+            # Needed step for python3 compatibility
+            if not is_text_string(data):
+                data = data.decode()
+            data = json.loads(data)
+
+            if is_pynsist() or running_in_mac_app():
+                if self.releases is None:
+                    self.releases = [
+                        item['tag_name'].replace('v', '') for item in data
+                    ]
+                    self.releases = list(reversed(self.releases))
+            elif is_anaconda() and self.url != pypi_url:
+                if self.releases is None:
+                    spyder_data = data['packages'].get('spyder')
+                    if spyder_data:
+                        self.releases = [spyder_data["version"]]
+            else:
+                if self.releases is None:
+                    self.releases = [data['info']['version']]
+
+            result = self.check_update_available()
+            self.update_available, self.latest_release = result
         except URLError:
-            error_msg = _('Unable to connect to the internet. <br><br>Make '
-                          'sure the connection is working properly.')
+            error_msg = _(
+                'It was not possible to connect to the internet to check for '
+                'Spyder updates.'
+                '.<br><br>'
+                'Make sure the connection is working properly.'
+            )
         except Exception:
-            error_msg = _('Unable to check for updates.')
+            error = traceback.format_exc()
+            formatted_error = error.replace('\n', '<br>').replace(' ', '&nbsp;')
+
+            error_msg = _(
+                'It was not possible to check for Spyder updates due to the '
+                'following error:'
+                '<br><br>'
+                '<tt>{}</tt>'
+            ).format(formatted_error)
 
         # Don't show dialog when starting up spyder and an error occur
         if not (self.startup and error_msg is not None):


Test patch:
diff --git a/spyder/utils/tests/test_conda.py b/spyder/utils/tests/test_conda.py
--- a/spyder/utils/tests/test_conda.py
+++ b/spyder/utils/tests/test_conda.py
@@ -19,7 +19,8 @@
 from spyder.config.utils import is_anaconda
 from spyder.utils.conda import (
     add_quotes, find_conda, get_conda_activation_script, get_conda_env_path,
-    get_conda_root_prefix, get_list_conda_envs, get_list_conda_envs_cache)
+    get_conda_root_prefix, get_list_conda_envs, get_list_conda_envs_cache,
+    get_spyder_conda_channel)
 
 
 if not is_anaconda():
@@ -90,5 +91,12 @@ def test_get_list_conda_envs_cache():
     assert (time1 - time0) < 0.01
 
 
+@pytest.mark.skipif(not running_in_ci(), reason="Only meant for CIs")
+def test_get_spyder_conda_channel():
+    channel, channel_url = get_spyder_conda_channel()
+    assert channel == "pypi"
+    assert channel_url == "https://conda.anaconda.org/pypi"
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/spyder/workers/tests/test_update.py b/spyder/workers/tests/test_update.py
--- a/spyder/workers/tests/test_update.py
+++ b/spyder/workers/tests/test_update.py
@@ -6,56 +6,103 @@
 
 import pytest
 
-from spyder.config.utils import is_anaconda
 from spyder.workers.updates import WorkerUpdates
 
 
-def test_update(qtbot):
-    """Test we offer updates for lower versions."""
-    worker = WorkerUpdates(None, False, version="1.0.0")
+@pytest.mark.parametrize("is_anaconda", [True, False])
+@pytest.mark.parametrize("is_pypi", [True, False])
+@pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"])
+@pytest.mark.parametrize(
+    "spyder_conda_channel", [
+        ("pkgs/main", "https://repo.anaconda.com/pkgs/main"),
+        ("conda-forge", "https://conda.anaconda.org/conda-forge")
+    ]
+)
+def test_updates(qtbot, mocker, is_anaconda, is_pypi, version,
+                 spyder_conda_channel):
+    """
+    Test whether or not we offer updates for Anaconda and PyPI according to the
+    current Spyder version.
+    """
+    mocker.patch(
+        "spyder.workers.updates.is_anaconda",
+        return_value=is_anaconda
+    )
+
+    if is_anaconda:
+        if is_pypi:
+            channel = ("pypi", "https://conda.anaconda.org/pypi")
+        else:
+            channel = spyder_conda_channel
+
+        mocker.patch(
+            "spyder.workers.updates.get_spyder_conda_channel",
+            return_value=channel
+        )
+
+    worker = WorkerUpdates(None, False, version=version)
     worker.start()
-    assert worker.update_available
 
+    update = worker.update_available
+    assert update if version.split('.')[0] == '1' else not update
+    assert len(worker.releases) == 1
+
+
+@pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"])
+def test_updates_for_installers(qtbot, mocker, version):
+    """
+    Test whether or not we offer updates for our installers according to the
+    current Spyder version.
+    """
+    mocker.patch("spyder.workers.updates.is_anaconda", return_value=False)
+    mocker.patch("spyder.workers.updates.is_pynsist", return_value=True)
 
-def test_no_update(qtbot):
-    """Test we don't offer updates for very high versions."""
-    worker = WorkerUpdates(None, False, version="1000.0.0")
+    worker = WorkerUpdates(None, False, version=version)
     worker.start()
-    assert not worker.update_available
+
+    update = worker.update_available
+    assert update if version.split('.')[0] == '1' else not update
+    assert len(worker.releases) > 1
 
 
-def test_no_update_development(qtbot):
+def test_no_update_development(qtbot, mocker):
     """Test we don't offer updates for development versions."""
+    mocker.patch(
+        "spyder.workers.updates.get_spyder_conda_channel",
+        return_value=("pypi", "https://conda.anaconda.org/pypi")
+    )
+
     worker = WorkerUpdates(None, False, version="3.3.2.dev0",
                            releases=['3.3.1'])
     worker.start()
     assert not worker.update_available
 
 
-def test_update_pre_to_pre(qtbot):
+def test_update_pre_to_pre(qtbot, mocker):
     """Test we offer updates between prereleases."""
+    mocker.patch(
+        "spyder.workers.updates.get_spyder_conda_channel",
+        return_value=("pypi", "https://conda.anaconda.org/pypi")
+    )
+
     worker = WorkerUpdates(None, False, version="4.0.0a1",
                            releases=['4.0.0b5'])
     worker.start()
     assert worker.update_available
 
 
-def test_update_pre_to_final(qtbot):
+def test_update_pre_to_final(qtbot, mocker):
     """Test we offer updates from prereleases to the final versions."""
+    mocker.patch(
+        "spyder.workers.updates.get_spyder_conda_channel",
+        return_value=("pypi", "https://conda.anaconda.org/pypi")
+    )
+
     worker = WorkerUpdates(None, False, version="4.0.0b3",
                            releases=['4.0.0'])
     worker.start()
     assert worker.update_available
 
 
-@pytest.mark.skipif(not is_anaconda(),
-                    reason='It only makes sense for Anaconda.')
-def test_releases_anaconda(qtbot):
-    """Test we don't include spyder-kernels releases in detected releases."""
-    worker = WorkerUpdates(None, False, version="3.3.1")
-    worker.start()
-    assert '0.2.4' not in worker.releases
-
-
 if __name__ == "__main__":
     pytest.main()