From 50187ebe97aa9a3dd57e1bf18e3a4923a01f8274 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Thu, 30 Mar 2023 22:30:23 +0200 Subject: [PATCH 01/41] add profiler plugin --- spyder/app/tests/test_mainwindow.py | 164 +++ spyder/config/main.py | 5 + spyder/images/dark/profile_cell.svg | 37 + spyder/images/dark/profile_selection.svg | 37 + spyder/images/light/profile_cell.svg | 37 + spyder/images/light/profile_selection.svg | 37 + .../ipythonconsole/widgets/debugging.py | 6 +- spyder/plugins/profiler/api.py | 25 - spyder/plugins/profiler/confpage.py | 36 +- spyder/plugins/profiler/images/profiler.png | Bin 1392 -> 0 bytes spyder/plugins/profiler/plugin.py | 274 ++-- .../plugins/profiler/tests/test_profiler.py | 71 +- .../plugins/profiler/widgets/main_widget.py | 1135 ++++------------- .../profiler/widgets/profiler_data_tree.py | 697 ++++++++++ spyder/plugins/profiler/widgets/run_conf.py | 77 -- spyder/plugins/toolbar/api.py | 1 + spyder/plugins/toolbar/plugin.py | 1 + spyder/utils/icon_manager.py | 5 +- 18 files changed, 1483 insertions(+), 1162 deletions(-) create mode 100644 spyder/images/dark/profile_cell.svg create mode 100644 spyder/images/dark/profile_selection.svg create mode 100644 spyder/images/light/profile_cell.svg create mode 100644 spyder/images/light/profile_selection.svg delete mode 100644 spyder/plugins/profiler/api.py delete mode 100644 spyder/plugins/profiler/images/profiler.png create mode 100644 spyder/plugins/profiler/widgets/profiler_data_tree.py delete mode 100644 spyder/plugins/profiler/widgets/run_conf.py diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 066e8040c33..a6a4dd2cf08 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -5153,6 +5153,170 @@ def test_add_external_plugins_to_dependencies(main_window, qtbot): assert 'spyder-boilerplate' in external_names +def test_profiler(main_window, qtbot, tmpdir): + """Test if profiler works.""" + ipyconsole = main_window.ipyconsole + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + control = ipyconsole.get_widget().get_focus_widget() + profiler = main_window.profiler + profile_tree = profiler.get_widget() + + sleep_str = '' + + # Test simple profile + with qtbot.waitSignal(shell.executed): + shell.execute("import time") + + assert len(profile_tree.current_widget().data_tree.get_items(2)) == 0 + + with qtbot.waitSignal(shell.executed): + shell.execute("%profile time.sleep(0.1)") + qtbot.wait(1000) + + assert len(profile_tree.current_widget().data_tree.get_items(2)) == 1 + item = profile_tree.current_widget().data_tree.get_items(2)[0].item_key[2] + assert item == sleep_str + + # Make sure the ordering methods don't reveal the root element. + profile_tree.toggle_tree_action.setChecked(True) + assert len(profile_tree.current_widget().data_tree.get_items(0)) == 1 + item = profile_tree.current_widget().data_tree.get_items(2)[0].item_key[2] + assert item == sleep_str + profile_tree.toggle_tree_action.setChecked(False) + assert len(profile_tree.current_widget().data_tree.get_items(2)) == 1 + item = profile_tree.current_widget().data_tree.get_items(2)[0].item_key[2] + assert item == sleep_str + profile_tree.slow_local_tree() + assert len(profile_tree.current_widget().data_tree.get_items(0)) == 3 + + # Test profilecell + # Write code with a cell to a file + code = "result = 10; fname = __file__; time.sleep(0); time.time()" + p = tmpdir.join("cell-test.py") + p.write(code) + main_window.editor.load(to_text_string(p)) + + # Execute profile cell + with qtbot.waitSignal(shell.executed): + shell.execute("%profilecell -i 0 " + repr(to_text_string(p))) + + qtbot.wait(1000) + + # Verify that the `result` variable is defined + assert shell.get_value('result') == 10 + + # Verify that the `fname` variable is `cell-test.py` + assert "cell-test.py" in shell.get_value('fname') + + # Verify that two elements are in the profiler + # Actually 3 because globals is included too + assert len(profile_tree.current_widget().data_tree.get_items(2)) == 3 + + # Test profilefile + code = ( + "import time\n" + "def f():\n" + " g()\n" + " time.sleep(1)\n" + "def g(stop=False):\n" + " time.sleep(1)\n" + " if not stop:\n" + " g(True)\n" + "f()" + ) + p = tmpdir.join("cell-test_2.py") + p.write(code) + main_window.editor.load(to_text_string(p)) + + with qtbot.waitSignal(shell.executed): + shell.execute("%profilefile " + repr(to_text_string(p))) + qtbot.wait(1000) + # Check callee tree + profile_tree.toggle_tree_action.setChecked(False) + assert len(profile_tree.current_widget().data_tree.get_items(1)) == 3 + values = ["f", sleep_str, "g"] + for item, val in zip( + profile_tree.current_widget().data_tree.get_items(1), values): + assert val == item.item_key[2] + + # Check caller tree + profile_tree.toggle_tree_action.setChecked(True) + assert len(profile_tree.current_widget().data_tree.get_items(1)) == 3 + values = [sleep_str, "f", "g"] + for item, val in zip( + profile_tree.current_widget().data_tree.get_items(1), values): + assert val == item.item_key[2] + + # Check local time + profile_tree.slow_local_tree() + assert len(profile_tree.current_widget().data_tree.get_items(1)) == 11 + + # Check no errors happened + assert "error" not in control.toPlainText().lower() + + # Test profiling while debugging + # Reset the tree + with qtbot.waitSignal(shell.executed): + shell.execute("%profile 0") + assert len(profile_tree.current_widget().data_tree.get_items(1)) == 0 + + with qtbot.waitSignal(shell.executed): + shell.execute("%debug 0") + + assert shell.is_debugging() + with qtbot.waitSignal(shell.executed): + shell.execute("%profilefile " + repr(to_text_string(p))) + qtbot.wait(1000) + + assert len(profile_tree.current_widget().data_tree.get_items(1)) == 3 + assert shell.is_debugging() + # Make sure the shell is not broken + with qtbot.waitSignal(shell.executed): + shell.execute("13 + 1234") + assert shell.is_debugging() + assert "1247" in shell._control.toPlainText() + with qtbot.waitSignal(shell.executed): + shell.execute("q") + assert not shell.is_debugging() + + +def test_profiler_namespace(main_window, qtbot, tmpdir): + """Test that the profile magic finds the right namespace""" + ipyconsole = main_window.ipyconsole + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + # Test profilefile + code = ( + "result = 10\n" + "%profile print(result)" + ) + p = tmpdir.join("test_prof.ipy") + p.write(code) + main_window.editor.load(to_text_string(p)) + + with qtbot.waitSignal(shell.executed): + shell.execute("%debug 0") + with qtbot.waitSignal(shell.executed): + shell.execute("%runfile " + repr(to_text_string(p))) + # Make sure no errors are shown + assert "error" not in shell._control.toPlainText().lower() + + with qtbot.waitSignal(shell.executed): + shell.execute("q") + + # Test in main namespace + with qtbot.waitSignal(shell.executed): + shell.execute("%reset -f") + + with qtbot.waitSignal(shell.executed): + shell.execute("%runfile " + repr(to_text_string(p))) + # Make sure no errors are shown + assert "error" not in shell._control.toPlainText().lower() + + def test_locals_globals_var_debug(main_window, qtbot, tmpdir): """Test that the debugger can handle variables named globals and locals.""" ipyconsole = main_window.ipyconsole diff --git a/spyder/config/main.py b/spyder/config/main.py index 1091aff0fa9..3bc642dff39 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -351,6 +351,8 @@ ('profiler', { 'enable': True, + 'switch_to_plugin': True, + 'n_slow_children': 25 }), ('pylint', { @@ -501,6 +503,9 @@ 'pylint/run file in pylint': "F8", # -- Profiler -- 'profiler/run file in profiler': "F10", + 'profiler/run cell in profiler': "Alt+F10", + 'profiler/run selection in profiler': "", + 'profiler/find_action': "Ctrl+F", # -- IPython console -- 'ipython_console/new tab': "Ctrl+T", 'ipython_console/reset namespace': "Ctrl+Alt+R", diff --git a/spyder/images/dark/profile_cell.svg b/spyder/images/dark/profile_cell.svg new file mode 100644 index 00000000000..490c123fb28 --- /dev/null +++ b/spyder/images/dark/profile_cell.svg @@ -0,0 +1,37 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/spyder/images/dark/profile_selection.svg b/spyder/images/dark/profile_selection.svg new file mode 100644 index 00000000000..27685f881a8 --- /dev/null +++ b/spyder/images/dark/profile_selection.svg @@ -0,0 +1,37 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/spyder/images/light/profile_cell.svg b/spyder/images/light/profile_cell.svg new file mode 100644 index 00000000000..3533b648b22 --- /dev/null +++ b/spyder/images/light/profile_cell.svg @@ -0,0 +1,37 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/spyder/images/light/profile_selection.svg b/spyder/images/light/profile_selection.svg new file mode 100644 index 00000000000..2989cebc3d6 --- /dev/null +++ b/spyder/images/light/profile_selection.svg @@ -0,0 +1,37 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/spyder/plugins/ipythonconsole/widgets/debugging.py b/spyder/plugins/ipythonconsole/widgets/debugging.py index fb77b2ca4aa..8bc504b278a 100644 --- a/spyder/plugins/ipythonconsole/widgets/debugging.py +++ b/spyder/plugins/ipythonconsole/widgets/debugging.py @@ -20,7 +20,7 @@ from IPython.lib.lexers import ( IPython3Lexer, Python3Lexer, bygroups, using ) -from pygments.token import Keyword, Operator +from pygments.token import Keyword, Operator, Text from pygments.util import ClassNotFound from qtconsole.rich_jupyter_widget import RichJupyterWidget from qtpy.QtCore import QEvent @@ -37,7 +37,9 @@ class SpyderIPy3Lexer(IPython3Lexer): spyder_tokens = [ (r'(!)(\w+)(.*\n)', bygroups(Operator, Keyword, using(Python3Lexer))), (r'(%)(\w+)(.*\n)', bygroups(Operator, Keyword, using(Python3Lexer))), - ] + (r'(?s)(\s*)(%%profile)([^\n]*\n)(.*)', bygroups( + Text, Operator, Text, using(Python3Lexer))), + ] tokens['root'] = spyder_tokens + tokens['root'] diff --git a/spyder/plugins/profiler/api.py b/spyder/plugins/profiler/api.py deleted file mode 100644 index 41f1d9aeb1a..00000000000 --- a/spyder/plugins/profiler/api.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © Spyder Project Contributors -# Licensed under the terms of the MIT License -# (see spyder/__init__.py for details) -""" -Profiler Plugin. -""" -# Standard library imports -from typing import TypedDict - -# Local imports -from spyder.plugins.profiler.widgets.main_widget import ( # noqa - ProfilerWidgetActions, ProfilerWidgetInformationToolbarSections, - ProfilerWidgetMainToolbarSections, ProfilerWidgetToolbars) - - -class ProfilerPyConfiguration(TypedDict): - """Profiler execution parameters for Python files.""" - - # True if the script is using custom arguments. False otherwise - args_enabled: bool - - # Custom arguments to pass to the script when profiling. - args: str diff --git a/spyder/plugins/profiler/confpage.py b/spyder/plugins/profiler/confpage.py index a2a1044bcd9..2de9f2b63ef 100644 --- a/spyder/plugins/profiler/confpage.py +++ b/spyder/plugins/profiler/confpage.py @@ -6,36 +6,28 @@ """Profiler config page.""" -from qtpy.QtCore import Qt -from qtpy.QtWidgets import QGroupBox, QLabel, QVBoxLayout +from qtpy.QtWidgets import QVBoxLayout from spyder.api.preferences import PluginConfigPage from spyder.config.base import _ -from spyder.plugins.profiler.widgets.main_widget import ProfilerWidget class ProfilerConfigPage(PluginConfigPage): def setup_page(self): - results_group = QGroupBox(_("Results")) - results_label1 = QLabel(_("Profiler plugin results " - "(the output of python's profile/cProfile)\n" - "are stored here:")) - results_label1.setWordWrap(True) - - # Warning: do not try to regroup the following QLabel contents with - # widgets above -- this string was isolated here in a single QLabel - # on purpose: to fix spyder-ide/spyder#863. - results_label2 = QLabel(ProfilerWidget.DATAPATH) - - results_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) - results_label2.setWordWrap(True) - - results_layout = QVBoxLayout() - results_layout.addWidget(results_label1) - results_layout.addWidget(results_label2) - results_group.setLayout(results_layout) + switch_to_plugin_cb = self.create_checkbox( + _("Open profiler when profiling finishes"), + 'switch_to_plugin', + tip=_( + "This option switches to the profiler plugin " + "when a profiling has ended.")) + + slow_spin = self.create_spinbox( + _("Maximum number of items displayed with large local time"), + _(""), 'n_slow_children', + min_=1, max_=1000000, step=1) vlayout = QVBoxLayout() - vlayout.addWidget(results_group) + vlayout.addWidget(switch_to_plugin_cb) + vlayout.addWidget(slow_spin) vlayout.addStretch(1) self.setLayout(vlayout) diff --git a/spyder/plugins/profiler/images/profiler.png b/spyder/plugins/profiler/images/profiler.png deleted file mode 100644 index 91c768f8d01f686c4d293af0442184a780171b54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1392 zcmV-$1&{iPP){M1f=>P)Bo2RiLcr=4m2p;-uqC0MM8fk1#L`znSlF9~}<2mu2M zNL`|$fI@V@fLoog*s>VG4bkc-m=GMQh}*a4W|9o9*gNx?`|f-9obSEo+;iWuWlJ;w z-xFJj!^8kFNVE~T#E(nazs<29aTbf3)8W93!%k1n7QbeqlKDRJ?L3YS4wScLh1A9#dz6RVbT{vpx9X4h8fp~~EX&bD@3xip5e zJtr|gHwQci?%cQu=hQSToySqC)}pzg(Iaw__~>l{JK3zb7zI){ZN{wAiQAJmpi$<~ zPV$hdNJRLCP{c;WASqUXS^G2|J$#7rqGD+CIgd!oo16!-uUbxn-1JNg^`A$vN&{m> z6{?C#!6}s(=^ujQjvYyx6Hr}VhRPBhxa@4${vJZLt^!6wjYlN>W#`L@(I*Q}AeTyU zYvMX`GSX4RaTMn=GOk2?j2ttwPM3ACSd2Qo5#@!&D9+bnYhn_{hHWUL{M@6^QM>j+ zK4`GoB^w$FbIV~kr|rl}NkxT5?b<#=9wx=us111Z00F*Y)NiXnB_+97tA<*cjft^w zbhI2oflB4sh5urKZR`z?$Ak6MX>9l{1YD*PB`Ph-RBGr73J|e21QX*I@nGgYd__X+ zqSYI+Rj8ovoH7dvI#s8;Er{6|?h&Y8EO5$p;W89q8`0fihBPD;93@q=B?tM`hm`5S zUlRybRSmi+4+4<@!X^`hI~x$d>7d{Wp@>$XiN;w;Dd@Md(!hnq0@LlS?TFYY#hLCN zC{r?^(`Zqcmy5M@46laC(M`L1?p|7!PymB~53|f4XJj&*OHD^|MkbD%&B&vp-FAo- zI_!%D9Ic015fvpv2_?!;C`5XEDs)^PBFpty;0sy&yrmn@p3DCLs%imMA?ykT{DUMY z(rOVYi@*WX0XEICSistK+~UfCblvZ+p5g26d7?Eq&x)k$7!+|C;PL5Q@)qzYFFd}l zcmBGdU{ah7~=&O*En2fCCG_ME?fj>`( zAX=@M$KZRz!bpoxfSzilRKwxMkS8k&(Fy9o`3dhDz= zyb^m4bp}25Ha5YaEyKR1Cc6A)VeK2ZZtghd^V$n9z(3%Fe(T^I6Jht!5mO5q=_u}~ z*^WF)U~^qPnhmv3XDHFW$AoJ`BbdBk!?_bZ^k0}q$I;F?iA3_rYn^+VP$>B7;Qkix zUu1Z209;lcN|pKOJADQd7sp{aYQ~AqV>sK}i}{C-@Ymol40^+i*iZb^Tb;8;TmP5{ zj*W`$BL53`soAF-7#p!+%r=hRE(?tMn)!sd1PhVuUG#aW5 z{rh(B{im+h=qS<@I#fCNH`9_+2O_1iW|6NbmgcP{z9g0}Dc}})k61~3PJB&#OZ-gy y>gE^XJK`JS3t| List[RunResult]: - def stop_profiler(self): - """ - Stop profiler. - """ - self.get_widget().stop() + console = self.get_plugin(Plugins.IPythonConsole) + if console is None: + return - @run_execute(context=RunContext.File) - def run_file( + exec_params = conf['params'] + params: IPythonConsolePyConfiguration = exec_params['executor_params'] + params["run_method"] = "profilefile" + + return console.exec_files(input, conf) + + @run_execute(context=RunContext.Cell) + def profile_cell( self, input: RunConfiguration, conf: ExtendedRunExecutionParameters - ) -> List[PossibleRunResult]: - self.switch_to_plugin() + ) -> List[RunResult]: + + console = self.get_plugin(Plugins.IPythonConsole) + if console is None: + return + + run_input: CellRun = input['run_input'] + if run_input['copy']: + code = run_input['cell'] + if not code.strip(): + # Empty cell + return + console.run_selection("%%profile\n" + code) + return exec_params = conf['params'] - cwd_opts = exec_params['working_dir'] - params: ProfilerPyConfiguration = exec_params['executor_params'] + params: IPythonConsolePyConfiguration = exec_params['executor_params'] + params["run_method"] = "profilecell" - run_input: FileRun = input['run_input'] - filename = run_input['path'] + return console.exec_cell(input, conf) - wdir = cwd_opts['path'] - args = params['args'] - self.get_widget().analyze( - filename, - wdir=wdir, - args=args - ) + @run_execute(context=RunContext.Selection) + def profile_selection( + self, + input: RunConfiguration, + conf: ExtendedRunExecutionParameters + ) -> List[RunResult]: + + console = self.get_plugin(Plugins.IPythonConsole) + if console is None: + return + + run_input: SelectionRun = input['run_input'] + code = run_input['selection'] + if not code.strip(): + # No selection + return + run_input['selection'] = "%%profile\n" + code + + return console.exec_selection(input, conf) diff --git a/spyder/plugins/profiler/tests/test_profiler.py b/spyder/plugins/profiler/tests/test_profiler.py index f0ce12fb72b..33bb7ba1bfe 100644 --- a/spyder/plugins/profiler/tests/test_profiler.py +++ b/spyder/plugins/profiler/tests/test_profiler.py @@ -10,11 +10,10 @@ # Third party imports -from qtpy.QtGui import QIcon import pytest # Local imports -from spyder.plugins.profiler.widgets.main_widget import ProfilerDataTree +from spyder.plugins.profiler.widgets.profiler_data_tree import TreeWidgetItem from spyder.utils.palette import SpyderPalette @@ -22,27 +21,11 @@ SUCESS = SpyderPalette.COLOR_SUCCESS_1 -# --- Fixtures -# ----------------------------------------------------------------------------- -@pytest.fixture -def profiler_datatree_bot(qtbot): - """Set up Profiler widget.""" - # Avoid qtawesome startup errors - ProfilerDataTree.create_icon = lambda x, y: QIcon() - ProfilerDataTree.CONF_SECTION = '' - tree = ProfilerDataTree(None) - qtbot.addWidget(tree) - tree.show() - yield tree - tree.destroy() - - # --- Tests # ----------------------------------------------------------------------------- -def test_format_measure(profiler_datatree_bot): +def test_format_measure(): """ Test ProfilerDataTree.format_measure().""" - tree = profiler_datatree_bot - fm = tree.format_measure + fm = TreeWidgetItem.format_measure assert fm(125) == '125' assert fm(1.25e-8) == '12.50 ns' assert fm(1.25e-5) == u'12.50 \u03BCs' @@ -60,45 +43,15 @@ def test_format_measure(profiler_datatree_bot): assert fm(-12555.5) == '3h:29min' -def test_color_string(profiler_datatree_bot): - """ Test ProfilerDataTree.color_string().""" - tree = profiler_datatree_bot - cs = tree.color_string - - tree.compare_file = 'test' - assert cs([5.0]) == ['5.00 s', ['', 'black']] - assert cs([1.251e-5, 1.251e-5]) == [u'12.51 \u03BCs', ['', 'black']] - assert cs([5.0, 4.0]) == ['5.00 s', ['+1000.00 ms', ERROR]] - assert cs([4.0, 5.0]) == ['4.00 s', ['-1000.00 ms', SUCESS]] - - tree.compare_file = None - assert cs([4.0, 5.0]) == ['4.00 s', ['', 'black']] - - -def test_format_output(profiler_datatree_bot): - """ Test ProfilerDataTree.format_output().""" - tree = profiler_datatree_bot - fo = tree.format_output - - # Mock Stats class to be able to use fixed data for input. - class Stats: - stats = {} - - tree.stats1 = [Stats(), Stats()] - tree.stats1[0].stats = {('key1'): (1, 1000, 3.5, 1.5, {}), - ('key2'): (1, 1200, 2.0, 2.0, {}) - } - tree.stats1[1].stats = {('key1'): (1, 1000, 3.7, 1.3, {}), - ('key2'): (1, 1199, 2.4, 2.4, {}) - } - - tree.compare_file = 'test' - assert list((fo('key1'))) == [['1000', ['', 'black']], - ['3.50 s', ['-200.00 ms', SUCESS]], - ['1.50 s', ['+200.00 ms', ERROR]]] - assert list((fo('key2'))) == [['1200', ['+1', ERROR]], - ['2.00 s', ['-400.00 ms', SUCESS]], - ['2.00 s', ['-400.00 ms', SUCESS]]] +def test_color_string(): + """ Test ProfilerDataTree.color_diff().""" + cs = TreeWidgetItem.color_diff + assert cs(0.) == ('', 'black') + assert cs(1.) == ('+1000.00 ms', ERROR) + assert cs(-1.) == ('-1000.00 ms', SUCESS) + assert cs(0) == ('', 'black') + assert cs(1) == ('+1', ERROR) + assert cs(-1) == ('-1', SUCESS) if __name__ == "__main__": diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index 30c369e8113..8ec36ee75d2 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -14,123 +14,65 @@ """ # Standard library imports -import logging -import os import os.path as osp -import re -import sys -import time -from itertools import islice # Third party imports -from qtpy import PYQT5 from qtpy.compat import getopenfilename, getsavefilename -from qtpy.QtCore import QByteArray, QProcess, QProcessEnvironment, Qt, Signal -from qtpy.QtGui import QColor -from qtpy.QtWidgets import (QApplication, QLabel, QMessageBox, QTreeWidget, - QTreeWidgetItem, QVBoxLayout) +from qtpy.QtCore import Signal # Local imports -from spyder.api.config.decorators import on_conf_change from spyder.api.translations import _ -from spyder.api.widgets.main_widget import PluginMainWidget -from spyder.api.widgets.mixins import SpyderWidgetMixin -from spyder.config.base import get_conf_path -from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor -from spyder.py3compat import to_text_string -from spyder.utils.misc import get_python_executable, getcwd_or_home -from spyder.utils.palette import SpyderPalette, QStylePalette -from spyder.utils.programs import shell_split -from spyder.utils.qthelpers import get_item_user_text, set_item_user_text -from spyder.widgets.comboboxes import PythonModulesComboBox +from spyder.utils.misc import getcwd_or_home +from spyder.api.shellconnect.main_widget import ShellConnectMainWidget +from spyder.plugins.profiler.widgets.profiler_data_tree import ( + ProfilerSubWidget) -# Logging -logger = logging.getLogger(__name__) - - -# --- Constants -# ---------------------------------------------------------------------------- -MAIN_TEXT_COLOR = QStylePalette.COLOR_TEXT_1 - class ProfilerWidgetActions: # Triggers - Browse = 'browse_action' Clear = 'clear_action' Collapse = 'collapse_action' Expand = 'expand_action' + ToggleTreeDirection = "tree_direction_action" + ToggleBuiltins = "toggle_builtins_action" + Home = "HomeAction" + SlowLocal = 'slow_local_action' LoadData = 'load_data_action' - Run = 'run_action' SaveData = 'save_data_action' - ShowOutput = 'show_output_action' + Search = "find_action" + Undo = "undo_action" + Redo = "redo_action" -class ProfilerWidgetToolbars: - Information = 'information_toolbar' +class ProfilerWidgetMenus: + EmptyContextMenu = 'empty' + PopulatedContextMenu = 'populated' -class ProfilerWidgetMainToolbarSections: - Main = 'main_section' +class ProfilerContextMenuSections: + Locals = 'locals_section' -class ProfilerWidgetInformationToolbarSections: - Main = 'main_section' +class ProfilerWidgetContextMenuActions: + ShowCallees = "show_callees_action" + ShowCallers = "show_callers_action" -class ProfilerWidgetMainToolbarItems: - FileCombo = 'file_combo' +class ProfilerWidgetMainToolbarSections: + Main = 'main_section' class ProfilerWidgetInformationToolbarItems: - Stretcher1 = 'stretcher_1' - Stretcher2 = 'stretcher_2' - DateLabel = 'date_label' - - -# --- Utils -# ---------------------------------------------------------------------------- -def is_profiler_installed(): - from spyder.utils.programs import is_module_installed - return is_module_installed('cProfile') and is_module_installed('pstats') - - -def gettime_s(text): - """ - Parse text and return a time in seconds. - - The text is of the format 0h : 0.min:0.0s:0 ms:0us:0 ns. - Spaces are not taken into account and any of the specifiers can be ignored. - """ - pattern = r'([+-]?\d+\.?\d*) ?([mμnsinh]+)' - matches = re.findall(pattern, text) - if len(matches) == 0: - return None - time = 0. - for res in matches: - tmp = float(res[0]) - if res[1] == 'ns': - tmp *= 1e-9 - elif res[1] == u'\u03BCs': - tmp *= 1e-6 - elif res[1] == 'ms': - tmp *= 1e-3 - elif res[1] == 'min': - tmp *= 60 - elif res[1] == 'h': - tmp *= 3600 - time += tmp - return time + Stretcher = 'stretcher' # --- Widgets # ---------------------------------------------------------------------------- -class ProfilerWidget(PluginMainWidget): +class ProfilerWidget(ShellConnectMainWidget): """ Profiler widget. """ - ENABLE_SPINNER = True - DATAPATH = get_conf_path('profiler.results') # --- Signals # ------------------------------------------------------------------------ @@ -149,85 +91,58 @@ class ProfilerWidget(PluginMainWidget): Word to select on given row. """ - sig_started = Signal() - """This signal is emitted to inform the profiling process has started.""" - - sig_finished = Signal() - """This signal is emitted to inform the profile profiling has finished.""" - def __init__(self, name=None, plugin=None, parent=None): super().__init__(name, plugin, parent) - self.set_conf('text_color', MAIN_TEXT_COLOR) - - # Attributes - self._last_wdir = None - self._last_args = None - self.pythonpath = None - self.error_output = None - self.output = None - self.running = False - self.text_color = self.get_conf('text_color') - - # Widgets - self.process = None - self.filecombo = PythonModulesComboBox( - self, id_=ProfilerWidgetMainToolbarItems.FileCombo) - self.datatree = ProfilerDataTree(self) - self.datelabel = QLabel() - self.datelabel.ID = ProfilerWidgetInformationToolbarItems.DateLabel - - # Layout - layout = QVBoxLayout() - layout.addWidget(self.datatree) - self.setLayout(layout) - - # Signals - self.datatree.sig_edit_goto_requested.connect( - self.sig_edit_goto_requested) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('Profiler') - def get_focus_widget(self): - return self.datatree - def setup(self): - self.start_action = self.create_action( - ProfilerWidgetActions.Run, - text=_("Run profiler"), - tip=_("Run profiler"), - icon=self.create_icon('run'), - triggered=self.run, - ) - browse_action = self.create_action( - ProfilerWidgetActions.Browse, - text='', - tip=_('Select Python file'), - icon=self.create_icon('fileopen'), - triggered=lambda x: self.select_file(), - ) - self.log_action = self.create_action( - ProfilerWidgetActions.ShowOutput, - text=_("Output"), - tip=_("Show program's output"), - icon=self.create_icon('log'), - triggered=self.show_log, - ) self.collapse_action = self.create_action( ProfilerWidgetActions.Collapse, text=_('Collapse'), tip=_('Collapse one level up'), icon=self.create_icon('collapse'), - triggered=lambda x=None: self.datatree.change_view(-1), + triggered=lambda x=None: self.current_widget( + ).data_tree.change_view(-1), ) self.expand_action = self.create_action( ProfilerWidgetActions.Expand, text=_('Expand'), tip=_('Expand one level down'), icon=self.create_icon('expand'), - triggered=lambda x=None: self.datatree.change_view(1), + triggered=lambda x=None: self.current_widget( + ).data_tree.change_view(1), + ) + self.home_action = self.create_action( + ProfilerWidgetActions.Home, + text=_("Reset tree"), + tip=_('Go back to full tree'), + icon=self.create_icon('home'), + triggered=self.home_tree, + ) + self.toggle_tree_action = self.create_action( + ProfilerWidgetActions.ToggleTreeDirection, + text=_("Switch tree direction"), + tip=_('Switch tree direction between callers and callees'), + icon=self.create_icon('swap'), + toggled=self.toggle_tree, + ) + self.slow_local_action = self.create_action( + ProfilerWidgetActions.SlowLocal, + text=_("Show items with large local time"), + tip=_('Show items with large local time'), + icon=self.create_icon('slow'), + triggered=self.slow_local_tree, + ) + self.toggle_builtins_action = self.create_action( + ProfilerWidgetActions.ToggleBuiltins, + text=_("Hide builtins"), + tip=_('Hide builtins'), + icon=self.create_icon('hide'), + toggled=self.toggle_builtins, ) self.save_action = self.create_action( ProfilerWidgetActions.SaveData, @@ -241,7 +156,7 @@ def setup(self): text=_("Load data"), tip=_('Load profiling data for comparison'), icon=self.create_icon('fileimport'), - triggered=self.compare, + triggered=self.load_data, ) self.clear_action = self.create_action( ProfilerWidgetActions.Clear, @@ -251,803 +166,257 @@ def setup(self): triggered=self.clear, ) self.clear_action.setEnabled(False) - self.save_action.setEnabled(False) - - # Main Toolbar - toolbar = self.get_main_toolbar() - for item in [self.filecombo, browse_action, self.start_action]: + search_action = self.create_action( + ProfilerWidgetActions.Search, + text=_("Search"), + icon=self.create_icon('find'), + toggled=self.toggle_finder, + register_shortcut=True + ) + undo_action = self.create_action( + ProfilerWidgetActions.Undo, + text=_("Previous View"), + icon=self.create_icon('undo'), + triggered=self.undo, + register_shortcut=True + ) + redo_action = self.create_action( + ProfilerWidgetActions.Redo, + text=_("Next View"), + icon=self.create_icon('redo'), + triggered=self.redo, + register_shortcut=True + ) + main_toolbar = self.get_main_toolbar() + + for item in [ + self.home_action, + undo_action, + redo_action, + self.collapse_action, + self.expand_action, + self.toggle_tree_action, + self.toggle_builtins_action, + self.slow_local_action, + search_action, + self.create_stretcher( + id_=ProfilerWidgetInformationToolbarItems.Stretcher), + self.save_action, + self.load_action, + self.clear_action + ]: self.add_item_to_toolbar( item, - toolbar=toolbar, + toolbar=main_toolbar, section=ProfilerWidgetMainToolbarSections.Main, ) - - # Secondary Toolbar - secondary_toolbar = self.create_toolbar( - ProfilerWidgetToolbars.Information) - for item in [self.collapse_action, self.expand_action, - self.create_stretcher( - id_=ProfilerWidgetInformationToolbarItems.Stretcher1), - self.datelabel, - self.create_stretcher( - id_=ProfilerWidgetInformationToolbarItems.Stretcher2), - self.log_action, - self.save_action, self.load_action, self.clear_action]: - self.add_item_to_toolbar( + # ---- Context menu actions + self.show_callees_action = self.create_action( + ProfilerWidgetContextMenuActions.ShowCallees, + _("Show callees"), + icon=self.create_icon('2downarrow'), + triggered=self.show_callees + ) + self.show_callers_action = self.create_action( + ProfilerWidgetContextMenuActions.ShowCallers, + _("Show callers"), + icon=self.create_icon('2uparrow'), + triggered=self.show_callers + ) + # ---- Context menu to show when there are frames present + self.context_menu = self.create_menu( + ProfilerWidgetMenus.PopulatedContextMenu) + for item in [self.show_callers_action, self.show_callees_action]: + self.add_item_to_menu( item, - toolbar=secondary_toolbar, - section=ProfilerWidgetInformationToolbarSections.Main, + menu=self.context_menu, + section=ProfilerContextMenuSections.Locals, ) - # Setup - if not is_profiler_installed(): - # This should happen only on certain GNU/Linux distributions - # or when this a home-made Python build because the Python - # profilers are included in the Python standard library - for widget in (self.datatree, self.filecombo, - self.start_action): - widget.setDisabled(True) - url = 'https://docs.python.org/3/library/profile.html' - text = '%s %s' % (_('Please install'), url, - _("the Python profiler modules")) - self.datelabel.setText(text) - def update_actions(self): - if self.running: - icon = self.create_icon('stop') + """Update actions.""" + widget = self.current_widget() + search_action = self.get_action(ProfilerWidgetActions.Search) + toggle_tree_action = self.get_action( + ProfilerWidgetActions.ToggleTreeDirection) + toggle_builtins_action = self.get_action( + ProfilerWidgetActions.ToggleBuiltins) + + if widget is None: + search = False + inverted_tree = False + ignore_builtins = False else: - icon = self.create_icon('run') - self.start_action.setIcon(icon) + search = widget.finder_is_visible() + inverted_tree = widget.data_tree.inverted_tree + ignore_builtins = widget.data_tree.ignore_builtins - self.load_action.setEnabled(not self.running) - self.clear_action.setEnabled(not self.running) - self.start_action.setEnabled(bool(self.filecombo.currentText())) + search_action.setChecked(search) + toggle_tree_action.setChecked(inverted_tree) + toggle_builtins_action.setChecked(ignore_builtins) - # --- Private API + # --- Public API # ------------------------------------------------------------------------ - def _kill_if_running(self): - """Kill the profiling process if it is running.""" - if self.process is not None: - if self.process.state() == QProcess.Running: - self.process.close() - self.process.waitForFinished(1000) - - self.update_actions() - - def _finished(self, exit_code, exit_status): - """ - Parse results once the profiling process has ended. + def home_tree(self): + """Invert tree.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.home_tree() - Parameters - ---------- - exit_code: int - QProcess exit code. - exit_status: str - QProcess exit status. - """ - self.running = False - self.show_errorlog() # If errors occurred, show them. - self.output = self.error_output + self.output - self.datelabel.setText('') - self.show_data(justanalyzed=True) - self.save_action.setEnabled(True) - self.update_actions() + def toggle_tree(self, state): + """Invert tree.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.inverted_tree = state + widget.data_tree.refresh_tree() - def _read_output(self, error=False): - """ - Read otuput from QProcess. + def toggle_builtins(self, state): + """Invert tree.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.ignore_builtins = state + widget.data_tree.refresh_tree() - Parameters - ---------- - error: bool, optional - Process QProcess output or error channels. Default is False. - """ - if error: - self.process.setReadChannel(QProcess.StandardError) - else: - self.process.setReadChannel(QProcess.StandardOutput) + def slow_local_tree(self): + """Show items with large local times""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.show_slow() - qba = QByteArray() - while self.process.bytesAvailable(): - if error: - qba += self.process.readAllStandardError() - else: - qba += self.process.readAllStandardOutput() + def undo(self): + """Undo change.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.undo() - text = to_text_string(qba.data(), encoding='utf-8') - if error: - self.error_output += text - else: - self.output += text + def redo(self): + """Redo changes.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.redo() - @on_conf_change(section='pythonpath_manager', option='spyder_pythonpath') - def _update_pythonpath(self, value): - self.pythonpath = value + def show_callers(self): + """Invert tree.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.show_selected() + if not self.toggle_tree_action.isChecked(): + self.toggle_tree_action.setChecked(True) + + def show_callees(self): + """Invert tree.""" + widget = self.current_widget() + if widget is None: + return + widget.data_tree.show_selected() + if self.toggle_tree_action.isChecked(): + self.toggle_tree_action.setChecked(False) - # --- Public API - # ------------------------------------------------------------------------ def save_data(self): """Save data.""" + widget = self.current_widget() + if widget is None: + return title = _("Save profiler result") filename, _selfilter = getsavefilename( self, title, getcwd_or_home(), - _("Profiler result") + " (*.Result)", + _("Profiler result") + " (*.prof)", ) extension = osp.splitext(filename)[1].lower() if not extension: # Needed to prevent trying to save a data file without extension # See spyder-ide/spyder#19633 - filename = filename + '.Result' + filename = filename + '.prof' if filename: - self.datatree.save_data(filename) + widget.data_tree.save_data(filename) - def compare(self): + def load_data(self): """Compare previous saved run with last run.""" + widget = self.current_widget() + if widget is None: + return filename, _selfilter = getopenfilename( self, _("Select script to compare"), getcwd_or_home(), - _("Profiler result") + " (*.Result)", + _("Profiler result") + " (*.prof)", ) if filename: - self.datatree.compare(filename) - self.show_data() - self.save_action.setEnabled(True) + widget.data_tree.compare(filename) + widget.data_tree.home_tree() self.clear_action.setEnabled(True) def clear(self): """Clear data in tree.""" - self.datatree.compare(None) - self.datatree.hide_diff_cols(True) - self.show_data() - self.clear_action.setEnabled(False) - - def analyze(self, filename, wdir=None, args=None): - """ - Start the profiling process. - - Parameters - ---------- - wdir: str - Working directory path string. Default is None. - args: list - Arguments to pass to the profiling process. Default is None. - """ - if not is_profiler_installed(): + widget = self.current_widget() + if widget is None: return + widget.data_tree.compare(None) + widget.data_tree.home_tree() + self.clear_action.setEnabled(False) - self._kill_if_running() - - # TODO: storing data is not implemented yet - # index, _data = self.get_data(filename) - combo = self.filecombo - items = [combo.itemText(idx) for idx in range(combo.count())] - index = None - if index is None and filename not in items: - self.filecombo.addItem(filename) - self.filecombo.setCurrentIndex(self.filecombo.count() - 1) - else: - self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) - - self.filecombo.selected() - if self.filecombo.is_valid(): - if wdir is None: - wdir = osp.dirname(filename) - - self.start(wdir, args) - - def select_file(self, filename=None): - """ - Select filename to profile. - - Parameters - ---------- - filename: str, optional - Path to filename to profile. default is None. - - Notes - ----- - If no `filename` is provided an open filename dialog will be used. - """ - if filename is None: - self.sig_redirect_stdio_requested.emit(False) - filename, _selfilter = getopenfilename( - self, - _("Select Python file"), - getcwd_or_home(), - _("Python files") + " (*.py ; *.pyw)" - ) - self.sig_redirect_stdio_requested.emit(True) - - if filename: - self.analyze(filename) - - def show_log(self): - """Show process output log.""" - if self.output: - output_dialog = TextEditor( - self.output, - title=_("Profiler output"), - readonly=True, - parent=self, - ) - output_dialog.resize(700, 500) - output_dialog.exec_() - - def show_errorlog(self): - """Show process error log.""" - if self.error_output: - output_dialog = TextEditor( - self.error_output, - title=_("Profiler output"), - readonly=True, - parent=self, - ) - output_dialog.resize(700, 500) - output_dialog.exec_() - - def start(self, wdir=None, args=None): - """ - Start the profiling process. - - Parameters - ---------- - wdir: str - Working directory path string. Default is None. - args: list - Arguments to pass to the profiling process. Default is None. - """ - filename = to_text_string(self.filecombo.currentText()) - if wdir is None: - wdir = self._last_wdir - if wdir is None: - wdir = osp.basename(filename) - - if args is None: - args = self._last_args - if args is None: - args = [] - - self._last_wdir = wdir - self._last_args = args - - self.datelabel.setText(_('Profiling, please wait...')) - - self.process = QProcess(self) - self.process.setProcessChannelMode(QProcess.SeparateChannels) - self.process.setWorkingDirectory(wdir) - self.process.readyReadStandardOutput.connect(self._read_output) - self.process.readyReadStandardError.connect( - lambda: self._read_output(error=True)) - self.process.finished.connect( - lambda ec, es=QProcess.ExitStatus: self._finished(ec, es)) - self.process.finished.connect(self.stop_spinner) - - # Start with system environment - proc_env = QProcessEnvironment() - for k, v in os.environ.items(): - proc_env.insert(k, v) - proc_env.insert("PYTHONIOENCODING", "utf8") - proc_env.remove('PYTHONPATH') - proc_env.remove('PYTHONEXECUTABLE') # needed on macOS to set sys.path correctly - if self.pythonpath is not None: - logger.debug(f"Pass Pythonpath {self.pythonpath} to process") - proc_env.insert('PYTHONPATH', os.pathsep.join(self.pythonpath)) - self.process.setProcessEnvironment(proc_env) - - executable = self.get_conf('executable', section='main_interpreter') - - self.output = '' - self.error_output = '' - self.running = True - self.start_spinner() + def create_new_widget(self, shellwidget): + """Create new profiler widget.""" + widget = ProfilerSubWidget(self) + widget.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) + widget.sig_display_requested.connect(self.display_request) + widget.set_context_menu(self.context_menu) + widget.sig_hide_finder_requested.connect(self.hide_finder) - p_args = ['-m', 'cProfile', '-o', self.DATAPATH] - if os.name == 'nt': - # On Windows, one has to replace backslashes by slashes to avoid - # confusion with escape characters (otherwise, for example, '\t' - # will be interpreted as a tabulation): - p_args.append(osp.normpath(filename).replace(os.sep, '/')) - else: - p_args.append(filename) + shellwidget.kernel_handler.kernel_comm.register_call_handler( + "show_profile_file", widget.show_profile_buffer) + widget.shellwidget = shellwidget - if args: - p_args.extend(shell_split(args)) + return widget - self.process.start(executable, p_args) - running = self.process.waitForStarted() - if not running: - QMessageBox.critical( - self, - _("Error"), - _("Process failed to start"), - ) - self.update_actions() + def close_widget(self, widget): + """Close profiler widget.""" + widget.sig_edit_goto_requested.disconnect( + self.sig_edit_goto_requested) + widget.sig_display_requested.disconnect(self.display_request) + widget.sig_hide_finder_requested.disconnect(self.hide_finder) - def stop(self): - """Stop the running process.""" - self.running = False - self.process.close() - self.process.waitForFinished(1000) - self.stop_spinner() - self.update_actions() + # Unregister + widget.shellwidget.kernel_handler.kernel_comm.register_call_handler( + "show_profile_file", None) + widget.setParent(None) + widget.close() - def run(self): - """Toggle starting or running the profiling process.""" - if self.running: - self.stop() - else: - self.start() + def switch_widget(self, widget, old_widget): + """Switch widget.""" + pass - def show_data(self, justanalyzed=False): + def display_request(self, widget): """ - Show analyzed data on results tree. + Display request from ProfilerDataTree. - Parameters - ---------- - justanalyzed: bool, optional - Default is False. + Only display if this is the current widget. """ - if not justanalyzed: - self.output = None - - self.log_action.setEnabled(self.output is not None - and len(self.output) > 0) - self._kill_if_running() - filename = to_text_string(self.filecombo.currentText()) - if not filename: + if ( + self.current_widget() is widget + and self.get_conf("switch_to_plugin") + ): + self.get_plugin().switch_to_plugin() + + def toggle_finder(self, checked): + """Show or hide finder.""" + widget = self.current_widget() + if widget is None: return + widget.toggle_finder(checked) - self.datelabel.setText(_('Sorting data, please wait...')) - QApplication.processEvents() - - self.datatree.load_data(self.DATAPATH) - self.datatree.show_tree() - - text_style = "%s " - date_text = text_style % (self.text_color, - time.strftime("%Y-%m-%d %H:%M:%S", - time.localtime())) - self.datelabel.setText(date_text) - - -class TreeWidgetItem(QTreeWidgetItem): - def __init__(self, parent=None): - QTreeWidgetItem.__init__(self, parent) - - def __lt__(self, otherItem): - column = self.treeWidget().sortColumn() - try: - if column == 1 or column == 3: # TODO: Hardcoded Column - t0 = gettime_s(self.text(column)) - t1 = gettime_s(otherItem.text(column)) - if t0 is not None and t1 is not None: - return t0 > t1 - - return float(self.text(column)) > float(otherItem.text(column)) - except ValueError: - return self.text(column) > otherItem.text(column) - - -class ProfilerDataTree(QTreeWidget, SpyderWidgetMixin): - """ - Convenience tree widget (with built-in model) - to store and view profiler data. - - The quantities calculated by the profiler are as follows - (from profile.Profile): - [0] = The number of times this function was called, not counting direct - or indirect recursion, - [1] = Number of times this function appears on the stack, minus one - [2] = Total time spent internal to this function - [3] = Cumulative time that this function was present on the stack. In - non-recursive functions, this is the total execution time from start - to finish of each invocation of a function, including time spent in - all subfunctions. - [4] = A dictionary indicating for each function name, the number of times - it was called by us. - """ - SEP = r"<[=]>" # separator between filename and linenumber - # (must be improbable as a filename to avoid splitting the filename itself) - - # Signals - sig_edit_goto_requested = Signal(str, int, str) - - def __init__(self, parent=None): - if PYQT5: - super().__init__(parent, class_parent=parent) - else: - QTreeWidget.__init__(self, parent) - SpyderWidgetMixin.__init__(self, class_parent=parent) - - self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'), - _('Local Time'), _('Diff'), _('Calls'), _('Diff'), - _('File:line')] - self.icon_list = { - 'module': self.create_icon('python'), - 'function': self.create_icon('function'), - 'builtin': self.create_icon('python'), - 'constructor': self.create_icon('class') - } - self.profdata = None # To be filled by self.load_data() - self.stats = None # To be filled by self.load_data() - self.stats1 = [] # To be filled by self.load_data() - self.item_depth = None - self.item_list = None - self.items_to_be_shown = None - self.current_view_depth = None - self.compare_file = None - self.setColumnCount(len(self.header_list)) - self.setHeaderLabels(self.header_list) - self.initialize_view() - self.itemActivated.connect(self.item_activated) - self.itemExpanded.connect(self.item_expanded) - - def set_item_data(self, item, filename, line_number): - """Set tree item user data: filename (string) and line_number (int)""" - set_item_user_text(item, '%s%s%d' % (filename, self.SEP, line_number)) - - def get_item_data(self, item): - """Get tree item user data: (filename, line_number)""" - filename, line_number_str = get_item_user_text(item).split(self.SEP) - return filename, int(line_number_str) - - def initialize_view(self): - """Clean the tree and view parameters""" - self.clear() - self.item_depth = 0 # To be use for collapsing/expanding one level - self.item_list = [] # To be use for collapsing/expanding one level - self.items_to_be_shown = {} - self.current_view_depth = 0 - - def load_data(self, profdatafile): - """Load profiler data saved by profile/cProfile module""" - import pstats - # Fixes spyder-ide/spyder#6220. - try: - stats_indi = [pstats.Stats(profdatafile), ] - except (OSError, IOError): - self.profdata = None - return - self.profdata = stats_indi[0] - - if self.compare_file is not None: - # Fixes spyder-ide/spyder#5587. - try: - stats_indi.append(pstats.Stats(self.compare_file)) - except (OSError, IOError) as e: - QMessageBox.critical( - self, _("Error"), - _("Error when trying to load profiler results. " - "The error was

" - "{0}").format(e)) - self.compare_file = None - map(lambda x: x.calc_callees(), stats_indi) - self.profdata.calc_callees() - self.stats1 = stats_indi - self.stats = stats_indi[0].stats - - def compare(self, filename): - self.hide_diff_cols(False) - self.compare_file = filename - - def hide_diff_cols(self, hide): - for i in (2, 4, 6): - self.setColumnHidden(i, hide) - - def save_data(self, filename): - """Save profiler data.""" - if len(self.stats1) > 0: - self.stats1[0].dump_stats(filename) - - def find_root(self): - """Find a function without a caller""" - # Fixes spyder-ide/spyder#8336. - if self.profdata is not None: - self.profdata.sort_stats("cumulative") - else: - return - for func in self.profdata.fcn_list: - if ('~', 0) != func[0:2] and not func[2].startswith( - ''): - # This skips the profiler function at the top of the list - # it does only occur in Python 3 - return func - - def find_callees(self, parent): - """Find all functions called by (parent) function.""" - # FIXME: This implementation is very inneficient, because it - # traverses all the data to find children nodes (callees) - return self.profdata.all_callees[parent] - - def show_tree(self): - """Populate the tree with profiler data and display it.""" - self.initialize_view() # Clear before re-populating - self.setItemsExpandable(True) - self.setSortingEnabled(False) - rootkey = self.find_root() # This root contains profiler overhead - if rootkey is not None: - self.populate_tree(self, self.find_callees(rootkey)) - self.resizeColumnToContents(0) - self.setSortingEnabled(True) - self.sortItems(1, Qt.AscendingOrder) # FIXME: hardcoded index - self.change_view(1) - - def function_info(self, functionKey): - """Returns processed information about the function's name and file.""" - node_type = 'function' - filename, line_number, function_name = functionKey - if function_name == '': - modulePath, moduleName = osp.split(filename) - node_type = 'module' - if moduleName == '__init__.py': - modulePath, moduleName = osp.split(modulePath) - function_name = '<' + moduleName + '>' - if not filename or filename == '~': - file_and_line = '(built-in)' - node_type = 'builtin' - else: - if function_name == '__init__': - node_type = 'constructor' - file_and_line = '%s : %d' % (filename, line_number) - return filename, line_number, function_name, file_and_line, node_type - - @staticmethod - def format_measure(measure): - """Get format and units for data coming from profiler task.""" - # Convert to a positive value. - measure = abs(measure) - - # For number of calls - if isinstance(measure, int): - return to_text_string(measure) - - # For time measurements - if 1.e-9 < measure <= 1.e-6: - measure = u"{0:.2f} ns".format(measure / 1.e-9) - elif 1.e-6 < measure <= 1.e-3: - measure = u"{0:.2f} \u03BCs".format(measure / 1.e-6) - elif 1.e-3 < measure <= 1: - measure = u"{0:.2f} ms".format(measure / 1.e-3) - elif 1 < measure <= 60: - measure = u"{0:.2f} s".format(measure) - elif 60 < measure <= 3600: - m, s = divmod(measure, 3600) - if s > 60: - m, s = divmod(measure, 60) - s = to_text_string(s).split(".")[-1] - measure = u"{0:.0f}.{1:.2s} min".format(m, s) - else: - h, m = divmod(measure, 3600) - if m > 60: - m /= 60 - measure = u"{0:.0f}h:{1:.0f}min".format(h, m) - return measure - - def color_string(self, x): - """Return a string formatted delta for the values in x. - - Args: - x: 2-item list of integers (representing number of calls) or - 2-item list of floats (representing seconds of runtime). - - Returns: - A list with [formatted x[0], [color, formatted delta]], where - color reflects whether x[1] is lower, greater, or the same as - x[0]. - """ - diff_str = "" - color = "black" - - if len(x) == 2 and self.compare_file is not None: - difference = x[0] - x[1] - if difference: - color, sign = ((SpyderPalette.COLOR_SUCCESS_1, '-') - if difference < 0 - else (SpyderPalette.COLOR_ERROR_1, '+')) - diff_str = '{}{}'.format(sign, self.format_measure(difference)) - return [self.format_measure(x[0]), [diff_str, color]] - - def format_output(self, child_key): - """ Formats the data. - - self.stats1 contains a list of one or two pstat.Stats() instances, with - the first being the current run and the second, the saved run, if it - exists. Each Stats instance is a dictionary mapping a function to - 5 data points - cumulative calls, number of calls, total time, - cumulative time, and callers. - - format_output() converts the number of calls, total time, and - cumulative time to a string format for the child_key parameter. - """ - data = [x.stats.get(child_key, [0, 0, 0, 0, {}]) for x in self.stats1] - return (map(self.color_string, islice(zip(*data), 1, 4))) - - def populate_tree(self, parentItem, children_list): - """ - Recursive method to create each item (and associated data) - in the tree. - """ - for child_key in children_list: - self.item_depth += 1 - (filename, line_number, function_name, file_and_line, node_type - ) = self.function_info(child_key) - - ((total_calls, total_calls_dif), (loc_time, loc_time_dif), - (cum_time, cum_time_dif)) = self.format_output(child_key) - - child_item = TreeWidgetItem(parentItem) - self.item_list.append(child_item) - self.set_item_data(child_item, filename, line_number) - - # FIXME: indexes to data should be defined by a dictionary on init - child_item.setToolTip(0, _('Function or module name')) - child_item.setData(0, Qt.DisplayRole, function_name) - child_item.setIcon(0, self.icon_list[node_type]) - - child_item.setToolTip(1, _('Time in function ' - '(including sub-functions)')) - child_item.setData(1, Qt.DisplayRole, cum_time) - child_item.setTextAlignment(1, Qt.AlignRight) - - child_item.setData(2, Qt.DisplayRole, cum_time_dif[0]) - child_item.setForeground(2, QColor(cum_time_dif[1])) - child_item.setTextAlignment(2, Qt.AlignLeft) - - child_item.setToolTip(3, _('Local time in function ' - '(not in sub-functions)')) - - child_item.setData(3, Qt.DisplayRole, loc_time) - child_item.setTextAlignment(3, Qt.AlignRight) - - child_item.setData(4, Qt.DisplayRole, loc_time_dif[0]) - child_item.setForeground(4, QColor(loc_time_dif[1])) - child_item.setTextAlignment(4, Qt.AlignLeft) - - child_item.setToolTip(5, _('Total number of calls ' - '(including recursion)')) - - child_item.setData(5, Qt.DisplayRole, total_calls) - child_item.setTextAlignment(5, Qt.AlignRight) - - child_item.setData(6, Qt.DisplayRole, total_calls_dif[0]) - child_item.setForeground(6, QColor(total_calls_dif[1])) - child_item.setTextAlignment(6, Qt.AlignLeft) - - child_item.setToolTip(7, _('File:line ' - 'where function is defined')) - child_item.setData(7, Qt.DisplayRole, file_and_line) - #child_item.setExpanded(True) - if self.is_recursive(child_item): - child_item.setData(7, Qt.DisplayRole, '(%s)' % _('recursion')) - child_item.setDisabled(True) - else: - callees = self.find_callees(child_key) - if self.item_depth < 3: - self.populate_tree(child_item, callees) - elif callees: - child_item.setChildIndicatorPolicy(child_item.ShowIndicator) - self.items_to_be_shown[id(child_item)] = callees - self.item_depth -= 1 - - def item_activated(self, item): - filename, line_number = self.get_item_data(item) - self.sig_edit_goto_requested.emit(filename, line_number, '') - - def item_expanded(self, item): - if item.childCount() == 0 and id(item) in self.items_to_be_shown: - callees = self.items_to_be_shown[id(item)] - self.populate_tree(item, callees) - - def is_recursive(self, child_item): - """Returns True is a function is a descendant of itself.""" - ancestor = child_item.parent() - # FIXME: indexes to data should be defined by a dictionary on init - while ancestor: - if (child_item.data(0, Qt.DisplayRole - ) == ancestor.data(0, Qt.DisplayRole) and - child_item.data(7, Qt.DisplayRole - ) == ancestor.data(7, Qt.DisplayRole)): - return True - else: - ancestor = ancestor.parent() - return False - - def get_top_level_items(self): - """Iterate over top level items""" - return [self.topLevelItem(_i) - for _i in range(self.topLevelItemCount())] - - def get_items(self, maxlevel): - """Return all items with a level <= `maxlevel`""" - itemlist = [] - - def add_to_itemlist(item, maxlevel, level=1): - level += 1 - for index in range(item.childCount()): - citem = item.child(index) - itemlist.append(citem) - if level <= maxlevel: - add_to_itemlist(citem, maxlevel, level) - - for tlitem in self.get_top_level_items(): - itemlist.append(tlitem) - if maxlevel > 0: - add_to_itemlist(tlitem, maxlevel=maxlevel) - return itemlist - - def change_view(self, change_in_depth): - """Change view depth by expanding or collapsing all same-level nodes""" - self.current_view_depth += change_in_depth - if self.current_view_depth < 0: - self.current_view_depth = 0 - self.collapseAll() - if self.current_view_depth > 0: - for item in self.get_items(maxlevel=self.current_view_depth - 1): - item.setExpanded(True) - - -# ============================================================================= -# Tests -# ============================================================================= -def primes(n): - """ - Simple test function - Taken from http://www.huyng.com/posts/python-performance-analysis/ - """ - if n == 2: - return [2] - elif n < 2: - return [] - s = list(range(3, n + 1, 2)) - mroot = n ** 0.5 - half = (n + 1) // 2 - 1 - i = 0 - m = 3 - while m <= mroot: - if s[i]: - j = (m * m - 3) // 2 - s[j] = 0 - while j < half: - s[j] = 0 - j += m - i = i + 1 - m = 2 * i + 3 - return [2] + [x for x in s if x] - - -def test(): - """Run widget test""" - from spyder.utils.qthelpers import qapplication - import inspect - import tempfile - from unittest.mock import MagicMock - - primes_sc = inspect.getsource(primes) - fd, script = tempfile.mkstemp(suffix='.py') - with os.fdopen(fd, 'w') as f: - f.write("# -*- coding: utf-8 -*-" + "\n\n") - f.write(primes_sc + "\n\n") - f.write("primes(100000)") - - plugin_mock = MagicMock() - plugin_mock.CONF_SECTION = 'profiler' - - app = qapplication(test_time=5) - widget = ProfilerWidget('test', plugin=plugin_mock) - widget._setup() - widget.setup() - widget.get_conf('executable', get_python_executable(), - section='main_interpreter') - widget.resize(800, 600) - widget.show() - widget.analyze(script) - sys.exit(app.exec_()) - - -if __name__ == '__main__': - test() + def hide_finder(self): + """Hide finder.""" + action = self.get_action(ProfilerWidgetActions.Search) + action.setChecked(False) diff --git a/spyder/plugins/profiler/widgets/profiler_data_tree.py b/spyder/plugins/profiler/widgets/profiler_data_tree.py new file mode 100644 index 00000000000..c51742d4619 --- /dev/null +++ b/spyder/plugins/profiler/widgets/profiler_data_tree.py @@ -0,0 +1,697 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# based on pylintgui.py by Pierre Raybaut +# +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Profiler widget. + +See the official documentation on python profiling: +https://docs.python.org/3/library/profile.html +""" + +# Standard library imports +import os +import os.path as osp +import tempfile + +# Third party imports +from qtpy import PYQT5 +from qtpy.QtCore import Qt, Signal +from qtpy.QtGui import QColor +from qtpy.QtWidgets import (QWidget, QMessageBox, QTreeWidget, + QTreeWidgetItem, QVBoxLayout) + +# Local imports +from spyder.api.config.mixins import SpyderConfigurationAccessor +from spyder.api.translations import _ +from spyder.api.widgets.mixins import SpyderWidgetMixin +from spyder.py3compat import to_text_string +from spyder.utils.palette import SpyderPalette +from spyder.utils.qthelpers import set_item_user_text +from spyder.widgets.helperwidgets import FinderWidget + + +class ProfilerKey: + """ + Class to save the indexes of the profiler keys + + The quantities calculated by the profiler are as follows + (from profile.Profile): + [0] = The number of times this function was called, not counting direct + or indirect recursion, + [1] = Number of times this function appears on the stack, minus one + [2] = Total time spent internal to this function + [3] = Cumulative time that this function was present on the stack. In + non-recursive functions, this is the total execution time from start + to finish of each invocation of a function, including time spent in + all subfunctions. + [4] = A dictionary indicating for each function name, the number of times + it was called by us. + """ + Calls = 0 + TotalCalls = 1 + LocalTime = 2 + TotalTime = 3 + Callers = 4 + + +class ProfilerSubWidget(QWidget, SpyderWidgetMixin): + """Profiler widget for shellwidget""" + + # Signals + sig_edit_goto_requested = Signal(str, int, str) + sig_display_requested = Signal(object) + sig_hide_finder_requested = Signal() + + def __init__(self, parent=None): + if PYQT5: + super().__init__(parent, class_parent=parent) + else: + QWidget.__init__(self, parent) + SpyderWidgetMixin.__init__(self, class_parent=parent) + # Finder + self.data_tree = None + self.finder = None + self.setup() + + def toggle_finder(self, show): + """Show and hide the finder.""" + if self.finder is None: + return + self.finder.set_visible(show) + if not show: + self.data_tree.setFocus() + self.data_tree._show_tree() + + def do_find(self, text): + """Search for text.""" + if self.data_tree is not None: + self.data_tree.do_find(text) + + def finder_is_visible(self): + """Check if the finder is visible.""" + if self.finder is None: + return False + return self.finder.isVisible() + + def setup(self): + """Setup widget.""" + self.data_tree = ProfilerDataTree(self) + self.data_tree.sig_edit_goto_requested.connect( + self.sig_edit_goto_requested) + + self.finder = FinderWidget(self) + self.finder.sig_find_text.connect(self.do_find) + self.finder.sig_hide_finder_requested.connect( + self.sig_hide_finder_requested) + + # Setup layout. + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.data_tree) + layout.addSpacing(1) + layout.addWidget(self.finder) + self.setLayout(layout) + + def show_profile_buffer(self, prof_buffer, lib_pathlist): + """Show profile file.""" + if not prof_buffer: + return + with tempfile.TemporaryDirectory() as dir: + filename = os.path.join(dir, "tem.prof") + with open(filename, "bw") as f: + f.write(prof_buffer) + self.data_tree.lib_pathlist = lib_pathlist + self.data_tree.load_data(filename) + # Show + self.data_tree._show_tree() + self.sig_display_requested.emit(self) + + def set_context_menu(self, menu): + self.data_tree.menu = menu + + +class TreeWidgetItem(QTreeWidgetItem): + """Item to show in the tree. It represent a function call.""" + def __init__(self, parent, item_key, profile_data, compare_data, icon_list, + index_dict): + QTreeWidgetItem.__init__(self, parent) + self.item_key = item_key + self.index_dict = index_dict + # Order is from profile data + self.total_calls, self.local_time, self.total_time = profile_data[1:4] + (filename, line_number, function_name, file_and_line, node_type + ) = self.function_info(item_key) + self.function_name = function_name + self.filename = filename + self.line_number = line_number + + self.set_item_data(filename, line_number) + self.setIcon(self.index_dict["function_name"], icon_list[node_type]) + self.set_tooltips() + + data = { + "function_name": function_name, + "total_time": self.format_measure(self.total_time), + "local_time": self.format_measure(self.local_time), + "number_calls": self.format_measure(self.total_calls), + "file:line": file_and_line + } + self.set_data(data) + alignment = { + "total_time": Qt.AlignRight, + "local_time": Qt.AlignRight, + "number_calls": Qt.AlignRight, + } + self.set_alignment(alignment) + + if self.is_recursive(): + self.setData(self.index_dict["file:line"], + Qt.DisplayRole, '(%s)' % _('recursion')) + self.setDisabled(True) + + if compare_data is None: + return + + diff_data = {} + diff_colors = {} + # Keep same order as profile data + compare_keys = [ + "unused", + "number_calls_diff", + "local_time_diff", + "total_time_diff" + ] + for i in range(1, 4): + diff_str, color = self.color_diff( + profile_data[i] - compare_data[i]) + diff_data[compare_keys[i]] = diff_str + diff_colors[compare_keys[i]] = color + + self.set_data(diff_data) + self.set_color(diff_colors) + diff_alignment = { + "total_time_diff": Qt.AlignLeft, + "local_time_diff": Qt.AlignLeft, + "number_calls_diff": Qt.AlignLeft + } + self.set_alignment(diff_alignment) + + def set_tooltips(self): + """Set tooltips.""" + tooltips = { + "function_name": _('Function or module name'), + "total_time": _('Time in function (including sub-functions)'), + "local_time": _('Local time in function (not in sub-functions)'), + "number_calls": _('Total number of calls (including recursion)'), + "file:line": _('File:line where function is defined')} + for k, v in tooltips.items(): + self.setToolTip(self.index_dict[k], v) + + def set_data(self, data): + """Set data in columns.""" + for k, v in data.items(): + self.setData(self.index_dict[k], Qt.DisplayRole, v) + + def set_color(self, colors): + """Set colors""" + for k, v in colors.items(): + self.setForeground(self.index_dict[k], QColor(v)) + + def set_alignment(self, alignment): + """Set alignment.""" + for k, v in alignment.items(): + self.setTextAlignment(self.index_dict[k], v) + + @staticmethod + def color_diff(difference): + """Color difference.""" + diff_str = "" + color = "black" + if difference: + color, sign = ( + (SpyderPalette.COLOR_SUCCESS_1, '-') + if difference < 0 + else (SpyderPalette.COLOR_ERROR_1, '+') + ) + diff_str = '{}{}'.format( + sign, TreeWidgetItem.format_measure(difference)) + return diff_str, color + + @staticmethod + def format_measure(measure): + """Get format and units for data coming from profiler task.""" + # Convert to a positive value. + measure = abs(measure) + + # For number of calls + if isinstance(measure, int): + return to_text_string(measure) + + # For time measurements + if 1.e-9 < measure <= 1.e-6: + measure = u"{0:.2f} ns".format(measure / 1.e-9) + elif 1.e-6 < measure <= 1.e-3: + measure = u"{0:.2f} \u03BCs".format(measure / 1.e-6) + elif 1.e-3 < measure <= 1: + measure = u"{0:.2f} ms".format(measure / 1.e-3) + elif 1 < measure <= 60: + measure = u"{0:.2f} s".format(measure) + elif 60 < measure <= 3600: + m, s = divmod(measure, 3600) + if s > 60: + m, s = divmod(measure, 60) + s = to_text_string(s).split(".")[-1] + measure = u"{0:.0f}.{1:.2s} min".format(m, s) + else: + h, m = divmod(measure, 3600) + if m > 60: + m /= 60 + measure = u"{0:.0f}h:{1:.0f}min".format(h, m) + return measure + + def __lt__(self, otherItem): + column = self.treeWidget().sortColumn() + try: + if column == self.index_dict["total_time"]: + return self.total_time > otherItem.total_time + if column == self.index_dict["local_time"]: + return self.local_time > otherItem.local_time + + return float(self.text(column)) > float(otherItem.text(column)) + except ValueError: + return self.text(column) > otherItem.text(column) + + def set_item_data(self, filename, line_number): + """Set tree item user data: filename (string) and line_number (int)""" + # separator between filename and linenumber + SEP = r"<[=]>" + # (must be improbable as a filename) + set_item_user_text(self, '%s%s%d' % (filename, SEP, line_number)) + + def function_info(self, functionKey): + """Returns processed information about the function's name and file.""" + node_type = 'function' + filename, line_number, function_name = functionKey + if function_name == '': + modulePath, moduleName = osp.split(filename) + node_type = 'module' + if moduleName == '__init__.py': + modulePath, moduleName = osp.split(modulePath) + function_name = '<' + moduleName + '>' + if not filename or filename == '~': + file_and_line = '(built-in)' + node_type = 'builtin' + else: + if function_name == '__init__': + node_type = 'constructor' + file_and_line = '%s : %d' % (filename, line_number) + return filename, line_number, function_name, file_and_line, node_type + + def is_recursive(self): + """Returns True is a function is a descendant of itself.""" + ancestor = self.parent() + while ancestor: + if (self.function_name == ancestor.function_name + and self.filename == ancestor.filename + and self.line_number == ancestor.line_number): + return True + else: + ancestor = ancestor.parent() + return False + + +class ProfilerDataTree(QTreeWidget, SpyderConfigurationAccessor): + """ + Convenience tree widget (with built-in model) + to store and view profiler data. + + The quantities calculated by the profiler are as follows + (from profile.Profile): + [0] = The number of times this function was called, not counting direct + or indirect recursion, + [1] = Number of times this function appears on the stack, minus one + [2] = Total time spent internal to this function + [3] = Cumulative time that this function was present on the stack. In + non-recursive functions, this is the total execution time from start + to finish of each invocation of a function, including time spent in + all subfunctions. + [4] = A dictionary indicating for each function name, the number of times + it was called by us. + """ + + CONF_SECTION = 'profiler' + + # Signals + sig_edit_goto_requested = Signal(str, int, str) + + def __init__(self, parent=None): + if PYQT5: + super().__init__(parent) + else: + QTreeWidget.__init__(self, parent) + self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'), + _('Local Time'), _('Diff'), _('Calls'), _('Diff'), + _('File:line')] + self.icon_list = { + 'module': parent.create_icon('python'), + 'function': parent.create_icon('function'), + 'builtin': parent.create_icon('python'), + 'constructor': parent.create_icon('class') + } + self.index_dict = { + "function_name": 0, + "total_time": 1, + "total_time_diff": 2, + "local_time": 3, + "local_time_diff": 4, + "number_calls": 5, + "number_calls_diff": 6, + "file:line": 7 + } + self.profdata = None # To be filled by self.load_data() + self.items_to_be_shown = None + self.current_view_depth = None + self.compare_data = None + self.inverted_tree = False + self.ignore_builtins = False + self.root_key = None + self.menu = None + self._last_children = None + self.setColumnCount(len(self.header_list)) + self.setHeaderLabels(self.header_list) + self.initialize_view() + self.itemActivated.connect(self.item_activated) + self.itemExpanded.connect(self.item_expanded) + self.lib_pathlist = None + self.history = [] + self.redo_history = [] + + def contextMenuEvent(self, event): + """Reimplement Qt method""" + if self.menu is None: + return + if self.profdata: + self.menu.popup(event.globalPos()) + event.accept() + + def initialize_view(self): + """Clean the tree and view parameters""" + self.clear() + self.items_to_be_shown = {} + self.current_view_depth = 0 + if (self.compare_data is not None + and self.compare_data is not self.profdata): + self.hide_diff_cols(False) + else: + self.hide_diff_cols(True) + + def load_data(self, profdatafile): + """Load profiler data saved by profile/cProfile module""" + self.history = [] + self.redo_history = [] + if not os.path.isfile(profdatafile): + self.profdata = None + return + import pstats + # Fixes spyder-ide/spyder#6220. + try: + self.profdata = pstats.Stats(profdatafile) + self.profdata.calc_callees() + self.root_key = self.find_root() + except (OSError, IOError): + self.profdata = None + return + + def compare(self, filename): + """Load compare file.""" + if filename is None: + self.compare_data = None + return + import pstats + # Fixes spyder-ide/spyder#5587. + try: + self.compare_data = pstats.Stats(filename) + self.compare_data.calc_callees() + if self.profdata is None: + # Show the compare data as prof_data + self.profdata = self.compare_data + self.root_key = self.find_root() + except (OSError, IOError) as e: + QMessageBox.critical( + self, _("Error"), + _("Error when trying to load profiler results. " + "The error was

" + "{0}").format(e)) + self.compare_data = None + + def hide_diff_cols(self, hide): + """Hide difference columns.""" + for i in (self.index_dict["total_time_diff"], + self.index_dict["local_time_diff"], + self.index_dict["number_calls_diff"]): + self.setColumnHidden(i, hide) + + def save_data(self, filename): + """Save profiler data.""" + self.profdata.dump_stats(filename) + + def find_root(self): + """Find a function without a caller""" + # Fixes spyder-ide/spyder#8336. + if self.profdata is not None: + self.profdata.sort_stats("cumulative") + else: + return + for func in self.profdata.fcn_list: + if ('~', 0) != func[0:2] and not func[2].startswith( + ''): + # This skips the profiler function at the top of the list + # it does only occur in Python 3 + return func + + def is_builtin(self, key): + """Check if key is buit-in.""" + path = key[0] + if not path: + return True + if path == "~": + return True + if path.startswith("<"): + return True + path = os.path.normcase(os.path.normpath(path)) + if self.lib_pathlist is not None: + for libpath in self.lib_pathlist: + libpath = os.path.normcase(os.path.normpath(libpath)) + commonpath = os.path.commonpath([libpath, path]) + if libpath == commonpath: + return True + return False + + def find_children(self, parent): + """Find all functions called by (parent) function.""" + if self.inverted_tree: + # Return callers + return self.profdata.stats[parent][ProfilerKey.Callers] + else: + # Return callees + callees = self.profdata.all_callees[parent] + if self.ignore_builtins: + callees = [c for c in callees if not self.is_builtin(c)] + return callees + + def do_find(self, text): + """Find all function that match text.""" + if self.profdata is None: + # Nothing to show + return + + children = self.profdata.fcn_list + children = [c for c in children if text in c[-1]] + self._show_tree(children) + + def show_slow(self): + """Show slow items.""" + if self.profdata is None: + # Nothing to show + return + # Show items with large local time + children = self.profdata.fcn_list + # Only keep top n_slow_children + N_children = self.get_conf('n_slow_children') + children = sorted(children, + key=lambda item: self.profdata.stats[item][ + ProfilerKey.LocalTime], + reverse=True) + if self.ignore_builtins: + children = [c for c in children if not self.is_builtin(c)] + children = children[:N_children] + self._show_tree(children, max_items=N_children) + + def refresh_tree(self): + """Refresh tree.""" + self._show_tree(self._last_children) + + def home_tree(self): + """Reset tree.""" + self._show_tree() + + def show_selected(self): + """Show current item.""" + self._show_tree([self.currentItem().item_key]) + + def undo(self): + """Undo change.""" + if len(self.history) > 1: + self.redo_history.append(self.history.pop(-1)) + self._show_tree(self.history.pop(-1), reset_redo=False) + + def redo(self): + """Redo changes.""" + if len(self.redo_history) > 0: + self._show_tree(self.redo_history.pop(-1), reset_redo=False) + + def _show_tree(self, children=None, max_items=None, + reset_redo=True): + """Populate the tree with profiler data and display it.""" + if self.profdata is None: + # Nothing to show + return + + self._last_children = children + + # List of internal functions to exclude + internal_list = [ + ('~', 0, "")] + # List of frames to hide at the top + head_list = [self.root_key, ] + head_list += list( + self.profdata.stats[self.root_key][ProfilerKey.Callers]) + + if children is None: + if self.inverted_tree: + # Show all callees + self.tree_state = None + children = [] + for key, value in self.profdata.all_callees.items(): + if key in internal_list or key in head_list: + continue + if self.ignore_builtins: + if not self.is_builtin(key): + non_builtin_callees = [ + k for k in value if not self.is_builtin(k)] + if len(non_builtin_callees) == 0: + children.append(key) + else: + if len(value) == 0: + children.append(key) + else: + # Show all called + self.tree_state = None + rootkey = self.root_key # This root contains profiler overhead + if rootkey is not None: + children = self.find_children(rootkey) + else: + if self.ignore_builtins: + children = [c for c in children if not self.is_builtin(c)] + children = [c for c in children if c not in internal_list] + if max_items is not None: + children = children[:max_items] + + self.initialize_view() # Clear before re-populating + self.setItemsExpandable(True) + self.setSortingEnabled(False) + if children is not None: + if len(self.history) == 0 or self.history[-1] != children: + # Do not add twice the same element + self.history.append(children) + if reset_redo: + self.redo_history = [] + # Populate the tree + self.populate_tree(self, children) + self.setSortingEnabled(True) + self.sortItems(self.index_dict["total_time"], Qt.AscendingOrder) + if len(children) < 100: + # Only expand if not too many children are shown + self.change_view(1) + self.resizeColumnToContents(0) + + def populate_tree(self, parentItem, children_list): + """ + Recursive method to create each item (and associated data) + in the tree. + """ + for child_key in children_list: + item_profdata, item_compdata = self.get_item_data(child_key) + child_item = TreeWidgetItem( + parentItem, + child_key, + item_profdata, + item_compdata, + self.icon_list, + self.index_dict) + if not child_item.is_recursive(): + grandchildren_list = self.find_children(child_key) + if grandchildren_list: + child_item.setChildIndicatorPolicy( + child_item.ShowIndicator) + self.items_to_be_shown[id(child_item)] = grandchildren_list + + def get_item_data(self, item_key): + """Return the profile and compare data for the item_key.""" + item_profdata = self.profdata.stats.get( + item_key, [0, 0, 0, 0, {}]) + item_compdata = None + if self.compare_data is not None: + item_compdata = self.compare_data.stats.get( + item_key, [0, 0, 0, 0, {}]) + return item_profdata, item_compdata + + def item_activated(self, item): + """Request editor to find item.""" + self.sig_edit_goto_requested.emit(item.filename, item.line_number, '') + + def item_expanded(self, item): + """Fill item children.""" + if item.childCount() == 0 and id(item) in self.items_to_be_shown: + children_list = self.items_to_be_shown[id(item)] + self.populate_tree(item, children_list) + + def get_top_level_items(self): + """Iterate over top level items.""" + return [self.topLevelItem(_i) + for _i in range(self.topLevelItemCount())] + + def get_items(self, maxlevel): + """Return all items with a level <= `maxlevel`""" + itemlist = [] + + def add_to_itemlist(item, maxlevel, level=1): + level += 1 + for index in range(item.childCount()): + citem = item.child(index) + itemlist.append(citem) + if level <= maxlevel: + add_to_itemlist(citem, maxlevel, level) + + for tlitem in self.get_top_level_items(): + itemlist.append(tlitem) + if maxlevel > 0: + add_to_itemlist(tlitem, maxlevel=maxlevel) + return itemlist + + def change_view(self, change_in_depth): + """ + Change the view depth by expand or collapsing all same-level nodes. + """ + self.current_view_depth += change_in_depth + if self.current_view_depth < 0: + self.current_view_depth = 0 + self.collapseAll() + if self.current_view_depth > 0: + for item in self.get_items(maxlevel=self.current_view_depth-1): + item.setExpanded(True) diff --git a/spyder/plugins/profiler/widgets/run_conf.py b/spyder/plugins/profiler/widgets/run_conf.py deleted file mode 100644 index ffd95e3f061..00000000000 --- a/spyder/plugins/profiler/widgets/run_conf.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © Spyder Project Contributors -# Licensed under the terms of the MIT License -# (see spyder/__init__.py for details) - -"""Profiler run executor configurations.""" - -# Standard library imports -import os.path as osp - -# Third-party imports -from qtpy.compat import getexistingdirectory -from qtpy.QtWidgets import ( - QGroupBox, QVBoxLayout, QGridLayout, QCheckBox, QLineEdit) - -# Local imports -from spyder.api.translations import _ -from spyder.plugins.run.api import ( - RunExecutorConfigurationGroup, Context, RunConfigurationMetadata) -from spyder.utils.misc import getcwd_or_home - - -class ProfilerPyConfigurationGroup(RunExecutorConfigurationGroup): - """External console Python run configuration options.""" - - def __init__(self, parent, context: Context, input_extension: str, - input_metadata: RunConfigurationMetadata): - super().__init__(parent, context, input_extension, input_metadata) - - self.dir = None - - # --- General settings ---- - common_group = QGroupBox(_("Script settings")) - - common_layout = QGridLayout(common_group) - - self.clo_cb = QCheckBox(_("Command line options:")) - common_layout.addWidget(self.clo_cb, 0, 0) - self.clo_edit = QLineEdit() - self.clo_cb.toggled.connect(self.clo_edit.setEnabled) - self.clo_edit.setEnabled(False) - common_layout.addWidget(self.clo_edit, 0, 1) - - layout = QVBoxLayout(self) - layout.addWidget(common_group) - layout.addStretch(100) - - def select_directory(self): - """Select directory""" - basedir = str(self.wd_edit.text()) - if not osp.isdir(basedir): - basedir = getcwd_or_home() - directory = getexistingdirectory(self, _("Select directory"), basedir) - if directory: - self.wd_edit.setText(directory) - self.dir = directory - - @staticmethod - def get_default_configuration() -> dict: - return { - 'args_enabled': False, - 'args': '' - } - - def set_configuration(self, config: dict): - args_enabled = config['args_enabled'] - args = config['args'] - - self.clo_cb.setChecked(args_enabled) - self.clo_edit.setText(args) - - def get_configuration(self) -> dict: - return { - 'args_enabled': self.clo_cb.isChecked(), - 'args': self.clo_edit.text(), - } diff --git a/spyder/plugins/toolbar/api.py b/spyder/plugins/toolbar/api.py index daea37a98b1..3d0561c526d 100644 --- a/spyder/plugins/toolbar/api.py +++ b/spyder/plugins/toolbar/api.py @@ -15,6 +15,7 @@ class ApplicationToolbars: File = 'file_toolbar' Run = 'run_toolbar' Debug = 'debug_toolbar' + Profile = 'profile_toolbar' Main = 'main_toolbar' WorkingDirectory = 'working_directory_toolbar' diff --git a/spyder/plugins/toolbar/plugin.py b/spyder/plugins/toolbar/plugin.py index 2b85a62845f..e459e117438 100644 --- a/spyder/plugins/toolbar/plugin.py +++ b/spyder/plugins/toolbar/plugin.py @@ -54,6 +54,7 @@ def on_initialize(self): create_app_toolbar(ApplicationToolbars.File, _("File toolbar")) create_app_toolbar(ApplicationToolbars.Run, _("Run toolbar")) create_app_toolbar(ApplicationToolbars.Debug, _("Debug toolbar")) + create_app_toolbar(ApplicationToolbars.Profile, _("Profile toolbar")) create_app_toolbar(ApplicationToolbars.Main, _("Main toolbar")) @on_plugin_available(plugin=Plugins.MainMenu) diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py index 09799d9ea37..7b4c2c01c7c 100644 --- a/spyder/utils/icon_manager.py +++ b/spyder/utils/icon_manager.py @@ -199,6 +199,9 @@ def __init__(self): 'MessageBoxWarning': [('mdi.alert',), {'color': self.MAIN_FG_COLOR}], 'arredit': [('mdi.table-edit',), {'color': self.MAIN_FG_COLOR}], 'home': [('mdi.home',), {'color': self.MAIN_FG_COLOR}], + 'swap': [('mdi.swap-vertical',), {'color': self.MAIN_FG_COLOR}], + 'hide': [('mdi.eye-off',), {'color': self.MAIN_FG_COLOR}], + 'slow': [('mdi.speedometer-slow',), {'color': self.MAIN_FG_COLOR}], 'show': [('mdi.eye',), {'color': self.MAIN_FG_COLOR}], 'plot': [('mdi.chart-line',), {'color': self.MAIN_FG_COLOR}], 'hist': [('mdi.chart-histogram',), {'color': self.MAIN_FG_COLOR}], @@ -330,7 +333,7 @@ def __init__(self): 'tour.next': [('mdi.skip-next',), {'color': self.MAIN_FG_COLOR}], 'tour.end': [('mdi.skip-forward',), {'color': self.MAIN_FG_COLOR}], # --- Third party plugins ------------------------------------------------ - 'profiler': [('mdi.timer-outline',), {'color': self.MAIN_FG_COLOR}], + 'profiler': [('mdi.timer',), {'color': SpyderPalette.ICON_5}], 'condapackages': [('mdi.archive',), {'color': self.MAIN_FG_COLOR}], 'spyder.example': [('mdi.eye',), {'color': self.MAIN_FG_COLOR}], 'spyder.autopep8': [('mdi.eye',), {'color': self.MAIN_FG_COLOR}], From 467aa9d67320f73d7688087a37f5981bebd97a93 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Thu, 30 Mar 2023 22:30:41 +0200 Subject: [PATCH 02/41] git subrepo clone --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "ee83722b7" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "ee83722b7" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +- .../console/tests/test_console_kernel.py | 30 +++++++ .../spyder_kernels/customize/code_runner.py | 88 +++++++++++++++++++ 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 4350b7d6899..146fba0999d 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git - branch = magics_runfile - commit = bf6f4448eca0f9098074a2b7d8eac6f5f3dc465b - parent = 8dadff9acaff81a13f77b19f876c9961371a77dc + branch = improve_namespace + commit = ee83722b71d4ff1e8942a4b262b92a445f7a9dd6 + parent = 50187ebe97aa9a3dd57e1bf18e3a4923a01f8274 method = merge cmdver = 0.4.5 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 06349c0dd22..ceda7c3f71e 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 @@ -1406,5 +1406,35 @@ def test_django_settings(kernel): assert "'settings':" in nsview +@flaky(max_runs=3) +def test_running_namespace_profile(tmpdir): + """ + Test that profile can get variables from running namespace. + """ + # Command to start the kernel + cmd = "from spyder_kernels.console import start; start.main()" + + with setup_kernel(cmd) as client: + # Remove all variables + client.execute_interactive("%reset -f", timeout=TIMEOUT) + + # Write defined variable code to a file + code = "result = 10\n%profile print(result)\nsucess=True" + d = tmpdir.join("defined-test.ipy") + d.write(code) + + # Run code file `d` + client.execute_interactive("%runfile {}" + .format(repr(str(d))), timeout=TIMEOUT) + + # Verify that `result` is defined in the current namespace + client.inspect('sucess') + msg = client.get_shell_msg(timeout=TIMEOUT) + while "found" not in msg['content']: + msg = client.get_shell_msg(timeout=TIMEOUT) + content = msg['content'] + assert content['found'] + + if __name__ == "__main__": pytest.main() diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py index e3eb2a4a73a..911e7e12800 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -197,6 +197,29 @@ def debugfile(self, line, local_ns=None): context_locals=local_ns, ) + @runfile_arguments + @needs_local_scope + @line_magic + def profilefile(self, line, local_ns=None): + """ + Profile a file. + """ + args, local_ns = self._parse_runfile_argstring( + self.profilefile, line, local_ns) + + with self._profile_exec() as prof_exec: + self._exec_file( + filename=args.filename, + canonic_filename=args.canonic_filename, + wdir=args.wdir, + current_namespace=args.current_namespace, + args=args.args, + exec_fun=prof_exec, + post_mortem=args.post_mortem, + context_globals=args.namespace, + context_locals=local_ns, + ) + @runcell_arguments @needs_local_scope @line_magic @@ -235,6 +258,36 @@ def debugcell(self, line, local_ns=None): context_locals=local_ns, ) + @runcell_arguments + @needs_local_scope + @line_magic + def profilecell(self, line, local_ns=None): + """ + Profile a code cell from an editor. + """ + args = self._parse_runcell_argstring(self.profilecell, line) + + with self._profile_exec() as prof_exec: + return self._exec_cell( + cell_id=args.cell_id, + filename=args.filename, + canonic_filename=args.canonic_filename, + exec_fun=prof_exec, + post_mortem=args.post_mortem, + context_globals=self.shell.user_ns, + context_locals=local_ns, + ) + + @no_var_expand + @needs_local_scope + @line_cell_magic + def profile(self, line, cell=None, local_ns=None): + """Profile the given line.""" + if cell is not None: + line += "\n" + cell + with self._profile_exec() as prof_exec: + return prof_exec(line, self.shell.user_ns, local_ns) + @contextmanager def _debugger_exec(self, filename, continue_if_has_breakpoints): """Get an exec function to use for debugging.""" @@ -274,6 +327,41 @@ def debug_exec(code, glob, loc): # debugger (self) is not aware of this. session._previous_step = None + @contextmanager + def _profile_exec(self): + """Get an exec function for profiling.""" + with tempfile.TemporaryDirectory() as tempdir: + # Reset the tracing function in case we are debugging + trace_fun = sys.gettrace() + sys.settrace(None) + # Get a file to save the results + profile_filename = os.path.join(tempdir, "profile.prof") + try: + if self.shell.is_debugging(): + def prof_exec(code, glob, loc): + # if we are debugging (tracing), call_tracing is + # necessary for profiling + return sys.call_tracing(cProfile.runctx, ( + code, glob, loc, profile_filename + )) + yield prof_exec + else: + yield partial(cProfile.runctx, filename=profile_filename) + finally: + # Resect tracing function + sys.settrace(trace_fun) + if os.path.isfile(profile_filename): + # Send result to frontend + with open(profile_filename, "br") as f: + profile_result = f.read() + try: + frontend_request(blocking=False).show_profile_file( + profile_result, create_pathlist() + ) + except CommError: + logger.debug( + "Could not send profile result to the frontend.") + def _exec_file( self, filename=None, From d9b674f07765d6ea014a65a11fba20ccbef11ed4 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sun, 2 Apr 2023 23:35:21 +0200 Subject: [PATCH 03/41] git subrepo clone --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "6fcc786a5" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "6fcc786a5" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 ++-- external-deps/spyder-kernels/CHANGELOG.md | 24 +++++++++++++ .../console/tests/test_console_kernel.py | 30 ---------------- .../spyder_kernels/customize/code_runner.py | 35 ++++++++++++------- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 28eb111ded1..31e203660b6 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git - branch = master - commit = 35193f2b1e16b3b71563355855795a70316ab1ba - parent = f658b5fb4c570a26a0a2f4ae6b6b949be3591631 + branch = improve_namespace + commit = 6fcc786a5673d930cab5895cb776ab7787d366ab + parent = ee0cf117ae8d320174ab8ed61fdd2cc13798ad3e method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/CHANGELOG.md b/external-deps/spyder-kernels/CHANGELOG.md index a0b46eabaae..a89419ca194 100644 --- a/external-deps/spyder-kernels/CHANGELOG.md +++ b/external-deps/spyder-kernels/CHANGELOG.md @@ -1,5 +1,29 @@ # History of changes +## Version 2.4.3 (2023-04-02) + +### Issues Closed + +* [Issue 440](https://github.com/spyder-ide/spyder-kernels/issues/440) - distutils and LooseVersion deprecation ([PR 450](https://github.com/spyder-ide/spyder-kernels/pull/450) by [@ccordoba12](https://github.com/ccordoba12)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 452](https://github.com/spyder-ide/spyder-kernels/pull/452) - PR: Fix error when executing empty Python script, by [@rear1019](https://github.com/rear1019) +* [PR 450](https://github.com/spyder-ide/spyder-kernels/pull/450) - PR: Remove usage of `distutils.LooseVersion`, by [@ccordoba12](https://github.com/ccordoba12) ([440](https://github.com/spyder-ide/spyder-kernels/issues/440)) +* [PR 449](https://github.com/spyder-ide/spyder-kernels/pull/449) - PR: Add support for Jupyter-client 8, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 448](https://github.com/spyder-ide/spyder-kernels/pull/448) - PR: Skip IPython versions that give buggy code completions, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 442](https://github.com/spyder-ide/spyder-kernels/pull/442) - PR: Add FreeBSD to `test_user_sitepackages_in_pathlist`, by [@rhurlin](https://github.com/rhurlin) +* [PR 434](https://github.com/spyder-ide/spyder-kernels/pull/434) - PR: Use `allow_pickle=True` when loading Numpy arrays, by [@nkleinbaer](https://github.com/nkleinbaer) +* [PR 430](https://github.com/spyder-ide/spyder-kernels/pull/430) - PR: Inform GUI about position of exception in post mortem debugging, by [@rear1019](https://github.com/rear1019) + +In this release 7 pull requests were closed. + + +---- + + ## Version 2.4.2 (2023-01-17) ### Issues Closed 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 065b53a15b5..ab8f665ae54 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 @@ -1406,35 +1406,5 @@ def test_django_settings(kernel): assert "'settings':" in nsview -@flaky(max_runs=3) -def test_running_namespace_profile(tmpdir): - """ - Test that profile can get variables from running namespace. - """ - # Command to start the kernel - cmd = "from spyder_kernels.console import start; start.main()" - - with setup_kernel(cmd) as client: - # Remove all variables - client.execute_interactive("%reset -f", timeout=TIMEOUT) - - # Write defined variable code to a file - code = "result = 10\n%profile print(result)\nsucess=True" - d = tmpdir.join("defined-test.ipy") - d.write(code) - - # Run code file `d` - client.execute_interactive("%runfile {}" - .format(repr(str(d))), timeout=TIMEOUT) - - # Verify that `result` is defined in the current namespace - client.inspect('sucess') - msg = client.get_shell_msg(timeout=TIMEOUT) - while "found" not in msg['content']: - msg = client.get_shell_msg(timeout=TIMEOUT) - content = msg['content'] - assert content['found'] - - if __name__ == "__main__": pytest.main() diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py index 8fbc755fec2..15dd62f2cc6 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -14,10 +14,13 @@ import bdb import builtins from contextlib import contextmanager +import cProfile +from functools import partial import io import logging import os import pdb +import tempfile import shlex import sys import time @@ -30,6 +33,8 @@ ) from IPython.core.magic import ( needs_local_scope, + no_var_expand, + line_cell_magic, magics_class, Magics, line_magic, @@ -37,11 +42,12 @@ from IPython.core import magic_arguments # Local imports -from spyder_kernels.comms.frontendcomm import frontend_request +from spyder_kernels.comms.frontendcomm import frontend_request, CommError from spyder_kernels.customize.namespace_manager import NamespaceManager from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.customize.umr import UserModuleReloader -from spyder_kernels.customize.utils import capture_last_Expr, canonic +from spyder_kernels.customize.utils import ( + capture_last_Expr, canonic, create_pathlist) # For logging @@ -196,9 +202,7 @@ def debugfile(self, line, local_ns=None): @needs_local_scope @line_magic def profilefile(self, line, local_ns=None): - """ - Profile a file. - """ + """Profile a file.""" args, local_ns = self._parse_runfile_argstring( self.profilefile, line, local_ns) @@ -257,9 +261,7 @@ def debugcell(self, line, local_ns=None): @needs_local_scope @line_magic def profilecell(self, line, local_ns=None): - """ - Profile a code cell from an editor. - """ + """Profile a code cell.""" args = self._parse_runcell_argstring(self.profilecell, line) with self._profile_exec() as prof_exec: @@ -311,24 +313,30 @@ def _profile_exec(self): # Reset the tracing function in case we are debugging trace_fun = sys.gettrace() sys.settrace(None) + # Get a file to save the results profile_filename = os.path.join(tempdir, "profile.prof") + try: if self.shell.is_debugging(): def prof_exec(code, glob, loc): - # if we are debugging (tracing), call_tracing is - # necessary for profiling + """ + If we are debugging (tracing), call_tracing is + necessary for profiling. + """ return sys.call_tracing(cProfile.runctx, ( code, glob, loc, profile_filename )) + yield prof_exec else: yield partial(cProfile.runctx, filename=profile_filename) finally: - # Resect tracing function + # Reset tracing function sys.settrace(trace_fun) + + # Send result to frontend if os.path.isfile(profile_filename): - # Send result to frontend with open(profile_filename, "br") as f: profile_result = f.read() try: @@ -337,7 +345,8 @@ def prof_exec(code, glob, loc): ) except CommError: logger.debug( - "Could not send profile result to the frontend.") + "Could not send profile result to the frontend." + ) def _exec_file( self, From ee208b7130b04d71cc1604d0ba4933d7c81e41f3 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Mon, 19 Jun 2023 09:23:25 +0200 Subject: [PATCH 04/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "46225bae5" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "46225bae5" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 8 ++++---- .../spyder_kernels/customize/code_runner.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 41bbff86bda..b0948d9985c 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git - branch = master - commit = a40a207f3f739761fa6be980e001c9a9e9a75c04 - parent = 41eb51124a578de8677b1a8818ba5eeaea0e9c08 + branch = improve_namespace + commit = 46225bae5961250f5aa7af230e12c157dc2bc419 + parent = bc8869495bf0b3bb5d731d0aebd6ffb2519a3f8b method = merge - cmdver = 0.4.3 + cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py index 15dd62f2cc6..411d4b857fd 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -309,7 +309,16 @@ def debug_exec(code, glob, loc): @contextmanager def _profile_exec(self): """Get an exec function for profiling.""" - with tempfile.TemporaryDirectory() as tempdir: + tmp_dir = None + if sys.platform.startswith('linux'): + # Do not use /tmp for temporary files + try: + from xdg.BaseDirectory import xdg_data_home + tmp_dir = xdg_data_home + os.makedirs(tmp_dir, exist_ok=True) + except Exception: + tmp_dir = None + with tempfile.TemporaryDirectory(dir=tmp_dir) as tempdir: # Reset the tracing function in case we are debugging trace_fun = sys.gettrace() sys.settrace(None) From ef4e20852361913e420bfb1870b5c20ef0008048 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Mon, 19 Jun 2023 15:59:29 +0200 Subject: [PATCH 05/41] Add profile to run menu --- spyder/plugins/mainmenu/api.py | 1 + spyder/plugins/profiler/plugin.py | 17 +++++++---------- spyder/plugins/run/plugin.py | 7 +++++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/spyder/plugins/mainmenu/api.py b/spyder/plugins/mainmenu/api.py index a28733da19e..a5cfde1ecb4 100644 --- a/spyder/plugins/mainmenu/api.py +++ b/spyder/plugins/mainmenu/api.py @@ -64,6 +64,7 @@ class RunMenuSections: Run = 'run_section' RunExtras = 'run_extras_section' RunInExecutors = 'executors_section' + Profile = 'profile_section' class DebugMenuSections: StartDebug = 'start_debug_section' diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index d983b8d188f..2c3e0f70da5 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -16,7 +16,7 @@ from spyder.api.plugin_registration.decorators import ( on_plugin_available, on_plugin_teardown) from spyder.api.translations import _ -from spyder.plugins.mainmenu.api import ApplicationMenus, DebugMenuSections +from spyder.plugins.mainmenu.api import ApplicationMenus, RunMenuSections from spyder.plugins.profiler.confpage import ProfilerConfigPage from spyder.plugins.profiler.widgets.main_widget import ProfilerWidget from spyder.api.shellconnect.mixins import ShellConnectMixin @@ -124,9 +124,8 @@ def on_run_available(self): shortcut_context=self.NAME, register_shortcut=True, add_to_menu={ - "menu": ApplicationMenus.Debug, - "section": DebugMenuSections.StartDebug, - "before_section": DebugMenuSections.ControlDebug + "menu": ApplicationMenus.Run, + "section": RunMenuSections.Profile, }, add_to_toolbar=ApplicationToolbars.Profile ) @@ -140,9 +139,8 @@ def on_run_available(self): shortcut_context=self.NAME, register_shortcut=True, add_to_menu={ - "menu": ApplicationMenus.Debug, - "section": DebugMenuSections.StartDebug, - "before_section": DebugMenuSections.ControlDebug + "menu": ApplicationMenus.Run, + "section": RunMenuSections.Profile, }, add_to_toolbar=ApplicationToolbars.Profile ) @@ -156,9 +154,8 @@ def on_run_available(self): shortcut_context=self.NAME, register_shortcut=True, add_to_menu={ - "menu": ApplicationMenus.Debug, - "section": DebugMenuSections.StartDebug, - "before_section": DebugMenuSections.ControlDebug + "menu": ApplicationMenus.Run, + "section": RunMenuSections.Profile, }, add_to_toolbar=ApplicationToolbars.Profile ) diff --git a/spyder/plugins/run/plugin.py b/spyder/plugins/run/plugin.py index a5d7f070622..3b05b7690d0 100644 --- a/spyder/plugins/run/plugin.py +++ b/spyder/plugins/run/plugin.py @@ -87,6 +87,7 @@ def get_icon(self): def on_initialize(self): self.pending_toolbar_actions = [] self.pending_menu_actions = [] + self.main_menu_ready = False self.pending_shortcut_actions = [] self.all_run_actions = {} self.menu_actions = set({}) @@ -119,6 +120,8 @@ def on_main_menu_available(self): RunMenuSections.Run, before_section=RunMenuSections.RunExtras ) + + self.main_menu_ready = True while self.pending_menu_actions != []: action, menu_id, menu_section, before_section = ( @@ -499,7 +502,7 @@ def create_run_button( before_section = add_to_menu.get('before_section', None) main_menu = self.get_plugin(Plugins.MainMenu) - if main_menu: + if self.main_menu_ready and main_menu: main_menu.add_item_to_application_menu( action, menu_id, menu_section, before_section=before_section @@ -684,7 +687,7 @@ def create_run_in_executor_button( before_section = add_to_menu.get('before_section', None) main_menu = self.get_plugin(Plugins.MainMenu) - if main_menu: + if self.main_menu_ready and main_menu: main_menu.add_item_to_application_menu( action, menu_id, menu_section, before_section=before_section From 36fb18140131e209240d8ab4736769b1aeb1f0fd Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Mon, 19 Jun 2023 21:40:40 +0200 Subject: [PATCH 06/41] refresh --- .../plugins/profiler/widgets/main_widget.py | 37 ++++++++++++++++++- .../profiler/widgets/profiler_data_tree.py | 5 +++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index 8ec36ee75d2..211ca80a2f7 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -255,6 +255,35 @@ def update_actions(self): toggle_tree_action.setChecked(inverted_tree) toggle_builtins_action.setChecked(ignore_builtins) + tree_empty = True + can_redo = False + can_undo = False + widget = self.current_widget() + if widget is not None: + tree_empty = widget.data_tree.profdata is None + can_undo = len(widget.data_tree.history) > 1 + can_redo = len(widget.data_tree.redo_history) > 0 + + for action_name in [ + ProfilerWidgetActions.Collapse, + ProfilerWidgetActions.Expand, + ProfilerWidgetActions.ToggleTreeDirection, + ProfilerWidgetActions.ToggleBuiltins, + ProfilerWidgetActions.Home, + ProfilerWidgetActions.SlowLocal, + ProfilerWidgetActions.SaveData, + ProfilerWidgetActions.Search + ]: + action = self.get_action(action_name) + action.setEnabled(not tree_empty) + + undo_action = self.get_action(ProfilerWidgetActions.Undo) + redo_action = self.get_action(ProfilerWidgetActions.Redo) + + undo_action.setEnabled(can_undo) + redo_action.setEnabled(can_redo) + + # --- Public API # ------------------------------------------------------------------------ def home_tree(self): @@ -356,6 +385,7 @@ def load_data(self): widget.data_tree.compare(filename) widget.data_tree.home_tree() self.clear_action.setEnabled(True) + self.update_actions() def clear(self): """Clear data in tree.""" @@ -365,12 +395,14 @@ def clear(self): widget.data_tree.compare(None) widget.data_tree.home_tree() self.clear_action.setEnabled(False) + self.update_actions() def create_new_widget(self, shellwidget): """Create new profiler widget.""" widget = ProfilerSubWidget(self) widget.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) widget.sig_display_requested.connect(self.display_request) + widget.sig_refresh.connect(self.update_actions) widget.set_context_menu(self.context_menu) widget.sig_hide_finder_requested.connect(self.hide_finder) @@ -382,8 +414,8 @@ def create_new_widget(self, shellwidget): def close_widget(self, widget): """Close profiler widget.""" - widget.sig_edit_goto_requested.disconnect( - self.sig_edit_goto_requested) + widget.sig_edit_goto_requested.disconnect(self.sig_edit_goto_requested) + widget.sig_refresh.disconnect(self.update_actions) widget.sig_display_requested.disconnect(self.display_request) widget.sig_hide_finder_requested.disconnect(self.hide_finder) @@ -403,6 +435,7 @@ def display_request(self, widget): Only display if this is the current widget. """ + self.update_actions() if ( self.current_widget() is widget and self.get_conf("switch_to_plugin") diff --git a/spyder/plugins/profiler/widgets/profiler_data_tree.py b/spyder/plugins/profiler/widgets/profiler_data_tree.py index c51742d4619..cbbdcff8c3e 100644 --- a/spyder/plugins/profiler/widgets/profiler_data_tree.py +++ b/spyder/plugins/profiler/widgets/profiler_data_tree.py @@ -66,6 +66,7 @@ class ProfilerSubWidget(QWidget, SpyderWidgetMixin): sig_edit_goto_requested = Signal(str, int, str) sig_display_requested = Signal(object) sig_hide_finder_requested = Signal() + sig_refresh = Signal() def __init__(self, parent=None): if PYQT5: @@ -103,6 +104,7 @@ def setup(self): self.data_tree = ProfilerDataTree(self) self.data_tree.sig_edit_goto_requested.connect( self.sig_edit_goto_requested) + self.data_tree.sig_refresh.connect(self.sig_refresh) self.finder = FinderWidget(self) self.finder.sig_find_text.connect(self.do_find) @@ -348,6 +350,7 @@ class ProfilerDataTree(QTreeWidget, SpyderConfigurationAccessor): # Signals sig_edit_goto_requested = Signal(str, int, str) + sig_refresh = Signal() def __init__(self, parent=None): if PYQT5: @@ -619,6 +622,8 @@ def _show_tree(self, children=None, max_items=None, # Only expand if not too many children are shown self.change_view(1) self.resizeColumnToContents(0) + + self.sig_refresh.emit() def populate_tree(self, parentItem, children_list): """ From 9c1ed9398f27d7aa8938f29295fe346c8e5866fd Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Tue, 20 Jun 2023 05:23:33 +0200 Subject: [PATCH 07/41] remove self. before actions --- .../plugins/profiler/widgets/main_widget.py | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index 211ca80a2f7..209b83d0544 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -100,7 +100,7 @@ def get_title(self): return _('Profiler') def setup(self): - self.collapse_action = self.create_action( + collapse_action = self.create_action( ProfilerWidgetActions.Collapse, text=_('Collapse'), tip=_('Collapse one level up'), @@ -108,7 +108,7 @@ def setup(self): triggered=lambda x=None: self.current_widget( ).data_tree.change_view(-1), ) - self.expand_action = self.create_action( + expand_action = self.create_action( ProfilerWidgetActions.Expand, text=_('Expand'), tip=_('Expand one level down'), @@ -116,56 +116,55 @@ def setup(self): triggered=lambda x=None: self.current_widget( ).data_tree.change_view(1), ) - self.home_action = self.create_action( + home_action = self.create_action( ProfilerWidgetActions.Home, text=_("Reset tree"), tip=_('Go back to full tree'), icon=self.create_icon('home'), triggered=self.home_tree, ) - self.toggle_tree_action = self.create_action( + toggle_tree_action = self.create_action( ProfilerWidgetActions.ToggleTreeDirection, text=_("Switch tree direction"), tip=_('Switch tree direction between callers and callees'), icon=self.create_icon('swap'), toggled=self.toggle_tree, ) - self.slow_local_action = self.create_action( + slow_local_action = self.create_action( ProfilerWidgetActions.SlowLocal, text=_("Show items with large local time"), tip=_('Show items with large local time'), icon=self.create_icon('slow'), triggered=self.slow_local_tree, ) - self.toggle_builtins_action = self.create_action( + toggle_builtins_action = self.create_action( ProfilerWidgetActions.ToggleBuiltins, text=_("Hide builtins"), tip=_('Hide builtins'), icon=self.create_icon('hide'), toggled=self.toggle_builtins, ) - self.save_action = self.create_action( + save_action = self.create_action( ProfilerWidgetActions.SaveData, text=_("Save data"), tip=_('Save profiling data'), icon=self.create_icon('filesave'), triggered=self.save_data, ) - self.load_action = self.create_action( + load_action = self.create_action( ProfilerWidgetActions.LoadData, text=_("Load data"), tip=_('Load profiling data for comparison'), icon=self.create_icon('fileimport'), triggered=self.load_data, ) - self.clear_action = self.create_action( + clear_action = self.create_action( ProfilerWidgetActions.Clear, text=_("Clear comparison"), tip=_("Clear comparison"), icon=self.create_icon('editdelete'), triggered=self.clear, ) - self.clear_action.setEnabled(False) search_action = self.create_action( ProfilerWidgetActions.Search, text=_("Search"), @@ -190,20 +189,20 @@ def setup(self): main_toolbar = self.get_main_toolbar() for item in [ - self.home_action, + home_action, undo_action, redo_action, - self.collapse_action, - self.expand_action, - self.toggle_tree_action, - self.toggle_builtins_action, - self.slow_local_action, + collapse_action, + expand_action, + toggle_tree_action, + toggle_builtins_action, + slow_local_action, search_action, self.create_stretcher( id_=ProfilerWidgetInformationToolbarItems.Stretcher), - self.save_action, - self.load_action, - self.clear_action + save_action, + load_action, + clear_action ]: self.add_item_to_toolbar( item, @@ -258,11 +257,13 @@ def update_actions(self): tree_empty = True can_redo = False can_undo = False + can_clear = False widget = self.current_widget() if widget is not None: tree_empty = widget.data_tree.profdata is None can_undo = len(widget.data_tree.history) > 1 can_redo = len(widget.data_tree.redo_history) > 0 + can_clear = widget.data_tree.compare_data is not None for action_name in [ ProfilerWidgetActions.Collapse, @@ -282,6 +283,9 @@ def update_actions(self): undo_action.setEnabled(can_undo) redo_action.setEnabled(can_redo) + + clear_action = self.get_action(ProfilerWidgetActions.Clear) + clear_action.setEnabled(can_clear) # --- Public API @@ -336,8 +340,11 @@ def show_callers(self): if widget is None: return widget.data_tree.show_selected() - if not self.toggle_tree_action.isChecked(): - self.toggle_tree_action.setChecked(True) + + toggle_tree_action = self.get_action( + ProfilerWidgetActions.ToggleTreeDirection) + if not toggle_tree_action.isChecked(): + toggle_tree_action.setChecked(True) def show_callees(self): """Invert tree.""" @@ -345,8 +352,11 @@ def show_callees(self): if widget is None: return widget.data_tree.show_selected() - if self.toggle_tree_action.isChecked(): - self.toggle_tree_action.setChecked(False) + + toggle_tree_action = self.get_action( + ProfilerWidgetActions.ToggleTreeDirection) + if toggle_tree_action.isChecked(): + toggle_tree_action.setChecked(False) def save_data(self): """Save data.""" @@ -384,7 +394,6 @@ def load_data(self): if filename: widget.data_tree.compare(filename) widget.data_tree.home_tree() - self.clear_action.setEnabled(True) self.update_actions() def clear(self): @@ -394,7 +403,6 @@ def clear(self): return widget.data_tree.compare(None) widget.data_tree.home_tree() - self.clear_action.setEnabled(False) self.update_actions() def create_new_widget(self, shellwidget): From d4852788f7ee8e5f538089ec5742bccf8384ccc2 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Wed, 21 Jun 2023 07:11:09 +0200 Subject: [PATCH 08/41] fix test --- spyder/app/tests/test_mainwindow.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index fe216d8fecf..3ec35ea9d0d 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -63,6 +63,7 @@ from spyder.plugins.ipythonconsole.api import IPythonConsolePyConfiguration from spyder.plugins.mainmenu.api import ApplicationMenus from spyder.plugins.layout.layouts import DefaultLayouts +from spyder.plugins.profiler.main_widget import ProfilerWidgetActions from spyder.plugins.toolbar.api import ApplicationToolbars from spyder.plugins.run.api import ( RunExecutionParameters, ExtendedRunExecutionParameters, WorkingDirOpts, @@ -5177,12 +5178,15 @@ def test_profiler(main_window, qtbot, tmpdir): item = profile_tree.current_widget().data_tree.get_items(2)[0].item_key[2] assert item == sleep_str + toggle_tree_action = profile_tree.get_action( + ProfilerWidgetActions.ToggleTreeDirection) + # Make sure the ordering methods don't reveal the root element. - profile_tree.toggle_tree_action.setChecked(True) + toggle_tree_action.setChecked(True) assert len(profile_tree.current_widget().data_tree.get_items(0)) == 1 item = profile_tree.current_widget().data_tree.get_items(2)[0].item_key[2] assert item == sleep_str - profile_tree.toggle_tree_action.setChecked(False) + toggle_tree_action.setChecked(False) assert len(profile_tree.current_widget().data_tree.get_items(2)) == 1 item = profile_tree.current_widget().data_tree.get_items(2)[0].item_key[2] assert item == sleep_str @@ -5232,7 +5236,7 @@ def test_profiler(main_window, qtbot, tmpdir): shell.execute("%profilefile " + repr(to_text_string(p))) qtbot.wait(1000) # Check callee tree - profile_tree.toggle_tree_action.setChecked(False) + toggle_tree_action.setChecked(False) assert len(profile_tree.current_widget().data_tree.get_items(1)) == 3 values = ["f", sleep_str, "g"] for item, val in zip( @@ -5240,7 +5244,7 @@ def test_profiler(main_window, qtbot, tmpdir): assert val == item.item_key[2] # Check caller tree - profile_tree.toggle_tree_action.setChecked(True) + toggle_tree_action.setChecked(True) assert len(profile_tree.current_widget().data_tree.get_items(1)) == 3 values = [sleep_str, "f", "g"] for item, val in zip( From 3c5d2003f390613b2f51728d53d724881924acdf Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Wed, 21 Jun 2023 07:39:45 +0200 Subject: [PATCH 09/41] fix manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 4b3fb2dce0e..1906ded6433 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include MANIFEST.in include README.md include LICENSE.txt include changelogs/Spyder-5.md +include changelogs/Spyder-6.md include AUTHORS.txt include NOTICE.txt include bootstrap.py From fdc1d574ab75c6c03c0ae81b23c8c0b37f439a9d Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Wed, 21 Jun 2023 08:23:32 +0200 Subject: [PATCH 10/41] fix import --- spyder/app/tests/test_mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 3ec35ea9d0d..265937ddf08 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -63,7 +63,7 @@ from spyder.plugins.ipythonconsole.api import IPythonConsolePyConfiguration from spyder.plugins.mainmenu.api import ApplicationMenus from spyder.plugins.layout.layouts import DefaultLayouts -from spyder.plugins.profiler.main_widget import ProfilerWidgetActions +from spyder.plugins.profiler.widgets.main_widget import ProfilerWidgetActions from spyder.plugins.toolbar.api import ApplicationToolbars from spyder.plugins.run.api import ( RunExecutionParameters, ExtendedRunExecutionParameters, WorkingDirOpts, From ce40f8cace487dca42d1602e0beac061ba82b5fd Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sun, 25 Jun 2023 18:00:51 +0200 Subject: [PATCH 11/41] profile ipy files --- spyder/plugins/profiler/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 41c8d14277b..72d24029236 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -63,7 +63,7 @@ def on_initialize(self): self.python_editor_run_configuration = { 'origin': self.NAME, - 'extension': 'py', + 'extension': ['py', 'ipy'], 'contexts': [ { 'name': 'File' @@ -89,7 +89,7 @@ def on_initialize(self): 'priority': 10 }, { - 'input_extension': 'py', + 'input_extension': ['py', 'ipy'], 'context': { 'name': 'Cell' }, @@ -99,7 +99,7 @@ def on_initialize(self): 'priority': 10 }, { - 'input_extension': 'py', + 'input_extension': ['py', 'ipy'], 'context': { 'name': 'Selection' }, From 5e608a1b9e1a70a19d8277a629f5eca8c113887d Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Tue, 1 Aug 2023 10:04:25 +0200 Subject: [PATCH 12/41] git subrepo clone (merge) --branch=profile_script --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "0b38a5610" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "profile_script" commit: "0b38a5610" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +- .../spyder_kernels/customize/code_runner.py | 110 +----------------- 2 files changed, 5 insertions(+), 111 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 7e8a2577e8d..5e35b13305f 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git - branch = remove_locals_inspection - commit = adb31866c64079bc023c4626f1e2cce3c51ab5f6 - parent = d4fa5fd95e2ebe89cf49453ecd13cda9c72f73c2 + branch = profile_script + commit = 0b38a56107237fb12e7019b27c3bf19b57dc4f8e + parent = f5c48e446b00a8e616bae4f824a4d59c0314e133 method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py index 411d4b857fd..f4321eaa3de 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -14,13 +14,10 @@ import bdb import builtins from contextlib import contextmanager -import cProfile -from functools import partial import io import logging import os import pdb -import tempfile import shlex import sys import time @@ -33,8 +30,6 @@ ) from IPython.core.magic import ( needs_local_scope, - no_var_expand, - line_cell_magic, magics_class, Magics, line_magic, @@ -42,12 +37,11 @@ from IPython.core import magic_arguments # Local imports -from spyder_kernels.comms.frontendcomm import frontend_request, CommError +from spyder_kernels.comms.frontendcomm import frontend_request from spyder_kernels.customize.namespace_manager import NamespaceManager from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.customize.umr import UserModuleReloader -from spyder_kernels.customize.utils import ( - capture_last_Expr, canonic, create_pathlist) +from spyder_kernels.customize.utils import capture_last_Expr, canonic # For logging @@ -198,27 +192,6 @@ def debugfile(self, line, local_ns=None): context_locals=local_ns, ) - @runfile_arguments - @needs_local_scope - @line_magic - def profilefile(self, line, local_ns=None): - """Profile a file.""" - args, local_ns = self._parse_runfile_argstring( - self.profilefile, line, local_ns) - - with self._profile_exec() as prof_exec: - self._exec_file( - filename=args.filename, - canonic_filename=args.canonic_filename, - wdir=args.wdir, - current_namespace=args.current_namespace, - args=args.args, - exec_fun=prof_exec, - post_mortem=args.post_mortem, - context_globals=args.namespace, - context_locals=local_ns, - ) - @runcell_arguments @needs_local_scope @line_magic @@ -257,34 +230,6 @@ def debugcell(self, line, local_ns=None): context_locals=local_ns, ) - @runcell_arguments - @needs_local_scope - @line_magic - def profilecell(self, line, local_ns=None): - """Profile a code cell.""" - args = self._parse_runcell_argstring(self.profilecell, line) - - with self._profile_exec() as prof_exec: - return self._exec_cell( - cell_id=args.cell_id, - filename=args.filename, - canonic_filename=args.canonic_filename, - exec_fun=prof_exec, - post_mortem=args.post_mortem, - context_globals=self.shell.user_ns, - context_locals=local_ns, - ) - - @no_var_expand - @needs_local_scope - @line_cell_magic - def profile(self, line, cell=None, local_ns=None): - """Profile the given line.""" - if cell is not None: - line += "\n" + cell - with self._profile_exec() as prof_exec: - return prof_exec(line, self.shell.user_ns, local_ns) - @contextmanager def _debugger_exec(self, filename, continue_if_has_breakpoints): """Get an exec function to use for debugging.""" @@ -306,57 +251,6 @@ def debug_exec(code, glob, loc): # Enter recursive debugger yield debug_exec - @contextmanager - def _profile_exec(self): - """Get an exec function for profiling.""" - tmp_dir = None - if sys.platform.startswith('linux'): - # Do not use /tmp for temporary files - try: - from xdg.BaseDirectory import xdg_data_home - tmp_dir = xdg_data_home - os.makedirs(tmp_dir, exist_ok=True) - except Exception: - tmp_dir = None - with tempfile.TemporaryDirectory(dir=tmp_dir) as tempdir: - # Reset the tracing function in case we are debugging - trace_fun = sys.gettrace() - sys.settrace(None) - - # Get a file to save the results - profile_filename = os.path.join(tempdir, "profile.prof") - - try: - if self.shell.is_debugging(): - def prof_exec(code, glob, loc): - """ - If we are debugging (tracing), call_tracing is - necessary for profiling. - """ - return sys.call_tracing(cProfile.runctx, ( - code, glob, loc, profile_filename - )) - - yield prof_exec - else: - yield partial(cProfile.runctx, filename=profile_filename) - finally: - # Reset tracing function - sys.settrace(trace_fun) - - # Send result to frontend - if os.path.isfile(profile_filename): - with open(profile_filename, "br") as f: - profile_result = f.read() - try: - frontend_request(blocking=False).show_profile_file( - profile_result, create_pathlist() - ) - except CommError: - logger.debug( - "Could not send profile result to the frontend." - ) - def _exec_file( self, filename=None, From 5d8dda2a4f7ab72258ec828beec2a8b42a0c8e2c Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sat, 5 Aug 2023 07:55:00 +0200 Subject: [PATCH 13/41] Apply suggestions from code review Co-authored-by: Carlos Cordoba --- spyder/plugins/profiler/plugin.py | 4 +- .../plugins/profiler/widgets/main_widget.py | 81 ++++++++++--------- .../profiler/widgets/profiler_data_tree.py | 66 ++++++++++----- 3 files changed, 91 insertions(+), 60 deletions(-) diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 84ba48428a7..0ee535c4669 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -15,11 +15,11 @@ from spyder.api.plugins import Plugins, SpyderDockablePlugin from spyder.api.plugin_registration.decorators import ( on_plugin_available, on_plugin_teardown) +from spyder.api.shellconnect.mixins import ShellConnectPluginMixin from spyder.api.translations import _ from spyder.plugins.mainmenu.api import ApplicationMenus, RunMenuSections from spyder.plugins.profiler.confpage import ProfilerConfigPage from spyder.plugins.profiler.widgets.main_widget import ProfilerWidget -from spyder.api.shellconnect.mixins import ShellConnectMixin from spyder.plugins.toolbar.api import ApplicationToolbars from spyder.plugins.ipythonconsole.api import IPythonConsolePyConfiguration from spyder.plugins.run.api import ( @@ -33,7 +33,7 @@ # --- Plugin # ---------------------------------------------------------------------------- -class Profiler(SpyderDockablePlugin, ShellConnectMixin, RunExecutor): +class Profiler(SpyderDockablePlugin, ShellConnectPluginMixin, RunExecutor): """ Profiler (after python's profile and pstats). """ diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index 209b83d0544..f6ea263982d 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -202,13 +202,13 @@ def setup(self): id_=ProfilerWidgetInformationToolbarItems.Stretcher), save_action, load_action, - clear_action - ]: + clear_action]: self.add_item_to_toolbar( item, toolbar=main_toolbar, section=ProfilerWidgetMainToolbarSections.Main, ) + # ---- Context menu actions self.show_callees_action = self.create_action( ProfilerWidgetContextMenuActions.ShowCallees, @@ -222,7 +222,6 @@ def setup(self): icon=self.create_icon('2uparrow'), triggered=self.show_callers ) - # ---- Context menu to show when there are frames present self.context_menu = self.create_menu( ProfilerWidgetMenus.PopulatedContextMenu) for item in [self.show_callers_action, self.show_callees_action]: @@ -287,18 +286,52 @@ def update_actions(self): clear_action = self.get_action(ProfilerWidgetActions.Clear) clear_action.setEnabled(can_clear) + # --- ShellConnectPluginMixin API + # ------------------------------------------------------------------------ + def create_new_widget(self, shellwidget): + """Create new profiler widget.""" + widget = ProfilerSubWidget(self) + widget.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) + widget.sig_display_requested.connect(self.display_request) + widget.sig_refresh.connect(self.update_actions) + widget.set_context_menu(self.context_menu) + widget.sig_hide_finder_requested.connect(self.hide_finder) + + shellwidget.kernel_handler.kernel_comm.register_call_handler( + "show_profile_file", widget.show_profile_buffer) + widget.shellwidget = shellwidget + + return widget + + def close_widget(self, widget): + """Close profiler widget.""" + widget.sig_edit_goto_requested.disconnect(self.sig_edit_goto_requested) + widget.sig_refresh.disconnect(self.update_actions) + widget.sig_display_requested.disconnect(self.display_request) + widget.sig_hide_finder_requested.disconnect(self.hide_finder) + + # Unregister + widget.shellwidget.kernel_handler.kernel_comm.register_call_handler( + "show_profile_file", None) + widget.setParent(None) + widget.close() + + def switch_widget(self, widget, old_widget): + """Switch widget.""" + pass + # --- Public API # ------------------------------------------------------------------------ def home_tree(self): - """Invert tree.""" + """Show home tree.""" widget = self.current_widget() if widget is None: return widget.data_tree.home_tree() def toggle_tree(self, state): - """Invert tree.""" + """Toggle tree.""" widget = self.current_widget() if widget is None: return @@ -306,7 +339,7 @@ def toggle_tree(self, state): widget.data_tree.refresh_tree() def toggle_builtins(self, state): - """Invert tree.""" + """Toggle builtins.""" widget = self.current_widget() if widget is None: return @@ -335,7 +368,7 @@ def redo(self): widget.data_tree.redo() def show_callers(self): - """Invert tree.""" + """Show callers.""" widget = self.current_widget() if widget is None: return @@ -347,7 +380,7 @@ def show_callers(self): toggle_tree_action.setChecked(True) def show_callees(self): - """Invert tree.""" + """Show callees.""" widget = self.current_widget() if widget is None: return @@ -405,38 +438,6 @@ def clear(self): widget.data_tree.home_tree() self.update_actions() - def create_new_widget(self, shellwidget): - """Create new profiler widget.""" - widget = ProfilerSubWidget(self) - widget.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) - widget.sig_display_requested.connect(self.display_request) - widget.sig_refresh.connect(self.update_actions) - widget.set_context_menu(self.context_menu) - widget.sig_hide_finder_requested.connect(self.hide_finder) - - shellwidget.kernel_handler.kernel_comm.register_call_handler( - "show_profile_file", widget.show_profile_buffer) - widget.shellwidget = shellwidget - - return widget - - def close_widget(self, widget): - """Close profiler widget.""" - widget.sig_edit_goto_requested.disconnect(self.sig_edit_goto_requested) - widget.sig_refresh.disconnect(self.update_actions) - widget.sig_display_requested.disconnect(self.display_request) - widget.sig_hide_finder_requested.disconnect(self.hide_finder) - - # Unregister - widget.shellwidget.kernel_handler.kernel_comm.register_call_handler( - "show_profile_file", None) - widget.setParent(None) - widget.close() - - def switch_widget(self, widget, old_widget): - """Switch widget.""" - pass - def display_request(self, widget): """ Display request from ProfilerDataTree. diff --git a/spyder/plugins/profiler/widgets/profiler_data_tree.py b/spyder/plugins/profiler/widgets/profiler_data_tree.py index cbbdcff8c3e..3aa252a71c0 100644 --- a/spyder/plugins/profiler/widgets/profiler_data_tree.py +++ b/spyder/plugins/profiler/widgets/profiler_data_tree.py @@ -74,6 +74,7 @@ def __init__(self, parent=None): else: QWidget.__init__(self, parent) SpyderWidgetMixin.__init__(self, class_parent=parent) + # Finder self.data_tree = None self.finder = None @@ -123,12 +124,14 @@ def show_profile_buffer(self, prof_buffer, lib_pathlist): """Show profile file.""" if not prof_buffer: return + with tempfile.TemporaryDirectory() as dir: filename = os.path.join(dir, "tem.prof") with open(filename, "bw") as f: f.write(prof_buffer) self.data_tree.lib_pathlist = lib_pathlist self.data_tree.load_data(filename) + # Show self.data_tree._show_tree() self.sig_display_requested.emit(self) @@ -142,12 +145,15 @@ class TreeWidgetItem(QTreeWidgetItem): def __init__(self, parent, item_key, profile_data, compare_data, icon_list, index_dict): QTreeWidgetItem.__init__(self, parent) + self.item_key = item_key self.index_dict = index_dict + # Order is from profile data self.total_calls, self.local_time, self.total_time = profile_data[1:4] (filename, line_number, function_name, file_and_line, node_type ) = self.function_info(item_key) + self.function_name = function_name self.filename = filename self.line_number = line_number @@ -168,7 +174,7 @@ def __init__(self, parent, item_key, profile_data, compare_data, icon_list, "total_time": Qt.AlignRight, "local_time": Qt.AlignRight, "number_calls": Qt.AlignRight, - } + } self.set_alignment(alignment) if self.is_recursive(): @@ -187,7 +193,7 @@ def __init__(self, parent, item_key, profile_data, compare_data, icon_list, "number_calls_diff", "local_time_diff", "total_time_diff" - ] + ] for i in range(1, 4): diff_str, color = self.color_diff( profile_data[i] - compare_data[i]) @@ -200,7 +206,7 @@ def __init__(self, parent, item_key, profile_data, compare_data, icon_list, "total_time_diff": Qt.AlignLeft, "local_time_diff": Qt.AlignLeft, "number_calls_diff": Qt.AlignLeft - } + } self.set_alignment(diff_alignment) def set_tooltips(self): @@ -210,7 +216,8 @@ def set_tooltips(self): "total_time": _('Time in function (including sub-functions)'), "local_time": _('Local time in function (not in sub-functions)'), "number_calls": _('Total number of calls (including recursion)'), - "file:line": _('File:line where function is defined')} + "file:line": _('File:line where function is defined') + } for k, v in tooltips.items(): self.setToolTip(self.index_dict[k], v) @@ -239,7 +246,7 @@ def color_diff(difference): (SpyderPalette.COLOR_SUCCESS_1, '-') if difference < 0 else (SpyderPalette.COLOR_ERROR_1, '+') - ) + ) diff_str = '{}{}'.format( sign, TreeWidgetItem.format_measure(difference)) return diff_str, color @@ -299,12 +306,14 @@ def function_info(self, functionKey): """Returns processed information about the function's name and file.""" node_type = 'function' filename, line_number, function_name = functionKey + if function_name == '': modulePath, moduleName = osp.split(filename) node_type = 'module' if moduleName == '__init__.py': modulePath, moduleName = osp.split(modulePath) function_name = '<' + moduleName + '>' + if not filename or filename == '~': file_and_line = '(built-in)' node_type = 'builtin' @@ -312,6 +321,7 @@ def function_info(self, functionKey): if function_name == '__init__': node_type = 'constructor' file_and_line = '%s : %d' % (filename, line_number) + return filename, line_number, function_name, file_and_line, node_type def is_recursive(self): @@ -357,6 +367,7 @@ def __init__(self, parent=None): super().__init__(parent) else: QTreeWidget.__init__(self, parent) + self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'), _('Local Time'), _('Diff'), _('Calls'), _('Diff'), _('File:line')] @@ -407,8 +418,10 @@ def initialize_view(self): self.clear() self.items_to_be_shown = {} self.current_view_depth = 0 - if (self.compare_data is not None - and self.compare_data is not self.profdata): + if ( + self.compare_data is not None + and self.compare_data is not self.profdata + ): self.hide_diff_cols(False) else: self.hide_diff_cols(True) @@ -421,6 +434,7 @@ def load_data(self, profdatafile): self.profdata = None return import pstats + # Fixes spyder-ide/spyder#6220. try: self.profdata = pstats.Stats(profdatafile) @@ -436,6 +450,7 @@ def compare(self, filename): self.compare_data = None return import pstats + # Fixes spyder-ide/spyder#5587. try: self.compare_data = pstats.Stats(filename) @@ -446,10 +461,12 @@ def compare(self, filename): self.root_key = self.find_root() except (OSError, IOError) as e: QMessageBox.critical( - self, _("Error"), + self, + _("Error"), _("Error when trying to load profiler results. " "The error was

" - "{0}").format(e)) + "{0}").format(e) + ) self.compare_data = None def hide_diff_cols(self, hide): @@ -464,15 +481,17 @@ def save_data(self, filename): self.profdata.dump_stats(filename) def find_root(self): - """Find a function without a caller""" + """Find a function without a caller.""" # Fixes spyder-ide/spyder#8336. if self.profdata is not None: self.profdata.sort_stats("cumulative") else: return for func in self.profdata.fcn_list: - if ('~', 0) != func[0:2] and not func[2].startswith( - ''): + if ( + ('~', 0) != func[0:2] + and not func[2].startswith('') + ): # This skips the profiler function at the top of the list # it does only occur in Python 3 return func @@ -486,6 +505,7 @@ def is_builtin(self, key): return True if path.startswith("<"): return True + path = os.path.normcase(os.path.normpath(path)) if self.lib_pathlist is not None: for libpath in self.lib_pathlist: @@ -493,6 +513,7 @@ def is_builtin(self, key): commonpath = os.path.commonpath([libpath, path]) if libpath == commonpath: return True + return False def find_children(self, parent): @@ -522,14 +543,18 @@ def show_slow(self): if self.profdata is None: # Nothing to show return + # Show items with large local time children = self.profdata.fcn_list + # Only keep top n_slow_children N_children = self.get_conf('n_slow_children') - children = sorted(children, - key=lambda item: self.profdata.stats[item][ - ProfilerKey.LocalTime], - reverse=True) + children = sorted( + children, + key=lambda item: self.profdata.stats[item][ProfilerKey.LocalTime], + reverse=True + ) + if self.ignore_builtins: children = [c for c in children if not self.is_builtin(c)] children = children[:N_children] @@ -569,7 +594,9 @@ def _show_tree(self, children=None, max_items=None, # List of internal functions to exclude internal_list = [ - ('~', 0, "")] + ('~', 0, "") + ] + # List of frames to hide at the top head_list = [self.root_key, ] head_list += list( @@ -614,6 +641,7 @@ def _show_tree(self, children=None, max_items=None, self.history.append(children) if reset_redo: self.redo_history = [] + # Populate the tree self.populate_tree(self, children) self.setSortingEnabled(True) @@ -638,7 +666,8 @@ def populate_tree(self, parentItem, children_list): item_profdata, item_compdata, self.icon_list, - self.index_dict) + self.index_dict + ) if not child_item.is_recursive(): grandchildren_list = self.find_children(child_key) if grandchildren_list: @@ -687,6 +716,7 @@ def add_to_itemlist(item, maxlevel, level=1): itemlist.append(tlitem) if maxlevel > 0: add_to_itemlist(tlitem, maxlevel=maxlevel) + return itemlist def change_view(self, change_in_depth): From 6aaa12e59cb56f5eaf57953a55068dd4014f0401 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sat, 5 Aug 2023 07:55:42 +0200 Subject: [PATCH 14/41] Update spyder/plugins/profiler/widgets/profiler_data_tree.py Co-authored-by: Carlos Cordoba --- spyder/plugins/profiler/widgets/profiler_data_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/spyder/plugins/profiler/widgets/profiler_data_tree.py b/spyder/plugins/profiler/widgets/profiler_data_tree.py index 3aa252a71c0..c832463eb1f 100644 --- a/spyder/plugins/profiler/widgets/profiler_data_tree.py +++ b/spyder/plugins/profiler/widgets/profiler_data_tree.py @@ -717,6 +717,7 @@ def add_to_itemlist(item, maxlevel, level=1): if maxlevel > 0: add_to_itemlist(tlitem, maxlevel=maxlevel) + return itemlist def change_view(self, change_in_depth): From 89f953859ee7c2e91ad6c46a887889b867f6c14f Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sat, 5 Aug 2023 07:57:05 +0200 Subject: [PATCH 15/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "e5f76b33f" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "e5f76b33f" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +- .../spyder_kernels/comms/decorators.py | 27 ----- .../spyder_kernels/console/kernel.py | 70 ++++++----- .../spyder_kernels/console/shell.py | 9 +- .../console/tests/test_console_kernel.py | 28 +---- .../spyder_kernels/customize/code_runner.py | 111 +++++++++++++++++- .../spyder_kernels/customize/spyderpdb.py | 16 --- .../spyder_kernels/utils/nsview.py | 10 +- 8 files changed, 161 insertions(+), 116 deletions(-) delete mode 100644 external-deps/spyder-kernels/spyder_kernels/comms/decorators.py diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 5e35b13305f..fa079775443 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git - branch = profile_script - commit = 0b38a56107237fb12e7019b27c3bf19b57dc4f8e - parent = f5c48e446b00a8e616bae4f824a4d59c0314e133 + branch = improve_namespace + commit = e5f76b33fc1a54477348347b49415cf1fa0ade52 + parent = 6aaa12e59cb56f5eaf57953a55068dd4014f0401 method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/comms/decorators.py b/external-deps/spyder-kernels/spyder_kernels/comms/decorators.py deleted file mode 100644 index b2633404a07..00000000000 --- a/external-deps/spyder-kernels/spyder_kernels/comms/decorators.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © Spyder Project Contributors -# Licensed under the terms of the MIT License -# (see spyder/__init__.py for details) - -""" -Comms decorators. -""" - - -def comm_handler(fun): - """Decorator to mark comm handler methods.""" - fun._is_comm_handler = True - return fun - - -def register_comm_handlers(instance, frontend_comm): - """ - Registers an instance whose methods have been marked with comm_handler. - """ - for method_name in instance.__class__.__dict__: - method = getattr(instance, method_name) - if hasattr(method, '_is_comm_handler'): - frontend_comm.register_call_handler( - method_name, method) - diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py index d900d8a754d..d1a585a301a 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py @@ -30,8 +30,6 @@ # Local imports from spyder_kernels.comms.frontendcomm import FrontendComm -from spyder_kernels.comms.decorators import ( - register_comm_handlers, comm_handler) from spyder_kernels.utils.iofuncs import iofunctions from spyder_kernels.utils.mpl import ( MPL_BACKENDS_FROM_SPYDER, MPL_BACKENDS_TO_SPYDER, INLINE_FIGURE_FORMATS) @@ -41,6 +39,7 @@ from spyder_kernels.comms.utils import WriteContext + logger = logging.getLogger(__name__) @@ -61,8 +60,40 @@ def __init__(self, *args, **kwargs): self.frontend_comm = FrontendComm(self) # All functions that can be called through the comm - register_comm_handlers(self, self.frontend_comm) - register_comm_handlers(self.shell, self.frontend_comm) + handlers = { + 'set_pdb_configuration': self.shell.set_pdb_configuration, + 'get_value': self.get_value, + 'load_data': self.load_data, + 'save_namespace': self.save_namespace, + 'is_defined': self.is_defined, + 'get_doc': self.get_doc, + 'get_source': self.get_source, + 'set_value': self.set_value, + 'remove_value': self.remove_value, + 'copy_value': self.copy_value, + 'set_cwd': self.set_cwd, + 'get_syspath': self.get_syspath, + 'get_env': self.get_env, + 'close_all_mpl_figures': self.close_all_mpl_figures, + 'show_mpl_backend_errors': self.show_mpl_backend_errors, + 'get_namespace_view': self.get_namespace_view, + 'set_namespace_view_settings': self.set_namespace_view_settings, + 'get_var_properties': self.get_var_properties, + 'set_sympy_forecolor': self.set_sympy_forecolor, + 'update_syspath': self.update_syspath, + 'is_special_kernel_valid': self.is_special_kernel_valid, + 'get_matplotlib_backend': self.get_matplotlib_backend, + 'get_mpl_interactive_backend': self.get_mpl_interactive_backend, + 'pdb_input_reply': self.shell.pdb_input_reply, + 'enable_faulthandler': self.enable_faulthandler, + 'get_current_frames': self.get_current_frames, + 'request_pdb_stop': self.shell.request_pdb_stop, + 'raise_interrupt_signal': self.shell.raise_interrupt_signal, + 'get_fault_text': self.get_fault_text, + } + for call_id in handlers: + self.frontend_comm.register_call_handler( + call_id, handlers[call_id]) self.namespace_view_settings = {} self._mpl_backend_error = None @@ -114,7 +145,6 @@ def publish_state(self): except Exception: pass - @comm_handler def enable_faulthandler(self): """ Open a file to save the faulthandling and identifiers for @@ -140,7 +170,6 @@ def enable_faulthandler(self): faulthandler.enable(self.faulthandler_handle) return self.faulthandler_handle.name, main_id, system_ids - @comm_handler def get_fault_text(self, fault_filename, main_id, ignore_ids): """Get fault text from old run.""" # Read file @@ -234,8 +263,8 @@ def filter_stack(self, stack, is_main): stack = [] return stack - @comm_handler - def get_current_frames(self, ignore_internal_threads=True): + def get_current_frames(self, ignore_internal_threads=True, + capture_locals=False): """Get the current frames.""" ignore_list = self.get_system_threads_id() main_id = threading.main_thread().ident @@ -246,6 +275,9 @@ def get_current_frames(self, ignore_internal_threads=True): for thread_id, frame in sys._current_frames().items(): stack = traceback.StackSummary.extract( traceback.walk_stack(frame)) + if capture_locals: + for f_summary, f in zip(stack, traceback.walk_stack(frame)): + f_summary.locals = self.get_namespace_view(frame=f[0]) stack.reverse() if ignore_internal_threads: if thread_id in ignore_list: @@ -260,12 +292,10 @@ def get_current_frames(self, ignore_internal_threads=True): return frames # --- For the Variable Explorer - @comm_handler def set_namespace_view_settings(self, settings): """Set namespace_view_settings.""" self.namespace_view_settings = settings - @comm_handler def get_namespace_view(self, frame=None): """ Return the namespace view @@ -301,7 +331,6 @@ def get_namespace_view(self, frame=None): else: return None - @comm_handler def get_var_properties(self): """ Get some properties of the variables in the current @@ -332,32 +361,27 @@ def get_var_properties(self): else: return None - @comm_handler def get_value(self, name): """Get the value of a variable""" ns = self.shell._get_current_namespace() return ns[name] - @comm_handler def set_value(self, name, value): """Set the value of a variable""" ns = self.shell._get_reference_namespace(name) ns[name] = value self.log.debug(ns) - @comm_handler def remove_value(self, name): """Remove a variable""" ns = self.shell._get_reference_namespace(name) ns.pop(name) - @comm_handler def copy_value(self, orig_name, new_name): """Copy a variable""" ns = self.shell._get_reference_namespace(orig_name) ns[new_name] = ns[orig_name] - @comm_handler def load_data(self, filename, ext, overwrite=False): """ Load data from filename. @@ -394,7 +418,6 @@ def load_data(self, filename, ext, overwrite=False): return None - @comm_handler def save_namespace(self, filename): """Save namespace into filename""" ns = self.shell._get_current_namespace() @@ -448,7 +471,6 @@ def interrupt_eventloop(self): self.loopback_socket, self.session.msg("interrupt_eventloop")) # --- For the Help plugin - @comm_handler def is_defined(self, obj, force_import=False): """Return True if object is defined in current namespace""" from spyder_kernels.utils.dochelpers import isdefined @@ -456,7 +478,6 @@ def is_defined(self, obj, force_import=False): ns = self.shell._get_current_namespace(with_magics=True) return isdefined(obj, force_import=force_import, namespace=ns) - @comm_handler def get_doc(self, objtxt): """Get object documentation dictionary""" try: @@ -470,7 +491,6 @@ def get_doc(self, objtxt): if valid: return getdoc(obj) - @comm_handler def get_source(self, objtxt): """Get object source""" from spyder_kernels.utils.dochelpers import getsource @@ -480,7 +500,6 @@ def get_source(self, objtxt): return getsource(obj) # -- For Matplolib - @comm_handler def get_matplotlib_backend(self): """Get current matplotlib backend.""" try: @@ -489,7 +508,6 @@ def get_matplotlib_backend(self): except Exception: return None - @comm_handler def get_mpl_interactive_backend(self): """ Get current Matplotlib interactive backend. @@ -598,7 +616,6 @@ def set_autocall(self, autocall): self._set_config_option('ZMQInteractiveShell.autocall', autocall) # --- Additional methods - @comm_handler def set_cwd(self, dirname): """Set current working directory.""" self._cwd_initialised = True @@ -612,17 +629,14 @@ def get_cwd(self): except (IOError, OSError): pass - @comm_handler def get_syspath(self): """Return sys.path contents.""" return sys.path[:] - @comm_handler def get_env(self): """Get environment variables.""" return os.environ.copy() - @comm_handler def close_all_mpl_figures(self): """Close all Matplotlib figures.""" try: @@ -631,7 +645,6 @@ def close_all_mpl_figures(self): except: pass - @comm_handler def is_special_kernel_valid(self): """ Check if optional dependencies are available for special consoles. @@ -654,7 +667,6 @@ def is_special_kernel_valid(self): return u'cython' return None - @comm_handler def update_syspath(self, path_dict, new_path_dict): """ Update the PYTHONPATH of the kernel. @@ -876,13 +888,11 @@ def _set_mpl_inline_rc_config(self, option, value): # Needed in case matplolib isn't installed pass - @comm_handler def show_mpl_backend_errors(self): """Show Matplotlib backend errors after the prompt is ready.""" if self._mpl_backend_error is not None: print(self._mpl_backend_error) # spyder: test-skip - @comm_handler def set_sympy_forecolor(self, background_color='dark'): """Set SymPy forecolor depending on console background.""" if os.environ.get('SPY_SYMPY_O') == 'True': diff --git a/external-deps/spyder-kernels/spyder_kernels/console/shell.py b/external-deps/spyder-kernels/spyder_kernels/console/shell.py index 49cde462ce3..6dbbd7598c6 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/shell.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/shell.py @@ -28,7 +28,6 @@ from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.customize.code_runner import SpyderCodeRunner from spyder_kernels.comms.frontendcomm import CommError -from spyder_kernels.comms.decorators import comm_handler from spyder_kernels.utils.mpl import automatic_backend @@ -101,7 +100,6 @@ def enable_matplotlib(self, gui=None): return gui, backend # --- For Pdb namespace integration - @comm_handler def set_pdb_configuration(self, pdb_conf): """ Set Pdb configuration. @@ -263,6 +261,10 @@ def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None, try: etype, value, tb = self._get_exc_info(exc_tuple) stack = traceback.extract_tb(tb.tb_next) + for f_summary, f in zip( + stack, traceback.walk_tb(tb.tb_next)): + f_summary.locals = self.kernel.get_namespace_view( + frame=f[0]) self.kernel.frontend_call(blocking=False).show_traceback( etype, value, stack) except Exception: @@ -272,7 +274,6 @@ def register_debugger_sigint(self): """Register sigint handler.""" signal.signal(signal.SIGINT, self.spyderkernel_sigint_handler) - @comm_handler def raise_interrupt_signal(self): """Raise interrupt signal.""" if os.name == "nt": @@ -292,7 +293,6 @@ def raise_interrupt_signal(self): else: self.kernel._send_interrupt_children() - @comm_handler def request_pdb_stop(self): """Request pdb to stop at the next possible position.""" pdb_session = self.pdb_session @@ -351,7 +351,6 @@ async def run_code(self, *args, **kwargs): except KeyboardInterrupt: self.showtraceback() - @comm_handler def pdb_input_reply(self, line, echo_stack_entry=True): """Get a pdb command from the frontend.""" debugger = self.pdb_session 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 65870f02cfb..d7efae21fa7 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,8 +227,7 @@ def kernel(request): 'False_', 'True_' ], - 'minmax': False, - 'filter_on':True + 'minmax': False } # Teardown @@ -289,31 +288,6 @@ 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/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py index f4321eaa3de..680787fab79 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -14,10 +14,13 @@ import bdb import builtins from contextlib import contextmanager +import cProfile +from functools import partial import io import logging import os import pdb +import tempfile import shlex import sys import time @@ -30,6 +33,8 @@ ) from IPython.core.magic import ( needs_local_scope, + no_var_expand, + line_cell_magic, magics_class, Magics, line_magic, @@ -37,11 +42,12 @@ from IPython.core import magic_arguments # Local imports -from spyder_kernels.comms.frontendcomm import frontend_request +from spyder_kernels.comms.frontendcomm import frontend_request, CommError from spyder_kernels.customize.namespace_manager import NamespaceManager from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.customize.umr import UserModuleReloader -from spyder_kernels.customize.utils import capture_last_Expr, canonic +from spyder_kernels.customize.utils import ( + capture_last_Expr, canonic, create_pathlist) # For logging @@ -192,6 +198,27 @@ def debugfile(self, line, local_ns=None): context_locals=local_ns, ) + @runfile_arguments + @needs_local_scope + @line_magic + def profilefile(self, line, local_ns=None): + """Profile a file.""" + args, local_ns = self._parse_runfile_argstring( + self.profilefile, line, local_ns) + + with self._profile_exec() as prof_exec: + self._exec_file( + filename=args.filename, + canonic_filename=args.canonic_filename, + wdir=args.wdir, + current_namespace=args.current_namespace, + args=args.args, + exec_fun=prof_exec, + post_mortem=args.post_mortem, + context_globals=args.namespace, + context_locals=local_ns, + ) + @runcell_arguments @needs_local_scope @line_magic @@ -230,6 +257,34 @@ def debugcell(self, line, local_ns=None): context_locals=local_ns, ) + @runcell_arguments + @needs_local_scope + @line_magic + def profilecell(self, line, local_ns=None): + """Profile a code cell.""" + args = self._parse_runcell_argstring(self.profilecell, line) + + with self._profile_exec() as prof_exec: + return self._exec_cell( + cell_id=args.cell_id, + filename=args.filename, + canonic_filename=args.canonic_filename, + exec_fun=prof_exec, + post_mortem=args.post_mortem, + context_globals=self.shell.user_ns, + context_locals=local_ns, + ) + + @no_var_expand + @needs_local_scope + @line_cell_magic + def profile(self, line, cell=None, local_ns=None): + """Profile the given line.""" + if cell is not None: + line += "\n" + cell + with self._profile_exec() as prof_exec: + return prof_exec(line, self.shell.user_ns, local_ns) + @contextmanager def _debugger_exec(self, filename, continue_if_has_breakpoints): """Get an exec function to use for debugging.""" @@ -251,6 +306,58 @@ def debug_exec(code, glob, loc): # Enter recursive debugger yield debug_exec + @contextmanager + def _profile_exec(self): + """Get an exec function for profiling.""" + tmp_dir = None + if sys.platform.startswith('linux'): + # Do not use /tmp for temporary files + try: + from xdg.BaseDirectory import xdg_data_home + tmp_dir = xdg_data_home + os.makedirs(tmp_dir, exist_ok=True) + except Exception: + tmp_dir = None + + with tempfile.TemporaryDirectory(dir=tmp_dir) as tempdir: + # Reset the tracing function in case we are debugging + trace_fun = sys.gettrace() + sys.settrace(None) + + # Get a file to save the results + profile_filename = os.path.join(tempdir, "profile.prof") + + try: + if self.shell.is_debugging(): + def prof_exec(code, glob, loc): + """ + If we are debugging (tracing), call_tracing is + necessary for profiling. + """ + return sys.call_tracing(cProfile.runctx, ( + code, glob, loc, profile_filename + )) + + yield prof_exec + else: + yield partial(cProfile.runctx, filename=profile_filename) + finally: + # Reset tracing function + sys.settrace(trace_fun) + + # Send result to frontend + if os.path.isfile(profile_filename): + with open(profile_filename, "br") as f: + profile_result = f.read() + try: + frontend_request(blocking=False).show_profile_file( + profile_result, create_pathlist() + ) + except CommError: + logger.debug( + "Could not send profile result to the frontend." + ) + def _exec_file( self, filename=None, diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py index b9d76f63adf..ec505368b4e 100755 --- a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py @@ -17,7 +17,6 @@ import traceback import threading from collections import namedtuple -from functools import lru_cache from IPython.core.autocall import ZMQExitAutocall from IPython.core.debugger import Pdb as ipyPdb @@ -679,21 +678,6 @@ def _cmdloop(self): "For copying text while debugging, use Ctrl+Shift+C", file=self.stdout) - @lru_cache - def canonic(self, filename): - """Return canonical form of filename.""" - return super().canonic(filename) - - def do_exitdb(self, arg): - """Exit the debugger""" - self._set_stopinfo(self.botframe, None, -1) - sys.settrace(None) - frame = sys._getframe().f_back - while frame and frame is not self.botframe: - del frame.f_trace - frame = frame.f_back - return 1 - def cmdloop(self, intro=None): """ Repeatedly issue a prompt, accept input, parse an initial prefix diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py index d7e0ce5a727..c19927f8fe7 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py @@ -589,8 +589,7 @@ 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, - filter_on=True): + excluded_names=None, exclude_callables_and_modules=None): """Keep objects in namespace view according to different criteria.""" output_dict = {} def _is_string(obj): @@ -606,7 +605,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 @@ -618,8 +617,7 @@ 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', - 'filter_on') + 'show_special_attributes', 'exclude_callables_and_modules') def get_supported_types(): @@ -675,7 +673,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, filter_on=settings['filter_on']) + excluded_names=excluded_names) def make_remote_view(data, settings, more_excluded_names=None): From 83883b0863888ab9789567c55ca934427de21068 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sat, 5 Aug 2023 08:00:17 +0200 Subject: [PATCH 16/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "9e8773c81" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "9e8773c81" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 4 +- .../spyder_kernels/comms/decorators.py | 27 +++++++ .../spyder_kernels/console/kernel.py | 70 ++++++++----------- .../spyder_kernels/console/shell.py | 9 +-- .../console/tests/test_console_kernel.py | 28 +++++++- .../spyder_kernels/customize/spyderpdb.py | 16 +++++ .../spyder_kernels/utils/nsview.py | 10 +-- .../utils/tests/test_iofuncs.py | 7 +- 8 files changed, 117 insertions(+), 54 deletions(-) create mode 100644 external-deps/spyder-kernels/spyder_kernels/comms/decorators.py diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index fa079775443..66e349c60da 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 = improve_namespace - commit = e5f76b33fc1a54477348347b49415cf1fa0ade52 - parent = 6aaa12e59cb56f5eaf57953a55068dd4014f0401 + commit = 9e8773c81fd5562758f6225a902ede24b9b8e40d + parent = 89f953859ee7c2e91ad6c46a887889b867f6c14f method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/comms/decorators.py b/external-deps/spyder-kernels/spyder_kernels/comms/decorators.py new file mode 100644 index 00000000000..b2633404a07 --- /dev/null +++ b/external-deps/spyder-kernels/spyder_kernels/comms/decorators.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Comms decorators. +""" + + +def comm_handler(fun): + """Decorator to mark comm handler methods.""" + fun._is_comm_handler = True + return fun + + +def register_comm_handlers(instance, frontend_comm): + """ + Registers an instance whose methods have been marked with comm_handler. + """ + for method_name in instance.__class__.__dict__: + method = getattr(instance, method_name) + if hasattr(method, '_is_comm_handler'): + frontend_comm.register_call_handler( + method_name, method) + diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py index d1a585a301a..d900d8a754d 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py @@ -30,6 +30,8 @@ # Local imports from spyder_kernels.comms.frontendcomm import FrontendComm +from spyder_kernels.comms.decorators import ( + register_comm_handlers, comm_handler) from spyder_kernels.utils.iofuncs import iofunctions from spyder_kernels.utils.mpl import ( MPL_BACKENDS_FROM_SPYDER, MPL_BACKENDS_TO_SPYDER, INLINE_FIGURE_FORMATS) @@ -39,7 +41,6 @@ from spyder_kernels.comms.utils import WriteContext - logger = logging.getLogger(__name__) @@ -60,40 +61,8 @@ def __init__(self, *args, **kwargs): self.frontend_comm = FrontendComm(self) # All functions that can be called through the comm - handlers = { - 'set_pdb_configuration': self.shell.set_pdb_configuration, - 'get_value': self.get_value, - 'load_data': self.load_data, - 'save_namespace': self.save_namespace, - 'is_defined': self.is_defined, - 'get_doc': self.get_doc, - 'get_source': self.get_source, - 'set_value': self.set_value, - 'remove_value': self.remove_value, - 'copy_value': self.copy_value, - 'set_cwd': self.set_cwd, - 'get_syspath': self.get_syspath, - 'get_env': self.get_env, - 'close_all_mpl_figures': self.close_all_mpl_figures, - 'show_mpl_backend_errors': self.show_mpl_backend_errors, - 'get_namespace_view': self.get_namespace_view, - 'set_namespace_view_settings': self.set_namespace_view_settings, - 'get_var_properties': self.get_var_properties, - 'set_sympy_forecolor': self.set_sympy_forecolor, - 'update_syspath': self.update_syspath, - 'is_special_kernel_valid': self.is_special_kernel_valid, - 'get_matplotlib_backend': self.get_matplotlib_backend, - 'get_mpl_interactive_backend': self.get_mpl_interactive_backend, - 'pdb_input_reply': self.shell.pdb_input_reply, - 'enable_faulthandler': self.enable_faulthandler, - 'get_current_frames': self.get_current_frames, - 'request_pdb_stop': self.shell.request_pdb_stop, - 'raise_interrupt_signal': self.shell.raise_interrupt_signal, - 'get_fault_text': self.get_fault_text, - } - for call_id in handlers: - self.frontend_comm.register_call_handler( - call_id, handlers[call_id]) + register_comm_handlers(self, self.frontend_comm) + register_comm_handlers(self.shell, self.frontend_comm) self.namespace_view_settings = {} self._mpl_backend_error = None @@ -145,6 +114,7 @@ def publish_state(self): except Exception: pass + @comm_handler def enable_faulthandler(self): """ Open a file to save the faulthandling and identifiers for @@ -170,6 +140,7 @@ def enable_faulthandler(self): faulthandler.enable(self.faulthandler_handle) return self.faulthandler_handle.name, main_id, system_ids + @comm_handler def get_fault_text(self, fault_filename, main_id, ignore_ids): """Get fault text from old run.""" # Read file @@ -263,8 +234,8 @@ def filter_stack(self, stack, is_main): stack = [] return stack - def get_current_frames(self, ignore_internal_threads=True, - capture_locals=False): + @comm_handler + def get_current_frames(self, ignore_internal_threads=True): """Get the current frames.""" ignore_list = self.get_system_threads_id() main_id = threading.main_thread().ident @@ -275,9 +246,6 @@ def get_current_frames(self, ignore_internal_threads=True, for thread_id, frame in sys._current_frames().items(): stack = traceback.StackSummary.extract( traceback.walk_stack(frame)) - if capture_locals: - for f_summary, f in zip(stack, traceback.walk_stack(frame)): - f_summary.locals = self.get_namespace_view(frame=f[0]) stack.reverse() if ignore_internal_threads: if thread_id in ignore_list: @@ -292,10 +260,12 @@ def get_current_frames(self, ignore_internal_threads=True, return frames # --- For the Variable Explorer + @comm_handler def set_namespace_view_settings(self, settings): """Set namespace_view_settings.""" self.namespace_view_settings = settings + @comm_handler def get_namespace_view(self, frame=None): """ Return the namespace view @@ -331,6 +301,7 @@ def get_namespace_view(self, frame=None): else: return None + @comm_handler def get_var_properties(self): """ Get some properties of the variables in the current @@ -361,27 +332,32 @@ def get_var_properties(self): else: return None + @comm_handler def get_value(self, name): """Get the value of a variable""" ns = self.shell._get_current_namespace() return ns[name] + @comm_handler def set_value(self, name, value): """Set the value of a variable""" ns = self.shell._get_reference_namespace(name) ns[name] = value self.log.debug(ns) + @comm_handler def remove_value(self, name): """Remove a variable""" ns = self.shell._get_reference_namespace(name) ns.pop(name) + @comm_handler def copy_value(self, orig_name, new_name): """Copy a variable""" ns = self.shell._get_reference_namespace(orig_name) ns[new_name] = ns[orig_name] + @comm_handler def load_data(self, filename, ext, overwrite=False): """ Load data from filename. @@ -418,6 +394,7 @@ def load_data(self, filename, ext, overwrite=False): return None + @comm_handler def save_namespace(self, filename): """Save namespace into filename""" ns = self.shell._get_current_namespace() @@ -471,6 +448,7 @@ def interrupt_eventloop(self): self.loopback_socket, self.session.msg("interrupt_eventloop")) # --- For the Help plugin + @comm_handler def is_defined(self, obj, force_import=False): """Return True if object is defined in current namespace""" from spyder_kernels.utils.dochelpers import isdefined @@ -478,6 +456,7 @@ def is_defined(self, obj, force_import=False): ns = self.shell._get_current_namespace(with_magics=True) return isdefined(obj, force_import=force_import, namespace=ns) + @comm_handler def get_doc(self, objtxt): """Get object documentation dictionary""" try: @@ -491,6 +470,7 @@ def get_doc(self, objtxt): if valid: return getdoc(obj) + @comm_handler def get_source(self, objtxt): """Get object source""" from spyder_kernels.utils.dochelpers import getsource @@ -500,6 +480,7 @@ def get_source(self, objtxt): return getsource(obj) # -- For Matplolib + @comm_handler def get_matplotlib_backend(self): """Get current matplotlib backend.""" try: @@ -508,6 +489,7 @@ def get_matplotlib_backend(self): except Exception: return None + @comm_handler def get_mpl_interactive_backend(self): """ Get current Matplotlib interactive backend. @@ -616,6 +598,7 @@ def set_autocall(self, autocall): self._set_config_option('ZMQInteractiveShell.autocall', autocall) # --- Additional methods + @comm_handler def set_cwd(self, dirname): """Set current working directory.""" self._cwd_initialised = True @@ -629,14 +612,17 @@ def get_cwd(self): except (IOError, OSError): pass + @comm_handler def get_syspath(self): """Return sys.path contents.""" return sys.path[:] + @comm_handler def get_env(self): """Get environment variables.""" return os.environ.copy() + @comm_handler def close_all_mpl_figures(self): """Close all Matplotlib figures.""" try: @@ -645,6 +631,7 @@ def close_all_mpl_figures(self): except: pass + @comm_handler def is_special_kernel_valid(self): """ Check if optional dependencies are available for special consoles. @@ -667,6 +654,7 @@ def is_special_kernel_valid(self): return u'cython' return None + @comm_handler def update_syspath(self, path_dict, new_path_dict): """ Update the PYTHONPATH of the kernel. @@ -888,11 +876,13 @@ def _set_mpl_inline_rc_config(self, option, value): # Needed in case matplolib isn't installed pass + @comm_handler def show_mpl_backend_errors(self): """Show Matplotlib backend errors after the prompt is ready.""" if self._mpl_backend_error is not None: print(self._mpl_backend_error) # spyder: test-skip + @comm_handler def set_sympy_forecolor(self, background_color='dark'): """Set SymPy forecolor depending on console background.""" if os.environ.get('SPY_SYMPY_O') == 'True': diff --git a/external-deps/spyder-kernels/spyder_kernels/console/shell.py b/external-deps/spyder-kernels/spyder_kernels/console/shell.py index 6dbbd7598c6..49cde462ce3 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/shell.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/shell.py @@ -28,6 +28,7 @@ from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.customize.code_runner import SpyderCodeRunner from spyder_kernels.comms.frontendcomm import CommError +from spyder_kernels.comms.decorators import comm_handler from spyder_kernels.utils.mpl import automatic_backend @@ -100,6 +101,7 @@ def enable_matplotlib(self, gui=None): return gui, backend # --- For Pdb namespace integration + @comm_handler def set_pdb_configuration(self, pdb_conf): """ Set Pdb configuration. @@ -261,10 +263,6 @@ def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None, try: etype, value, tb = self._get_exc_info(exc_tuple) stack = traceback.extract_tb(tb.tb_next) - for f_summary, f in zip( - stack, traceback.walk_tb(tb.tb_next)): - f_summary.locals = self.kernel.get_namespace_view( - frame=f[0]) self.kernel.frontend_call(blocking=False).show_traceback( etype, value, stack) except Exception: @@ -274,6 +272,7 @@ def register_debugger_sigint(self): """Register sigint handler.""" signal.signal(signal.SIGINT, self.spyderkernel_sigint_handler) + @comm_handler def raise_interrupt_signal(self): """Raise interrupt signal.""" if os.name == "nt": @@ -293,6 +292,7 @@ def raise_interrupt_signal(self): else: self.kernel._send_interrupt_children() + @comm_handler def request_pdb_stop(self): """Request pdb to stop at the next possible position.""" pdb_session = self.pdb_session @@ -351,6 +351,7 @@ async def run_code(self, *args, **kwargs): except KeyboardInterrupt: self.showtraceback() + @comm_handler def pdb_input_reply(self, line, echo_stack_entry=True): """Get a pdb command from the frontend.""" debugger = self.pdb_session 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/customize/spyderpdb.py b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py index ec505368b4e..b9d76f63adf 100755 --- a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py @@ -17,6 +17,7 @@ import traceback import threading from collections import namedtuple +from functools import lru_cache from IPython.core.autocall import ZMQExitAutocall from IPython.core.debugger import Pdb as ipyPdb @@ -678,6 +679,21 @@ def _cmdloop(self): "For copying text while debugging, use Ctrl+Shift+C", file=self.stdout) + @lru_cache + def canonic(self, filename): + """Return canonical form of filename.""" + return super().canonic(filename) + + def do_exitdb(self, arg): + """Exit the debugger""" + self._set_stopinfo(self.botframe, None, -1) + sys.settrace(None) + frame = sys._getframe().f_back + while frame and frame is not self.botframe: + del frame.f_trace + frame = frame.f_back + return 1 + def cmdloop(self, intro=None): """ Repeatedly issue a prompt, accept input, parse an initial prefix 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/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_iofuncs.py b/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_iofuncs.py index ebb07c85b07..d88b2e103d5 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_iofuncs.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_iofuncs.py @@ -340,13 +340,14 @@ def test_spydata_export(input_namespace, expected_namespace, pass -def test_save_load_hdf5_files(): +def test_save_load_hdf5_files(tmp_path): """Simple test to check that we can save and load HDF5 files.""" + h5_file = tmp_path / "test.h5" data = {'a' : [1, 2, 3, 4], 'b' : 4.5} - iofuncs.save_hdf5(data, "test.h5") + iofuncs.save_hdf5(data, h5_file) expected = ({'a': np.array([1, 2, 3, 4]), 'b': np.array(4.5)}, None) - assert repr(iofuncs.load_hdf5("test.h5")) == repr(expected) + assert repr(iofuncs.load_hdf5(h5_file)) == repr(expected) def test_load_dicom_files(): From ab3b70c1b6cfd631200301dbdfa73bb3c276153f Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sat, 5 Aug 2023 08:07:20 +0200 Subject: [PATCH 17/41] temps folder on linux --- .../profiler/widgets/profiler_data_tree.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/profiler/widgets/profiler_data_tree.py b/spyder/plugins/profiler/widgets/profiler_data_tree.py index c832463eb1f..037472fe319 100644 --- a/spyder/plugins/profiler/widgets/profiler_data_tree.py +++ b/spyder/plugins/profiler/widgets/profiler_data_tree.py @@ -16,6 +16,7 @@ # Standard library imports import os import os.path as osp +import sys import tempfile # Third party imports @@ -124,8 +125,18 @@ def show_profile_buffer(self, prof_buffer, lib_pathlist): """Show profile file.""" if not prof_buffer: return - - with tempfile.TemporaryDirectory() as dir: + + tmp_dir = None + if sys.platform.startswith('linux'): + # Do not use /tmp for temporary files + try: + from xdg.BaseDirectory import xdg_data_home + tmp_dir = xdg_data_home + os.makedirs(tmp_dir, exist_ok=True) + except Exception: + tmp_dir = None + + with tempfile.TemporaryDirectory(dir=tmp_dir) as dir: filename = os.path.join(dir, "tem.prof") with open(filename, "bw") as f: f.write(prof_buffer) From 1befd67b65caf41722382a4ed78eb24610770093 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Fri, 17 Nov 2023 08:07:07 +0100 Subject: [PATCH 18/41] pep8 --- spyder/app/tests/test_mainwindow.py | 18 +++++++++--------- spyder/plugins/profiler/plugin.py | 3 --- spyder/plugins/profiler/widgets/main_widget.py | 6 ++---- .../profiler/widgets/profiler_data_tree.py | 5 ++--- spyder/plugins/run/plugin.py | 8 ++++---- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 2431e8e2577..26d160eb83a 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -5399,7 +5399,7 @@ def test_profiler(main_window, qtbot, tmpdir): toggle_tree_action = profile_tree.get_action( ProfilerWidgetActions.ToggleTreeDirection) - + # Make sure the ordering methods don't reveal the root element. toggle_tree_action.setChecked(True) assert len(profile_tree.current_widget().data_tree.get_items(0)) == 1 @@ -6769,33 +6769,33 @@ def test_quotes_rename_ipy(main_window, qtbot, tmpdir): assert "801" in control.toPlainText() assert "error" not in control.toPlainText() assert "\\.ipy" in control.toPlainText() - + # Create an untitled file main_window.editor.new() - + assert "untitled" in main_window.editor.get_current_filename() - + code_editor = main_window.editor.get_focus_widget() code_editor.set_text("print(20 + 780)") - + with qtbot.waitSignal(shell.executed): qtbot.mouseClick(main_window.run_cell_button, Qt.LeftButton) assert "800" in control.toPlainText() assert "error" not in control.toPlainText() assert "untitled" in control.toPlainText() - + # Save file in a new folder code_editor.set_text("print(19 + 780)") - + with tempfile.TemporaryDirectory() as td: - + editorstack = main_window.editor.get_current_editorstack() editorstack.select_savename = lambda fn: os.path.join(td, "fn.ipy") main_window.editor.save() with qtbot.waitSignal(shell.executed): qtbot.mouseClick(main_window.run_cell_button, Qt.LeftButton) - + assert "799" in control.toPlainText() assert "error" not in control.toPlainText() assert "fn.ipy" in control.toPlainText() diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 0ee535c4669..25c79518e0d 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -29,8 +29,6 @@ from spyder.plugins.editor.api.run import CellRun, SelectionRun - - # --- Plugin # ---------------------------------------------------------------------------- class Profiler(SpyderDockablePlugin, ShellConnectPluginMixin, RunExecutor): @@ -242,7 +240,6 @@ def profile_cell( return console.exec_cell(input, conf) - @run_execute(context=RunContext.Selection) def profile_selection( self, diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index f6ea263982d..e4eb380db16 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -28,7 +28,6 @@ ProfilerSubWidget) - class ProfilerWidgetActions: # Triggers Clear = 'clear_action' @@ -282,7 +281,7 @@ def update_actions(self): undo_action.setEnabled(can_undo) redo_action.setEnabled(can_redo) - + clear_action = self.get_action(ProfilerWidgetActions.Clear) clear_action.setEnabled(can_clear) @@ -320,7 +319,6 @@ def switch_widget(self, widget, old_widget): """Switch widget.""" pass - # --- Public API # ------------------------------------------------------------------------ def home_tree(self): @@ -385,7 +383,7 @@ def show_callees(self): if widget is None: return widget.data_tree.show_selected() - + toggle_tree_action = self.get_action( ProfilerWidgetActions.ToggleTreeDirection) if toggle_tree_action.isChecked(): diff --git a/spyder/plugins/profiler/widgets/profiler_data_tree.py b/spyder/plugins/profiler/widgets/profiler_data_tree.py index 037472fe319..2f3cc2d96ac 100644 --- a/spyder/plugins/profiler/widgets/profiler_data_tree.py +++ b/spyder/plugins/profiler/widgets/profiler_data_tree.py @@ -125,7 +125,7 @@ def show_profile_buffer(self, prof_buffer, lib_pathlist): """Show profile file.""" if not prof_buffer: return - + tmp_dir = None if sys.platform.startswith('linux'): # Do not use /tmp for temporary files @@ -661,7 +661,7 @@ def _show_tree(self, children=None, max_items=None, # Only expand if not too many children are shown self.change_view(1) self.resizeColumnToContents(0) - + self.sig_refresh.emit() def populate_tree(self, parentItem, children_list): @@ -728,7 +728,6 @@ def add_to_itemlist(item, maxlevel, level=1): if maxlevel > 0: add_to_itemlist(tlitem, maxlevel=maxlevel) - return itemlist def change_view(self, change_in_depth): diff --git a/spyder/plugins/run/plugin.py b/spyder/plugins/run/plugin.py index 4c91ee17ecc..220a4eec021 100644 --- a/spyder/plugins/run/plugin.py +++ b/spyder/plugins/run/plugin.py @@ -109,7 +109,7 @@ def on_main_menu_available(self): RunMenuSections.Run, before_section=RunMenuSections.RunExtras ) - + self.main_menu_ready = True while self.pending_menu_actions != []: @@ -266,7 +266,7 @@ def register_run_configuration_metadata( """ self.get_container().register_run_configuration_metadata( provider, metadata) - + def get_currently_selected_configuration(self): """ Get currently selected configuration @@ -426,7 +426,7 @@ def create_run_button( main toolbar. If a string, it must be a toolbar_id add_to_menu: object If True, then the action will be added to the Run menu. - If a dictionnary, it corresponds to + If a dictionnary, it corresponds to {'menu': ..., 'section': ..., 'before_section': ...} re_run: bool If True, then the button will act as a re-run button instead of @@ -620,7 +620,7 @@ def create_run_in_executor_button( main toolbar. If a string, it will be a toolbat id add_to_menu: object If True, then the action will be added to the Run menu. - If a dictionnary, it corresponds to + If a dictionnary, it corresponds to {'menu': ..., 'section': ..., 'before_section': ...} Returns From e6aece796a245b25b85f93d84ce9537840cfd0ab Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Tue, 12 Mar 2024 03:12:18 +0100 Subject: [PATCH 19/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "920e94057" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "920e94057" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 3656c83409b..cd7c5a31d5a 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 = improve_namespace - commit = 9e8773c81fd5562758f6225a902ede24b9b8e40d - parent = 89f953859ee7c2e91ad6c46a887889b867f6c14f + commit = 920e94057c1d312e760ed6e78daaa074da64a089 + parent = d480379fefca2d20d8f02643538b54c8151eb99c method = merge - cmdver = 0.4.3 + cmdver = 0.4.5 From 42c562205109d967257fc318f7dbb1e03d334a20 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Thu, 14 Mar 2024 06:39:34 +0100 Subject: [PATCH 20/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "af3fe0a6e" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "af3fe0a6e" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 4 ++-- .../spyder-kernels/spyder_kernels/customize/code_runner.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index cd7c5a31d5a..87f51fc076e 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 = improve_namespace - commit = 920e94057c1d312e760ed6e78daaa074da64a089 - parent = d480379fefca2d20d8f02643538b54c8151eb99c + commit = af3fe0a6e5a9ab87e9bd31ceacfd59cbf3d849a3 + parent = bf4d6facb1bd46c0ec42bcf7a1cc9a64288a5597 method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py index ab2d3e654b8..e8485a55670 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -47,7 +47,7 @@ from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.customize.umr import UserModuleReloader from spyder_kernels.customize.utils import ( - capture_last_Expr, canonic, exec_encapsulate_locals + capture_last_Expr, canonic, create_pathlist, exec_encapsulate_locals ) From d3d79ac3348959869d11f5a68d3a39ee287e44ae Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Sun, 17 Mar 2024 10:29:50 +0100 Subject: [PATCH 21/41] git subrepo clone --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "281be2ba8" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "281be2ba8" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 4 ++-- .../spyder-kernels/spyder_kernels/customize/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 87f51fc076e..0b95df82097 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 = improve_namespace - commit = af3fe0a6e5a9ab87e9bd31ceacfd59cbf3d849a3 - parent = bf4d6facb1bd46c0ec42bcf7a1cc9a64288a5597 + commit = 281be2ba8b74e7808cb054fb011a2a223a73db0d + parent = 6e663290e3ade6bef7b8c7270eeb536b91fa7449 method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/utils.py b/external-deps/spyder-kernels/spyder_kernels/customize/utils.py index fff18581b2c..23e3c37e0f1 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/utils.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/utils.py @@ -206,7 +206,7 @@ def exec_encapsulate_locals( exec_fun = exec if filename is None: filename = "" - exec_fun(compile(code_ast, filename, "exec"), globals) + exec_fun(compile(code_ast, filename, "exec"), globals, None) finally: if use_locals_hack: # Cleanup code From e22b6f930238f9a65a80adcbe98fb2869589950e Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Thu, 23 May 2024 15:37:10 +0200 Subject: [PATCH 22/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "9acc3dd22" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "9acc3dd22" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index b407b059823..91a6ad976ce 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 = improve_namespace - commit = 281be2ba8b74e7808cb054fb011a2a223a73db0d - parent = 6e663290e3ade6bef7b8c7270eeb536b91fa7449 + commit = 9acc3dd2210d28446afca060e4ca97fe21465c8f + parent = 8532ccdfd262eedf45f48cf6dd4f7a92819ab583 method = merge - cmdver = 0.4.3 + cmdver = 0.4.5 From e41455fc8c0aa93e366d66a1074a295bbb9fb6fc Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Wed, 29 May 2024 06:39:35 +0200 Subject: [PATCH 23/41] fix test --- spyder/plugins/profiler/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 2a7545c51a2..37dc09af174 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -84,7 +84,7 @@ def on_initialize(self): 'name': 'File' }, 'output_formats': [], - 'configuration_widget': ProfilerPyConfigurationGroup, + 'configuration_widget': IPythonConfigOptions, 'requires_cwd': True, 'priority': 3 }, From 2b8343a359814d8d1b67499301ec28d125762b41 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Thu, 30 May 2024 22:25:48 +0200 Subject: [PATCH 24/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "53b9706d9" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "53b9706d9" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 09b91543241..7bb7eca42db 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git - branch = master - commit = cf597289f1ee3f09fdda5a162228848fc3cf8453 - parent = ef54914c81fc81214493e08e646af44d7f82de35 + branch = improve_namespace + commit = 53b9706d9e0ff3608ec2112ec8001ac87fdf34f2 + parent = 413f6191e04f0a652f530b7de163d7dd0b0b6b26 method = merge cmdver = 0.4.5 From 381fe236e9de4b5cd16534dfd687f399fc8ac8a9 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Tue, 3 Sep 2024 07:07:20 +0200 Subject: [PATCH 25/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "6796a55cd" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "6796a55cd" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 58e3d7e4b4c..a2b119512cc 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 = improve_namespace - commit = 53b9706d9e0ff3608ec2112ec8001ac87fdf34f2 - parent = 413f6191e04f0a652f530b7de163d7dd0b0b6b26 + commit = 6796a55cd35bd3bf5dc7cf0609a744f6edb7d071 + parent = 097ccc2e24b4e9e61c036bc1c091413da1c2f2f4 method = merge - cmdver = 0.4.3 + cmdver = 0.4.5 From bd87d1fe1ec52cf301a78548584605f11cb84d32 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Tue, 3 Sep 2024 07:15:34 +0200 Subject: [PATCH 26/41] fix import --- spyder/plugins/profiler/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 96d03a5c1f2..2ea52ca1ab7 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -25,7 +25,7 @@ from spyder.plugins.run.api import ( RunContext, RunExecutor, RunConfiguration, ExtendedRunExecutionParameters, RunResult, run_execute) -from spyder.plugins.ipythonconsole.widgets.config import IPythonConfigOptions +from spyder.plugins.ipythonconsole.widgets.run_conf import IPythonConfigOptions from spyder.plugins.editor.api.run import CellRun, SelectionRun From c9d0eabef9e5ca6c017154bd1ac3450474668008 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 6 Sep 2024 19:10:18 -0500 Subject: [PATCH 27/41] Backport PR #22459: PR: Update workflows to run in the `6.x` branch (CI) --- .github/workflows/test-files.yml | 4 ++-- .github/workflows/test-linux.yml | 4 ++-- .github/workflows/test-mac.yml | 4 ++-- .github/workflows/test-win.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-files.yml b/.github/workflows/test-files.yml index b98d0f042f2..3e3eaf93b01 100644 --- a/.github/workflows/test-files.yml +++ b/.github/workflows/test-files.yml @@ -4,8 +4,8 @@ on: push: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' @@ -21,8 +21,8 @@ on: pull_request: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 8463e978d8e..60c1c2bbc5e 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -4,8 +4,8 @@ on: push: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' @@ -21,8 +21,8 @@ on: pull_request: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index 90bb5a35dfd..fb6c35916d8 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -4,8 +4,8 @@ on: push: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' @@ -21,8 +21,8 @@ on: pull_request: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml index c91ff90d9d6..36c1c55097b 100644 --- a/.github/workflows/test-win.yml +++ b/.github/workflows/test-win.yml @@ -4,8 +4,8 @@ on: push: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' @@ -21,8 +21,8 @@ on: pull_request: branches: - master + - 6.* - 5.* - - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' From 61e6fdd1d379d3958cadc5ba09c8feb8e5d9f835 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:03:40 -0700 Subject: [PATCH 28/41] Backport PR #22437 on branch 6.x (PR: Fix bug when calling `update_edit_menu` at startup (Editor)) (#22439) --- spyder/plugins/editor/widgets/main_widget.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/editor/widgets/main_widget.py b/spyder/plugins/editor/widgets/main_widget.py index faa231594bd..8a3424373c1 100644 --- a/spyder/plugins/editor/widgets/main_widget.py +++ b/spyder/plugins/editor/widgets/main_widget.py @@ -1250,7 +1250,11 @@ def update_edit_menu(self): editor = self.get_current_editor() readwrite_editor = possible_text_widget == editor - if readwrite_editor and not editor.isReadOnly(): + # We need the first validation to avoid a bug at startup. That probably + # happens when the menu is tried to be rendered automatically in some + # Linux distros. + # Fixes spyder-ide/spyder#22432 + if editor is not None and readwrite_editor and not editor.isReadOnly(): # Case where the current editor has the focus if not self.is_file_opened(): return From 334ce4cf5a8508caca75a8b45de52b3a53084538 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:31:21 -0700 Subject: [PATCH 29/41] Backport PR #22438 on branch 6.x (PR: Enable `autoreload` magic on all operating systems (Config)) (#22462) --- spyder/config/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index 7c21b5376c7..90a7359c6d4 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -162,7 +162,7 @@ 'greedy_completer': False, 'jedi_completer': False, 'autocall': 0, - 'autoreload': not WIN, + 'autoreload': True, 'symbolic_math': False, 'in_prompt': '', 'out_prompt': '', @@ -672,4 +672,4 @@ # or if you want to *rename* options, then you need to do a MAJOR update in # version, e.g. from 3.0.0 to 4.0.0 # 3. You don't need to touch this value if you're just adding a new option -CONF_VERSION = '84.1.0' +CONF_VERSION = '84.2.0' From a44926b59d1b3c532c50dde3574b5a7d98a66948 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 7 Sep 2024 18:42:34 -0500 Subject: [PATCH 30/41] Fix development version That correctly reflects what the next version is going to be. [ci skip] --- spyder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/__init__.py b/spyder/__init__.py index c2ea89b5126..3806df387a0 100644 --- a/spyder/__init__.py +++ b/spyder/__init__.py @@ -31,7 +31,7 @@ from packaging.version import parse -version_info = (6, 1, 0, "dev0") +version_info = (6, 0, 1, "a1", "dev0") __version__ = str(parse('.'.join(map(str, version_info)))) __installer_version__ = __version__ From 7913ca84a1020d31d311138213bd3919b446e16b Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:32:27 -0700 Subject: [PATCH 31/41] Backport PR #22474 on branch 6.x (PR: Update org.spyder_ide.spyder.appdata.xml) (#22475) --- scripts/org.spyder_ide.spyder.appdata.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/org.spyder_ide.spyder.appdata.xml b/scripts/org.spyder_ide.spyder.appdata.xml index 0637623a6e8..e99278374de 100644 --- a/scripts/org.spyder_ide.spyder.appdata.xml +++ b/scripts/org.spyder_ide.spyder.appdata.xml @@ -47,6 +47,15 @@ https://opencollective.com/spyder + + + + + + + + + From b1f96ae20df400119853398048a16e9b8ae0beab Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Tue, 10 Sep 2024 08:29:58 -0700 Subject: [PATCH 32/41] Backport PR #22476 on branch 6.x (PR: Fix issues showing the in-app appeal message) (#22477) Co-authored-by: Carlos Cordoba --- spyder/app/mainwindow.py | 13 ++++++++++--- spyder/plugins/application/plugin.py | 10 +++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 32b3254f972..d6ce018dfe9 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -847,10 +847,11 @@ def post_visible_setup(self): assert 'pandas' not in sys.modules assert 'matplotlib' not in sys.modules - # Call on_mainwindow_visible for all plugins, except Layout because it - # needs to be called first (see above). + # Call on_mainwindow_visible for all plugins, except Layout and + # Application because they need to be called first (see above) and last + # (see below), respectively. for plugin_name in PLUGIN_REGISTRY: - if plugin_name != Plugins.Layout: + if plugin_name not in (Plugins.Layout, Plugins.Application): plugin = PLUGIN_REGISTRY.get_plugin(plugin_name) try: plugin.on_mainwindow_visible() @@ -860,6 +861,12 @@ def post_visible_setup(self): self.restore_scrollbar_position.emit() + # This must be called after restore_scrollbar_position.emit so that + # the in-app appeal dialog has focus on macOS. + # Fixes spyder-ide/spyder#22454. + self.get_plugin(Plugins.Application).on_mainwindow_visible() + QApplication.processEvents() + # Server to maintain just one Spyder instance and open files in it if # the user tries to start other instances with # $ spyder foo.py diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py index 1aa25aa7c84..3ae21547916 100644 --- a/spyder/plugins/application/plugin.py +++ b/spyder/plugins/application/plugin.py @@ -156,12 +156,16 @@ def on_mainwindow_visible(self): screen.logicalDotsPerInchChanged.connect( container.show_dpi_change_message) - # Show appeal the fifth time Spyder starts + # Show appeal the fifth and 25th time Spyder starts spyder_runs = self.get_conf("spyder_runs_for_appeal", default=1) - if spyder_runs == 5: + if spyder_runs in [5, 25]: container.inapp_appeal_status.show_appeal() + + # Increase counting in one to not get stuck at this point. + # Fixes spyder-ide/spyder#22457 + self.set_conf("spyder_runs_for_appeal", spyder_runs + 1) else: - if spyder_runs < 5: + if spyder_runs < 25: self.set_conf("spyder_runs_for_appeal", spyder_runs + 1) # ---- Private API From 1b86cb7b451ef7c899f7628a86b4a1c11231b8e1 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:50:22 -0700 Subject: [PATCH 33/41] Backport PR #22484 on branch 6.x (PR: Change default format in array editor to `''`) (#22488) Co-authored-by: Jitse Niesen --- .../variableexplorer/widgets/arrayeditor.py | 7 +++++-- .../widgets/tests/test_arrayeditor.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index bd06d2d262a..0effb87fdba 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -71,7 +71,7 @@ class ArrayEditorWidgets: ToolbarStretcher = 'toolbar_stretcher' -# Note: string and unicode data types will be formatted with 's' (see below) +# Note: string and unicode data types will be formatted with '' (see below) SUPPORTED_FORMATS = { 'single': '.6g', 'double': '.6g', @@ -657,7 +657,10 @@ def __init__(self, parent, data, readonly=False): self.old_data_shape = self.data.shape self.data.shape = (1, 1) - format_spec = SUPPORTED_FORMATS.get(data.dtype.name, 's') + # Use '' as default format specifier, because 's' does not produce + # a `str` for arrays with strings, see spyder-ide/spyder#22466 + format_spec = SUPPORTED_FORMATS.get(data.dtype.name, '') + self.model = ArrayModel(self.data, format_spec=format_spec, readonly=readonly, parent=self) self.view = ArrayView(self, self.model, data.dtype, data.shape) diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_arrayeditor.py b/spyder/plugins/variableexplorer/widgets/tests/test_arrayeditor.py index b482575267e..f7fd7773fef 100644 --- a/spyder/plugins/variableexplorer/widgets/tests/test_arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/tests/test_arrayeditor.py @@ -115,6 +115,23 @@ def test_type_errors(setup_arrayeditor, qtbot): assert_array_equal(contents, np.ones(10)) +@pytest.mark.parametrize( + 'data', + [np.array([['a', 'b'], ['c', 'd']])] +) +def test_string_array_data_is_str(setup_arrayeditor): + """ + Verify that the displayed data of an array of strings is of type `str`. + + Regression test for spyder-ide/spyder#22466. + """ + dlg = setup_arrayeditor + idx = dlg.arraywidget.model.index(0, 0) + data = dlg.arraywidget.model.data(idx) + assert data == 'a' + assert type(data) is str + + @pytest.mark.skipif(not sys.platform.startswith('linux'), reason="Only works on Linux") @pytest.mark.parametrize( From 24bdc68ae028e6d5174caa25829d28bcf2f03d49 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Sat, 14 Sep 2024 08:19:44 -0700 Subject: [PATCH 34/41] Backport PR #22502 on branch 6.x (PR: Reset `undocked before hiding` state of all plugins before applying layout) (#22503) Co-authored-by: Carlos Cordoba --- spyder/plugins/layout/api.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/layout/api.py b/spyder/plugins/layout/api.py index 80aa9093df0..6133d66847a 100644 --- a/spyder/plugins/layout/api.py +++ b/spyder/plugins/layout/api.py @@ -356,7 +356,7 @@ def set_main_window_layout(self, main_window, dockable_plugins): # is applied base_plugins_to_hide = [] - # Before applying a new layout all plugins need to be hidden + # Actions to be performed before applying the layout for plugin in dockable_plugins: all_plugin_ids.append(plugin.NAME) @@ -367,6 +367,14 @@ def set_main_window_layout(self, main_window, dockable_plugins): ): external_plugins_to_show.append(plugin.NAME) + # Restore undocked_before_hiding state so that the layout is + # applied as expected, i.e. all plugins are shown in their expected + # positions. It also avoids an error that leaves the main window in + # an inconsistent state. + # Fixes spyder-ide/spyder#22494 + plugin.set_conf('window_was_undocked_before_hiding', False) + + # Hide all plugins plugin.toggle_view(False) # Add plugins without an area assigned to the default area and made From b2843d1120bc4e2ea2e4b8558131efc4f5495bce Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Sat, 14 Sep 2024 21:42:35 -0700 Subject: [PATCH 35/41] Backport PR #22504 on branch 6.x (PR: Don't use script to get env vars if Spyder is launched from a terminal (Utils)) (#22506) Co-authored-by: Carlos Cordoba --- spyder/utils/environ.py | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/spyder/utils/environ.py b/spyder/utils/environ.py index 677e73556ee..5b1d36606ed 100644 --- a/spyder/utils/environ.py +++ b/spyder/utils/environ.py @@ -16,12 +16,14 @@ import re import sys from textwrap import dedent + try: import winreg except Exception: pass # Third party imports +import psutil from qtpy.QtWidgets import QMessageBox # Local imports @@ -94,6 +96,7 @@ def get_user_environment_variables(): Key-value pairs of environment variables. """ env_var = {} + if os.name == 'nt': key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") num_values = winreg.QueryInfoKey(key)[1] @@ -101,21 +104,31 @@ def get_user_environment_variables(): [winreg.EnumValue(key, k)[:2] for k in range(num_values)] ) elif os.name == 'posix': - try: - user_env_script = _get_user_env_script() - proc = run_shell_command(user_env_script, env={}, text=True) - - # Use timeout to fix spyder-ide/spyder#21172 - stdout, stderr = proc.communicate( - timeout=3 if running_in_ci() else 0.5 - ) - - if stderr: - logger.info(stderr.strip()) - if stdout: - env_var = eval(stdout, None) - except Exception as exc: - logger.info(exc) + # Detect if the Spyder process was launched from a system terminal. + # This is None if that was not the case. + launched_from_terminal = psutil.Process(os.getpid()).terminal() + + # We only need to do this if Spyder was **not** launched from a + # terminal. Otherwise, it'll inherit the env vars present in it. + # Fixes spyder-ide/spyder#22415 + if not launched_from_terminal: + try: + user_env_script = _get_user_env_script() + proc = run_shell_command(user_env_script, env={}, text=True) + + # Use timeout to fix spyder-ide/spyder#21172 + stdout, stderr = proc.communicate( + timeout=3 if running_in_ci() else 0.5 + ) + + if stderr: + logger.info(stderr.strip()) + if stdout: + env_var = eval(stdout, None) + except Exception as exc: + logger.info(exc) + else: + env_var = dict(os.environ) return env_var From 7284d89cf8c326e3d6b76b5749e1de61854f9ea6 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:00:05 -0700 Subject: [PATCH 36/41] Backport PR #22509 on branch 6.x (PR: Don't kill kernel process tree when running in Binder (IPython console)) (#22510) Co-authored-by: Carlos Cordoba --- MANIFEST.in | 1 - README.md | 9 ++++----- img_src/czi.png | Bin 0 -> 10971 bytes spyder/config/base.py | 8 ++++++++ spyder/plugins/ipythonconsole/utils/manager.py | 12 +++++++++--- 5 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 img_src/czi.png diff --git a/MANIFEST.in b/MANIFEST.in index 5ac834fe448..d1ab79b2cd6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include scripts/* include img_src/*.ico include img_src/spyder.png -include img_src/oxygen_icon_set/* include MANIFEST.in include README.md include LICENSE.txt diff --git a/README.md b/README.md index 48968816b2e..43ab48c6848 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ ## Try Spyder online -[![Binder Spyder latest release](https://img.shields.io/badge/launch-latest%20release-579ACA.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/spyder-ide/binder-environments/spyder-stable?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fspyder-ide%252FSpyder-Workshop%26urlpath%3Ddesktop%252F%26branch%3Dmaster) :point_left: Click on this link to run the [latest Spyder version](https://github.com/spyder-ide/spyder/releases/latest) in your browser. +[![Binder for the latest Spyder release](https://img.shields.io/badge/launch-latest%20release-579ACA.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/spyder-ide/binder-environments/spyder-stable?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fspyder-ide%252FSpyder-Workshop%26urlpath%3Ddesktop%252F%26branch%3Dmaster) :point_left: Click on this link to run the [latest Spyder version](https://github.com/spyder-ide/spyder/releases/latest) in your browser. -[![Binder Spyder from 5.x](https://img.shields.io/badge/launch-5.x-E66581.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/spyder-ide/binder-environments/5.x?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fspyder-ide%252Fspyder%26urlpath%3Ddesktop%252F%26branch%3D5.x%26depth%3D500) :point_left: Click on this link to check the next Spyder 5 version. +[![Binder for Spyder from the 6.x branch](https://img.shields.io/badge/launch-6.x-E66581.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/spyder-ide/binder-environments/6.x?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fspyder-ide%252Fspyder%26urlpath%3Ddesktop%252F%26branch%3D6.x%26depth%3D500) :point_left: Click on this link to check the next Spyder 6 version. -[![Binder Spyder from master](https://img.shields.io/badge/launch-master-E66581.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/spyder-ide/binder-environments/master?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fspyder-ide%252Fspyder%26urlpath%3Ddesktop%252F%26branch%3Dmaster%26depth%3D500) :point_left: Click on this link to test changes in our `master` branch. +[![Binder for Spyder from the master branch](https://img.shields.io/badge/launch-master-E66581.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/spyder-ide/binder-environments/master?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fspyder-ide%252Fspyder%26urlpath%3Ddesktop%252F%26branch%3Dmaster%26depth%3D500) :point_left: Click on this link to test changes in our `master` branch. ---- @@ -219,8 +219,7 @@ The rest our dependencies (both required and optional) are declared in Spyder is funded thanks to the generous support of - -[![Quansight](https://user-images.githubusercontent.com/16781833/142477716-53152d43-99a0-470c-a70b-c04bbfa97dd4.png)](https://www.quansight.com/)[![Numfocus](https://i2.wp.com/numfocus.org/wp-content/uploads/2017/07/NumFocus_LRG.png?fit=320%2C148&ssl=1)](https://numfocus.org/) +[![Chan Zuckerberg Initiative](https://raw.githubusercontent.com/spyder-ide/spyder/master/img_src/czi.png)](https://chanzuckerberg.com/)[![Numfocus](https://i2.wp.com/numfocus.org/wp-content/uploads/2017/07/NumFocus_LRG.png?fit=320%2C148&ssl=1)](https://numfocus.org/) and the donations we have received from our users around the world through [Open Collective](https://opencollective.com/spyder/): diff --git a/img_src/czi.png b/img_src/czi.png new file mode 100644 index 0000000000000000000000000000000000000000..71d301a7a3e197fa9f811b63eb5ebe5e6a37a226 GIT binary patch literal 10971 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

W-XX`}B- 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 L%1e~J*Z2QFoXBdq literal 0 HcmV?d00001 diff --git a/spyder/config/base.py b/spyder/config/base.py index d859893967d..bd33f981654 100644 --- a/spyder/config/base.py +++ b/spyder/config/base.py @@ -69,6 +69,14 @@ def running_in_ci_with_conda(): return running_in_ci() and os.environ.get('USE_CONDA', None) == 'true' +def running_in_binder(): + """Return True if currently running in Binder.""" + return ( + os.environ.get("BINDER_REPO_URL") + and "spyder-ide/binder-environments" in os.environ["BINDER_REPO_URL"] + ) + + def is_stable_version(version): """ Return true if version is stable, i.e. with letters in the final component. diff --git a/spyder/plugins/ipythonconsole/utils/manager.py b/spyder/plugins/ipythonconsole/utils/manager.py index a1604a6418e..477d9529aac 100644 --- a/spyder/plugins/ipythonconsole/utils/manager.py +++ b/spyder/plugins/ipythonconsole/utils/manager.py @@ -18,6 +18,9 @@ from qtconsole.manager import QtKernelManager from traitlets import DottedObjectName +# Local imports +from spyder.config.base import running_in_binder + class SpyderKernelManager(QtKernelManager): """ @@ -93,9 +96,12 @@ async def _async_kill_kernel(self, restart: bool = False) -> None: if self.has_kernel: assert self.provisioner is not None - # This is the additional line that was added to properly - # kill the kernel started by Spyder. - await self.kill_proc_tree(self.provisioner.process.pid) + # This is the additional line that was added to properly kill the + # kernel started by Spyder. + # Note: We can't do this in Binder because it freezes Spyder. + # Fixes spyder-ide/spyder#22124 + if not running_in_binder(): + await self.kill_proc_tree(self.provisioner.process.pid) await self.provisioner.kill(restart=restart) From 27747d195eaaa4761416cde1899f299a3c52a675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Althviz=20Mor=C3=A9?= <16781833+dalthviz@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:56:19 -0500 Subject: [PATCH 37/41] Backport PR #22442: PR: Update contributing, release and maintenance instructions for backporting --- Announcements.md | 5 +--- CONTRIBUTING.md | 43 +++++------------------------ MAINTENANCE.md | 36 +++++++++++-------------- RELEASE.md | 70 +++++++++++++++++++++++++++++++----------------- 4 files changed, 69 insertions(+), 85 deletions(-) diff --git a/Announcements.md b/Announcements.md index c87658e8064..140d663b46c 100644 --- a/Announcements.md +++ b/Announcements.md @@ -111,8 +111,7 @@ For a complete list of changes, please see our [changelog](https://github.com/spyder-ide/spyder/blob/6.x/CHANGELOG.md) Spyder 5.0 has been a huge success and we hope 6.0 will be as successful. For that we -fixed 123 bugs, merged 292 pull requests from about 22 authors and added more than -3098 commits between these two releases. +fixed 123 bugs and merged 292 pull requests from about 22 authors. Don't forget to follow Spyder updates/news on the project's [website](https://www.spyder-ide.org). @@ -167,8 +166,6 @@ I'm pleased to announce the second beta of our next major version: Spyder **6.0* We've been working on this version for more than one year now and it's working relatively well. We encourage all people who like the bleeding edge to give it a try. -This release candidate version includes more than 74 commits over our latest release -candidate (6.0.0rc1). Spyder 6.0 comes with the following interesting new features and fixes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68452e327ab..20133c05d81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,47 +124,18 @@ $ python runtests.py ## Spyder Branches -When you start to work on a new pull request (PR), you need to be sure that your work is done on top of the correct Spyder branch, and that you base your PR on Github against it. - -To guide you, issues on Github are marked with a milestone that indicates the correct branch to use. If not, follow these guidelines: - -* Use the `5.x` branch for bugfixes only (*e.g.* milestones `v5.0.1` or `v5.1.2`) -* Use `master` to introduce new features or break compatibility with previous Spyder versions (*e.g.* milestones `v6.0beta1` or `v6.0beta2`). - -You should also submit bugfixes to `5.x` or `master` for errors that are only present in those respective branches. - -To start working on a new PR, you need to execute these commands, filling in the branch names where appropriate: +To start working on a new pull request you need to execute these commands, filling in the branch name where appropriate: ```bash -$ git checkout -$ git pull upstream +$ git checkout master +$ git pull upstream master $ git checkout -b ``` -### Changing the base branch - -If you started your work in the wrong base branch, or want to backport it, you can change the base branch using `git rebase --onto`, like this: - -```bash -$ git rebase --onto -``` - -For example, backporting `my_branch` from `master` to `5.x`: - -```bash -$ git rebase --onto 5.x master my_branch -``` - - ## Making contributions that depend on pull requests in spyder-kernels -Spyder and spyder-kernels are developed jointly because a lot of communication happens between them in order to run code written in the editor in the IPython console. The way the branches on their respective repos are linked appears in the table below: - -| Spyder branch | Associated spyder-kernels branch | -| ------------------- | --------------------------------- | -| 5.x | 2.x | -| master (future 6.x) | master (future 3.x) | +Spyder and spyder-kernels are developed jointly because a lot of communication happens between them in order to run code written in the editor in the IPython console. For this reason, a clone of spyder-kernels is placed in the `external-deps` subfolder of the Spyder repository. The instructions on this section will help you in case you need to make changes that touch both repositories at the same time. @@ -176,7 +147,7 @@ echo 'source /path/to/git-subrepo/.rc' >> ~/.bashrc source ~/.bashrc ``` -As an example, let's assume that (i) your Github user name is `myuser`; (ii) you have two git repositories placed at `~/spyder` and `~/spyder-kernels` that link to `https://github.com/myuser/spyder` and `https://github.com/myuser/spyder-kernels` respectively; and (iii) you have two branches named `fix_in_spyder` and `fix_in_kernel` in each of these git repos respectively. If you want to open a joint PR in `spyder` and `spyder-kernels` that link these branches, here is how to do it: +As an example, let's assume that (i) your Github user name is `myuser`; (ii) you have two git clones placed at `~/spyder` and `~/spyder-kernels` that link to `https://github.com/myuser/spyder` and `https://github.com/myuser/spyder-kernels` respectively; and (iii) you have two branches named `fix_in_spyder` and `fix_in_kernel` in each of these git repos respectively. If you want to open a joint PR in `spyder` and `spyder-kernels` that link these branches, here is how to do it: * Go to the `~/spyder` folder, checkout your `fix_in_spyder` branch and replace the spyder-kernels clone in the `external-deps` subfolder by a clone of your `fix_in_kernel` branch: @@ -207,11 +178,9 @@ As an example, let's assume that (i) your Github user name is `myuser`; (ii) you * When your `fix_in_kernel` PR is merged, you need to update Spyder's `fix_in_spyder` branch because the clone in Spyder's repo must point out again to the spyder-kernel's repo and not to your own clone. For that, please run: ``` - $ git subrepo pull external-deps/spyder-kernels -r https://github.com/spyder-ide/spyder-kernels.git -b -u -f + $ git subrepo pull external-deps/spyder-kernels -r https://github.com/spyder-ide/spyder-kernels.git -b master -u -f ``` -where `` needs to be `2.x` if your `fix_in_spyder` branch was done against Spyder's `5.x` branch; and `master`, if you did it against our `master` branch here. - ## Making contributions that depend on pull requests in python-lsp-server or qtconsole diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 79ab911a70d..0f1ffa6bf03 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -1,31 +1,27 @@ These are some instructions meant for maintainers of this repo. * To avoid pushing to our main repo by accident, please use `https` for your `usptream` remote. That should make git to ask for your credentials (at least in Unix systems). -* After merging a PR against the stable branch (e.g. `5.x`), you need to immediately merge it against `master` and push your changes to Github. - For that you need to perform the following steps in your local clone: - - git checkout 5.x - - git fetch upstream - - git merge upstream/5.x - - git checkout master - - git merge 5.x - - Commit with the following message: +* After merging a PR that needs to be included in stable branch (e.g. `6.x`), you need to call the `meeseeksdev` bot by adding a comment to the same PR with the followng syntax: - Merge from 5.x: PR # + `@meeseeksdev please backport to 6.x` - Fixes # +* If `meeseeksdev` fails to do the backport, you need to manually open a PR against the stable branch to do it with the following actions: - If the PR doesn't fix any issue, the second line is unnecessary. - - git push upstream master + - `git checkout 6.x` + - `git checkout -b backport-of-pr-` + - `git cherry-pick ` + - Solve conflicts -* To merge against `master` a PR that involved updating our spyder-kernels subrepo in the stable branch (e.g. `5.x`), you need to perform the following actions: +* If a PR that involved updating our spyder-kernels subrepo and needs to be included in the stable branch (e.g. `6.x`), you need to manually create a PR against it with the following actions: - - git checkout master - - git merge 5.x - - git reset -- external-deps/spyder-kernels - - git checkout -- external-deps/spyder-kernels - - git commit with the files left and the same message format as above. - - git subrepo pull external-deps/spyder-kernels + - `git checkout 6.x` + - `git checkout -b backport-of-pr-` + - `git cherry-pick ` + - `git reset -- external-deps/spyder-kernels` + - `git checkout -- external-deps/spyder-kernels` + - `git commit` with the files left + - `git subrepo pull external-deps/spyder-kernels` * If a PR in spyder-kernels solves an issue in Spyder but was not part of a PR that updated its subrepo, you need to open one that does precisely that, i.e. update its subrepo, in order to fix that issue. @@ -33,6 +29,6 @@ These are some instructions meant for maintainers of this repo. * There's a bot that constantly monitors all issues in order to close duplicates or already solved issues and inform users what they can do about them (basically wait to be fixed or update). - The patterns detected by the bot and the messages shown to users can be configured in `.github/workflows/duplicates.yml` (only avaiable in our `master` branch because there's no need to have it in `5.x`). + The patterns detected by the bot and the messages shown to users can be configured in `.github/workflows/duplicates.yml` (only avaiable in our `master` branch because there's no need to have it in the stable one). Please open a PR to add new messages or update previous ones, so other members of the team can decide if the messages are appropriate. diff --git a/RELEASE.md b/RELEASE.md index 10c6d96223e..8bd7c18971b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,6 +2,20 @@ To release a new version of Spyder you need to follow these steps: + +## Create backport PR for new minor versions + +Before releasing a new minor version (e.g. 6.1.0 after 6.0.x) that needs to include many changes only available in `master`, it's necessary to create a PR to backport those changes to the stable branch. + +For that you need to run the following commands: + +- `git checkout 6.x` +- `git checkout -b backport-for-minor-version` +- `git diff master 6.x > minor.patch` +- `patch -p1 -R < minor.patch` +- `git add .` and `git commit -m "Backport changes for X.X.X"` + + ## Update translation strings (at least one week before the release) * Install [gettext-helpers](https://github.com/spyder-ide/gettext-helpers) from source. @@ -32,6 +46,7 @@ To release a new version of Spyder you need to follow these steps: https://github.com/spyder-ide/spyder/issues/14117 + ## Before starting the release ### Merge translations from Crowdin @@ -73,15 +88,11 @@ To release a new version of Spyder you need to follow these steps: * Don't forget to remove your local checkout of `translate/` because that's going to be outdated for next time. -* Update the `master` and `` branches as necessary. For example, if translations were done for the stable branch `6.x`, you could do the update with +* Update the `` branch as necessary. For example, if translations were done for the stable branch `6.x`, you could do the update with git checkout 6.x git fetch upstream git merge upstream/6.x - git checkout master - git merge 6.x - Merge from 6.x: PR #xxxxx - git push upstream master ### Update core dependencies @@ -130,7 +141,7 @@ To release a new version of Spyder you need to follow these steps: ### Check release candidate -* Update version in `__init__.py` (set release version, remove '.dev0', add 'rcX'), then +* Update version in `__init__.py` (set release version, remove 'dev0', add 'rcX'), then git add . git commit -m "Release X.X.XrcX [ci skip]" @@ -146,26 +157,24 @@ To release a new version of Spyder you need to follow these steps: * If one of the previous steps fail, merge a fix PR and start the process again with an incremented 'rcX' commit. -## To do the PyPI release and version tag -* Close the current milestone on Github +## Update Changelog and Announcements -* git pull or git fetch/merge the respective branch that will be released (e.g `6.x` - stable branch or `master` - alphas or betas of a new major version). +* Create a PR in master to update those files by following the steps below. -* For a new major release (e.g. version 6.0.0 after 5.5.6): +* For a new major release (e.g. 6.0.0): - - `git checkout -b 6.x` - - `git checkout master` - - Update version in `__init__.py` to reflect next major version as dev version (i.e `7.0.0.dev0`). - - `git add .` and `git commit -m "Bump version to 7.0"` - - `git checkout 6.x` - In `CHANGELOG.md`, move entry for current version to the `Older versions` section and add one for the new version. - `git add .` and `git commit -m "Add link to changelog of new major version"` + - In `.github/workflows/test-*.yml`, add `6.*` to the `branches` sections and remove the oldest branch from them (e.g. `4.*`). + - `git add .` and `git commit -m "CI: Update workflows to run in new stable branch [ci skip]"` -* Update `changelogs/Spyder-X.md` (`changelogs/Spyder-6.md` for Spyder 6 for example) with `loghub spyder-ide/spyder -m vX.X.X` +* For the first alpha of a new major version (e.g 7.0.0a1): - - When releasing the first alpha of a new major version (e.g. Spyder 7), you need to add a new file called `changelogs/Spyder-X+1.md` to the tree (`changelogs/Spyder-7.md` for Spyder 7 for example). - - After that, add `changelogs/Spyder-X+1.md` to `MANIFEST.in`, remove `changelogs/Spyder-X.md` from it and add that path to the `check-manifest/ignore` section of `setup.cfg`. + - Add a new file called `changelogs/Spyder-X+1.md` to the tree (e.g. `changelogs/Spyder-7.md`). + - Add `changelogs/Spyder-X+1.md` to `MANIFEST.in`, remove `changelogs/Spyder-X.md` from it and add that path to the `check-manifest/ignore` section of `setup.cfg`. + +* Update `changelogs/Spyder-X.md` (`changelogs/Spyder-6.md` for Spyder 6 for example) with `loghub spyder-ide/spyder -m vX.X.X` * Add sections for `New features`, `Important fixes` and `New API features` in `changelogs/Spyder-X.md`. For this take a look at closed issues and PRs for the current milestone. @@ -175,9 +184,26 @@ To release a new version of Spyder you need to follow these steps: * `git add .` and `git commit -m "Update Announcements"` +* Once merged, backport the PR that contains these changes to the stable branch (e.g. `6.x`) + + +## To do the PyPI release and version tag + +* Close the current milestone on Github + +* git pull or git fetch/merge the respective branch that will be released (e.g `6.x` - stable branch or `master` - alphas/betas/rcs of a new minor/major version). + +* For a new major release (e.g. version 6.0.0 after 5.5.6): + + - `git checkout -b 6.x` + - `git checkout master` + - Update version in `__init__.py` to reflect next minor version as dev version (i.e `6.1.0a1.dev0`). + - `git add .` and `git commit -m "Bump version to new minor version"` + - `git checkout 6.x` + * `git clean -xfdi` and select option `1` -* Update version in `__init__.py` (set release version, remove 'dev0') +* Update version in `__init__.py` (Remove '{a/b/rc}X' and 'dev0' for stable versions; or remove 'dev0' for pre-releases) * `git add .` and `git commit -m "Release X.X.X"` @@ -203,16 +229,12 @@ To release a new version of Spyder you need to follow these steps: * `git tag -a vX.X.X -m "Release X.X.X"` -* Update version in `__init__.py` (add 'dev0' and increment minor) +* Update version in `__init__.py` (add 'a1', 'dev0' and increment patch version for stable versions; or increment alpha/beta/rc version for pre-releases) * `git add .` and `git commit -m "Back to work [ci skip]"` * Push changes and new tag to the corresponding branches. When doing a stable release from `6.x`, for example, you could push changes with - git checkout master - git merge 6.x - git commit -m "Release X.X.X [ci skip]" - git push upstream master git push upstream 6.x git push upstream --tags From e307e2584da513461cf65e962cc937e967d9adcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Althviz=20Mor=C3=A9?= <16781833+dalthviz@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:57:15 -0500 Subject: [PATCH 38/41] Backport PR #22424: PR: Prioritize conda-forge channel so that stable releases are pulled above unstable ones (Installers) --- .github/scripts/installer_test.sh | 24 +++++++++++++++----- .github/workflows/installers-conda.yml | 31 +++++++++++++++++++++++--- installers-conda/build_installers.py | 11 +++++++-- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/.github/scripts/installer_test.sh b/.github/scripts/installer_test.sh index 1291164afc2..870a51b4a5d 100755 --- a/.github/scripts/installer_test.sh +++ b/.github/scripts/installer_test.sh @@ -25,7 +25,7 @@ check_prefix() { fi if [[ -d "$base_prefix" ]]; then - echo "\nContents of ${base_prefix}:" + echo -e "\nContents of ${base_prefix}:" ls -al $base_prefix else echo "Base prefix does not exist!" @@ -49,15 +49,16 @@ check_shortcut() { shortcut=$($pythonexe $menuinst shortcut --mode=user) if [[ -e "${shortcut}" ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then - echo "\n${shortcut}/Contents/MacOS contents:" + echo -e "\nContents of ${shortcut}/Contents/MacOS:" ls -al "${shortcut}/Contents/MacOS" - echo -e "\n$shortcut/Contents/Info.plist contents:" + echo -e "\nContents of $shortcut/Contents/Info.plist:" cat "${shortcut}/Contents/Info.plist" script=$(compgen -G "${shortcut}/Contents/MacOS/spyder"*-script) - echo -e "\n${script} contents:" + echo -e "\nContents of ${script}:" cat "${script}" + echo "" elif [[ "$OSTYPE" == "linux"* ]]; then - echo -e "\n${shortcut} contents:" + echo -e "\nContents of ${shortcut}:" cat $shortcut fi else @@ -66,10 +67,23 @@ check_shortcut() { fi } +check_spyder_version() { + runtime_python=${base_prefix}/envs/spyder-runtime/bin/python + actual_version=$(${runtime_python} -c "import spyder; print(spyder.__version__)") + echo -e "\nExpected version = ${SPYVER}" + echo "Actual version = ${actual_version}" + if [[ "${SPYVER}" != "${actual_version}" ]]; then + echo "Error: installed Spyder version is incorrect!" + exit_status=1 + fi +} + install || exit 1 +echo -e "\n#############" echo "Install info:" check_prefix check_uninstall check_shortcut +check_spyder_version exit $exit_status diff --git a/.github/workflows/installers-conda.yml b/.github/workflows/installers-conda.yml index a64179b6ddf..c40f4bc0730 100644 --- a/.github/workflows/installers-conda.yml +++ b/.github/workflows/installers-conda.yml @@ -264,7 +264,9 @@ jobs: [[ -n $CNAME ]] && args=("--cert-id" "$CNAME") || args=() python build_installers.py ${args[@]} + SPYVER=$(python build_installers.py --version) PKG_NAME=$(ls $DISTDIR | grep Spyder-) + echo "SPYVER=$SPYVER" >> $GITHUB_ENV echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV echo "ARTIFACT_NAME=${PKG_NAME%.*}" >> $GITHUB_ENV echo "PKG_PATH=$DISTDIR/$PKG_NAME" >> $GITHUB_ENV @@ -279,7 +281,14 @@ jobs: run: | set base_prefix=%USERPROFILE%\AppData\Local\spyder-6 start /wait %PKG_PATH% /InstallationType=JustMe /NoRegistry=1 /S - if exist %base_prefix%\install.log type %base_prefix%\install.log + + echo. + if exist %base_prefix%\install.log ( + echo Log output: + type %base_prefix%\install.log + ) else ( + echo No log found at %base_prefix%\install.log + ) set mode=system for /F "tokens=*" %%i in ( @@ -287,12 +296,28 @@ jobs: ) do ( set shortcut=%%~fi ) + echo. if exist "%shortcut%" ( - echo "Spyder installed successfully" + echo Spyder installed successfully ) else ( - echo "Spyder NOT installed successfully" + echo Spyder NOT installed successfully + EXIT /B 1 + ) + + set runtime_python=%base_prefix%\envs\spyder-runtime\python + for /F "tokens=*" %%i in ( + '%runtime_python% -c "import spyder; print(spyder.__version__)"' + ) do ( + set actual_version=%%~i + ) + echo. + echo Expected version = %SPYVER% + echo Actual version = %actual_version% + if %SPYVER% neq %actual_version% ( + echo Error: installed Spyder version is incorrect! EXIT /B 1 ) + EXIT /B %ERRORLEVEL% - name: Notarize or Compute Checksum diff --git a/installers-conda/build_installers.py b/installers-conda/build_installers.py index f3344b66d42..42fff748eee 100644 --- a/installers-conda/build_installers.py +++ b/installers-conda/build_installers.py @@ -128,6 +128,10 @@ "--conda-lock", action="store_true", help="Create conda-lock file and exit." ) +p.add_argument( + "--version", action="store_true", + help="Print Spyder version and exit." +) args = p.parse_args() yaml = YAML() @@ -186,10 +190,10 @@ def _create_conda_lock(env_type='base'): definitions = { "channels": [ - CONDA_BLD_PATH, + "conda-forge", "conda-forge/label/spyder_dev", "conda-forge/label/spyder_kernels_rc", - "conda-forge" + CONDA_BLD_PATH, ], "dependencies": [k + v for k, v in specs.items()], "platforms": [TARGET_PLATFORM] @@ -511,5 +515,8 @@ def main(): _create_conda_lock(env_type='base') _create_conda_lock(env_type='runtime') sys.exit() + if args.version: + print(SPYVER) + sys.exit() main() From 949e11bfa472b63471ca58d240ac1fbc3ed30262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Althviz=20Mor=C3=A9?= <16781833+dalthviz@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:42:26 -0500 Subject: [PATCH 39/41] Backport PR #22490: PR: Prevent error when updating `sys.path` in consoles (IPython console) --- spyder/plugins/ipythonconsole/widgets/shell.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py index 91c29a969e9..31f41dd84f5 100644 --- a/spyder/plugins/ipythonconsole/widgets/shell.py +++ b/spyder/plugins/ipythonconsole/widgets/shell.py @@ -705,10 +705,14 @@ def set_color_scheme(self, color_scheme, reset=True): ) def update_syspath(self, path_dict, new_path_dict): - """Update sys.path contents on kernel.""" - self.call_kernel( - interrupt=True, - blocking=False).update_syspath(path_dict, new_path_dict) + """Update sys.path contents in the kernel.""" + # Prevent error when the kernel is not available and users open/close + # projects or use the Python path manager. + # Fixes spyder-ide/spyder#21563 + if self.kernel_handler is not None: + self.call_kernel(interrupt=True, blocking=False).update_syspath( + path_dict, new_path_dict + ) def request_syspath(self): """Ask the kernel for sys.path contents.""" From 3fccd356a16449f4e33c6f460167f11a29609b2e Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Wed, 18 Sep 2024 07:13:58 +0200 Subject: [PATCH 40/41] git subrepo clone (merge) --branch=improve_namespace --force https://github.com/impact27/spyder-kernels.git external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "40a123630" upstream: origin: "https://github.com/impact27/spyder-kernels.git" branch: "improve_namespace" commit: "40a123630" git-subrepo: version: "0.4.5" origin: "https://github.com/ingydotnet/git-subrepo" commit: "aa416e4" --- external-deps/spyder-kernels/.gitrepo | 4 ++-- external-deps/spyder-kernels/spyder_kernels/_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index a2b119512cc..a53c5a901ee 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 = improve_namespace - commit = 6796a55cd35bd3bf5dc7cf0609a744f6edb7d071 - parent = 097ccc2e24b4e9e61c036bc1c091413da1c2f2f4 + commit = 40a123630716bbd846f908695e7c317032ddfb21 + parent = 9cac28ae234d7130767822046025e3760b2d802c method = merge cmdver = 0.4.5 diff --git a/external-deps/spyder-kernels/spyder_kernels/_version.py b/external-deps/spyder-kernels/spyder_kernels/_version.py index 1d3a5157c94..2d20379b9d0 100644 --- a/external-deps/spyder-kernels/spyder_kernels/_version.py +++ b/external-deps/spyder-kernels/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (4, 0, 0, 'dev0') +VERSION_INFO = (3, 1, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) From 2f1cf2d26990f196754e7564a632efb48a1ea2d6 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Fri, 4 Oct 2024 03:22:42 -0400 Subject: [PATCH 41/41] fix empty pane widget --- spyder/plugins/profiler/widgets/main_widget.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index e4eb380db16..0c40dc46d10 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -26,6 +26,7 @@ from spyder.api.shellconnect.main_widget import ShellConnectMainWidget from spyder.plugins.profiler.widgets.profiler_data_tree import ( ProfilerSubWidget) +from spyder.widgets.helperwidgets import PaneEmptyWidget class ProfilerWidgetActions: @@ -238,8 +239,10 @@ def update_actions(self): ProfilerWidgetActions.ToggleTreeDirection) toggle_builtins_action = self.get_action( ProfilerWidgetActions.ToggleBuiltins) + + widget_inactive = widget is None or isinstance(widget, PaneEmptyWidget) - if widget is None: + if widget_inactive: search = False inverted_tree = False ignore_builtins = False @@ -257,7 +260,8 @@ def update_actions(self): can_undo = False can_clear = False widget = self.current_widget() - if widget is not None: + widget_inactive = widget is None or isinstance(widget, PaneEmptyWidget) + if not widget_inactive: tree_empty = widget.data_tree.profdata is None can_undo = len(widget.data_tree.history) > 1 can_redo = len(widget.data_tree.redo_history) > 0