Repository: spyder-ide/spyder

Issue:
Add wheel for rtree to Mac installer
We don't declare `rtree` as a dependency in our `setup.py` because it's a binary package and doesn't have wheels in PyPI.

So to add it to the Mac app we'd have either to find a Mac wheel for it (easy) or to compile it in Github Actions (hard).

@mrclary, could you take a look at this one?


Discussion:
@ccordoba12, code snippets are working on macOS app. What exactly was `rtree` used for? Is there some behavior that I can look at?
`rtree` allows us to paint a box around the snippet regions, so users can move between them using `Tab`. Could you test for that?
@ccordoba12 @dalthviz, FYI I think that Toblerity/rtree#163 will have wheels uploaded to PyPi soon for windows, linux, and macOS.

Resolving patch:
diff --git a/installers/macOS/setup.py b/installers/macOS/setup.py
--- a/installers/macOS/setup.py
+++ b/installers/macOS/setup.py
@@ -13,13 +13,8 @@
 
 import os
 import sys
-import shutil
-import argparse
-import pkg_resources
 from logging import getLogger, StreamHandler, Formatter
 from setuptools import setup
-from dmgbuild import build_dmg
-from dmgbuild.core import DMGError
 
 # Setup logger
 fmt = Formatter('%(asctime)s [%(levelname)s] [%(name)s] -> %(message)s')
@@ -29,18 +24,14 @@
 logger.addHandler(h)
 logger.setLevel('INFO')
 
-# Setup paths
+# Define paths
 HERE = os.path.abspath(__file__)
 THISDIR = os.path.dirname(HERE)
 SPYREPO = os.path.realpath(os.path.join(THISDIR, '..', '..'))
 ICONFILE = os.path.join(SPYREPO, 'img_src', 'spyder.icns')
-
 SPYLINK = os.path.join(THISDIR, 'spyder')
-os.symlink(os.path.join(SPYREPO, 'spyder'), SPYLINK)
 
-from spyder import __version__ as SPYVER                         # noqa
-from spyder.config.utils import EDIT_FILETYPES, _get_extensions  # noqa
-from spyder.config.base import MAC_APP_NAME                      # noqa
+sys.path.append(SPYREPO)
 
 # Python version
 PYVER = [sys.version_info.major, sys.version_info.minor,
@@ -123,15 +114,24 @@ def make_app_bundle(dist_dir, make_lite=False):
         python38.zip/spyder/app/mac_stylesheet.qss'
     spyder_kernels :
         No module named spyder_kernels.console.__main__
+    textdistance :
+        NotADirectoryError: [Errno 20] Not a directory: '<path>/Resources/lib/
+        python39.zip/textdistance/libraries.json'
+    """
+    import shutil
+    import pkg_resources
+
+    from spyder import __version__ as SPYVER
+    from spyder.config.utils import EDIT_FILETYPES, _get_extensions
+    from spyder.config.base import MAC_APP_NAME
 
-   """
     build_type = 'lite' if make_lite else 'full'
     logger.info('Creating %s app bundle...', build_type)
 
     PACKAGES = ['alabaster', 'astroid', 'blib2to3', 'ipykernel', 'IPython',
                 'jedi', 'jinja2', 'keyring', 'parso', 'pygments', 'pylint',
                 'pyls', 'pyls_black', 'pyls_spyder', 'qtawesome', 'setuptools',
-                'sphinx', 'spyder', 'spyder_kernels']
+                'sphinx', 'spyder', 'spyder_kernels', 'textdistance']
 
     if make_lite:
         INCLUDES = []
@@ -147,6 +147,9 @@ def make_app_bundle(dist_dir, make_lite=False):
 
     EDIT_EXT = [ext[1:] for ext in _get_extensions(EDIT_FILETYPES)]
 
+    FRAMEWORKS = ['/usr/local/lib/libspatialindex.dylib',
+                  '/usr/local/lib/libspatialindex_c.dylib']  # for rtree
+
     OPTIONS = {
         'optimize': 0,
         'packages': PACKAGES,
@@ -154,6 +157,7 @@ def make_app_bundle(dist_dir, make_lite=False):
         'excludes': EXCLUDES,
         'iconfile': ICONFILE,
         'dist_dir': dist_dir,
+        'frameworks': FRAMEWORKS,
         'plist': {
             'CFBundleDocumentTypes': [{'CFBundleTypeExtensions': EDIT_EXT,
                                        'CFBundleTypeName': 'Text File',
@@ -169,9 +173,11 @@ def make_app_bundle(dist_dir, make_lite=False):
     shutil.copy2(os.path.join(SPYREPO, 'scripts', 'spyder'), app_script_path)
 
     try:
+        os.symlink(os.path.join(SPYREPO, 'spyder'), SPYLINK)
         setup(app=[app_script_path], options={'py2app': OPTIONS})
     finally:
         os.remove(app_script_path)
+        os.remove(SPYLINK)
 
     # Copy egg info from site-packages: fixes pkg_resources issue for pyls
     dest_dir = os.path.join(dist_dir, MAC_APP_NAME, 'Contents', 'Resources',
@@ -204,6 +210,11 @@ def make_disk_image(dist_dir, make_lite=False):
     """
     logger.info('Creating disk image...')
 
+    from dmgbuild import build_dmg
+    from dmgbuild.core import DMGError
+    from spyder import __version__ as SPYVER
+    from spyder.config.base import MAC_APP_NAME
+
     volume_name = '{}-{} Py-{}.{}.{}'.format(MAC_APP_NAME[:-4], SPYVER, *PYVER)
     dmgfile = os.path.join(dist_dir, 'Spyder')
     if make_lite:
@@ -234,6 +245,7 @@ def make_disk_image(dist_dir, make_lite=False):
 
 
 if __name__ == '__main__':
+    import argparse
     parser = argparse.ArgumentParser()
     parser.add_argument('-n', '--no-app', dest='make_app',
                         action='store_false', default=True,
@@ -253,15 +265,12 @@ def make_disk_image(dist_dir, make_lite=False):
 
     dist_dir = os.path.abspath(args.dist_dir)
 
-    try:
-        if args.make_app:
-            make_app_bundle(dist_dir, make_lite=args.make_lite)
-        else:
-            logger.info('Skipping app bundle.')
+    if args.make_app:
+        make_app_bundle(dist_dir, make_lite=args.make_lite)
+    else:
+        logger.info('Skipping app bundle.')
 
-        if args.make_dmg:
-            make_disk_image(dist_dir, make_lite=args.make_lite)
-        else:
-            logger.info('Skipping disk image.')
-    finally:
-        os.remove(SPYLINK)
+    if args.make_dmg:
+        make_disk_image(dist_dir, make_lite=args.make_lite)
+    else:
+        logger.info('Skipping disk image.')
diff --git a/spyder/utils/pyenv.py b/spyder/utils/pyenv.py
--- a/spyder/utils/pyenv.py
+++ b/spyder/utils/pyenv.py
@@ -43,21 +43,17 @@ def get_list_pyenv_envs():
     cmdstr = ' '.join([pyenv, 'versions', '--bare', '--skip-aliases'])
     try:
         out, __ = run_shell_command(cmdstr, env={}).communicate()
-        out = out.decode()
+        out = out.decode().strip()
     except Exception:
-        out = ''
+        return env_list
 
-    out = out.split('\n')
+    out = out.split('\n') if out else []
     for env in out:
         data = env.split(osp.sep)
         path = get_pyenv_path(data[-1])
 
-        if data[-1] == '':
-            name = 'internal' if running_in_mac_app(path) else 'system'
-        else:
-            name = 'pyenv: {}'.format(data[-1])
-        version = (
-            'Python 2.7' if data[-1] == '' else 'Python {}'.format(data[0]))
+        name = f'pyenv: {data[-1]}'
+        version = f'Python {data[0]}'
         env_list[name] = (path, version)
 
     PYENV_ENV_LIST_CACHE = env_list
diff --git a/spyder/widgets/status.py b/spyder/widgets/status.py
--- a/spyder/widgets/status.py
+++ b/spyder/widgets/status.py
@@ -325,7 +325,7 @@ def get_envs(self):
 
     def update_envs(self, worker, output, error):
         """Update the list of environments in the system."""
-        self.envs = output
+        self.envs.update(**output)
         for env in list(self.envs.keys()):
             path, version = self.envs[env]
             # Save paths in lowercase on Windows to avoid issues with
@@ -379,7 +379,7 @@ def _get_env_info(self, path):
             self.path_to_env[path] = name
             self.envs[name] = (path, version)
         __, version = self.envs[name]
-        return '{env} ({version})'.format(env=name, version=version)
+        return f'{name} ({version})'
 
     def get_tooltip(self):
         """Override api method."""


Test patch:
diff --git a/spyder/utils/tests/test_pyenv.py b/spyder/utils/tests/test_pyenv.py
--- a/spyder/utils/tests/test_pyenv.py
+++ b/spyder/utils/tests/test_pyenv.py
@@ -21,7 +21,7 @@
 @pytest.mark.skipif(os.name == 'nt', reason="Doesn't work on Windows")
 def test_get_list_pyenv_envs():
     output = get_list_pyenv_envs()
-    expected_envs = ['pyenv: 3.8.1', 'system']
+    expected_envs = ['pyenv: 3.8.1']
     assert set(expected_envs) == set(output.keys())