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: Add menu to use specific environment interpreter for a new console instance #20421

Merged
merged 34 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
013e591
Add menu consoles environment
jsbautista Jan 23, 2023
23e7e87
Add menu consoles environment
jsbautista Jan 23, 2023
631bf20
Add updates to console environments
jsbautista Jan 24, 2023
e49bd1c
Change text in Preferences
jsbautista Jan 24, 2023
c79d695
Add updates to console environments
jsbautista Feb 6, 2023
5e890e1
Add updates to console environments
jsbautista Feb 7, 2023
b14818b
Merge branch 'master' into masterConsoleEnvironment
jsbautista Feb 7, 2023
a62410f
Apply suggestions from code review
jsbautista Feb 13, 2023
d33be31
Apply suggestions from code review
jsbautista Feb 13, 2023
ab1d363
Apply suggestions from code review
jsbautista Feb 17, 2023
d946fee
Apply suggestions from code review
jsbautista Feb 27, 2023
cde591f
Apply suggestions from code review
jsbautista Mar 1, 2023
a347fc5
Apply suggestions from code review
jsbautista Mar 6, 2023
a7c5662
Merge branch 'masterConsoleEnvironment' of https://github.com/jsbauti…
jsbautista Mar 6, 2023
8f940b1
Apply suggestions from code review
jsbautista Mar 6, 2023
dc30583
Apply suggestions
jsbautista Mar 8, 2023
0251147
Merge branch 'master' into masterConsoleEnvironment
jsbautista Mar 8, 2023
ef84f78
Apply suggestions
jsbautista Mar 8, 2023
0aa1662
Apply suggestions
jsbautista Mar 12, 2023
867b8fe
Update IPython console plugin tests
dalthviz Mar 14, 2023
4ba249a
Fix IPython console mainwindow test
dalthviz Mar 14, 2023
4bb02d1
Fix some code style issues. Refactor getting envs information
dalthviz Mar 15, 2023
2c04843
Restore some removed blank lines. Update envs module docstring
dalthviz Mar 15, 2023
560853a
Merge branch 'master' into masterConsoleEnvironment
dalthviz Mar 22, 2023
ab339fe
Merge branch 'master' into masterConsoleEnvironment
dalthviz Apr 12, 2023
090a579
Merge branch 'master' into masterConsoleEnvironment
dalthviz Apr 13, 2023
373ad25
Simplify naming approach for the consoles tabs when created from the …
dalthviz Apr 14, 2023
f3164cb
Fix IPython console test
dalthviz Apr 17, 2023
778d6cf
Add style from QMenu to not expand more than one column
dalthviz Apr 17, 2023
d34bae8
Merge remote-tracking branch 'upstream/master' into masterConsoleEnvi…
dalthviz Apr 17, 2023
fc3afdf
Fix IPython console test
dalthviz Apr 17, 2023
86ef33c
Add test and remove extra parenthesis from client name method
dalthviz Apr 18, 2023
9cf998f
Apply suggestions from code review
dalthviz Apr 20, 2023
cc0b08d
Update stylesheet syntax and imports order
dalthviz Apr 20, 2023
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
10 changes: 8 additions & 2 deletions spyder/plugins/ipythonconsole/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,8 @@ def rename_client_tab(self, client, given_name):
self.get_widget().rename_client_tab(client, given_name)

def create_new_client(self, give_focus=True, filename='', is_cython=False,
is_pylab=False, is_sympy=False, given_name=None):
is_pylab=False, is_sympy=False, given_name=None,
path_to_custom_interpreter=None):
"""
Create a new client.

Expand All @@ -546,6 +547,10 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
given_name : str, optional
Initial name displayed in the tab of the client.
The default is None.
path_to_custom_interpreter : str, optional
Path to a custom interpreter the client should use regardless of
the interpreter selected in the preferences.
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
The default is None.

Returns
-------
Expand All @@ -557,7 +562,8 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
is_cython=is_cython,
is_pylab=is_pylab,
is_sympy=is_sympy,
given_name=given_name)
given_name=given_name,
path_to_custom_interpreter=path_to_custom_interpreter)

def create_client_for_file(self, filename, is_cython=False):
"""
Expand Down
19 changes: 16 additions & 3 deletions spyder/plugins/ipythonconsole/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ def __getattr__(self, attr):
cython_client = request.node.get_closest_marker('cython_client')
is_cython = True if cython_client else False

# Start a specific env client if requested
environment_client = request.node.get_closest_marker(
'environment_client')
given_name = None
path_to_custom_interpreter = None
if environment_client:
given_name = 'spytest-ž'
path_to_custom_interpreter = get_conda_test_env()[1]

# Use an external interpreter if requested
external_interpreter = request.node.get_closest_marker(
'external_interpreter')
Expand Down Expand Up @@ -197,9 +206,13 @@ def get_plugin(name):
debugger.on_ipython_console_available()
console.on_initialize()
console._register()
console.create_new_client(is_pylab=is_pylab,
is_sympy=is_sympy,
is_cython=is_cython)
console.create_new_client(
is_pylab=is_pylab,
is_sympy=is_sympy,
is_cython=is_cython,
given_name=given_name,
path_to_custom_interpreter=path_to_custom_interpreter
)
window.setCentralWidget(console.get_widget())

# Set exclamation mark to True
Expand Down
30 changes: 30 additions & 0 deletions spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,36 @@ def test_cython_client(ipyconsole, qtbot):
assert 'Error' not in control.toPlainText()


@flaky(max_runs=3)
@pytest.mark.order(1)
@pytest.mark.environment_client
@pytest.mark.skipif(not is_anaconda(), reason='Only works with Anaconda')
@pytest.mark.skipif(not running_in_ci(), reason='Only works on CIs')
@pytest.mark.skipif(not os.name == 'nt', reason='Works reliably on Windows')
def test_environment_client(ipyconsole, qtbot):
"""
Test that when creating console for a specific environment the conda
environment associated with the external interpreter
is activated before a kernel is created for it.
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
"""
# Wait until the window is fully up
shell = ipyconsole.get_current_shellwidget()

# Check console name
client = ipyconsole.get_current_client()
client.get_name() == "spytest-ž 1/A"

# Get conda activation environment variable
with qtbot.waitSignal(shell.executed):
shell.execute(
"import os; conda_prefix = os.environ.get('CONDA_PREFIX')"
)

expected_output = get_conda_test_env()[0].replace('\\', '/')
output = shell.get_value('conda_prefix').replace('\\', '/')
assert expected_output == output


@flaky(max_runs=3)
def test_tab_rename_for_slaves(ipyconsole, qtbot):
"""Test slave clients are renamed correctly."""
Expand Down
12 changes: 8 additions & 4 deletions spyder/plugins/ipythonconsole/utils/kernelspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ class SpyderKernelSpec(KernelSpec, SpyderConfigurationAccessor):
CONF_SECTION = 'ipython_console'

def __init__(self, is_cython=False, is_pylab=False,
is_sympy=False, **kwargs):
is_sympy=False, path_to_custom_interpreter=None,
**kwargs):
super(SpyderKernelSpec, self).__init__(**kwargs)
self.is_cython = is_cython
self.is_pylab = is_pylab
self.is_sympy = is_sympy

self.path_to_custom_interpreter = path_to_custom_interpreter
self.display_name = 'Python 3 (Spyder)'
self.language = 'python3'
self.resource_dir = ''
Expand All @@ -111,10 +112,13 @@ def __init__(self, is_cython=False, is_pylab=False,
def argv(self):
"""Command to start kernels"""
# Python interpreter used to start kernels
if self.get_conf('default', section='main_interpreter'):
if (self.get_conf('default', section='main_interpreter') and
not self.path_to_custom_interpreter):
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
pyexec = get_python_executable()
else:
pyexec = self.get_conf('executable', section='main_interpreter')
if self.path_to_custom_interpreter:
pyexec = self.path_to_custom_interpreter
if not has_spyder_kernels(pyexec):
raise SpyderKernelError(
ERROR_SPYDER_KERNEL_INSTALLED.format(
Expand Down Expand Up @@ -186,7 +190,7 @@ def env(self):

# Environment variables that we need to pass to the kernel
env_vars.update({
'SPY_EXTERNAL_INTERPRETER': not default_interpreter,
'SPY_EXTERNAL_INTERPRETER': not default_interpreter or self.path_to_custom_interpreter,
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
'SPY_UMR_ENABLED': self.get_conf(
'umr/enabled', section='main_interpreter'),
'SPY_UMR_VERBOSE': self.get_conf(
Expand Down
7 changes: 5 additions & 2 deletions spyder/plugins/ipythonconsole/widgets/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def __init__(self, parent, id_,
give_focus=True,
options_button=None,
handlers={},
initial_cwd=None):
initial_cwd=None,
forcing_custom_interpreter=False):
super(ClientWidget, self).__init__(parent)
SaveHistoryMixin.__init__(self, get_conf_path('history.py'))

Expand All @@ -108,6 +109,7 @@ def __init__(self, parent, id_,
self.menu_actions = menu_actions
self.given_name = given_name
self.initial_cwd = initial_cwd
self.forcing_custom_interpreter = forcing_custom_interpreter

# --- Other attrs
self.kernel_handler = None
Expand Down Expand Up @@ -518,7 +520,8 @@ def get_name(self):
# Adding id to name
client_id = self.id_['int_id'] + u'/' + self.id_['str_id']
name = name + u' ' + client_id
elif self.given_name in ["Pylab", "SymPy", "Cython"]:
elif (self.given_name in ["Pylab", "SymPy", "Cython"] or
self.forcing_custom_interpreter):
client_id = self.id_['int_id'] + u'/' + self.id_['str_id']
name = self.given_name + u' ' + client_id
else:
Expand Down
96 changes: 89 additions & 7 deletions spyder/plugins/ipythonconsole/widgets/main_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
from spyder.widgets.browser import FrameWebView
from spyder.widgets.findreplace import FindReplace
from spyder.widgets.tabs import Tabs
from spyder.utils.workers import WorkerManager
from spyder.utils.envs import get_list_envs
ccordoba12 marked this conversation as resolved.
Show resolved Hide resolved


# Logging
Expand All @@ -64,6 +66,7 @@ class IPythonConsoleWidgetActions:
CreateCythonClient = 'create cython client'
CreateSymPyClient = 'create cympy client'
CreatePyLabClient = 'create pylab client'
CreateNewClientEnvironment = 'create environment client'

# Current console actions
ClearConsole = 'Clear shell'
Expand Down Expand Up @@ -94,6 +97,7 @@ class IPythonConsoleWidgetActions:
class IPythonConsoleWidgetOptionsMenus:
SpecialConsoles = 'special_consoles_submenu'
Documentation = 'documentation_submenu'
EnvironmentConsoles = 'environment_consoles_submenu'


class IPythonConsoleWidgetOptionsMenuSections:
Expand Down Expand Up @@ -268,6 +272,8 @@ def __init__(self, name=None, plugin=None, parent=None):
self.interrupt_action = None
self.initial_conf_options = self.get_conf_options()
self.registered_spyder_kernel_handlers = {}
self.envs = {}
self.default_interpreter = sys.executable

# Disable infowidget if requested by the user
self.enable_infowidget = True
Expand Down Expand Up @@ -351,6 +357,12 @@ def __init__(self, name=None, plugin=None, parent=None):
# Initial value for the current working directory
self._current_working_directory = get_home_dir()

# Worker to compute envs in a thread
self._worker_manager = WorkerManager(max_threads=1)

# Update the list of envs at startup
self.get_envs()

def on_close(self):
self.mainwindow_close = True
self.close_all_clients()
Expand Down Expand Up @@ -488,12 +500,21 @@ def setup(self):

# --- Setting options menu
options_menu = self.get_options_menu()

self.console_environment_menu = self.create_menu(
IPythonConsoleWidgetOptionsMenus.EnvironmentConsoles,
_('New console in environment'))
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
self.console_environment_menu.setStyleSheet(
"QMenu { menu-scrollable: 1; }"
)
ccordoba12 marked this conversation as resolved.
Show resolved Hide resolved

self.special_console_menu = self.create_menu(
IPythonConsoleWidgetOptionsMenus.SpecialConsoles,
_('Special consoles'))
_('New special console'))

for item in [
self.create_client_action,
self.console_environment_menu,
self.special_console_menu,
self.connect_to_kernel_action]:
self.add_item_to_menu(
Expand Down Expand Up @@ -541,7 +562,8 @@ def setup(self):
icon=self.create_icon('ipython_console'),
triggered=self.create_cython_client,
)

self.console_environment_menu.aboutToShow.connect(
self.update_environment_menu)
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
for item in [
create_pylab_action,
create_sympy_action,
Expand Down Expand Up @@ -579,6 +601,7 @@ def setup(self):

for item in [
self.create_client_action,
self.console_environment_menu,
self.special_console_menu,
self.connect_to_kernel_action]:
self.add_item_to_menu(
Expand Down Expand Up @@ -650,6 +673,48 @@ def update_actions(self):
self.syspath_action.setEnabled(not error_or_loading)
self.show_time_action.setEnabled(not error_or_loading)

def get_envs(self):
"""
Get the list of environments/interpreters in a worker.
"""
self._worker_manager.terminate_all()
worker = self._worker_manager.create_python_worker(get_list_envs)
worker.sig_finished.connect(self.update_envs)
worker.start()

def update_envs(self, worker, output, error):
"""Update the list of environments in the system."""
self.envs.update(**output)
dalthviz marked this conversation as resolved.
Show resolved Hide resolved

def update_environment_menu(self):
"""
Update context menu submenu with entries for available interpreters.
"""
self.get_envs()
self.console_environment_menu.clear_actions()
for env_key, env_info in self.envs.items():
env_name = env_key.split()[-1]
path_to_interpreter, python_version = env_info
action = self.create_action(
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
name=env_key,
text=f'{env_key} ({python_version})',
icon=self.create_icon('ipython_console'),
triggered=(
lambda checked, env_name=env_name,
path_to_interpreter=path_to_interpreter:
self.create_environment_client(
env_name,
path_to_interpreter
)
),
overwrite=True
)
self.add_item_to_menu(
action,
menu=self.console_environment_menu
)
self.console_environment_menu._render()

# ---- GUI options
@on_conf_change(section='help', option='connect/ipython_console')
def change_clients_help_connection(self, value):
Expand Down Expand Up @@ -1269,9 +1334,10 @@ def config_options(self):
cfg._merge(spy_cfg)
return cfg

def interpreter_versions(self):
def interpreter_versions(self, path_to_custom_interpreter=None):
"""Python and IPython versions used by clients"""
if self.get_conf('default', section='main_interpreter'):
if (self.get_conf('default', section='main_interpreter')
and not path_to_custom_interpreter):
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
from IPython.core import release
versions = dict(
python_version=sys.version,
Expand All @@ -1281,6 +1347,8 @@ def interpreter_versions(self):
import subprocess
versions = {}
pyexec = self.get_conf('executable', section='main_interpreter')
if path_to_custom_interpreter:
pyexec = path_to_custom_interpreter
py_cmd = u'%s -c "import sys; print(sys.version)"' % pyexec
ipy_cmd = (
u'%s -c "import IPython.core.release as r; print(r.version)"'
Expand Down Expand Up @@ -1346,11 +1414,13 @@ def get_current_shellwidget(self):
@Slot(bool)
@Slot(str)
@Slot(bool, str)
@Slot(bool, str, str)
@Slot(bool, bool)
@Slot(bool, str, bool)
def create_new_client(self, give_focus=True, filename='', is_cython=False,
is_pylab=False, is_sympy=False, given_name=None,
cache=True, initial_cwd=None):
cache=True, initial_cwd=None,
path_to_custom_interpreter=None):
"""Create a new client"""
self.master_clients += 1
client_id = dict(int_id=str(self.master_clients),
Expand All @@ -1363,12 +1433,14 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
additional_options=self.additional_options(
is_pylab=is_pylab,
is_sympy=is_sympy),
interpreter_versions=self.interpreter_versions(),
interpreter_versions=self.interpreter_versions(
path_to_custom_interpreter),
context_menu_actions=self.context_menu_actions,
given_name=given_name,
give_focus=give_focus,
handlers=self.registered_spyder_kernel_handlers,
initial_cwd=initial_cwd,
forcing_custom_interpreter=path_to_custom_interpreter is not None
)

# Add client to widget
Expand All @@ -1380,7 +1452,8 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
kernel_spec = SpyderKernelSpec(
is_cython=is_cython,
is_pylab=is_pylab,
is_sympy=is_sympy
is_sympy=is_sympy,
path_to_custom_interpreter=path_to_custom_interpreter
)

try:
Expand Down Expand Up @@ -1481,6 +1554,15 @@ def create_cython_client(self):
"""Force creation of Cython client"""
self.create_new_client(is_cython=True, given_name="Cython")

def create_environment_client(
self, environment, path_to_custom_interpreter
):
"""Force creation of Environment client."""
dalthviz marked this conversation as resolved.
Show resolved Hide resolved
self.create_new_client(
given_name=environment,
path_to_custom_interpreter=path_to_custom_interpreter
)

@Slot(str)
def create_client_from_path(self, path):
"""Create a client with its cwd pointing to path."""
Expand Down
4 changes: 2 additions & 2 deletions spyder/plugins/maininterpreter/confpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def setup_page(self):
# Python executable Group
pyexec_group = QGroupBox(_("Python interpreter"))
pyexec_bg = QButtonGroup(pyexec_group)
pyexec_label = QLabel(_("Select the Python interpreter for all Spyder "
"consoles"))
pyexec_label = QLabel(_("Select the Python interpreter used for "
"default Spyder consoles and code completion"))
self.def_exec_radio = self.create_radiobutton(
_("Default (i.e. the same as Spyder's)"),
'default',
Expand Down
Loading