diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 4330f21a012..35069b9b996 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git branch = master - commit = 82222bbeb8cfa48f20cf762a272eb2aaadaebf2c - parent = fc973c39da8c97bbcd9d1dc1aff5ffcdefe96a32 + commit = 838f55d838641ec95bd0565baaac0cc34d0c93b0 + parent = ae04a3c7829f87671adf27f406aa0abbd374807d method = merge cmdver = 0.4.3 diff --git a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py index d7efae21fa7..65870f02cfb 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py @@ -227,7 +227,8 @@ def kernel(request): 'False_', 'True_' ], - 'minmax': False + 'minmax': False, + 'filter_on':True } # Teardown @@ -288,6 +289,31 @@ def test_get_namespace_view(kernel): assert "'python_type': 'int'" in nsview +@pytest.mark.parametrize("filter_on", [True, False]) +def test_get_namespace_view_filter_on(kernel, filter_on): + """ + Test the namespace view of the kernel with filters on and off. + """ + execute = asyncio.run(kernel.do_execute('a = 1', True)) + asyncio.run(kernel.do_execute('TestFilterOff = 1', True)) + + settings = kernel.namespace_view_settings + settings['filter_on'] = filter_on + settings['exclude_capitalized'] = True + nsview = kernel.get_namespace_view() + + if not filter_on: + assert 'a' in nsview + assert 'TestFilterOff' in nsview + else: + assert 'TestFilterOff' not in nsview + assert 'a' in nsview + + # Restore settings for other tests + settings['filter_on'] = True + settings['exclude_capitalized'] = False + + def test_get_var_properties(kernel): """ Test the properties fo the variables in the namespace. diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py index c19927f8fe7..d7e0ce5a727 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py @@ -589,7 +589,8 @@ def is_callable_or_module(value): def globalsfilter(input_dict, check_all=False, filters=None, exclude_private=None, exclude_capitalized=None, exclude_uppercase=None, exclude_unsupported=None, - excluded_names=None, exclude_callables_and_modules=None): + excluded_names=None, exclude_callables_and_modules=None, + filter_on=True): """Keep objects in namespace view according to different criteria.""" output_dict = {} def _is_string(obj): @@ -605,7 +606,7 @@ def _is_string(obj): (exclude_callables_and_modules and is_callable_or_module(value)) or (exclude_unsupported and not is_supported(value, check_all=check_all, filters=filters)) - ) + ) and filter_on if not excluded: output_dict[key] = value return output_dict @@ -617,7 +618,8 @@ def _is_string(obj): REMOTE_SETTINGS = ('check_all', 'exclude_private', 'exclude_uppercase', 'exclude_capitalized', 'exclude_unsupported', 'excluded_names', 'minmax', 'show_callable_attributes', - 'show_special_attributes', 'exclude_callables_and_modules') + 'show_special_attributes', 'exclude_callables_and_modules', + 'filter_on') def get_supported_types(): @@ -673,7 +675,7 @@ def get_remote_data(data, settings, mode, more_excluded_names=None): exclude_capitalized=settings['exclude_capitalized'], exclude_unsupported=settings['exclude_unsupported'], exclude_callables_and_modules=settings['exclude_callables_and_modules'], - excluded_names=excluded_names) + excluded_names=excluded_names, filter_on=settings['filter_on']) def make_remote_view(data, settings, more_excluded_names=None): diff --git a/spyder/config/main.py b/spyder/config/main.py index 18927c0659b..de5799f9c6e 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -179,7 +179,8 @@ 'truncate': True, 'minmax': False, 'show_callable_attributes': True, - 'show_special_attributes': False + 'show_special_attributes': False, + 'filter_on': True }), ('debugger', { diff --git a/spyder/plugins/ipythonconsole/comms/tests/test_comms.py b/spyder/plugins/ipythonconsole/comms/tests/test_comms.py index f8bf989e002..5cbfa44be80 100644 --- a/spyder/plugins/ipythonconsole/comms/tests/test_comms.py +++ b/spyder/plugins/ipythonconsole/comms/tests/test_comms.py @@ -60,7 +60,8 @@ def kernel(request): 'False_', 'True_' ], - 'minmax': False} + 'minmax': False, + 'filter_on': True} # Teardown def reset_kernel(): diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index c3ca41b109a..6a9a6d98f5a 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -1832,7 +1832,8 @@ def test_pdb_comprehension_namespace(ipyconsole, qtbot, tmpdir): 'excluded_names': [], 'minmax': False, 'show_callable_attributes': True, - 'show_special_attributes': False} + 'show_special_attributes': False, + 'filter_on': True} shell.call_kernel( interrupt=True diff --git a/spyder/plugins/maininterpreter/plugin.py b/spyder/plugins/maininterpreter/plugin.py index 822f926f1c8..907a93120bb 100644 --- a/spyder/plugins/maininterpreter/plugin.py +++ b/spyder/plugins/maininterpreter/plugin.py @@ -112,11 +112,12 @@ def set_custom_interpreter(self, interpreter): def _open_interpreter_preferences(self): """Open the Preferences dialog in the main interpreter section.""" self._main.show_preferences() - preferences = self._main.preferences - container = preferences.get_container() - dlg = container.dialog - index = dlg.get_index_by_name("main_interpreter") - dlg.set_current_index(index) + preferences = self.get_plugin(Plugins.Preferences) + if preferences: + container = preferences.get_container() + dlg = container.dialog + index = dlg.get_index_by_name("main_interpreter") + dlg.set_current_index(index) @Slot(str) def _add_to_custom_interpreters(self, interpreter): diff --git a/spyder/plugins/variableexplorer/confpage.py b/spyder/plugins/variableexplorer/confpage.py index a2e7a8d0ecd..bd3ae283cdd 100644 --- a/spyder/plugins/variableexplorer/confpage.py +++ b/spyder/plugins/variableexplorer/confpage.py @@ -13,15 +13,20 @@ from spyder.config.base import _ from spyder.api.preferences import PluginConfigPage + class VariableExplorerConfigPage(PluginConfigPage): def setup_page(self): filter_group = QGroupBox(_("Filter")) filter_data = [ - ('exclude_private', _("Exclude private references")), - ('exclude_capitalized', _("Exclude capitalized references")), - ('exclude_uppercase', _("Exclude all-uppercase references")), - ('exclude_unsupported', _("Exclude unsupported data types")), + ('exclude_private', + _("Exclude private references")), + ('exclude_capitalized', + _("Exclude capitalized references")), + ('exclude_uppercase', + _("Exclude all-uppercase references")), + ('exclude_unsupported', + _("Exclude unsupported data types")), ('exclude_callables_and_modules', _("Exclude callables and modules")) ] diff --git a/spyder/plugins/variableexplorer/plugin.py b/spyder/plugins/variableexplorer/plugin.py index be9ab2ddd80..ea6e4ed87ac 100644 --- a/spyder/plugins/variableexplorer/plugin.py +++ b/spyder/plugins/variableexplorer/plugin.py @@ -47,7 +47,10 @@ def get_icon(self): return self.create_icon('dictedit') def on_initialize(self): - pass + widget = self.get_widget() + widget.sig_open_preferences_requested.connect( + self._open_preferences + ) @on_plugin_available(plugin=Plugins.Preferences) def on_preferences_available(self): @@ -58,3 +61,15 @@ def on_preferences_available(self): def on_preferences_teardown(self): preferences = self.get_plugin(Plugins.Preferences) preferences.deregister_plugin_preferences(self) + + # ---- Private API + # ------------------------------------------------------------------------- + def _open_preferences(self): + """Open the Preferences dialog in the Variable Explorer section.""" + self._main.show_preferences() + preferences = self.get_plugin(Plugins.Preferences) + if preferences: + container = preferences.get_container() + dlg = container.dialog + index = dlg.get_index_by_name(self.NAME) + dlg.set_current_index(index) diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py index 23c36e9ffb3..15728cfca76 100644 --- a/spyder/plugins/variableexplorer/widgets/main_widget.py +++ b/spyder/plugins/variableexplorer/widgets/main_widget.py @@ -9,7 +9,7 @@ """ # Third party imports -from qtpy.QtCore import QTimer, Slot +from qtpy.QtCore import QTimer, Slot, Signal from qtpy.QtWidgets import QAction # Local imports @@ -18,6 +18,7 @@ from spyder.api.shellconnect.main_widget import ShellConnectMainWidget from spyder.plugins.variableexplorer.widgets.namespacebrowser import ( NamespaceBrowser) +from spyder.utils.icon_manager import ima from spyder.utils.programs import is_module_installed @@ -41,11 +42,17 @@ class VariableExplorerWidgetActions: ToggleExcludeCallablesAndModules = ( 'toggle_exclude_callables_and_modules_action') ToggleMinMax = 'toggle_minmax_action' + ToggleFilter = 'toggle_filter_variable_action' + + # Resize + ResizeRowsAction = 'resize_rows_action' + ResizeColumnsAction = 'resize_columns_action' class VariableExplorerWidgetOptionsMenuSections: Display = 'excludes_section' - Highlight = 'highlight_section' + Highlight = 'highlight_section' + Resize = 'resize_section' class VariableExplorerWidgetMainToolBarSections: @@ -58,8 +65,6 @@ class VariableExplorerWidgetMenus: class VariableExplorerContextMenuActions: - ResizeRowsAction = 'resize_rows_action' - ResizeColumnsAction = 'resize_columns_action' PasteAction = 'paste_action' CopyAction = 'copy' EditAction = 'edit_action' @@ -72,13 +77,14 @@ class VariableExplorerContextMenuActions: RenameAction = 'rename_action' DuplicateAction = 'duplicate_action' ViewAction = 'view_action' + EditFiltersAction = 'edit_filters_action' class VariableExplorerContextMenuSections: Edit = 'edit_section' Insert = 'insert_section' View = 'view_section' - Resize = 'resize_section' + Filter = 'Filter_section' # ============================================================================= @@ -94,13 +100,21 @@ class VariableExplorerWidget(ShellConnectMainWidget): INITIAL_FREE_MEMORY_TIME_TRIGGER = 60 * 1000 # ms SECONDARY_FREE_MEMORY_TIME_TRIGGER = 180 * 1000 # ms + sig_open_preferences_requested = Signal() + """ + Signal to open the variable explorer preferences. + """ + def __init__(self, name=None, plugin=None, parent=None): super().__init__(name, plugin, parent) + # Widgets self.context_menu = None self.empty_context_menu = None + self.filter_button = None + # ---- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): @@ -108,7 +122,7 @@ def get_title(self): def setup(self): # ---- Options menu actions - exclude_private_action = self.create_action( + self.exclude_private_action = self.create_action( VariableExplorerWidgetActions.ToggleExcludePrivate, text=_("Exclude private variables"), tip=_("Exclude variables that start with an underscore"), @@ -116,7 +130,7 @@ def setup(self): option='exclude_private', ) - exclude_uppercase_action = self.create_action( + self.exclude_uppercase_action = self.create_action( VariableExplorerWidgetActions.ToggleExcludeUpperCase, text=_("Exclude all-uppercase variables"), tip=_("Exclude variables whose name is uppercase"), @@ -124,7 +138,7 @@ def setup(self): option='exclude_uppercase', ) - exclude_capitalized_action = self.create_action( + self.exclude_capitalized_action = self.create_action( VariableExplorerWidgetActions.ToggleExcludeCapitalized, text=_("Exclude capitalized variables"), tip=_("Exclude variables whose name starts with a capital " @@ -133,7 +147,7 @@ def setup(self): option='exclude_capitalized', ) - exclude_unsupported_action = self.create_action( + self.exclude_unsupported_action = self.create_action( VariableExplorerWidgetActions.ToggleExcludeUnsupported, text=_("Exclude unsupported data types"), tip=_("Exclude references to data types that don't have " @@ -142,7 +156,7 @@ def setup(self): option='exclude_unsupported', ) - exclude_callables_and_modules_action = self.create_action( + self.exclude_callables_and_modules_action = self.create_action( VariableExplorerWidgetActions.ToggleExcludeCallablesAndModules, text=_("Exclude callables and modules"), tip=_("Exclude references to functions, modules and " @@ -204,16 +218,26 @@ def setup(self): register_shortcut=True, ) + self.filter_button = self.create_action( + VariableExplorerWidgetActions.ToggleFilter, + text="", + icon=ima.icon('filter'), + toggled=self._enable_filter_actions, + option='filter_on', + tip=_("Filter variables") + ) + self.filter_button.setCheckable(True) + # ---- Context menu actions resize_rows_action = self.create_action( - VariableExplorerContextMenuActions.ResizeRowsAction, + VariableExplorerWidgetActions.ResizeRowsAction, text=_("Resize rows to contents"), icon=self.create_icon('collapse_row'), triggered=self.resize_rows ) resize_columns_action = self.create_action( - VariableExplorerContextMenuActions.ResizeColumnsAction, + VariableExplorerWidgetActions.ResizeColumnsAction, _("Resize columns to contents"), icon=self.create_icon('collapse_column'), triggered=self.resize_columns @@ -279,6 +303,13 @@ def setup(self): triggered=self.insert_item ) + self.edit_filters = self.create_action( + VariableExplorerContextMenuActions.EditFiltersAction, + _("Edit filters"), + icon=self.create_icon('filter'), + triggered=self.sig_open_preferences_requested + ) + self.remove_action = self.create_action( VariableExplorerContextMenuActions.RemoveAction, _("Remove"), @@ -309,9 +340,11 @@ def setup(self): # Options menu options_menu = self.get_options_menu() - for item in [exclude_private_action, exclude_uppercase_action, - exclude_capitalized_action, exclude_unsupported_action, - exclude_callables_and_modules_action, + for item in [self.exclude_private_action, + self.exclude_uppercase_action, + self.exclude_capitalized_action, + self.exclude_unsupported_action, + self.exclude_callables_and_modules_action, self.show_minmax_action]: self.add_item_to_menu( item, @@ -319,10 +352,21 @@ def setup(self): section=VariableExplorerWidgetOptionsMenuSections.Display, ) + self._enable_filter_actions(self.get_conf('filter_on')) + + # Resize + for item in [resize_rows_action, resize_columns_action]: + self.add_item_to_menu( + item, + menu=options_menu, + section=VariableExplorerWidgetOptionsMenuSections.Resize, + ) + # Main toolbar main_toolbar = self.get_main_toolbar() for item in [import_data_action, save_action, save_as_action, - reset_namespace_action, search_action, refresh_action]: + reset_namespace_action, search_action, refresh_action, + self.filter_button]: self.add_item_to_toolbar( item, toolbar=main_toolbar, @@ -349,19 +393,19 @@ def setup(self): section=VariableExplorerContextMenuSections.Insert, ) - for item in [self.view_action, self.plot_action, self.hist_action, - self.imshow_action, self.show_minmax_action]: + for item in [self.edit_filters]: self.add_item_to_menu( item, menu=self.context_menu, - section=VariableExplorerContextMenuSections.View, + section=VariableExplorerContextMenuSections.Filter, ) - - for item in [resize_rows_action, resize_columns_action]: + + for item in [self.view_action, self.plot_action, self.hist_action, + self.imshow_action]: self.add_item_to_menu( item, menu=self.context_menu, - section=VariableExplorerContextMenuSections.Resize, + section=VariableExplorerContextMenuSections.View, ) # ---- Context menu when the variable explorer is empty @@ -557,7 +601,6 @@ def _set_actions_and_menus(self, nsb): editor.save_array_action = self.save_array_action editor.insert_action = self.insert_action editor.remove_action = self.remove_action - editor.minmax_action = self.show_minmax_action editor.rename_action = self.rename_action editor.duplicate_action = self.duplicate_action editor.view_action = self.view_action @@ -571,3 +614,11 @@ def _set_actions_and_menus(self, nsb): # several places in CollectionsEditor. editor.insert_action_above = QAction() editor.insert_action_below = QAction() + + def _enable_filter_actions(self, value): + """Handle the change of the filter state.""" + self.exclude_private_action.setEnabled(value) + self.exclude_uppercase_action.setEnabled(value) + self.exclude_capitalized_action.setEnabled(value) + self.exclude_unsupported_action.setEnabled(value) + self.exclude_callables_and_modules_action.setEnabled(value) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index f861e2e2db0..c199ef22322 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -406,6 +406,10 @@ def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return to_qvariant() value = self.get_value(index) + if role == Qt.ToolTipRole and index.column() == 3: + return value['view'] + elif role == Qt.ToolTipRole: + return value if index.column() == 4 and role == Qt.DisplayRole: # TODO: Check the effect of not hiding the column # Treating search scores as a table column simplifies the @@ -910,6 +914,10 @@ def mousePressEvent(self, event): and index_clicked in self.selectedIndexes(): self.clearSelection() else: + row = index_clicked.row() + # TODO: Remove hard coded "Value" column number (3 here) + index_clicked = index_clicked.child(row, 3) + self.edit(index_clicked) QTableView.mousePressEvent(self, event) else: self.clearSelection() @@ -926,6 +934,13 @@ def mouseDoubleClickEvent(self, event): else: event.accept() + def mouseMoveEvent(self, event): + """Change cursor shape.""" + if self.rowAt(event.y()) != -1: + self.setCursor(Qt.PointingHandCursor) + else: + self.setCursor(Qt.ArrowCursor) + def keyPressEvent(self, event): """Reimplement Qt methods""" if event.key() == Qt.Key_Delete: