diff --git a/CHANGELOG.md b/CHANGELOG.md index de4cfdd0..944e04c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # History of changes +## Version 3.0.0b2 (2023-08-22) + +### Pull Requests Merged + +* [PR 465](https://github.com/spyder-ide/spyder-kernels/pull/465) - Save temporary file in test to temporary location, by [@juliangilbey](https://github.com/juliangilbey) +* [PR 464](https://github.com/spyder-ide/spyder-kernels/pull/464) - Remove locals inspection, by [@impact27](https://github.com/impact27) +* [PR 460](https://github.com/spyder-ide/spyder-kernels/pull/460) - PR: Add a global filter flag to settings, by [@jsbautista](https://github.com/jsbautista) +* [PR 445](https://github.com/spyder-ide/spyder-kernels/pull/445) - PR: Add `exitdb` command and some speed optimizations to the debugger, by [@impact27](https://github.com/impact27) +* [PR 429](https://github.com/spyder-ide/spyder-kernels/pull/429) - PR: Add a comm handler decorator, by [@impact27](https://github.com/impact27) +* [PR 411](https://github.com/spyder-ide/spyder-kernels/pull/411) - PR: Remove `set_debug_state` and `do_where` calls, by [@impact27](https://github.com/impact27) + +In this release 6 pull requests were closed. + + +---- + + ## Version 3.0.0b1 (2023-06-14) ### Issues Closed @@ -43,6 +60,29 @@ In this release 25 pull requests were closed. ---- +## Version 2.5.0 (2023-11-06) + +### New features + +* Add support for chained exceptions to the debugger. +* Improve getting signatures from docstrings. +* Restore compatibility with Python 2. + +### Pull Requests Merged + +* [PR 475](https://github.com/spyder-ide/spyder-kernels/pull/475) - PR: Skip IPython 8.17.1 in our dependencies for Python 3.9+, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 474](https://github.com/spyder-ide/spyder-kernels/pull/474) - PR: More improvements to getting signatures from text, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 473](https://github.com/spyder-ide/spyder-kernels/pull/473) - PR: Improve getting signatures from docstrings and catch error when trying to get the signature of some objects, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 472](https://github.com/spyder-ide/spyder-kernels/pull/472) - PR: Add support for chained exceptions to the debugger, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 471](https://github.com/spyder-ide/spyder-kernels/pull/471) - PR: Improve the way we depend on IPython and IPykernel per Python version, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 469](https://github.com/spyder-ide/spyder-kernels/pull/469) - PR: Restore compatibility with Python 2, by [@ccordoba12](https://github.com/ccordoba12) + +In this release 6 pull requests were closed. + + +---- + + ## Version 2.4.4 (2023-06-29) ### Issues Closed diff --git a/requirements/posix.txt b/requirements/posix.txt index 005ccb31..aa335c18 100644 --- a/requirements/posix.txt +++ b/requirements/posix.txt @@ -1,6 +1,6 @@ cloudpickle ipykernel>=6.23.2,<7 -ipython>=7.31.1,<9 +ipython>=8.12.2,<9 jupyter_client>=7.4.9,<9 pyzmq>=22.1.0 wurlitzer>=1.0.3 diff --git a/requirements/windows.txt b/requirements/windows.txt index d87d5415..ec6e8483 100644 --- a/requirements/windows.txt +++ b/requirements/windows.txt @@ -1,5 +1,5 @@ cloudpickle ipykernel>=6.23.2,<7 -ipython>=7.31.1,<9 +ipython>=8.12.2,<9 jupyter_client>=7.4.9,<9 pyzmq>=22.1.0 diff --git a/setup.py b/setup.py index 78460701..b1eea42d 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,8 @@ def get_version(module='spyder_kernels'): REQUIREMENTS = [ 'cloudpickle', 'ipykernel>=6.23.2,<7', - 'ipython>=7.31.1,<9,!=8.8.0,!=8.9.0,!=8.10.0,!=8.11.0,!=8.12.0,!=8.12.1', + 'ipython>=8.12.2,<8.13; python_version=="3.8"', + 'ipython>=8.13.0,<9,!=8.17.1; python_version>"3.8"', 'jupyter-client>=7.4.9,<9', 'pyzmq>=22.1.0', 'wurlitzer>=1.0.3;platform_system!="Windows"', @@ -79,7 +80,7 @@ def get_version(module='spyder_kernels'): install_requires=REQUIREMENTS, extras_require={'test': TEST_REQUIREMENTS}, include_package_data=True, - python_requires='>=3.7', + python_requires='>=3.8', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Jupyter', @@ -88,10 +89,11 @@ def get_version(module='spyder_kernels'): 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Software Development :: Interpreters', ] ) diff --git a/spyder_kernels/comms/frontendcomm.py b/spyder_kernels/comms/frontendcomm.py index 1e7e86f4..9eaf2ee1 100644 --- a/spyder_kernels/comms/frontendcomm.py +++ b/spyder_kernels/comms/frontendcomm.py @@ -130,9 +130,11 @@ def _check_comm_reply(self): """ Send comm message to frontend to check if the iopub channel is ready """ - if len(self._pending_comms) == 0: + # Make sure the length doesn't change during iteration + pending_comms = list(self._pending_comms.values()) + if len(pending_comms) == 0: return - for comm in self._pending_comms.values(): + for comm in pending_comms: self._notify_comm_ready(comm) self.kernel.io_loop.call_later(1, self._check_comm_reply) diff --git a/spyder_kernels/comms/utils.py b/spyder_kernels/comms/utils.py index 68ea8c57..dc6b8c99 100644 --- a/spyder_kernels/comms/utils.py +++ b/spyder_kernels/comms/utils.py @@ -67,11 +67,8 @@ def __call__(self, string): if not self._warning_shown: self._warning_shown = True - # Don't print handler name for `show_mpl_backend_errors` - # because we have a specific message for it. # request_pdb_stop is expected to print messages. - if self._name not in [ - 'show_mpl_backend_errors', 'request_pdb_stop']: + if self._name not in ['request_pdb_stop']: self._write( "\nOutput from spyder call " + repr(self._name) + ":\n" ) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index d900d8a7..0585077a 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -29,12 +29,12 @@ from zmq.utils.garbage import gc # Local imports +import spyder_kernels 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) +from spyder_kernels.utils.mpl import automatic_backend, MPL_BACKENDS_TO_SPYDER from spyder_kernels.utils.nsview import ( get_remote_data, make_remote_view, get_size) from spyder_kernels.console.shell import SpyderShell @@ -65,7 +65,6 @@ def __init__(self, *args, **kwargs): register_comm_handlers(self.shell, self.frontend_comm) self.namespace_view_settings = {} - self._mpl_backend_error = None self.faulthandler_handle = None self._cwd_initialised = False @@ -77,6 +76,18 @@ def __init__(self, *args, **kwargs): # Socket to signal shell_stream locally self.loopback_socket = None + @property + def kernel_info(self): + # Used for checking correct version by spyder + infos = super().kernel_info + infos.update({ + "spyder_kernels_info": ( + spyder_kernels.__version__, + sys.executable + ) + }) + return infos + # -- Public API ----------------------------------------------------------- def frontend_call(self, blocking=False, broadcast=True, timeout=None, callback=None, display_error=False): @@ -114,7 +125,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,6 +150,11 @@ def enable_faulthandler(self): faulthandler.enable(self.faulthandler_handle) return self.faulthandler_handle.name, main_id, system_ids + @comm_handler + def safe_exec(self, filename): + """Safely execute a file using IPKernelApp._exec_file.""" + self.parent._exec_file(filename) + @comm_handler def get_fault_text(self, fault_filename, main_id, ignore_ids): """Get fault text from old run.""" @@ -260,11 +275,6 @@ 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): """ @@ -529,7 +539,7 @@ def get_mpl_interactive_backend(self): if framework is None: # Since no interactive backend has been set yet, this is # equivalent to having the inline one. - return 0 + return 'inline' elif framework in mapping: return MPL_BACKENDS_TO_SPYDER[mapping[framework]] else: @@ -538,25 +548,42 @@ def get_mpl_interactive_backend(self): # magic but not through our Preferences. return -1 - def set_matplotlib_backend(self, backend, pylab=False): - """Set matplotlib backend given a Spyder backend option.""" - mpl_backend = MPL_BACKENDS_FROM_SPYDER[str(backend)] - self._set_mpl_backend(mpl_backend, pylab=pylab) - - def set_mpl_inline_figure_format(self, figure_format): - """Set the inline figure format to use with matplotlib.""" - mpl_figure_format = INLINE_FIGURE_FORMATS[figure_format] - self._set_config_option( - 'InlineBackend.figure_format', mpl_figure_format) - - def set_mpl_inline_resolution(self, resolution): - """Set inline figure resolution.""" - self._set_mpl_inline_rc_config('figure.dpi', resolution) + @comm_handler + def set_matplotlib_conf(self, conf): + """Set matplotlib configuration""" + pylab_autoload_n = 'pylab/autoload' + pylab_backend_n = 'pylab/backend' + figure_format_n = 'pylab/inline/figure_format' + resolution_n = 'pylab/inline/resolution' + width_n = 'pylab/inline/width' + height_n = 'pylab/inline/height' + bbox_inches_n = 'pylab/inline/bbox_inches' + inline_backend = 'inline' + + if pylab_autoload_n in conf or pylab_backend_n in conf: + self._set_mpl_backend( + conf.get(pylab_backend_n, inline_backend), + pylab=conf.get(pylab_autoload_n, False) + ) + + if figure_format_n in conf: + self._set_config_option( + 'InlineBackend.figure_format', + conf[figure_format_n] + ) + + if resolution_n in conf: + self._set_mpl_inline_rc_config('figure.dpi', conf[resolution_n]) + + if width_n in conf and height_n in conf: + self._set_mpl_inline_rc_config( + 'figure.figsize', + (conf[width_n], conf[height_n]) + ) + + if bbox_inches_n in conf: + self.set_mpl_inline_bbox_inches(conf[bbox_inches_n]) - def set_mpl_inline_figure_size(self, width, height): - """Set inline figure size.""" - value = (width, height) - self._set_mpl_inline_rc_config('figure.figsize', value) def set_mpl_inline_bbox_inches(self, bbox_inches): """ @@ -599,11 +626,54 @@ def set_autocall(self, autocall): # --- Additional methods @comm_handler - def set_cwd(self, dirname): - """Set current working directory.""" - self._cwd_initialised = True - os.chdir(dirname) - self.publish_state() + def set_configuration(self, conf): + """Set kernel configuration""" + ret = {} + for key, value in conf.items(): + if key == "cwd": + self._cwd_initialised = True + os.chdir(value) + self.publish_state() + elif key == "namespace_view_settings": + self.namespace_view_settings = value + self.publish_state() + elif key == "pdb": + self.shell.set_pdb_configuration(value) + elif key == "faulthandler": + if value: + ret[key] = self.enable_faulthandler() + elif key == "special_kernel": + try: + self.set_special_kernel(value) + except Exception: + ret["special_kernel_error"] = value + elif key == "color scheme": + self.set_color_scheme(value) + elif key == "jedi_completer": + self.set_jedi_completer(value) + elif key == "greedy_completer": + self.set_greedy_completer(value) + elif key == "autocall": + self.set_autocall(value) + elif key == "matplotlib": + self.set_matplotlib_conf(value) + elif key == "update_gui": + self.shell.update_gui_frontend = value + elif key == "wurlitzer": + if value: + self._load_wurlitzer() + elif key == "autoreload_magic": + self._autoreload_magic(value) + return ret + + def set_color_scheme(self, color_scheme): + if color_scheme == "dark": + # Needed to change the colors of tracebacks + self.shell.run_line_magic("colors", "linux") + self.set_sympy_forecolor(background_color='dark') + elif color_scheme == "light": + self.shell.run_line_magic("colors", "lightbg") + self.set_sympy_forecolor(background_color='light') def get_cwd(self): """Get current working directory.""" @@ -631,28 +701,57 @@ def close_all_mpl_figures(self): except: pass - @comm_handler - def is_special_kernel_valid(self): + def set_special_kernel(self, special): """ Check if optional dependencies are available for special consoles. """ - try: - if os.environ.get('SPY_AUTOLOAD_PYLAB_O') == 'True': - import matplotlib - elif os.environ.get('SPY_SYMPY_O') == 'True': - import sympy - elif os.environ.get('SPY_RUN_CYTHON') == 'True': - import cython - except Exception: - # Use Exception instead of ImportError here because modules can - # fail to be imported due to a lot of issues. - if os.environ.get('SPY_AUTOLOAD_PYLAB_O') == 'True': - return u'matplotlib' - elif os.environ.get('SPY_SYMPY_O') == 'True': - return u'sympy' - elif os.environ.get('SPY_RUN_CYTHON') == 'True': - return u'cython' - return None + self.shell.special = None + if special is None: + return + + if special == "pylab": + import matplotlib + exec("from pylab import *", self.shell.user_ns) + self.shell.special = special + return + + if special == "sympy": + import sympy + sympy_init = "\n".join([ + "from sympy import *", + "x, y, z, t = symbols('x y z t')", + "k, m, n = symbols('k m n', integer=True)", + "f, g, h = symbols('f g h', cls=Function)", + "init_printing()", + ]) + exec(sympy_init, self.shell.user_ns) + self.shell.special = special + return + + if special == "cython": + import cython + + # Import pyximport to enable Cython files support for + # import statement + import pyximport + pyx_setup_args = {} + + # Add Numpy include dir to pyximport/distutils + try: + import numpy + pyx_setup_args['include_dirs'] = numpy.get_include() + except Exception: + pass + + # Setup pyximport and enable Cython files reload + pyximport.install(setup_args=pyx_setup_args, + reload_support=True) + + self.shell.run_line_magic("reload_ext", "Cython") + self.shell.special = special + return + + raise NotImplementedError(f"{special}") @comm_handler def update_syspath(self, path_dict, new_path_dict): @@ -805,6 +904,9 @@ def _set_mpl_backend(self, backend, pylab=False): magic = 'pylab' if pylab else 'matplotlib' + if backend == "auto": + backend = automatic_backend() + error = None try: # This prevents Matplotlib to automatically set the backend, which @@ -843,8 +945,8 @@ def _set_mpl_backend(self, backend, pylab=False): error = generic_error.format(err) + '\n\n' + additional_info except Exception: error = generic_error.format(traceback.format_exc()) - - self._mpl_backend_error = error + if error: + print(error) def _set_config_option(self, option, value): """ @@ -876,31 +978,30 @@ 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': - try: - from sympy import init_printing - if background_color == 'dark': - init_printing(forecolor='White', ip=self.shell) - elif background_color == 'light': - init_printing(forecolor='Black', ip=self.shell) - except Exception: - pass + if self.shell.special != "sympy": + return + + try: + from sympy import init_printing + if background_color == 'dark': + init_printing(forecolor='White', ip=self.shell) + elif background_color == 'light': + init_printing(forecolor='Black', ip=self.shell) + except Exception: + pass # --- Others - def _load_autoreload_magic(self): + def _autoreload_magic(self, enable): """Load %autoreload magic.""" try: - self.shell.run_line_magic('reload_ext', 'autoreload') - self.shell.run_line_magic('autoreload', '2') + if enable: + self.shell.run_line_magic('reload_ext', 'autoreload') + self.shell.run_line_magic('autoreload', "2") + else: + self.shell.run_line_magic('autoreload', "off") + except Exception: pass diff --git a/spyder_kernels/console/shell.py b/spyder_kernels/console/shell.py index 49cde462..5256bb67 100644 --- a/spyder_kernels/console/shell.py +++ b/spyder_kernels/console/shell.py @@ -23,11 +23,9 @@ from ipykernel.zmqshell import ZMQInteractiveShell # Local imports -import spyder_kernels from spyder_kernels.customize.namespace_manager import NamespaceManager 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 @@ -51,16 +49,12 @@ def __init__(self, *args, **kwargs): # Create _namespace_stack before __init__ self._namespace_stack = [] self._request_pdb_stop = False + self.special = None self._pdb_conf = {} super(SpyderShell, self).__init__(*args, **kwargs) self._allow_kbdint = False self.register_debugger_sigint() - - # Used for checking correct version by spyder - self._spyder_kernels_version = ( - spyder_kernels.__version__, - sys.executable - ) + self.update_gui_frontend = False # register post_execute self.events.register('post_execute', self.do_post_execute) @@ -94,14 +88,16 @@ def enable_matplotlib(self, gui=None): if gui is None or gui.lower() == "auto": gui = automatic_backend() gui, backend = super(SpyderShell, self).enable_matplotlib(gui) - try: - self.kernel.frontend_call(blocking=False).update_matplotlib_gui(gui) - except Exception: - pass + if self.update_gui_frontend: + try: + self.kernel.frontend_call( + blocking=False + ).update_matplotlib_gui(gui) + except Exception: + pass return gui, backend # --- For Pdb namespace integration - @comm_handler def set_pdb_configuration(self, pdb_conf): """ Set Pdb configuration. diff --git a/spyder_kernels/console/start.py b/spyder_kernels/console/start.py index 708319c0..eb910305 100644 --- a/spyder_kernels/console/start.py +++ b/spyder_kernels/console/start.py @@ -21,8 +21,6 @@ # Local imports from spyder_kernels.utils.misc import is_module_installed -from spyder_kernels.utils.mpl import ( - MPL_BACKENDS_FROM_SPYDER, INLINE_FIGURE_FORMATS) def import_spydercustomize(): @@ -48,24 +46,6 @@ def import_spydercustomize(): except ValueError: pass - -def sympy_config(mpl_backend): - """Sympy configuration""" - if mpl_backend is not None: - lines = """ -from sympy.interactive import init_session -init_session() -%matplotlib {0} -""".format(mpl_backend) - else: - lines = """ -from sympy.interactive import init_session -init_session() -""" - - return lines - - def kernel_config(): """Create a config object with IPython kernel options.""" from IPython.core.application import get_ipython_dir @@ -113,21 +93,6 @@ def kernel_config(): "del sys; del pdb" ) - # Run lines of code at startup - run_lines_o = os.environ.get('SPY_RUN_LINES_O') - if run_lines_o is not None: - spy_cfg.IPKernelApp.exec_lines += ( - [x.strip() for x in run_lines_o.split(';')] - ) - - # Load %autoreload magic - spy_cfg.IPKernelApp.exec_lines.append( - "get_ipython().kernel._load_autoreload_magic()") - - # Load wurlitzer extension - spy_cfg.IPKernelApp.exec_lines.append( - "get_ipython().kernel._load_wurlitzer()") - # Default inline backend configuration. # This is useful to have when people doesn't # use our config system to configure the @@ -150,63 +115,8 @@ def kernel_config(): 'figure.edgecolor': 'white' } - # Pylab configuration - mpl_backend = None if is_module_installed('matplotlib'): - # Set Matplotlib backend with Spyder options - pylab_o = os.environ.get('SPY_PYLAB_O') - backend_o = os.environ.get('SPY_BACKEND_O') - if pylab_o == 'True' and backend_o is not None: - mpl_backend = MPL_BACKENDS_FROM_SPYDER[backend_o] - # Inline backend configuration - if mpl_backend == 'inline': - # Figure format - format_o = os.environ.get('SPY_FORMAT_O') - formats = INLINE_FIGURE_FORMATS - if format_o is not None: - spy_cfg.InlineBackend.figure_format = formats[format_o] - - # Resolution - resolution_o = os.environ.get('SPY_RESOLUTION_O') - if resolution_o is not None: - spy_cfg.InlineBackend.rc['figure.dpi'] = float( - resolution_o) - - # Figure size - width_o = float(os.environ.get('SPY_WIDTH_O')) - height_o = float(os.environ.get('SPY_HEIGHT_O')) - if width_o is not None and height_o is not None: - spy_cfg.InlineBackend.rc['figure.figsize'] = (width_o, - height_o) - - # Print figure kwargs - bbox_inches_o = os.environ.get('SPY_BBOX_INCHES_O') - bbox_inches = 'tight' if bbox_inches_o == 'True' else None - spy_cfg.InlineBackend.print_figure_kwargs.update( - {'bbox_inches': bbox_inches}) - else: - # Set Matplotlib backend to inline for external kernels. - # Fixes issue 108 - mpl_backend = 'inline' - - # Automatically load Pylab and Numpy, or only set Matplotlib - # backend - autoload_pylab_o = os.environ.get('SPY_AUTOLOAD_PYLAB_O') == 'True' - command = "get_ipython().kernel._set_mpl_backend('{0}', {1})" - spy_cfg.IPKernelApp.exec_lines.append( - command.format(mpl_backend, autoload_pylab_o)) - - # Enable Cython magic - run_cython = os.environ.get('SPY_RUN_CYTHON') == 'True' - if run_cython and is_module_installed('Cython'): - spy_cfg.IPKernelApp.exec_lines.append('%reload_ext Cython') - - # Run a file at startup - use_file_o = os.environ.get('SPY_USE_FILE_O') - run_file_o = os.environ.get('SPY_RUN_FILE_O') - if use_file_o == 'True' and run_file_o is not None: - if osp.exists(run_file_o): - spy_cfg.IPKernelApp.file_to_run = run_file_o + spy_cfg.IPKernelApp.matplotlib = "inline" # Autocall autocall_o = os.environ.get('SPY_AUTOCALL_O') @@ -220,12 +130,6 @@ def kernel_config(): greedy_o = os.environ.get('SPY_GREEDY_O') == 'True' spy_cfg.IPCompleter.greedy = greedy_o - # Sympy loading - sympy_o = os.environ.get('SPY_SYMPY_O') == 'True' - if sympy_o and is_module_installed('sympy'): - lines = sympy_config(mpl_backend) - spy_cfg.IPKernelApp.exec_lines.append(lines) - # Disable the new mechanism to capture and forward low-level output # in IPykernel 6. For that we have Wurlitzer. spy_cfg.IPKernelApp.capture_fd_output = False diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py index 65870f02..a9564feb 100644 --- a/spyder_kernels/console/tests/test_console_kernel.py +++ b/spyder_kernels/console/tests/test_console_kernel.py @@ -32,7 +32,6 @@ # Local imports from spyder_kernels.utils.iofuncs import iofunctions -from spyder_kernels.utils.mpl import MPL_BACKENDS_FROM_SPYDER from spyder_kernels.utils.test_utils import get_kernel, get_log_text from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.comms.commbase import CommBase @@ -1138,7 +1137,7 @@ def test_locals_globals_in_pdb(kernel): @flaky(max_runs=3) -@pytest.mark.parametrize("backend", [None, 'inline', 'tk', 'qt5']) +@pytest.mark.parametrize("backend", [None, 'inline', 'tk', 'qt']) @pytest.mark.skipif( os.environ.get('USE_CONDA') != 'true', reason="Doesn't work with pip packages") @@ -1169,11 +1168,14 @@ def test_get_interactive_backend(backend): user_expressions = reply['content']['user_expressions'] value = user_expressions['output']['data']['text/plain'] + # remove quotes + value = value[1:-1] + # Assert we got the right interactive backend if backend is not None: - assert MPL_BACKENDS_FROM_SPYDER[value] == backend + assert value == backend else: - assert value == '0' + assert value == 'inline' def test_global_message(tmpdir): diff --git a/spyder_kernels/customize/code_runner.py b/spyder_kernels/customize/code_runner.py index 680787fa..bb238749 100644 --- a/spyder_kernels/customize/code_runner.py +++ b/spyder_kernels/customize/code_runner.py @@ -373,7 +373,7 @@ def _exec_file( """ Execute a file. """ - if self.umr.enabled: + if self.umr.enabled and self.shell.special != "cython": self.umr.run() if args is not None and not isinstance(args, str): raise TypeError("expected a character buffer object") @@ -431,7 +431,7 @@ def _exec_file( print("Working directory {} doesn't exist.\n".format(wdir)) try: - if self.umr.has_cython: + if self.shell.special == "cython": # Cython files with io.open(filename, encoding="utf-8") as f: self.shell.run_cell_magic("cython", "", f.read()) diff --git a/spyder_kernels/customize/spyderpdb.py b/spyder_kernels/customize/spyderpdb.py index b9d76f63..0d26986d 100755 --- a/spyder_kernels/customize/spyderpdb.py +++ b/spyder_kernels/customize/spyderpdb.py @@ -861,7 +861,7 @@ def get_pdb_state(self): hidden = self.hidden_frames(self.stack) pdb_stack = [f for f, h in zip(pdb_stack, hidden) if not h] # Adjust the index - pdb_index -= sum(hidden[:pdb_index]) + pdb_index -= sum([bool(i) for i in hidden[:pdb_index]]) state['stack'] = (pdb_stack, pdb_index) diff --git a/spyder_kernels/customize/tests/test_umr.py b/spyder_kernels/customize/tests/test_umr.py index 438f7eff..fe84791b 100644 --- a/spyder_kernels/customize/tests/test_umr.py +++ b/spyder_kernels/customize/tests/test_umr.py @@ -39,27 +39,6 @@ def square(x): return create_module -def test_umr_skip_cython(user_module): - """ - Test that the UMR doesn't try to reload modules when Cython - support is active. - """ - # Create user module - user_module('foo') - - # Activate Cython support - os.environ['SPY_RUN_CYTHON'] = 'True' - - # Create UMR - umr = UserModuleReloader() - - import foo - assert umr.is_module_reloadable(foo, 'foo') == False - - # Deactivate Cython support - os.environ['SPY_RUN_CYTHON'] = 'False' - - def test_umr_run(user_module): """Test that UMR's run method is working correctly.""" # Create user module @@ -72,12 +51,11 @@ def test_umr_run(user_module): umr = UserModuleReloader() from foo1.bar import square - umr.run() - umr.modnames_to_reload == ['foo', 'foo.bar'] + assert umr.run() == ['foo1', 'foo1.bar'] def test_umr_previous_modules(user_module): - """Test that UMR's previos_modules is working as expected.""" + """Test that UMR's previous_modules is working as expected.""" # Create user module user_module('foo2') diff --git a/spyder_kernels/customize/umr.py b/spyder_kernels/customize/umr.py index fa140020..e779ec33 100644 --- a/spyder_kernels/customize/umr.py +++ b/spyder_kernels/customize/umr.py @@ -49,13 +49,6 @@ def __init__(self, namelist=None, pathlist=None): # List of previously loaded modules self.previous_modules = list(sys.modules.keys()) - # List of module names to reload - self.modnames_to_reload = [] - - # Activate Cython support - self.has_cython = False - self.activate_cython() - # Check if the UMR is enabled or not enabled = os.environ.get("SPY_UMR_ENABLED", "") self.enabled = enabled.lower() == "true" @@ -66,54 +59,18 @@ def __init__(self, namelist=None, pathlist=None): def is_module_reloadable(self, module, modname): """Decide if a module is reloadable or not.""" - if self.has_cython: - # Don't return cached inline compiled .PYX files + if ( + path_is_library(getattr(module, '__file__', None), self.pathlist) + or self.is_module_in_namelist(modname) + ): return False else: - if (path_is_library(getattr(module, '__file__', None), - self.pathlist) or - self.is_module_in_namelist(modname)): - return False - else: - return True + return True def is_module_in_namelist(self, modname): """Decide if a module can be reloaded or not according to its name.""" return set(modname.split('.')) & set(self.namelist) - def activate_cython(self): - """ - Activate Cython support. - - We need to run this here because if the support is - active, we don't to run the UMR at all. - """ - run_cython = os.environ.get("SPY_RUN_CYTHON") == "True" - - if run_cython: - try: - __import__('Cython') - self.has_cython = True - except Exception: - pass - - if self.has_cython: - # Import pyximport to enable Cython files support for - # import statement - import pyximport - pyx_setup_args = {} - - # Add Numpy include dir to pyximport/distutils - try: - import numpy - pyx_setup_args['include_dirs'] = numpy.get_include() - except Exception: - pass - - # Setup pyximport and enable Cython files reload - pyximport.install(setup_args=pyx_setup_args, - reload_support=True) - def run(self): """ Delete user modules to force Python to deeply reload them @@ -122,18 +79,20 @@ def run(self): modules installed in subdirectories of Python interpreter's binary Do not del C modules """ - self.modnames_to_reload = [] + modnames_to_reload = [] for modname, module in list(sys.modules.items()): if modname not in self.previous_modules: # Decide if a module can be reloaded or not if self.is_module_reloadable(module, modname): - self.modnames_to_reload.append(modname) + modnames_to_reload.append(modname) del sys.modules[modname] else: continue # Report reloaded modules - if self.verbose and self.modnames_to_reload: - modnames = self.modnames_to_reload + if self.verbose and modnames_to_reload: + modnames = modnames_to_reload print("\x1b[4;33m%s\x1b[24m%s\x1b[0m" % ("Reloaded modules", ": "+", ".join(modnames))) + + return modnames_to_reload diff --git a/spyder_kernels/utils/dochelpers.py b/spyder_kernels/utils/dochelpers.py index 92477992..2aace3af 100644 --- a/spyder_kernels/utils/dochelpers.py +++ b/spyder_kernels/utils/dochelpers.py @@ -107,7 +107,15 @@ def getdoc(obj): doc['note'] = 'Function' doc['name'] = obj.__name__ if inspect.isfunction(obj): - sig = inspect.signature(obj) + # This is necessary to catch errors for objects without a + # signature, like numpy.where. + # Fixes spyder-ide/spyder#21148 + try: + sig = inspect.signature(obj) + except ValueError: + sig = getargspecfromtext(doc['docstring']) + if not sig: + sig = '(...)' doc['argspec'] = str(sig) if name == '': doc['name'] = name + ' lambda ' @@ -158,51 +166,78 @@ def getsource(obj): def getsignaturefromtext(text, objname): - """Get object signatures from text (object documentation) - Return a list containing a single string in most cases - Example of multiple signatures: PyQt5 objects""" + """Get object signature from text (i.e. object documentation).""" if isinstance(text, dict): text = text.get('docstring', '') + # Regexps - oneline_re = objname + r'\([^\)].+?(?<=[\w\]\}\'"])\)(?!,)' - multiline_re = objname + r'\([^\)]+(?<=[\w\]\}\'"])\)(?!,)' - multiline_end_parenleft_re = r'(%s\([^\)]+(\),\n.+)+(?<=[\w\]\}\'"])\))' + args_re = r'(\(.+?\))' + if objname: + signature_re = objname + args_re + else: + identifier_re = r'(\w+)' + signature_re = identifier_re + args_re + # Grabbing signatures if not text: text = '' - sigs_1 = re.findall(oneline_re + '|' + multiline_re, text) - sigs_2 = [g[0] for g in re.findall(multiline_end_parenleft_re % objname, text)] - all_sigs = sigs_1 + sigs_2 + + sigs = re.findall(signature_re, text) + # The most relevant signature is usually the first one. There could be - # others in doctests but those are not so important - if all_sigs: - return all_sigs[0] - else: - return '' + # others in doctests or other places, but those are not so important. + sig = '' + if sigs: + # Default signatures returned by IPython. + # Notes: + # * These are not real signatures but only used to provide a + # placeholder. + # * We skip them if we can find other signatures in `text`. + # * This is necessary because we also use this function in Spyder + # to parse the content of inspect replies that come from the + # kernel, which can include these signatures. + default_ipy_sigs = ['(*args, **kwargs)', '(self, /, *args, **kwargs)'] + + if objname: + real_sigs = [s for s in sigs if s not in default_ipy_sigs] + + if real_sigs: + sig = real_sigs[0] + else: + sig = sigs[0] + else: + valid_sigs = [s for s in sigs if s[0].isidentifier()] + + if valid_sigs: + real_sigs = [ + s for s in valid_sigs if s[1] not in default_ipy_sigs + ] -# Fix for Issue 1953 -# TODO: Add more signatures and remove this hack in 2.4 -getsignaturesfromtext = getsignaturefromtext + if real_sigs: + sig = real_sigs[0][1] + else: + sig = valid_sigs[0][1] + + return sig def getargspecfromtext(text): """ Try to get the formatted argspec of a callable from the first block of its - docstring + docstring. - This will return something like - '(foo, bar, k=1)' + This will return something like `(x, y, k=1)`. """ blocks = text.split("\n\n") - first_block = blocks[0].strip() + first_block = blocks[0].strip().replace('\n', '') return getsignaturefromtext(first_block, '') def getargsfromtext(text, objname): - """Get arguments from text (object documentation)""" + """Get arguments from text (object documentation).""" signature = getsignaturefromtext(text, objname) if signature: - argtxt = signature[signature.find('(')+1:-1] + argtxt = signature[signature.find('(') + 1:-1] return argtxt.split(',') @@ -317,20 +352,3 @@ def isdefined(obj, force_import=False, namespace=None): return False base += '.'+attr return True - - -if __name__ == "__main__": - class Test: - def method(self, x, y=2): - pass - print(getargtxt(Test.__init__)) # spyder: test-skip - print(getargtxt(Test.method)) # spyder: test-skip - print(isdefined('numpy.take', force_import=True)) # spyder: test-skip - print(isdefined('__import__')) # spyder: test-skip - print(isdefined('.keys', force_import=True)) # spyder: test-skip - print(getobj('globals')) # spyder: test-skip - print(getobj('globals().keys')) # spyder: test-skip - print(getobj('+scipy.signal.')) # spyder: test-skip - print(getobj('4.')) # spyder: test-skip - print(getdoc(sorted)) # spyder: test-skip - print(getargtxt(sorted)) # spyder: test-skip diff --git a/spyder_kernels/utils/mpl.py b/spyder_kernels/utils/mpl.py index 7927e49d..00f47bac 100644 --- a/spyder_kernels/utils/mpl.py +++ b/spyder_kernels/utils/mpl.py @@ -11,13 +11,6 @@ from spyder_kernels.utils.misc import is_module_installed -# Mapping of inline figure formats -INLINE_FIGURE_FORMATS = { - '0': 'png', - '1': 'svg' -} - - # Inline backend if is_module_installed('matplotlib_inline'): inline_backend = 'module://matplotlib_inline.backend_inline' @@ -27,30 +20,20 @@ # Mapping of matlotlib backends options to Spyder MPL_BACKENDS_TO_SPYDER = { - inline_backend: 0, - 'Qt5Agg': 2, - 'QtAgg': 2, # For Matplotlib 3.5+ - 'TkAgg': 3, - 'MacOSX': 4, + inline_backend: "inline", + 'Qt5Agg': 'qt', + 'QtAgg': 'qt', # For Matplotlib 3.5+ + 'TkAgg': 'tk', + 'MacOSX': 'osx', } def automatic_backend(): """Get Matplolib automatic backend option.""" if is_module_installed('PyQt5'): - auto_backend = 'qt5' + auto_backend = 'qt' elif is_module_installed('_tkinter'): auto_backend = 'tk' else: auto_backend = 'inline' return auto_backend - - -# Mapping of Spyder options to backends -MPL_BACKENDS_FROM_SPYDER = { - '0': 'inline', - '1': automatic_backend(), - '2': 'qt5', - '3': 'tk', - '4': 'osx' -} diff --git a/spyder_kernels/utils/tests/test_dochelpers.py b/spyder_kernels/utils/tests/test_dochelpers.py index 90744879..05197a0a 100644 --- a/spyder_kernels/utils/tests/test_dochelpers.py +++ b/spyder_kernels/utils/tests/test_dochelpers.py @@ -18,8 +18,9 @@ import pytest # Local imports -from spyder_kernels.utils.dochelpers import (getargtxt, getdoc, getobj, - isdefined) +from spyder_kernels.utils.dochelpers import ( + getargtxt, getargspecfromtext, getdoc, getobj, getsignaturefromtext, + isdefined) class Test(object): @@ -58,5 +59,87 @@ def test_dochelpers(): assert getobj('4.') == '4' +def test_no_signature(): + """ + Test that we can get documentation for objects for which Python can't get a + signature directly because it gives an error. + + This is a regression test for issue spyder-ide/spyder#21148 + """ + import numpy as np + doc = getdoc(np.where) + signature = doc['argspec'] + assert signature and signature != "(...)" and signature.startswith("(") + assert doc['docstring'] + + +@pytest.mark.parametrize( + 'text, name, expected', + [ + # Simple text with and without name + ('foo(x, y)', 'foo', '(x, y)'), + ('foo(x, y)', '', '(x, y)'), + # Single arg + ('foo(x)', '', '(x)'), + ('foo(x = {})', '', '(x = {})'), + # Not a valid identifier + ('1a(x, y)', '', ''), + # Valid identifier + ('a1(x, y=2)', '', '(x, y=2)'), + # Unicode identifier with and without name + ('ΣΔ(x, y)', 'ΣΔ', '(x, y)'), + ('ΣΔ(x, y)', '', '(x, y)'), + # Multiple signatures in a single line + ('ΣΔ(x, y) foo(a, b)', '', '(x, y)'), + ('1a(x, y) foo(a, b)', '', '(a, b)'), + # Multiple signatures in multiple lines + ('foo(a, b = 1)\n\nΣΔ(x, y=2)', '', '(a, b = 1)'), + ('1a(a, b = 1)\n\nΣΔ(x, y=2)', '', '(x, y=2)'), + # Signature after math operations + ('2(3 + 5) 3*(99) ΣΔ(x, y)', '', '(x, y)'), + # No identifier + ('(x, y)', '', ''), + ('foo (a=1, b = 2)', '', ''), + # Empty signature + ('foo()', '', ''), + ('foo()', 'foo', ''), + ] +) +def test_getsignaturefromtext(text, name, expected): + assert getsignaturefromtext(text, name) == expected + + +def test_multisignature(): + """ + Test that we can get at least one signature from an object with multiple + ones declared in its docstring. + """ + def foo(): + """ + foo(x, y) foo(a, b) + foo(c, d) + """ + + signature = getargspecfromtext(foo.__doc__) + assert signature == "(x, y)" + + +def test_multiline_signature(): + """ + Test that we can get signatures splitted into multiple lines in a + docstring. + """ + def foo(): + """ + foo(x, + y) + + This is a docstring. + """ + + signature = getargspecfromtext(foo.__doc__) + assert signature.startswith("(x, ") + + if __name__ == "__main__": pytest.main()