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: Show conda and pyenv environments in Python interpreter (Preferences) #13950

Merged
merged 21 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion spyder/preferences/maininterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
from spyder.preferences.configdialog import GeneralConfigPage
from spyder.py3compat import PY2, is_text_string, to_text_string
from spyder.utils import icon_manager as ima
from spyder.utils.misc import get_python_executable
from spyder.utils import programs
from spyder.utils.conda import get_list_conda_envs
from spyder.utils.misc import get_python_executable
from spyder.utils.programs import get_list_pyenv_envs


class MainInterpreterConfigPage(GeneralConfigPage):
Expand All @@ -37,6 +39,15 @@ def __init__(self, parent, main):
self.pyexec_edit = None
self.cus_exec_combo = None

conda_env = get_list_conda_envs()
pyenv_env = get_list_pyenv_envs()
envs = {**conda_env, **pyenv_env}
valid_custom_list = []
for env in envs.keys():
path, _ = envs[env]
valid_custom_list.append(path)
self.set_option('custom_interpreters_list', valid_custom_list)

# Python executable selection (initializing default values as well)
executable = self.get_option('executable', get_python_executable())
if self.get_option('default'):
Expand Down
64 changes: 64 additions & 0 deletions spyder/utils/conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@
"""Conda/anaconda utilities."""

# Standard library imports
import json
import os
import os.path as osp
import subprocess
import sys

from spyder.config.base import running_in_mac_app, get_home_dir
from spyder.utils.programs import find_program


WINDOWS = os.name == 'nt'


def add_quotes(path):
"""Return quotes if needed for spaces on path."""
Expand Down Expand Up @@ -92,3 +101,58 @@ def get_conda_env_path(pyexec, quote=False):
conda_env = add_quotes(conda_env)

return conda_env


def get_list_conda_envs():
"""Return the list of all conda envs found in the system."""
conda_list = ['conda', 'env', 'list', '--json']
if running_in_mac_app():
# set PATHs for finding conda
old_path = os.environ['PATH']
home = get_home_dir()
os.environ['PATH'] = os.pathsep.join([
old_path,
os.path.join(home, 'opt', 'anaconda3', 'condabin'),
# could have miniconda
os.path.join(home, 'opt', 'miniconda3', 'condabin'),
# could be installed for all users
os.path.join('/opt', 'anaconda3', 'condabin'),
os.path.join('/opt', 'miniconda3', 'condabin')
])
conda_path = find_program('conda')
os.environ['PATH'] = old_path # restore PATH
if conda_path:
conda_list = [conda_path, 'env', 'list', '--json']
try:
out, err = subprocess.Popen(
conda_list,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()
out = out.decode()
err = err.decode()
out = json.loads(out)
except Exception:
out = {'envs': []}
err = ''
env_list = {}
for env in out['envs']:
name = env.split('/')[-1]
try:
path = osp.join(env, 'python') if WINDOWS else osp.join(
env, 'bin', 'python')
version, err = subprocess.Popen(
[path, '--version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()
version = version.decode()
err = err.decode()
except Exception:
version = ''
err = ''
name = ('base' if name.lower().startswith('anaconda') or
name.lower().startswith('miniconda') else name)
name = 'conda: {}'.format(name)
env_list[name] = (path, version.strip())
return env_list
71 changes: 70 additions & 1 deletion spyder/utils/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@
import psutil

# Local imports
from spyder.config.base import is_stable_version, running_under_pytest
from spyder.config.base import (is_stable_version, running_under_pytest,
get_home_dir, running_in_mac_app)
from spyder.config.utils import is_anaconda
from spyder.py3compat import PY2, is_text_string, to_text_string
from spyder.utils import encoding
from spyder.utils.misc import get_python_executable


WINDOWS = os.name == 'nt'


class ProgramError(Exception):
pass

Expand Down Expand Up @@ -989,3 +993,68 @@ def is_spyder_process(pid):
return any(conditions)
except (psutil.NoSuchProcess, psutil.AccessDenied):
return False


def get_pyenv_path(name):
"""Return the complete path of the pyenv."""
home = get_home_dir()
if WINDOWS:
path = osp.join(
home, '.pyenv', 'pyenv-win', 'versions', name, 'python')
elif name == '':
path = osp.join(home, '.pyenv', 'shims', 'python')
else:
path = osp.join(home, '.pyenv', 'versions', name, 'bin', 'python')
return path


def get_list_pyenv_envs():
"""Return the list of all pyenv envs found in the system."""
pyenv = 'pyenv'
if running_in_mac_app():
old_path = os.environ['PATH']
os.environ['PATH'] = os.pathsep.join([old_path, '/usr/local/bin'])
pyenv_path = find_program('pyenv')
os.environ['PATH'] = old_path # restore PATH
if pyenv_path:
pyenv = pyenv_path
try:
out, err = subprocess.Popen(
[pyenv, 'versions', '--bare', '--skip-aliases'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()
out = out.decode()
err = err.decode()
except Exception:
out = ''
err = ''
out = out.split('\n')
env_list = {}
for env in out:
data = env.split('/')
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]))
env_list[name] = (path, version)
return env_list


def get_interpreter_info(path):
"""Return version information of the selected Python interpreter."""
try:
out, err = subprocess.Popen(
[path, '-V'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()
out = out.decode()
err = err.decode()
except Exception:
out = ''
err = ''
return out.strip()
116 changes: 59 additions & 57 deletions spyder/widgets/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,19 @@

# Standard library imports
import os
import subprocess
import os.path as osp

# Third party imports
from qtpy.QtCore import Qt, QSize, QTimer, Signal
from qtpy.QtCore import Qt, QPoint, QSize, QTimer, Signal
from qtpy.QtGui import QFont, QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QWidget

# Local imports
from spyder.config.base import _, running_in_mac_app
from spyder.config.gui import get_font
from spyder.config import utils
from spyder.py3compat import PY3
from spyder.utils.qthelpers import create_waitspinner
from spyder.utils.conda import is_conda_env
from spyder.config.base import _
from spyder.utils.conda import get_list_conda_envs
from spyder.utils.programs import get_list_pyenv_envs, get_interpreter_info
from spyder.utils.qthelpers import (add_actions, create_action,
create_waitspinner)


class StatusBarWidget(QWidget):
Expand Down Expand Up @@ -233,53 +232,58 @@ def __init__(self, parent, statusbar, icon=None):
"""Status bar widget for displaying the current conda environment."""
self._interpreter = None
super(InterpreterStatus, self).__init__(parent, statusbar, icon=icon)

def _get_interpreter_env_info(self):
"""Get conda environment information."""
self.main = parent
self.env_actions = []
self.path_to_env = {}
conda_env = get_list_conda_envs()
pyenv_env = get_list_pyenv_envs()
self.envs = {**conda_env, **pyenv_env}
for env in list(self.envs.keys()):
path, version = self.envs[env]
self.path_to_env[path] = env
self.menu = QMenu(self)
self.sig_clicked.connect(self.show_menu)

def show_menu(self):
"""Display a menu when clicking on the widget."""
menu = self.menu
menu.clear()
text = _("Change default environment in Preferences...")
change_action = create_action(
self,
text=text,
triggered=self.open_interpreter_preferences,
)
add_actions(menu, [change_action])
rect = self.contentsRect()
os_height = 7 if os.name == 'nt' else 12
pos = self.mapToGlobal(
rect.topLeft() + QPoint(-40, -rect.height() - os_height))
menu.popup(pos)

def open_interpreter_preferences(self):
"""Open the Preferences dialog in the Python interpreter section."""
self.main.show_preferences()
dlg = self.main.prefs_dialog_instance
index = dlg.get_index_by_name("main_interpreter")
dlg.set_current_index(index)

def _get_env_info(self, path):
"""Get environment information."""
try:
out, err = subprocess.Popen(
[self._interpreter, '-V'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()

if PY3:
out = out.decode()
err = err.decode()
except Exception:
out = ''
err = ''

return out, err

def _process_interpreter_env_info(self):
"""Process conda environment information."""
out, err = self._get_interpreter_env_info()
out = out or err # Anaconda base python prints to stderr
out = out.split('\n')[0]
parts = out.split()

if len(parts) >= 2:
out = ' '.join(parts[:2])

if is_conda_env(pyexec=self._interpreter):
envs_folder = os.path.sep + 'envs' + os.path.sep
if envs_folder in self._interpreter:
if os.name == 'nt':
env = os.path.dirname(self._interpreter)
else:
env = os.path.dirname(os.path.dirname(self._interpreter))
env = os.path.basename(env)
name = self.path_to_env[path]
except KeyError:
if 'Spyder.app' in path:
name = 'internal'
elif 'conda' in path:
name = 'conda: {}'.format(osp.split(path)[-2])
else:
env = 'base'
env = 'conda: ' + env
elif running_in_mac_app(self._interpreter):
env = 'internal'
steff456 marked this conversation as resolved.
Show resolved Hide resolved
else:
env = 'venv' # Update when additional environments are supported

text = '{env} ({version})'.format(env=env, version=out)
return text
name = 'custom'
version = get_interpreter_info(path)
self.path_to_env[path] = name
self.envs[name] = (path, version)
_, version = self.envs[name]
return '{env} ({version})'.format(env=name, version=version)

def get_tooltip(self):
"""Override api method."""
Expand All @@ -288,9 +292,7 @@ def get_tooltip(self):
def update_interpreter(self, interpreter):
"""Set main interpreter and update information."""
self._interpreter = interpreter

text = self._process_interpreter_env_info()

text = self._get_env_info(interpreter)
self.set_value(text)
self.update_tooltip()

Expand Down
Loading