From fd7aac8438e3791f876aa7d2fde416cf5f093a7c Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Mon, 20 May 2024 10:36:39 -0700 Subject: [PATCH] git subrepo pull --branch=mpl390 --remote=https://github.com/mrclary/spyder-kernels.git --update external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "335583b37" upstream: origin: "https://github.com/mrclary/spyder-kernels.git" branch: "mpl390" commit: "335583b37" git-subrepo: version: "0.4.6" origin: "???" commit: "???" --- .../.github/workflows/linux-pip-tests.yml | 12 +- .../.github/workflows/linux-tests.yml | 6 +- external-deps/spyder-kernels/.gitrepo | 10 +- external-deps/spyder-kernels/CHANGELOG.md | 160 ++-- external-deps/spyder-kernels/README.md | 2 +- .../spyder-kernels/requirements/posix.txt | 1 + .../spyder-kernels/requirements/python-27.txt | 11 - .../spyder-kernels/requirements/tests.txt | 2 + external-deps/spyder-kernels/setup.cfg | 3 - external-deps/spyder-kernels/setup.py | 21 +- .../spyder-kernels/spyder_kernels/_version.py | 2 +- .../spyder_kernels/comms/commbase.py | 79 +- .../spyder_kernels/comms/decorators.py | 27 + .../spyder_kernels/comms/frontendcomm.py | 233 ++---- .../spyder_kernels/comms/utils.py | 82 ++ .../spyder_kernels/console/kernel.py | 746 +++++++++++------- .../spyder_kernels/console/shell.py | 342 +++++++- .../spyder_kernels/console/start.py | 142 +--- .../console/tests/test_console_kernel.py | 523 +++++++----- .../spyder_kernels/customize/code_runner.py | 614 ++++++++++++++ .../customize/namespace_manager.py | 80 +- .../customize/spydercustomize.py | 614 +------------- .../spyder_kernels/customize/spyderpdb.py | 601 ++++++-------- .../customize/tests/test_umr.py | 31 +- .../spyder_kernels/customize/umr.py | 73 +- .../spyder_kernels/customize/utils.py | 97 ++- .../spyder_kernels/py3compat.py | 360 --------- .../spyder_kernels/utils/dochelpers.py | 114 ++- .../spyder_kernels/utils/iofuncs.py | 202 +++-- .../spyder_kernels/utils/lazymodules.py | 4 +- .../spyder_kernels/utils/misc.py | 2 +- .../spyder_kernels/utils/mpl.py | 30 +- .../spyder_kernels/utils/nsview.py | 79 +- .../spyder_kernels/utils/tests/data.dcm | Bin 0 -> 138515 bytes .../utils/tests/test_dochelpers.py | 17 +- .../utils/tests/test_iofuncs.py | 33 +- .../spyder_kernels/utils/tests/test_nsview.py | 20 +- 37 files changed, 2754 insertions(+), 2621 deletions(-) delete mode 100644 external-deps/spyder-kernels/requirements/python-27.txt delete mode 100644 external-deps/spyder-kernels/setup.cfg create mode 100644 external-deps/spyder-kernels/spyder_kernels/comms/decorators.py create mode 100644 external-deps/spyder-kernels/spyder_kernels/comms/utils.py create mode 100644 external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py delete mode 100644 external-deps/spyder-kernels/spyder_kernels/py3compat.py create mode 100644 external-deps/spyder-kernels/spyder_kernels/utils/tests/data.dcm diff --git a/external-deps/spyder-kernels/.github/workflows/linux-pip-tests.yml b/external-deps/spyder-kernels/.github/workflows/linux-pip-tests.yml index 6b79e05b615..e88b9e6f1a6 100644 --- a/external-deps/spyder-kernels/.github/workflows/linux-pip-tests.yml +++ b/external-deps/spyder-kernels/.github/workflows/linux-pip-tests.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['2.7', '3.8', '3.9', '3.10'] + PYTHON_VERSION: ['3.8', '3.9', '3.10'] timeout-minutes: 20 steps: - name: Checkout branch @@ -46,10 +46,6 @@ jobs: shell: bash -l {0} run: | pip install -e .[test] - # Zict >2.0.0 is not compatible with Python 3 - if [ "$PYTHON_VERSION" = "2.7" ]; then - pip install zict==2.0.0 - fi - name: Show environment information shell: bash -l {0} run: | @@ -58,9 +54,9 @@ jobs: - name: Run tests shell: bash -l {0} run: | - xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -x -vv || \ - xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -x -vv || \ - xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -x -vv + xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -vv || \ + xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -vv || \ + xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -vv - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/external-deps/spyder-kernels/.github/workflows/linux-tests.yml b/external-deps/spyder-kernels/.github/workflows/linux-tests.yml index ff6f449a7f0..f09003084dc 100644 --- a/external-deps/spyder-kernels/.github/workflows/linux-tests.yml +++ b/external-deps/spyder-kernels/.github/workflows/linux-tests.yml @@ -62,9 +62,9 @@ jobs: - name: Run tests shell: bash -l {0} run: | - xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -x -vv || \ - xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -x -vv || \ - xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -x -vv + xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -vv || \ + xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -vv || \ + xvfb-run --auto-servernum pytest spyder_kernels --color=yes --cov=spyder_kernels -vv - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 7242b76b593..d26a9a79a88 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -4,9 +4,9 @@ ; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme ; [subrepo] - remote = https://github.com/spyder-ide/spyder-kernels.git - branch = 2.x - commit = 3b3765cea8ffdb039f9c33119f9688ebb17c0281 - parent = 34676f45e8828e2bd1a01c4e326dadf5ca8605f4 + remote = https://github.com/mrclary/spyder-kernels.git + branch = mpl390 + commit = 335583b37f50d322e7dd4ae51e1c0cfb5318bc2b + parent = ea3c29a472a4c65a7fa1336394f94fe7f00fe179 method = merge - cmdver = 0.4.3 + cmdver = 0.4.6 diff --git a/external-deps/spyder-kernels/CHANGELOG.md b/external-deps/spyder-kernels/CHANGELOG.md index 0bb2216b6b6..851f7c2a8e4 100644 --- a/external-deps/spyder-kernels/CHANGELOG.md +++ b/external-deps/spyder-kernels/CHANGELOG.md @@ -1,17 +1,121 @@ # History of changes -## Version 2.5.1 (2024-02-28) +## Version 3.0.0b6 (2024-05-15) + +### Issues Closed + +* [Issue 457](https://github.com/spyder-ide/spyder-kernels/issues/457) - Detecting the interactive backend started to fail on Mac in master ([PR 486](https://github.com/spyder-ide/spyder-kernels/pull/486) by [@ccordoba12](https://github.com/ccordoba12)) + +In this release 1 issue was closed. ### Pull Requests Merged -* [PR 479](https://github.com/spyder-ide/spyder-kernels/pull/479) - PR: Fix hangs with Maplotlib interactive backends, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 486](https://github.com/spyder-ide/spyder-kernels/pull/486) - PR: Run `test_get_interactive_backend` again on Mac, by [@ccordoba12](https://github.com/ccordoba12) ([457](https://github.com/spyder-ide/spyder-kernels/issues/457)) +* [PR 485](https://github.com/spyder-ide/spyder-kernels/pull/485) - PR: Fix Matplotlib interactive backend detection, by [@ccordoba12](https://github.com/ccordoba12) + +In this release 2 pull requests were closed. + +---- + +## Version 3.0.0b5 (2024-04-23) + +### Pull Requests Merged + +* [PR 481](https://github.com/spyder-ide/spyder-kernels/pull/481) - PR: Allow magic to edit locals while debugging, by [@impact27](https://github.com/impact27) +* [PR 480](https://github.com/spyder-ide/spyder-kernels/pull/480) - PR: Save faulthandler files under `xdg_data_home/spyder` on Linux, by [@ccordoba12](https://github.com/ccordoba12) + +In this release 2 pull requests were closed. + +---- + +## Version 3.0.0b4 (2024-02-08) + +### Pull Requests Merged + +* [PR 477](https://github.com/spyder-ide/spyder-kernels/pull/477) - PR: Handle new Inline backend options `fontsize` and `bottom`, by [@jitseniesen](https://github.com/jitseniesen) In this release 1 pull request was closed. +---- + +## Version 3.0.0b3 (2023-12-18) + +### Pull Requests Merged + +* [PR 476](https://github.com/spyder-ide/spyder-kernels/pull/476) - PR: Send back pickling error correctly, by [@impact27](https://github.com/impact27) +* [PR 467](https://github.com/spyder-ide/spyder-kernels/pull/467) - PR: Fix index when skipping hidden frames (Debugger), by [@impact27](https://github.com/impact27) +* [PR 466](https://github.com/spyder-ide/spyder-kernels/pull/466) - PR: Simplify kernel configuration, by [@impact27](https://github.com/impact27) + +In this release 3 pull requests were closed. + +---- + +## 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 + +* [Issue 425](https://github.com/spyder-ide/spyder-kernels/issues/425) - Possible minor issues related to post mortem debugging ([PR 444](https://github.com/spyder-ide/spyder-kernels/pull/444) by [@impact27](https://github.com/impact27)) +* [Issue 340](https://github.com/spyder-ide/spyder-kernels/issues/340) - Drop support for Python 2 ([PR 341](https://github.com/spyder-ide/spyder-kernels/pull/341) by [@impact27](https://github.com/impact27)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 456](https://github.com/spyder-ide/spyder-kernels/pull/456) - PR: Remove unnecessary code for old IPykernel versions in our tests, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 453](https://github.com/spyder-ide/spyder-kernels/pull/453) - PR: Move code that loads and saves HDF5 and DICOM files from Spyder, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 447](https://github.com/spyder-ide/spyder-kernels/pull/447) - PR: Create magics for run|debug file|cell, by [@impact27](https://github.com/impact27) +* [PR 446](https://github.com/spyder-ide/spyder-kernels/pull/446) - PR: Make call to interrupt children processes work for IPykernel greater than 6.21.2, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 444](https://github.com/spyder-ide/spyder-kernels/pull/444) - PR: Fix post mortem for debugging, by [@impact27](https://github.com/impact27) ([425](https://github.com/spyder-ide/spyder-kernels/issues/425)) +* [PR 443](https://github.com/spyder-ide/spyder-kernels/pull/443) - PR: Fix small typo in Readme, by [@davidbrochart](https://github.com/davidbrochart) +* [PR 437](https://github.com/spyder-ide/spyder-kernels/pull/437) - PR: Remove imports from __future__, by [@oscargus](https://github.com/oscargus) +* [PR 421](https://github.com/spyder-ide/spyder-kernels/pull/421) - PR: Update variable explorer from the kernel, by [@impact27](https://github.com/impact27) +* [PR 417](https://github.com/spyder-ide/spyder-kernels/pull/417) - PR: Fix error in `globalsfilter`, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 412](https://github.com/spyder-ide/spyder-kernels/pull/412) - PR: Use temporary file for faulthandler and make sure iopub is open for comms, by [@impact27](https://github.com/impact27) +* [PR 409](https://github.com/spyder-ide/spyder-kernels/pull/409) - PR: Add Python executable as part of kernel info, by [@impact27](https://github.com/impact27) +* [PR 408](https://github.com/spyder-ide/spyder-kernels/pull/408) - PR: Expose package version in SpyderShell, by [@impact27](https://github.com/impact27) +* [PR 403](https://github.com/spyder-ide/spyder-kernels/pull/403) - PR: Make debugger faster by avoiding unnecessary comm messages, by [@impact27](https://github.com/impact27) +* [PR 401](https://github.com/spyder-ide/spyder-kernels/pull/401) - PR: Wait for connection file to be written, by [@impact27](https://github.com/impact27) +* [PR 400](https://github.com/spyder-ide/spyder-kernels/pull/400) - PR: Use `execute_interactive` to print errors during tests, by [@impact27](https://github.com/impact27) +* [PR 397](https://github.com/spyder-ide/spyder-kernels/pull/397) - PR: Remove Python 2 code introduced when merging PR #395, by [@impact27](https://github.com/impact27) +* [PR 396](https://github.com/spyder-ide/spyder-kernels/pull/396) - PR: Add handlers to interrupt executions and enter the debugger after that, by [@impact27](https://github.com/impact27) +* [PR 390](https://github.com/spyder-ide/spyder-kernels/pull/390) - PR: Filter comm socket thread, by [@impact27](https://github.com/impact27) +* [PR 387](https://github.com/spyder-ide/spyder-kernels/pull/387) - PR: Minor changes to finish the migration to Python 3, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 366](https://github.com/spyder-ide/spyder-kernels/pull/366) - PR: Print warning when using `global` in an empty namespace, by [@impact27](https://github.com/impact27) +* [PR 341](https://github.com/spyder-ide/spyder-kernels/pull/341) - PR: Drop support for Python 2, by [@impact27](https://github.com/impact27) ([340](https://github.com/spyder-ide/spyder-kernels/issues/340)) +* [PR 339](https://github.com/spyder-ide/spyder-kernels/pull/339) - PR: Use control channel for comms, by [@impact27](https://github.com/impact27) +* [PR 286](https://github.com/spyder-ide/spyder-kernels/pull/286) - PR: Improve and refactor the way we run and debug code, by [@impact27](https://github.com/impact27) +* [PR 257](https://github.com/spyder-ide/spyder-kernels/pull/257) - PR: Notify frontend of Matplotlib backend change, by [@impact27](https://github.com/impact27) +* [PR 171](https://github.com/spyder-ide/spyder-kernels/pull/171) - PR: Publish Pdb stack frames to Spyder, by [@impact27](https://github.com/impact27) + +In this release 25 pull requests were closed. + +---- + +## Version 2.5.1 (2024-02-28) + +### Pull Requests Merged + +* [PR 479](https://github.com/spyder-ide/spyder-kernels/pull/479) - PR: Fix hangs with Maplotlib interactive backends, by [@ccordoba12](https://github.com/ccordoba12) + +---- + ## Version 2.5.0 (2023-11-06) ### New features @@ -31,9 +135,7 @@ In this release 1 pull request was closed. In this release 6 pull requests were closed. - ---- - +---- ## Version 2.4.4 (2023-06-29) @@ -52,10 +154,8 @@ In this release 1 issue was closed. In this release 4 pull requests were closed. - ---- - ## Version 2.4.3 (2023-04-02) ### Issues Closed @@ -76,10 +176,8 @@ In this release 1 issue was closed. In this release 7 pull requests were closed. - ---- - ## Version 2.4.2 (2023-01-17) ### Issues Closed @@ -95,10 +193,8 @@ In this release 1 issue was closed. In this release 2 pull requests were closed. - ---- - ## Version 2.4.1 (2022-12-29) ### Issues Closed @@ -113,10 +209,8 @@ In this release 1 issue was closed. In this release 1 pull request was closed. - ---- - ## Version 2.4.0 (2022-11-02) ### New features @@ -137,10 +231,8 @@ In this release 1 pull request was closed. In this release 8 pull requests were closed. - ---- - ## Version 2.3.3 (2022-08-28) ### Issues Closed @@ -156,10 +248,8 @@ In this release 1 issue was closed. In this release 2 pull requests were closed. - ---- - ## Version 2.3.2 (2022-07-06) ### Issues Closed @@ -181,10 +271,8 @@ In this release 1 issue was closed. In this release 8 pull requests were closed. - ---- - ## Version 2.3.1 (2022-05-21) ### Pull Requests Merged @@ -195,10 +283,8 @@ In this release 8 pull requests were closed. In this release 3 pull requests were closed. - --- - ## Version 2.3.0 (2022-03-30) ### New features @@ -230,10 +316,8 @@ In this release 4 issues were closed. In this release 10 pull requests were closed. - ---- - ## Version 2.2.1 (2022-01-13) ### Issues Closed @@ -266,10 +350,8 @@ In this release 4 issues were closed. In this release 16 pull requests were closed. - ---- - ## Version 2.2.0 (2021-11-22) ### New features @@ -301,10 +383,8 @@ In this release 3 issues were closed. In this release 10 pull requests were closed. - ---- - ## Version 2.1.3 (2021-10-02) ### Pull Requests Merged @@ -313,10 +393,8 @@ In this release 10 pull requests were closed. In this release 1 pull request was closed. - ---- - ## Version 2.1.2 (2021-09-28) ### Pull Requests Merged @@ -326,10 +404,8 @@ In this release 1 pull request was closed. In this release 2 pull requests were closed. - ---- - ## Version 2.1.1 (2021-09-01) ### Pull Requests Merged @@ -341,10 +417,8 @@ In this release 2 pull requests were closed. In this release 4 pull requests were closed. - ---- - ## Version 2.1.0 (2021-07-31) ### New features @@ -369,10 +443,8 @@ In this release 1 issue was closed. In this release 5 pull requests were closed. - ---- - ## Version 2.0.5 (2021-07-03) ### Pull Requests Merged @@ -381,10 +453,8 @@ In this release 5 pull requests were closed. In this release 1 pull request was closed. - ---- - ## Version 2.0.4 (2021-06-10) ### Issues Closed @@ -405,10 +475,8 @@ In this release 1 issue was closed. In this release 7 pull requests were closed. - ---- - ## Version 2.0.3 (2021-05-15) ### Pull Requests Merged @@ -417,10 +485,8 @@ In this release 7 pull requests were closed. In this release 1 pull request was closed. - ---- - ## Version 2.0.2 (2021-05-02) ### Pull Requests Merged @@ -431,18 +497,14 @@ In this release 1 pull request was closed. In this release 3 pull requests were closed. - ---- - ## Version 2.0.1 (2021-04-02) * This release also contains all fixes present in version 1.10.3 - ---- - ## Version 2.0.0 (2021-04-01) ### New features @@ -456,10 +518,8 @@ In this release 3 pull requests were closed. In this release 2 pull requests were closed. - ---- - ## Version 1.10.3 (2021-04-02) ### Pull Requests Merged @@ -468,10 +528,8 @@ In this release 2 pull requests were closed. In this release 1 pull request was closed. - ---- - ## Version 1.10.2 (2021-02-21) ### Pull Requests Merged @@ -484,10 +542,8 @@ In this release 1 pull request was closed. In this release 5 pull requests were closed. - ---- - ## Version 1.10.1 (2020-12-18) ### Issues Closed @@ -511,10 +567,8 @@ In this release 2 issues were closed. In this release 9 pull requests were closed. - ---- - ## Version 1.10.0 (2020-11-08) ### New features diff --git a/external-deps/spyder-kernels/README.md b/external-deps/spyder-kernels/README.md index 410e2fd51b5..b6307c10e96 100644 --- a/external-deps/spyder-kernels/README.md +++ b/external-deps/spyder-kernels/README.md @@ -8,7 +8,7 @@ Package that provides Jupyter kernels for use with the consoles of Spyder, the Scientific Python Development Environment. -These kernels can launched either through Spyder itself or in an independent +These kernels can be launched either through Spyder itself or in an independent Python session, and allow for interactive or file-based execution of Python code inside Spyder. diff --git a/external-deps/spyder-kernels/requirements/posix.txt b/external-deps/spyder-kernels/requirements/posix.txt index fc3b9f66612..0c22fa5774f 100644 --- a/external-deps/spyder-kernels/requirements/posix.txt +++ b/external-deps/spyder-kernels/requirements/posix.txt @@ -4,3 +4,4 @@ ipython>=8.12.2,<9 jupyter_client>=7.4.9,<9 pyzmq>=24.0.0 wurlitzer>=1.0.3 +pyxdg>=0.26 diff --git a/external-deps/spyder-kernels/requirements/python-27.txt b/external-deps/spyder-kernels/requirements/python-27.txt deleted file mode 100644 index c0d798825c3..00000000000 --- a/external-deps/spyder-kernels/requirements/python-27.txt +++ /dev/null @@ -1,11 +0,0 @@ -decorator<5 -backports.functools_lru_cache -cloudpickle -ipykernel>=4.5,<5 -jupyter_client>=5.3.4,<6 -pyzmq>=17,<20 -wurlitzer>=1.0.3 -# To avoid an error with conda -click =7 -# To avoid a problem with zict -zict <2.1.0 diff --git a/external-deps/spyder-kernels/requirements/tests.txt b/external-deps/spyder-kernels/requirements/tests.txt index 0bd4e273732..42ef0167b39 100644 --- a/external-deps/spyder-kernels/requirements/tests.txt +++ b/external-deps/spyder-kernels/requirements/tests.txt @@ -11,3 +11,5 @@ scipy xarray pillow django +h5py +pydicom diff --git a/external-deps/spyder-kernels/setup.cfg b/external-deps/spyder-kernels/setup.cfg deleted file mode 100644 index e606cea4f59..00000000000 --- a/external-deps/spyder-kernels/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[bdist_wheel] -# Code is written to work on both Python 2 and Python 3. -universal=1 diff --git a/external-deps/spyder-kernels/setup.py b/external-deps/spyder-kernels/setup.py index e2873c98e4f..79689b46055 100644 --- a/external-deps/spyder-kernels/setup.py +++ b/external-deps/spyder-kernels/setup.py @@ -36,21 +36,14 @@ def get_version(module='spyder_kernels'): REQUIREMENTS = [ - 'decorator<5; python_version<"3"', - 'backports.functools-lru-cache; python_version<"3"', 'cloudpickle', - 'ipykernel>=4.5,<5; python_version<"3"', - 'ipykernel>=6.16.1,<6.17; python_version<"3.8"', - 'ipykernel>=6.29.3,<7; python_version>="3.8"', - 'ipython<6; python_version<"3"', - 'ipython>=7.31.1,<8; python_version<"3.8"', + 'ipykernel>=6.29.3,<7', 'ipython>=8.12.2,<8.13; python_version=="3.8"', 'ipython>=8.13.0,<9,!=8.17.1; python_version>"3.8"', - 'jupyter-client>=5.3.4,<6; python_version<"3"', - 'jupyter-client>=7.4.9,<9; python_version>="3"', - 'pyzmq>=17,<20; python_version<"3"', - 'pyzmq>=24.0.0; python_version>="3"', + 'jupyter-client>=7.4.9,<9', + 'pyzmq>=24.0.0', 'wurlitzer>=1.0.3;platform_system!="Windows"', + 'pyxdg>=0.26;platform_system=="Linux"', ] TEST_REQUIREMENTS = [ @@ -67,6 +60,8 @@ def get_version(module='spyder_kernels'): 'xarray', 'pillow', 'django', + 'h5py', + 'pydicom' ] setup( @@ -85,6 +80,7 @@ def get_version(module='spyder_kernels'): install_requires=REQUIREMENTS, extras_require={'test': TEST_REQUIREMENTS}, include_package_data=True, + python_requires='>=3.8', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Jupyter', @@ -92,10 +88,7 @@ def get_version(module='spyder_kernels'): 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/external-deps/spyder-kernels/spyder_kernels/_version.py b/external-deps/spyder-kernels/spyder_kernels/_version.py index f4932aeef12..788cf5f4779 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 = (2, 6, 0, 'dev0') +VERSION_INFO = (3, 0, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) diff --git a/external-deps/spyder-kernels/spyder_kernels/comms/commbase.py b/external-deps/spyder-kernels/spyder_kernels/comms/commbase.py index b4dea83b1e7..20e3a8a3dca 100644 --- a/external-deps/spyder-kernels/spyder_kernels/comms/commbase.py +++ b/external-deps/spyder-kernels/spyder_kernels/comms/commbase.py @@ -50,8 +50,6 @@ 'call_name': The function name (mostly for debugging) } """ -from __future__ import print_function - import cloudpickle import pickle import logging @@ -59,13 +57,10 @@ import uuid import traceback -from spyder_kernels.py3compat import PY2, PY3 - logger = logging.getLogger(__name__) -# To be able to get and set variables between Python 2 and 3 -DEFAULT_PICKLE_PROTOCOL = 2 +DEFAULT_PICKLE_PROTOCOL = 4 # Max timeout (in secs) for blocking calls TIMEOUT = 3 @@ -132,7 +127,7 @@ def comm_excepthook(type, value, tb): sys.excepthook = comm_excepthook -class CommBase(object): +class CommBase: """ Class with the necessary attributes and methods to handle communications between a kernel and a frontend. @@ -154,8 +149,6 @@ def __init__(self): 'remote_call', self._handle_remote_call) self._register_message_handler( 'remote_call_reply', self._handle_remote_call_reply) - self.register_call_handler('_set_pickle_protocol', - self._set_pickle_protocol) def get_comm_id_list(self, comm_id=None): """Get a list of comms id.""" @@ -182,18 +175,6 @@ def is_open(self, comm_id=None): return len(self._comms) > 0 return comm_id in self._comms - def is_ready(self, comm_id=None): - """ - Check to see if the other side replied. - - The check is made with _set_pickle_protocol as this is the first call - made. If comm_id is not specified, check all comms. - """ - id_list = self.get_comm_id_list(comm_id) - if len(id_list) == 0: - return False - return all([self._comms[cid]['status'] == 'ready' for cid in id_list]) - def register_call_handler(self, call_name, handler): """ Register a remote call handler. @@ -203,15 +184,21 @@ def register_call_handler(self, call_name, handler): call_name : str The name of the called function. handler : callback - A function to handle the request, or `None` to unregister - `call_name`. + A function to handle the request. """ - if not handler: - self._remote_call_handlers.pop(call_name, None) - return - self._remote_call_handlers[call_name] = handler + def unregister_call_handler(self, call_name): + """ + Unegister a remote call handler. + + Parameters + ---------- + call_name : str + The name of the called function. + """ + self._remote_call_handlers.pop(call_name, None) + def remote_call(self, comm_id=None, callback=None, **settings): """Get a handler for remote calls.""" return RemoteCallFactory(self, comm_id, callback, **settings) @@ -252,7 +239,6 @@ def _set_pickle_protocol(self, protocol): """Set the pickle protocol used to send data.""" protocol = min(protocol, pickle.HIGHEST_PROTOCOL) self._comms[self.calling_comm_id]['pickle_protocol'] = protocol - self._comms[self.calling_comm_id]['status'] = 'ready' @property def _comm_name(self): @@ -309,15 +295,7 @@ def _comm_message(self, msg): # Load the buffer. Only one is supported. try: - if PY3: - # https://docs.python.org/3/library/pickle.html#pickle.loads - # Using encoding='latin1' is required for unpickling - # NumPy arrays and instances of datetime, date and time - # pickled by Python 2. - buffer = cloudpickle.loads(msg['buffers'][0], - encoding='latin-1') - else: - buffer = cloudpickle.loads(msg['buffers'][0]) + buffer = cloudpickle.loads(msg['buffers'][0]) except Exception as e: logger.debug( "Exception in cloudpickle.loads : %s" % str(e)) @@ -339,6 +317,10 @@ def _handle_remote_call(self, msg, buffer): """Handle a remote call.""" msg_dict = msg['content'] self.on_incoming_call(msg_dict) + if msg['content'].get('is_error', False): + # could not open the pickle + self._set_call_return_value(msg, buffer, is_error=True) + return try: return_value = self._remote_callback( msg_dict['call_name'], @@ -404,18 +386,20 @@ def on_incoming_call(self, call_dict): if "pickle_highest_protocol" in call_dict: self._set_pickle_protocol(call_dict["pickle_highest_protocol"]) - def _get_call_return_value(self, call_dict, call_data, comm_id): + def _send_call(self, call_dict, call_data, comm_id): + """Send call.""" + call_dict = self.on_outgoing_call(call_dict) + self._send_message( + 'remote_call', content=call_dict, data=call_data, + comm_id=comm_id) + + def _get_call_return_value(self, call_dict, comm_id): """ Send a remote call and return the reply. If settings['blocking'] == True, this will wait for a reply and return the replied value. """ - call_dict = self.on_outgoing_call(call_dict) - self._send_message( - 'remote_call', content=call_dict, data=call_data, - comm_id=comm_id) - settings = call_dict['settings'] blocking = 'blocking' in settings and settings['blocking'] @@ -432,7 +416,7 @@ def _get_call_return_value(self, call_dict, call_data, comm_id): else: timeout = TIMEOUT - self._wait_reply(call_id, call_name, timeout) + self._wait_reply(comm_id, call_id, call_name, timeout) reply = self._reply_inbox.pop(call_id) @@ -441,7 +425,7 @@ def _get_call_return_value(self, call_dict, call_data, comm_id): return reply['value'] - def _wait_reply(self, call_id, call_name, timeout): + def _wait_reply(self, comm_id, call_id, call_name, timeout): """ Wait for the other side reply. """ @@ -496,7 +480,7 @@ def _sync_error(self, error_wrapper): error_wrapper.raise_error() -class RemoteCallFactory(object): +class RemoteCallFactory: """Class to create `RemoteCall`s.""" def __init__(self, comms_wrapper, comm_id, callback, **settings): @@ -554,5 +538,6 @@ def __call__(self, *args, **kwargs): logger.debug("Call to unconnected comm: %s" % self._name) return self._comms_wrapper._register_call(call_dict, self._callback) + self._comms_wrapper._send_call(call_dict, call_data, self._comm_id) return self._comms_wrapper._get_call_return_value( - call_dict, call_data, self._comm_id) + call_dict, self._comm_id) 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/comms/frontendcomm.py b/external-deps/spyder-kernels/spyder_kernels/comms/frontendcomm.py index edf4adb8193..9eaf2ee1f43 100644 --- a/external-deps/spyder-kernels/spyder_kernels/comms/frontendcomm.py +++ b/external-deps/spyder-kernels/spyder_kernels/comms/frontendcomm.py @@ -9,33 +9,16 @@ - Implements _wait_reply, so blocking calls can be made. """ -import pickle -import socket +import asyncio import sys import threading import time from IPython.core.getipython import get_ipython -from jupyter_client.localinterfaces import localhost -from tornado import ioloop import zmq from spyder_kernels.comms.commbase import CommBase, CommError -from spyder_kernels.py3compat import TimeoutError, PY2 - - -if PY2: - import thread - - -def get_free_port(): - """Find a free port on the local machine.""" - sock = socket.socket() - sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8) - sock.bind((localhost(), 0)) - port = sock.getsockname()[1] - sock.close() - return port +from spyder_kernels.comms.utils import WriteContext def frontend_request(blocking, timeout=None): @@ -63,44 +46,9 @@ def __init__(self, kernel): self.kernel = kernel self.kernel.comm_manager.register_target( self._comm_name, self._comm_open) - - self.comm_port = None - self.register_call_handler('_send_comm_config', - self._send_comm_config) - - self.comm_lock = threading.RLock() - - # self.kernel.parent is IPKernelApp unless we are in tests - if self.kernel.parent: - # Create a new socket - self.context = zmq.Context() - self.comm_socket = self.context.socket(zmq.ROUTER) - self.comm_socket.linger = 1000 - - self.comm_port = get_free_port() - - self.comm_port = self.kernel.parent._bind_socket( - self.comm_socket, self.comm_port) - if hasattr(zmq, 'ROUTER_HANDOVER'): - # Set router-handover to workaround zeromq reconnect problems - # in certain rare circumstances. - # See ipython/ipykernel#270 and zeromq/libzmq#2892 - self.comm_socket.router_handover = 1 - - self.comm_thread_close = threading.Event() - self.comm_socket_thread = threading.Thread(target=self.poll_thread) - self.comm_socket_thread.start() - - # Patch parent.close . This function only exists in Python 3. - if not PY2: - parent_close = self.kernel.parent.close - - def close(): - """Close comm_socket_thread.""" - self.close_thread() - parent_close() - - self.kernel.parent.close = close + self.comm_lock = threading.Lock() + self._cached_messages = {} + self._pending_comms = {} def close(self, comm_id=None): """Close the comm and notify the other side.""" @@ -112,21 +60,6 @@ def _send_message(self, *args, **kwargs): with self.comm_lock: return super(FrontendComm, self)._send_message(*args, **kwargs) - def close_thread(self): - """Close comm.""" - self.comm_thread_close.set() - self.comm_socket.close() - self.context.term() - self.comm_socket_thread.join() - - def poll_thread(self): - """Receive messages from comm socket.""" - if not PY2: - # Create an event loop for the handlers. - ioloop.IOLoop().initialize() - while not self.comm_thread_close.is_set(): - self.poll_one() - def poll_one(self): """Receive one message from comm socket.""" out_stream = None @@ -135,7 +68,8 @@ def poll_one(self): # use the regular shell stream. out_stream = self.kernel.shell_streams[0] try: - ident, msg = self.kernel.session.recv(self.comm_socket, 0) + ident, msg = self.kernel.session.recv( + self.kernel.parent.control_socket, 0) except zmq.error.ContextTerminated: return except Exception: @@ -143,32 +77,12 @@ def poll_one(self): return msg_type = msg['header']['msg_type'] - if msg_type == 'shutdown_request': - self.comm_thread_close.set() - self._comm_close(msg) + handler = self.kernel.control_handlers.get(msg_type, None) + if handler is None: + self.kernel.log.warning("Unknown message type: %r", msg_type) return - - handler = self.kernel.shell_handlers.get(msg_type, None) try: - if handler is None: - self.kernel.log.warning("Unknown message type: %r", msg_type) - return - if PY2: - handler(out_stream, ident, msg) - return - - import asyncio - - if (getattr(asyncio, 'run', False) and - asyncio.iscoroutinefunction(handler)): - # This is needed for ipykernel 6+ - asyncio.run(handler(out_stream, ident, msg)) - else: - # This is required for Python 3.6, which doesn't have - # asyncio.run or ipykernel versions less than 6. The - # nice thing is that ipykernel 6, which requires - # asyncio, doesn't support Python 3.6. - handler(out_stream, ident, msg) + asyncio.run(handler(out_stream, ident, msg)) except Exception: self.kernel.log.error( "Exception in message handler:", exc_info=True) @@ -180,13 +94,14 @@ def poll_one(self): out_stream.flush(zmq.POLLOUT) def remote_call(self, comm_id=None, blocking=False, callback=None, - timeout=None): + timeout=None, display_error=False): """Get a handler for remote calls.""" return super(FrontendComm, self).remote_call( blocking=blocking, comm_id=comm_id, callback=callback, - timeout=timeout) + timeout=timeout, + display_error=display_error) def wait_until(self, condition, timeout=None): """Wait until condition is met. Returns False if timeout.""" @@ -196,7 +111,7 @@ def wait_until(self, condition, timeout=None): while not condition(): if timeout is not None and time.time() > t_start + timeout: return False - if threading.current_thread() is self.comm_socket_thread: + if threading.current_thread() is self.kernel.parent.control_thread: # Wait for a reply on the comm channel. self.poll_one() else: @@ -204,15 +119,52 @@ def wait_until(self, condition, timeout=None): time.sleep(0.01) return True + def cache_message(self, comm_id, msg): + """Message from a comm that might be opened later.""" + if comm_id not in self._cached_messages: + self._cached_messages[comm_id] = [] + self._cached_messages[comm_id].append(msg) + # --- Private -------- - def _wait_reply(self, call_id, call_name, timeout, retry=True): + def _check_comm_reply(self): + """ + Send comm message to frontend to check if the iopub channel is ready + """ + # 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 pending_comms: + self._notify_comm_ready(comm) + self.kernel.io_loop.call_later(1, self._check_comm_reply) + + def _notify_comm_ready(self, comm): + """Send messages about comm readiness to frontend.""" + self.remote_call( + comm_id=comm.comm_id, + callback=self._comm_ready_callback + )._comm_ready() + + def _comm_ready_callback(self, ret): + """A comm has replied, so process all cached messages related to it.""" + comm = self._pending_comms.pop(self.calling_comm_id, None) + if not comm: + return + # Cached messages for that comm + if comm.comm_id in self._cached_messages: + for msg in self._cached_messages[comm.comm_id]: + comm.handle_msg(msg) + self._cached_messages.pop(comm.comm_id) + + + def _wait_reply(self, comm_id, call_id, call_name, timeout, retry=True): """Wait until the frontend replies to a request.""" def reply_received(): """The reply is there!""" return call_id in self._reply_inbox if not self.wait_until(reply_received): if retry: - self._wait_reply(call_id, call_name, timeout, False) + self._wait_reply(comm_id, call_id, call_name, timeout, False) return raise TimeoutError( "Timeout while waiting for '{}' reply.".format( @@ -224,18 +176,14 @@ def _comm_open(self, comm, msg): """ self.calling_comm_id = comm.comm_id self._register_comm(comm) - self._set_pickle_protocol(msg['content']['data']['pickle_protocol']) - self._send_comm_config() - - def on_outgoing_call(self, call_dict): - """A message is about to be sent""" - call_dict["comm_port"] = self.comm_port - return super(FrontendComm, self).on_outgoing_call(call_dict) + self._set_pickle_protocol( + msg['content']['data']['pickle_highest_protocol']) - def _send_comm_config(self): - """Send the comm config to the frontend.""" - self.remote_call()._set_comm_port(self.comm_port) - self.remote_call()._set_pickle_protocol(pickle.HIGHEST_PROTOCOL) + # IOPub might not be connected yet, keep sending messages until a + # reply is received. + self._pending_comms[comm.comm_id] = comm + self._notify_comm_ready(comm) + self.kernel.io_loop.call_later(.3, self._check_comm_reply) def _comm_close(self, msg): """Close comm.""" @@ -263,60 +211,7 @@ def handle_msg(msg): def _remote_callback(self, call_name, call_args, call_kwargs): """Call the callback function for the remote call.""" - with self.comm_lock: - current_stdout = sys.stdout - current_stderr = sys.stderr - saved_stdout_write = current_stdout.write - saved_stderr_write = current_stderr.write - thread_id = thread.get_ident() if PY2 else threading.get_ident() - current_stdout.write = WriteWrapper( - saved_stdout_write, call_name, thread_id) - current_stderr.write = WriteWrapper( - saved_stderr_write, call_name, thread_id) - try: - return super(FrontendComm, self)._remote_callback( - call_name, call_args, call_kwargs) - finally: - current_stdout.write = saved_stdout_write - current_stderr.write = saved_stderr_write - - -class WriteWrapper(object): - """Wrapper to warn user when text is printed.""" - - def __init__(self, write, name, thread_id): - self._write = write - self._name = name - self._thread_id = thread_id - self._warning_shown = False - - def is_benign_message(self, message): - """Determine if a message is benign in order to filter it.""" - benign_messages = [ - # Fixes spyder-ide/spyder#14928 - # Fixes spyder-ide/spyder-kernels#343 - 'DeprecationWarning', - # Fixes spyder-ide/spyder-kernels#365 - 'IOStream.flush timed out' - ] - - return any([msg in message for msg in benign_messages]) - - def __call__(self, string): - """Print warning once.""" - thread_id = thread.get_ident() if PY2 else threading.get_ident() - if self._thread_id != thread_id: - return self._write(string) - - if not self.is_benign_message(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. - if repr(self._name) != "'show_mpl_backend_errors'": - self._write( - "\nOutput from spyder call " + repr(self._name) + ":\n" - ) + with WriteContext(call_name): + return super(FrontendComm, self)._remote_callback( + call_name, call_args, call_kwargs) - return self._write(string) diff --git a/external-deps/spyder-kernels/spyder_kernels/comms/utils.py b/external-deps/spyder-kernels/spyder_kernels/comms/utils.py new file mode 100644 index 00000000000..003323499af --- /dev/null +++ b/external-deps/spyder-kernels/spyder_kernels/comms/utils.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- +""" +Comms Utilities +""" + +import sys +import threading + + +class WriteContext(object): + class_lock = threading.RLock() + def __init__(self, prefix): + self.prefix = prefix + + def __enter__(self): + self.class_lock.acquire() + + self.files = [sys.stdout, sys.stderr] + self.saved_writes = [f.write for f in self.files] + + thread_id = threading.get_ident() + + for f in self.files: + f.write = WriteWrapper(f.write, self.prefix, thread_id) + + def __exit__(self, exc_type, exc_value, traceback): + try: + for f, old_write in zip(self.files, self.saved_writes): + f.write = old_write + finally: + self.class_lock.release() + + +class WriteWrapper(object): + """Wrapper to warn user when text is printed.""" + + def __init__(self, write, name, thread_id): + self._write = write + self._name = name + self._thread_id = thread_id + self._warning_shown = False + + def is_benign_message(self, message): + """Determine if a message is benign in order to filter it.""" + benign_messages = [ + # Fixes spyder-ide/spyder#14928 + # Fixes spyder-ide/spyder-kernels#343 + 'DeprecationWarning', + # Fixes spyder-ide/spyder-kernels#365 + 'IOStream.flush timed out', + # Avoid unnecessary messages from set_configuration when changing + # Matplotlib options. + "Warning: Cannot change to a different GUI toolkit", + "%pylab is deprecated", + "Populating the interactive namespace", + "\n" + ] + + return any([msg in message for msg in benign_messages]) + + def __call__(self, string): + """Print warning once.""" + if self._thread_id != threading.get_ident(): + return self._write(string) + + if not self.is_benign_message(string): + if not self._warning_shown: + self._warning_shown = True + + # request_pdb_stop is expected to print messages. + if self._name not in ['request_pdb_stop']: + self._write( + "\nOutput from spyder call " + repr(self._name) + ":\n" + ) + + return self._write(string) diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py index 306d2afab2d..15a407aa843 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py @@ -11,29 +11,34 @@ """ # Standard library imports +import faulthandler +import json import logging import os +import re import sys +import traceback +import tempfile import threading # Third-party imports from ipykernel.ipkernel import IPythonKernel -from ipykernel import eventloops +from ipykernel import get_connection_info from traitlets.config.loader import LazyConfigValue +import zmq +from zmq.utils.garbage import gc # Local imports -from spyder_kernels.py3compat import ( - TEXT_TYPES, to_text_string, PY3) +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 - -if PY3: - import faulthandler +from spyder_kernels.comms.utils import WriteContext logger = logging.getLogger(__name__) @@ -56,55 +61,39 @@ def __init__(self, *args, **kwargs): self.frontend_comm = FrontendComm(self) # All functions that can be called through the comm - handlers = { - 'set_breakpoints': self.set_spyder_breakpoints, - 'set_pdb_ignore_lib': self.set_pdb_ignore_lib, - 'set_pdb_execute_events': self.set_pdb_execute_events, - 'set_pdb_use_exclamation_mark': self.set_pdb_use_exclamation_mark, - '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_cwd': self.get_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.pdb_input_reply, - '_interrupt_eventloop': self._interrupt_eventloop, - 'enable_faulthandler': self.enable_faulthandler, - } - 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 - self._running_namespace = None self.faulthandler_handle = None + self._cwd_initialised = False + + # Add handlers to control to process messages while debugging + self.control_handlers['comm_msg'] = self.control_comm_msg + self.control_handlers['complete_request'] = self.shell_handlers[ + 'complete_request'] + + # Socket to signal shell_stream locally + self.loopback_socket = None + + # To track the interactive backend + self.interactive_backend = 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 do_shutdown(self, restart): - """Disable faulthandler if enabled before proceeding.""" - self.disable_faulthandler() - super(SpyderKernel, self).do_shutdown(restart) - def frontend_call(self, blocking=False, broadcast=True, - timeout=None, callback=None): + timeout=None, callback=None, display_error=False): """Call the frontend.""" # If not broadcast, send only to the calling comm if broadcast: @@ -116,45 +105,181 @@ def frontend_call(self, blocking=False, broadcast=True, blocking=blocking, comm_id=comm_id, callback=callback, - timeout=timeout) + timeout=timeout, + display_error=display_error) + + def get_state(self): + """"get current state to send to the frontend""" + state = {} + with WriteContext("get_state"): + if self._cwd_initialised: + state["cwd"] = self.get_cwd() + state["namespace_view"] = self.get_namespace_view() + state["var_properties"] = self.get_var_properties() + return state + + def publish_state(self): + """Publish the current kernel state""" + if not self.frontend_comm.is_open(): + # No one to send to + return + try: + self.frontend_call(blocking=False).update_state(self.get_state()) + except Exception: + pass - def enable_faulthandler(self, fn): + def enable_faulthandler(self): """ Open a file to save the faulthandling and identifiers for internal threads. """ - if not PY3: - # Not implemented + fault_dir = None + if sys.platform.startswith('linux'): + # Do not use /tmp for temporary files + try: + from xdg.BaseDirectory import xdg_data_home + fault_dir = os.path.join(xdg_data_home, "spyder") + os.makedirs(fault_dir, exist_ok=True) + except Exception: + fault_dir = None + + self.faulthandler_handle = tempfile.NamedTemporaryFile( + 'wt', suffix='.fault', dir=fault_dir + ) + + main_id = threading.main_thread().ident + system_ids = [ + thread.ident for thread in threading.enumerate() + if thread is not threading.main_thread() + ] + 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.""" + # Read file + try: + with open(fault_filename, 'r') as f: + fault = f.read() + except FileNotFoundError: return - self.disable_faulthandler() - f = open(fn, 'w') - self.faulthandler_handle = f - f.write("Main thread id:\n") - f.write(hex(threading.main_thread().ident)) - f.write('\nSystem threads ids:\n') - f.write(" ".join([hex(thread.ident) for thread in threading.enumerate() - if thread is not threading.main_thread()])) - f.write('\n') - faulthandler.enable(f) - - def disable_faulthandler(self): - """ - Cancel the faulthandling, close the file handle and remove the file. - """ - if not PY3: - # Not implemented + except UnicodeDecodeError as e: + return ( + "Can not read fault file!\n" + + "UnicodeDecodeError: " + str(e)) + + # Remove file + try: + os.remove(fault_filename) + except Exception: + pass + + # Process file + if not fault: return - if self.faulthandler_handle: - faulthandler.disable() - self.faulthandler_handle.close() - self.faulthandler_handle = None - # --- For the Variable Explorer - def set_namespace_view_settings(self, settings): - """Set namespace_view_settings.""" - self.namespace_view_settings = settings + thread_regex = ( + r"(Current thread|Thread) " + r"(0x[\da-f]+) \(most recent call first\):" + r"(?:.|\r\n|\r|\n)+?(?=Current thread|Thread|\Z)") + # Keep line for future improvements + # files_regex = r"File \"([^\"]+)\", line (\d+) in (\S+)" + + text = "" + start_idx = 0 + for idx, match in enumerate(re.finditer(thread_regex, fault)): + # Add anything non-matched + text += fault[start_idx:match.span()[0]] + start_idx = match.span()[1] + thread_id = int(match.group(2), base=16) + if thread_id != main_id: + if thread_id in ignore_ids: + continue + if "wurlitzer.py" in match.group(0): + # Wurlitzer threads are launched later + continue + text += "\n" + match.group(0) + "\n" + else: + try: + pattern = (r".*(?:/IPython/core/interactiveshell\.py|" + r"\\IPython\\core\\interactiveshell\.py).*") + match_internal = next(re.finditer(pattern, match.group(0))) + end_idx = match_internal.span()[0] + except StopIteration: + end_idx = None + text += "\nMain thread:\n" + match.group(0)[:end_idx] + "\n" + + # Add anything after match + text += fault[start_idx:] + return text + + def get_system_threads_id(self): + """Return the list of system threads id.""" + ignore_threads = [ + self.parent.poller, # Parent poller + self.shell.history_manager.save_thread, # history + self.parent.heartbeat, # heartbeat + self.parent.iopub_thread.thread, # iopub + gc.thread, # ZMQ garbage collector thread + self.parent.control_thread, # control + ] + return [ + thread.ident for thread in ignore_threads if thread is not None] + + def filter_stack(self, stack, is_main): + """Return the part of the stack the user needs to see.""" + # Remove wurlitzer frames + for frame_summary in stack: + if "wurlitzer.py" in frame_summary.filename: + return + # Cleanup main thread + if is_main: + start_idx = -1 + for idx in range(len(stack)): + if stack[idx].filename.endswith( + ("IPython/core/interactiveshell.py", + "IPython\\core\\interactiveshell.py")): + start_idx = idx + 1 + if start_idx != -1: + stack = stack[start_idx:] + else: + stack = [] + return stack + + @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 + frames = {} + thread_names = {thread.ident: thread.name + for thread in threading.enumerate()} + + for thread_id, frame in sys._current_frames().items(): + stack = traceback.StackSummary.extract( + traceback.walk_stack(frame)) + stack.reverse() + if ignore_internal_threads: + if thread_id in ignore_list: + continue + stack = self.filter_stack(stack, main_id == thread_id) + if stack is not None: + if thread_id in thread_names: + thread_name = thread_names[thread_id] + else: + thread_name = str(thread_id) + frames[thread_name] = stack + return frames - def get_namespace_view(self): + # --- For the Variable Explorer + @comm_handler + def get_namespace_view(self, frame=None): """ Return the namespace view @@ -183,12 +308,13 @@ def get_namespace_view(self): settings = self.namespace_view_settings if settings: - ns = self._get_current_namespace() + ns = self.shell._get_current_namespace(frame=frame) view = make_remote_view(ns, settings, EXCLUDED_NAMES) return view else: return None + @comm_handler def get_var_properties(self): """ Get some properties of the variables in the current @@ -196,7 +322,7 @@ def get_var_properties(self): """ settings = self.namespace_view_settings if settings: - ns = self._get_current_namespace() + ns = self.shell._get_current_namespace() data = get_remote_data(ns, settings, mode='editable', more_excluded_names=EXCLUDED_NAMES) @@ -219,27 +345,32 @@ def get_var_properties(self): else: return None + @comm_handler def get_value(self, name): """Get the value of a variable""" - ns = self._get_current_namespace() + 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._get_reference_namespace(name) + 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._get_reference_namespace(name) + 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._get_reference_namespace(orig_name) + 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. @@ -276,9 +407,10 @@ def load_data(self, filename, ext, overwrite=False): return None + @comm_handler def save_namespace(self, filename): """Save namespace into filename""" - ns = self._get_current_namespace() + ns = self.shell._get_current_namespace() settings = self.namespace_view_settings data = get_remote_data(ns, settings, mode='picklable', more_excluded_names=EXCLUDED_NAMES).copy() @@ -299,63 +431,45 @@ def do_complete(self, code, cursor_pos): return self.shell.pdb_session.do_complete(code, cursor_pos) return self._do_complete(code, cursor_pos) - def set_spyder_breakpoints(self, breakpoints): + def interrupt_eventloop(self): """ - Handle a message from the frontend - """ - if self.shell.pdb_session: - self.shell.pdb_session.set_spyder_breakpoints(breakpoints) - - def set_pdb_ignore_lib(self, state): - """ - Change the "Ignore libraries while stepping" debugger setting. - """ - if self.shell.pdb_session: - self.shell.pdb_session.pdb_ignore_lib = state + Interrupts the eventloop. - def set_pdb_execute_events(self, state): - """ - Handle a message from the frontend - """ - if self.shell.pdb_session: - self.shell.pdb_session.pdb_execute_events = state + To be used when the main thread is blocked by a call to self.eventloop. + This can be called from another thread, e.g. the control thread. - def set_pdb_use_exclamation_mark(self, state): + note: + Interrupting the eventloop is only implemented when a message is + received on the shell channel, but this message is queued and + won't be processed because an `execute` message is being + processed. """ - Set an option on the current debugging session to decide wether - the Pdb commands needs to be prefixed by '!' - """ - if self.shell.pdb_session: - self.shell.pdb_session.pdb_use_exclamation_mark = state + if not self.eventloop: + return - def pdb_input_reply(self, line, echo_stack_entry=True): - """Get a pdb command from the frontend.""" - debugger = self.shell.pdb_session - if debugger: - debugger._disable_next_stack_entry = not echo_stack_entry - debugger._cmd_input_line = line - if self.eventloop: - # Interrupting the eventloop is only implemented when a message is - # received on the shell channel, but this message is queued and - # won't be processed because an `execute` message is being - # processed. Therefore we process the message here (control chan.) - # and request a dummy message to be sent on the shell channel to - # stop the eventloop. This will call back `_interrupt_eventloop`. - self.frontend_call().request_interrupt_eventloop() + if self.loopback_socket is None: + # Add socket to signal shell_stream locally + self.loopback_socket = self.shell_stream.socket.context.socket( + zmq.DEALER) + port = json.loads(get_connection_info())['shell_port'] + self.loopback_socket.connect("tcp://127.0.0.1:%i" % port) + # Add dummy handler + self.shell_handlers["interrupt_eventloop"] = ( + lambda stream, ident, parent: None) - def _interrupt_eventloop(self): - """Interrupts the eventloop.""" - # Receiving the request is enough to stop the eventloop. - pass + self.session.send( + 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 - ns = self._get_current_namespace(with_magics=True) + 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: @@ -369,6 +483,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 @@ -378,82 +493,88 @@ def get_source(self, objtxt): return getsource(obj) # -- For Matplolib + @comm_handler def get_matplotlib_backend(self): """Get current matplotlib backend.""" try: import matplotlib - return MPL_BACKENDS_TO_SPYDER[matplotlib.get_backend()] + return MPL_BACKENDS_TO_SPYDER[matplotlib.get_backend().lower()] except Exception: return None + @comm_handler def get_mpl_interactive_backend(self): """ Get current Matplotlib interactive backend. This is different from the current backend because, for instance, the - user can set first the Qt5 backend, then the Inline one. In that case, - the current backend is Inline, but the current interactive one is Qt5, + user can set first the Qt backend, then the Inline one. In that case, + the current backend is Inline, but the current interactive one is Qt, and this backend can't be changed without a kernel restart. """ - # Mapping from frameworks to backend names. - mapping = { - 'qt': 'QtAgg', - 'tk': 'TkAgg', - 'macosx': 'MacOSX' - } - - # --- Get interactive framework - framework = None - - # Detect if there is a graphical framework running by checking the - # eventloop function attached to the kernel.eventloop attribute (see - # `ipykernel.eventloops.enable_gui` for context). - from IPython.core.getipython import get_ipython - loop_func = get_ipython().kernel.eventloop - - if loop_func is not None: - if loop_func == eventloops.loop_tk: - framework = 'tk' - elif loop_func == eventloops.loop_qt5: - framework = 'qt' - elif loop_func == eventloops.loop_cocoa: - framework = 'macosx' - else: - # Spyder doesn't handle other backends - framework = 'other' + # Backends that Spyder can handle + recognized_backends = {'qt', 'tk', 'macosx'} # --- Return backend according to framework - if framework is None: - # Since no interactive backend has been set yet, this is - # equivalent to having the inline one. - return 0 - elif framework in mapping: - return MPL_BACKENDS_TO_SPYDER[mapping[framework]] + if self.interactive_backend is None: + # Since no interactive backend has been set yet, this is equivalent + # to having the inline one. + return 'inline' + elif self.interactive_backend in recognized_backends: + return self.interactive_backend else: # This covers the case of other backends (e.g. Wx or Gtk) # which users can set interactively with the %matplotlib # 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[to_text_string(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' + fontsize_n = 'pylab/inline/fontsize' + bottom_n = 'pylab/inline/bottom' + 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 fontsize_n in conf: + self._set_mpl_inline_rc_config('font.size', conf[fontsize_n]) + + if bottom_n in conf: + self._set_mpl_inline_rc_config( + 'figure.subplot.bottom', + conf[bottom_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): """ @@ -461,8 +582,7 @@ def set_mpl_inline_bbox_inches(self, bbox_inches): The change is done by updating the 'print_figure_kwargs' config dict. """ - from IPython.core.getipython import get_ipython - config = get_ipython().kernel.config + config = self.config inline_config = ( config['InlineBackend'] if 'InlineBackend' in config else {}) print_figure_kwargs = ( @@ -496,9 +616,55 @@ def set_autocall(self, autocall): self._set_config_option('ZMQInteractiveShell.autocall', autocall) # --- Additional methods - def set_cwd(self, dirname): - """Set current working directory.""" - os.chdir(dirname) + @comm_handler + 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.""" @@ -507,14 +673,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: @@ -523,28 +692,59 @@ def close_all_mpl_figures(self): except: pass - 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 # noqa + exec("from pylab import *", self.shell.user_ns) + self.shell.special = special + return + + if special == "sympy": + import sympy # noqa + 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 # noqa + + # 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): """ Update the PYTHONPATH of the kernel. @@ -570,49 +770,6 @@ def update_syspath(self, path_dict, new_path_dict): # -- Private API --------------------------------------------------- # --- For the Variable Explorer - def _get_current_namespace(self, with_magics=False): - """ - Return current namespace - - This is globals() if not debugging, or a dictionary containing - both locals() and globals() for current frame when debugging - """ - ns = {} - if self.shell.is_debugging() and self.shell.pdb_session.curframe: - # Stopped at a pdb prompt - ns.update(self.shell.user_ns) - ns.update(self.shell._pdb_locals) - else: - # Give access to the running namespace if there is one - if self._running_namespace is None: - ns.update(self.shell.user_ns) - else: - # This is true when a file is executing. - running_globals, running_locals = self._running_namespace - ns.update(running_globals) - if running_locals is not None: - ns.update(running_locals) - - # Add magics to ns so we can show help about them on the Help - # plugin - if with_magics: - line_magics = self.shell.magics_manager.magics['line'] - cell_magics = self.shell.magics_manager.magics['cell'] - ns.update(line_magics) - ns.update(cell_magics) - return ns - - def _get_reference_namespace(self, name): - """ - Return namespace where reference name is defined - - It returns the globals() if reference has not yet been defined - """ - lcls = self.shell._pdb_locals - if name in lcls: - return lcls - return self.shell.user_ns - def _get_len(self, var): """Return sequence length""" try: @@ -703,10 +860,9 @@ def _eval(self, text): where *obj* is the object represented by *text* and *valid* is True if object evaluation did not raise any exception """ - from spyder_kernels.py3compat import is_text_string - assert is_text_string(text) - ns = self._get_current_namespace(with_magics=True) + assert isinstance(text, str) + ns = self.shell._get_current_namespace(with_magics=True) try: return eval(text, ns), True except: @@ -723,7 +879,6 @@ def _set_mpl_backend(self, backend, pylab=False): namespace from numpy and matplotlib """ import traceback - from IPython.core.getipython import get_ipython # Don't proceed further if there's any error while importing Matplotlib try: @@ -740,6 +895,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 @@ -747,7 +905,7 @@ def _set_mpl_backend(self, backend, pylab=False): matplotlib.rcParams['backend'] = 'Agg' # Set the backend - get_ipython().run_line_magic(magic, backend) + self.shell.run_line_magic(magic, backend) except RuntimeError as err: # This catches errors generated by ipykernel when # trying to set a backend. See issue 5541 @@ -778,8 +936,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): """ @@ -789,13 +947,12 @@ def _set_config_option(self, option, value): option: config option, for example 'InlineBackend.figure_format'. value: value of the option, for example 'SVG', 'Retina', etc. """ - from IPython.core.getipython import get_ipython try: base_config = "{option} = " value_line = ( - "'{value}'" if isinstance(value, TEXT_TYPES) else "{value}") + "'{value}'" if isinstance(value, str) else "{value}") config_line = base_config + value_line - get_ipython().run_line_magic( + self.shell.run_line_magic( 'config', config_line.format(option=option, value=value)) except Exception: @@ -812,31 +969,30 @@ def _set_mpl_inline_rc_config(self, option, value): # Needed in case matplolib isn't installed pass - 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 - 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 - from IPython.core.getipython import get_ipython - if background_color == 'dark': - init_printing(forecolor='White', ip=get_ipython()) - elif background_color == 'light': - init_printing(forecolor='Black', ip=get_ipython()) - 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.""" - from IPython.core.getipython import get_ipython try: - get_ipython().run_line_magic('reload_ext', 'autoreload') - get_ipython().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 @@ -844,12 +1000,11 @@ def _load_wurlitzer(self): """Load wurlitzer extension.""" # Wurlitzer has no effect on Windows if not os.name == 'nt': - from IPython.core.getipython import get_ipython # Enclose this in a try/except because if it fails the # console will be totally unusable. # Fixes spyder-ide/spyder#8668 try: - get_ipython().run_line_magic('reload_ext', 'wurlitzer') + self.shell.run_line_magic('reload_ext', 'wurlitzer') except Exception: pass @@ -865,3 +1020,32 @@ def _get_comm(self, comm_id): return self.comm_manager.comms[comm_id] except KeyError: pass + + def control_comm_msg(self, stream, ident, msg): + """ + Handler for comm_msg messages from control channel. + + If comm is not open yet, cache message. + """ + content = msg['content'] + comm_id = content['comm_id'] + comm = self.comm_manager.get_comm(comm_id) + if comm is None: + self.frontend_comm.cache_message(comm_id, msg) + return + try: + comm.handle_msg(msg) + except Exception: + self.comm_manager.log.error( + 'Exception in comm_msg for %s', comm_id, exc_info=True) + + def pre_handler_hook(self): + """Hook to execute before calling message handler""" + pass + + def post_handler_hook(self): + """Hook to execute after calling message handler""" + # keep ipykernel behavior of resetting sigint every call + self.shell.register_debugger_sigint() + # Reset tracing function so that pdb.set_trace works + sys.settrace(None) diff --git a/external-deps/spyder-kernels/spyder_kernels/console/shell.py b/external-deps/spyder-kernels/spyder_kernels/console/shell.py index 9d164701534..48cecaf3da0 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/shell.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/shell.py @@ -12,28 +12,64 @@ # Standard library imports import bdb +import logging +import os +import signal import sys +import traceback +from _thread import interrupt_main # Third-party imports from ipykernel.zmqshell import ZMQInteractiveShell +# Local imports +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.decorators import comm_handler +from spyder_kernels.utils.mpl import automatic_backend + + +logger = logging.getLogger(__name__) + class SpyderShell(ZMQInteractiveShell): """Spyder shell.""" + PDB_CONF_KEYS = [ + 'pdb_ignore_lib', + 'pdb_execute_events', + 'pdb_use_exclamation_mark', + 'pdb_stop_first_line', + 'breakpoints', + 'pdb_publish_stack' + ] + def __init__(self, *args, **kwargs): - # Create _pdb_obj before __init__ - self._pdb_obj = None + # 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() + self.update_gui_frontend = False # register post_execute self.events.register('post_execute', self.do_post_execute) - # ---- Methods overriden by us. + def init_magics(self): + """Init magics""" + super().init_magics() + self.register_magics(SpyderCodeRunner) + def ask_exit(self): """Engage the exit actions.""" - self.kernel.frontend_comm.close_thread() - return super(SpyderShell, self).ask_exit() + if self.active_eventloop not in [None, "inline"]: + # Some eventloops prevent the kernel from shutting down + self.enable_gui('inline') + super().ask_exit() def _showtraceback(self, etype, evalue, stb): """ @@ -47,36 +83,126 @@ def _showtraceback(self, etype, evalue, stb): stb = [''] super(SpyderShell, self)._showtraceback(etype, evalue, stb) - # ---- For Pdb namespace integration - def get_local_scope(self, stack_depth): - """Get local scope at given frame depth.""" - frame = sys._getframe(stack_depth + 1) - if self._pdb_frame is frame: - # Avoid calling f_locals on _pdb_frame - return self._pdb_obj.curframe_locals - else: - return frame.f_locals + def enable_matplotlib(self, gui=None): + """Enable matplotlib.""" + if gui is None or gui.lower() == "auto": + gui = automatic_backend() - def get_global_scope(self, stack_depth): - """Get global scope at given frame depth.""" - frame = sys._getframe(stack_depth + 1) - return frame.f_globals + enabled_gui, backend = super().enable_matplotlib(gui) + + # This is necessary for IPython 8.24+, which returns None after + # enabling the Inline backend. + if enabled_gui is None and gui == "inline": + enabled_gui = "inline" + gui = enabled_gui + + # To easily track the current interactive backend + if self.kernel.interactive_backend is None: + self.kernel.interactive_backend = gui if gui != "inline" else None + + 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 + def set_pdb_configuration(self, pdb_conf): + """ + Set Pdb configuration. + + Parameters + ---------- + pdb_conf: dict + Dictionary containing the configuration. Its keys are part of the + `PDB_CONF_KEYS` class constant. + """ + for key in self.PDB_CONF_KEYS: + if key in pdb_conf: + self._pdb_conf[key] = pdb_conf[key] + if self.pdb_session: + setattr(self.pdb_session, key, pdb_conf[key]) def is_debugging(self): """ Check if we are currently debugging. """ - return bool(self._pdb_frame) + for session in self._namespace_stack[::-1]: + if isinstance(session, SpyderPdb) and session.curframe is not None: + return True + return False @property def pdb_session(self): """Get current pdb session.""" - return self._pdb_obj + for session in self._namespace_stack[::-1]: + if isinstance(session, SpyderPdb): + return session + return None + + def add_pdb_session(self, pdb_obj): + """Add a pdb object to the stack.""" + if self.pdb_session == pdb_obj: + # Already added + return + self._namespace_stack.append(pdb_obj) + + # Set config to pdb obj + self.set_pdb_configuration(self._pdb_conf) + + def remove_pdb_session(self, pdb_obj): + """Remove a pdb object from the stack.""" + if self.pdb_session != pdb_obj: + # Already removed + return + self._namespace_stack.pop() + + if self.pdb_session: + # Set config to newly active pdb obj + self.set_pdb_configuration(self._pdb_conf) + + def add_namespace_manager(self, ns_manager): + """Add namespace manager to stack.""" + self._namespace_stack.append(ns_manager) + + def remove_namespace_manager(self, ns_manager): + """Remove namespace manager.""" + if self._namespace_stack[-1] != ns_manager: + logger.debug("The namespace stack is inconsistent.") + return + self._namespace_stack.pop() - @pdb_session.setter - def pdb_session(self, pdb_obj): - """Register Pdb session to use it later""" - self._pdb_obj = pdb_obj + def get_local_scope(self, stack_depth): + """ + Get local scope at a given frame depth. + + Needed for magics that use "needs_local_scope" such as timeit + """ + frame = sys._getframe(stack_depth + 1) + return self.context_locals(frame) + + def context_locals(self, frame=None): + """ + Get context locals. + + If frame is not None, make sure frame.f_locals is not registered in a + debugger and return frame.f_locals + """ + for session in self._namespace_stack[::-1]: + if isinstance(session, SpyderPdb) and session.curframe is not None: + if frame is None or frame == session.curframe: + return session.curframe_locals + elif frame is None and isinstance(session, NamespaceManager): + return session.ns_locals + + if frame is not None: + return frame.f_locals + + return None @property def _pdb_frame(self): @@ -84,32 +210,170 @@ def _pdb_frame(self): if self.pdb_session is not None: return self.pdb_session.curframe - @property - def _pdb_locals(self): - """ - Return current Pdb frame locals if available. Otherwise - return an empty dictionary - """ - if self._pdb_frame is not None: - return self._pdb_obj.curframe_locals - else: - return {} - @property def user_ns(self): """Get the current namespace.""" - if self._pdb_frame is not None: - return self._pdb_frame.f_globals - else: - return self.__user_ns + for session in self._namespace_stack[::-1]: + if isinstance(session, SpyderPdb) and session.curframe is not None: + # Return first debugging namespace + return session.curframe.f_globals + elif isinstance(session, NamespaceManager): + return session.ns_globals + + return self.__user_ns @user_ns.setter def user_ns(self, namespace): """Set user_ns.""" self.__user_ns = namespace + def _get_current_namespace(self, with_magics=False, frame=None): + """Return a copy of the current namespace.""" + if frame is not None: + ns = frame.f_globals.copy() + ns.update(self.context_locals(frame)) + return ns + + ns = {} + ns.update(self.user_ns) + context_locals = self.context_locals() + if context_locals: + ns.update(context_locals) + + # Add magics to ns so we can show help about them on the Help + # plugin + if with_magics: + line_magics = self.magics_manager.magics['line'] + cell_magics = self.magics_manager.magics['cell'] + ns.update(line_magics) + ns.update(cell_magics) + + return ns + + def _get_reference_namespace(self, name): + """ + Return namespace where reference name is defined + + It returns the user namespace if name has not yet been defined. + """ + lcls = self.context_locals() + if lcls and name in lcls: + + return lcls + return self.user_ns + + def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None, + exception_only=False, running_compiled_code=False): + """Display the exception that just occurred.""" + super(SpyderShell, self).showtraceback( + exc_tuple, filename, tb_offset, + exception_only, running_compiled_code) + if not exception_only: + try: + etype, value, tb = self._get_exc_info(exc_tuple) + stack = traceback.extract_tb(tb.tb_next) + self.kernel.frontend_call(blocking=False).show_traceback( + etype, value, stack) + except Exception: + return + + 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": + # Check if signal handler is callable to avoid + # 'int not callable' error (Python issue #23395) + if callable(signal.getsignal(signal.SIGINT)): + interrupt_main() + else: + self.kernel.log.error( + "Interrupt message not supported on Windows") + else: + # This is necessary to make the call below work for IPykernel + # versions equal or less than 6.21.2 and greater than it. + # See ipython/ipykernel#1101 + if hasattr(self.kernel, '_send_interupt_children'): + self.kernel._send_interupt_children() + 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 + if pdb_session: + if pdb_session.interrupting: + # interrupt already requested, wait + return + # trace_dispatch is active, stop at the next possible position + pdb_session.interrupt() + elif (self.spyderkernel_sigint_handler + == signal.getsignal(signal.SIGINT)): + # Use spyderkernel_sigint_handler + self._request_pdb_stop = True + self.raise_interrupt_signal() + else: + logger.debug( + "Can not signal main thread to stop as SIGINT " + "handler was replaced and the debugger is not active. " + "The current handler is: " + + repr(signal.getsignal(signal.SIGINT)) + ) + + def spyderkernel_sigint_handler(self, signum, frame): + """SIGINT handler.""" + if self._request_pdb_stop: + # SIGINT called from request_pdb_stop + self._request_pdb_stop = False + debugger = SpyderPdb() + debugger.interrupt() + debugger.set_trace(frame) + return + + pdb_session = self.pdb_session + if pdb_session: + # SIGINT called while debugging + if pdb_session.allow_kbdint: + raise KeyboardInterrupt + if pdb_session.interrupting: + # second call to interrupt, raise + raise KeyboardInterrupt + pdb_session.interrupt() + return + + if self._allow_kbdint: + # Do not raise KeyboardInterrupt in the middle of ipython code + raise KeyboardInterrupt + + async def run_code(self, *args, **kwargs): + """Execute a code object.""" + try: + try: + self._allow_kbdint = True + return await super().run_code(*args, **kwargs) + finally: + self._allow_kbdint = False + 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 + if not debugger: + return + debugger._disable_next_stack_entry = not echo_stack_entry + debugger._cmd_input_line = line + # Interrupts eventloop if needed + self.kernel.interrupt_eventloop() + def do_post_execute(self): """Flush __std*__ after execution.""" # Flush C standard streams. sys.__stderr__.flush() sys.__stdout__.flush() + self.kernel.publish_state() diff --git a/external-deps/spyder-kernels/spyder_kernels/console/start.py b/external-deps/spyder-kernels/spyder_kernels/console/start.py index f80111d76c9..eb910305957 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/start.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/start.py @@ -16,17 +16,11 @@ import sys import site +# Third-party imports from traitlets import DottedObjectName -import ipykernel # Local imports from spyder_kernels.utils.misc import is_module_installed -from spyder_kernels.utils.mpl import ( - MPL_BACKENDS_FROM_SPYDER, INLINE_FIGURE_FORMATS) - - -PY2 = sys.version[0] == '2' -IPYKERNEL_6 = ipykernel.__version__[0] >= '6' def import_spydercustomize(): @@ -52,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 @@ -96,10 +72,9 @@ def kernel_config(): # Until we implement Issue 1052 spy_cfg.InteractiveShell.xmode = 'Plain' - # Jedi completer. It's only available in Python 3 + # Jedi completer. jedi_o = os.environ.get('SPY_JEDI_O') == 'True' - if not PY2: - spy_cfg.IPCompleter.use_jedi = jedi_o + spy_cfg.IPCompleter.use_jedi = jedi_o # Clear terminal arguments input. # This needs to be done before adding the exec_lines that come from @@ -109,15 +84,6 @@ def kernel_config(): clear_argv = "import sys; sys.argv = ['']; del sys" spy_cfg.IPKernelApp.exec_lines = [clear_argv] - # Set our runfile in builtins here to prevent other packages shadowing it. - # This started to be a problem since IPykernel 6.3.0. - if not PY2: - spy_cfg.IPKernelApp.exec_lines.append( - "import builtins; " - "builtins.runfile = builtins.spyder_runfile; " - "del builtins.spyder_runfile; del builtins" - ) - # Prevent other libraries to change the breakpoint builtin. # This started to be a problem since IPykernel 6.3.0. if sys.version_info[0:2] >= (3, 7): @@ -127,27 +93,15 @@ 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 + # Default inline backend configuration. # This is useful to have when people doesn't # use our config system to configure the # inline backend but want to use # '%matplotlib inline' at runtime spy_cfg.InlineBackend.rc = { + # The typical default figure size is too large for inline use, + # so we shrink the figure size to 6x4, and tweak fonts to + # make that fit. 'figure.figsize': (6.0, 4.0), # 72 dpi matches SVG/qtconsole. # This only affects PNG export, as SVG has no dpi setting. @@ -161,86 +115,24 @@ 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') if autocall_o is not None: spy_cfg.ZMQInteractiveShell.autocall = int(autocall_o) - # To handle the banner by ourselves in IPython 3+ + # To handle the banner by ourselves spy_cfg.ZMQInteractiveShell.banner1 = '' # Greedy completer 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. - if not PY2: - spy_cfg.IPKernelApp.capture_fd_output = False + spy_cfg.IPKernelApp.capture_fd_output = False # Merge IPython and Spyder configs. Spyder prefs will have prevalence # over IPython ones @@ -262,7 +154,7 @@ def varexp(line): except: import matplotlib.pyplot as pyplot pyplot.figure(); - getattr(pyplot, funcname[2:])(ip.kernel._get_current_namespace()[name]) + getattr(pyplot, funcname[2:])(ip._get_current_namespace()[name]) pyplot.show() @@ -296,9 +188,8 @@ def main(): class SpyderKernelApp(IPKernelApp): - if IPYKERNEL_6: - outstream_class = DottedObjectName( - 'spyder_kernels.console.outstream.TTYOutStream') + outstream_class = DottedObjectName( + 'spyder_kernels.console.outstream.TTYOutStream') def init_pdb(self): """ @@ -309,6 +200,13 @@ def init_pdb(self): """ pass + def close(self): + """Close the loopback socket.""" + socket = self.kernel.loopback_socket + if socket and not socket.closed: + socket.close() + return super().close() + # Fire up the kernel instance. kernel = SpyderKernelApp.instance() kernel.kernel_class = SpyderKernel 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 3e6a06509d7..6cb817415ac 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 @@ -11,6 +11,7 @@ # Standard library imports import ast +import asyncio import os import os.path as osp from textwrap import dedent @@ -19,10 +20,10 @@ from subprocess import Popen, PIPE import sys import inspect +import uuid from collections import namedtuple # Test imports -import ipykernel import pytest from flaky import flaky from jupyter_core import paths @@ -30,23 +31,15 @@ import numpy as np # Local imports -from spyder_kernels.py3compat import PY2, PY3, to_text_string 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 - -# For ipykernel 6 -try: - import asyncio -except ImportError: - pass +from spyder_kernels.comms.commbase import CommBase # ============================================================================= # Constants and utility functions # ============================================================================= FILES_PATH = os.path.dirname(os.path.realpath(__file__)) -IPYKERNEL_6 = ipykernel.__version__[0] >= '6' TIMEOUT = 15 SETUP_TIMEOUT = 60 @@ -68,7 +61,7 @@ def setup_kernel(cmd): """start an embedded kernel in a subprocess, and wait for it to be ready This function was taken from the ipykernel project. - We plan to remove it when dropping support for python 2. + We plan to remove it. Yields ------- @@ -89,8 +82,6 @@ def setup_kernel(cmd): if kernel.poll() is not None: o,e = kernel.communicate() - if not PY3 and isinstance(e, bytes): - e = e.decode() raise IOError("Kernel failed to start:\n%s" % e) if not os.path.exists(connection_file): @@ -99,7 +90,16 @@ def setup_kernel(cmd): raise IOError("Connection file %r never arrived" % connection_file) client = BlockingKernelClient(connection_file=connection_file) - client.load_connection_file() + tic = time.time() + while True: + try: + client.load_connection_file() + break + except ValueError: + # The file is not written yet + if time.time() > tic + SETUP_TIMEOUT: + # Give up after 5s + raise IOError("Kernel failed to write connection file") client.start_channels() client.wait_for_ready() try: @@ -107,8 +107,87 @@ def setup_kernel(cmd): finally: client.stop_channels() finally: - if not PY2: - kernel.terminate() + kernel.terminate() + + +class Comm(): + """ + Comm base class, copied from qtconsole without the qt stuff + """ + + def __init__(self, target_name, kernel_client, + msg_callback=None, close_callback=None): + """ + Create a new comm. Must call open to use. + """ + self.target_name = target_name + self.kernel_client = kernel_client + self.comm_id = uuid.uuid1().hex + self._msg_callback = msg_callback + self._close_callback = close_callback + self._send_channel = self.kernel_client.shell_channel + + def _send_msg(self, msg_type, content, data, metadata, buffers): + """ + Send a message on the shell channel. + """ + if data is None: + data = {} + if content is None: + content = {} + content['comm_id'] = self.comm_id + content['data'] = data + msg = self.kernel_client.session.msg( + msg_type, content, metadata=metadata) + if buffers: + msg['buffers'] = buffers + return self._send_channel.send(msg) + + # methods for sending messages + def open(self, data=None, metadata=None, buffers=None): + """Open the kernel-side version of this comm""" + return self._send_msg( + 'comm_open', {'target_name': self.target_name}, + data, metadata, buffers) + + def send(self, data=None, metadata=None, buffers=None): + """Send a message to the kernel-side version of this comm""" + return self._send_msg( + 'comm_msg', {}, data, metadata, buffers) + + def close(self, data=None, metadata=None, buffers=None): + """Close the kernel-side version of this comm""" + return self._send_msg( + 'comm_close', {}, data, metadata, buffers) + + def on_msg(self, callback): + """Register a callback for comm_msg + + Will be called with the `data` of any comm_msg messages. + + Call `on_msg(None)` to disable an existing callback. + """ + self._msg_callback = callback + + def on_close(self, callback): + """Register a callback for comm_close + + Will be called with the `data` of the close message. + + Call `on_close(None)` to disable an existing callback. + """ + self._close_callback = callback + + # methods for handling incoming messages + def handle_msg(self, msg): + """Handle a comm_msg message""" + if self._msg_callback: + return self._msg_callback(msg) + + def handle_close(self, msg): + """Handle a comm_close message""" + if self._close_callback: + return self._close_callback(msg) # ============================================================================= @@ -147,15 +226,13 @@ def kernel(request): 'False_', 'True_' ], - 'minmax': False + 'minmax': False, + 'filter_on':True } # Teardown def reset_kernel(): - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('reset -f', True)) - else: - kernel.do_execute('reset -f', True) + asyncio.run(kernel.do_execute('reset -f', True)) request.addfinalizer(reset_kernel) return kernel @@ -200,10 +277,7 @@ def test_get_namespace_view(kernel): """ Test the namespace view of the kernel. """ - if IPYKERNEL_6: - execute = asyncio.run(kernel.do_execute('a = 1', True)) - else: - execute = kernel.do_execute('a = 1', True) + execute = asyncio.run(kernel.do_execute('a = 1', True)) nsview = repr(kernel.get_namespace_view()) assert "'a':" in nsview @@ -211,21 +285,39 @@ def test_get_namespace_view(kernel): assert "'size': 1" in nsview assert "'view': '1'" in nsview assert "'numpy_type': 'Unknown'" in nsview + assert "'python_type': 'int'" in nsview + - if PY3: - 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 "'python_type': u'int'" in nsview + 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. """ - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('a = 1', True)) - else: - kernel.do_execute('a = 1', True) + asyncio.run(kernel.do_execute('a = 1', True)) var_properties = repr(kernel.get_var_properties()) assert "'a'" in var_properties @@ -243,10 +335,7 @@ def test_get_var_properties(kernel): def test_get_value(kernel): """Test getting the value of a variable.""" name = 'a' - if IPYKERNEL_6: - asyncio.run(kernel.do_execute("a = 124", True)) - else: - kernel.do_execute("a = 124", True) + asyncio.run(kernel.do_execute("a = 124", True)) # Check data type send assert kernel.get_value(name) == 124 @@ -255,10 +344,7 @@ def test_get_value(kernel): def test_set_value(kernel): """Test setting the value of a variable.""" name = 'a' - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('a = 0', True)) - else: - kernel.do_execute('a = 0', True) + asyncio.run(kernel.do_execute('a = 0', True)) value = 10 kernel.set_value(name, value) log_text = get_log_text(kernel) @@ -272,10 +358,7 @@ def test_set_value(kernel): def test_remove_value(kernel): """Test the removal of a variable.""" name = 'a' - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('a = 1', True)) - else: - kernel.do_execute('a = 1', True) + asyncio.run(kernel.do_execute('a = 1', True)) var_properties = repr(kernel.get_var_properties()) assert "'a'" in var_properties @@ -297,10 +380,7 @@ def test_copy_value(kernel): """Test the copy of a variable.""" orig_name = 'a' new_name = 'b' - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('a = 1', True)) - else: - kernel.do_execute('a = 1', True) + asyncio.run(kernel.do_execute('a = 1', True)) var_properties = repr(kernel.get_var_properties()) assert "'a'" in var_properties @@ -336,11 +416,7 @@ def test_load_npz_data(kernel, load): namespace_file = osp.join(FILES_PATH, 'load_data.npz') extention = '.npz' overwrite, execute, variables = load - - if IPYKERNEL_6: - asyncio.run(kernel.do_execute(execute, True)) - else: - kernel.do_execute(execute, True) + asyncio.run(kernel.do_execute(execute, True)) kernel.load_data(namespace_file, extention, overwrite=overwrite) for var, value in variables.items(): @@ -368,11 +444,7 @@ def test_load_data(kernel): def test_save_namespace(kernel): """Test saving the namespace into filename.""" namespace_file = osp.join(FILES_PATH, 'save_data.spydata') - - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('b = 1', True)) - else: - kernel.do_execute('b = 1', True) + asyncio.run(kernel.do_execute('b = 1', True)) kernel.save_namespace(namespace_file) assert osp.isfile(namespace_file) @@ -400,7 +472,7 @@ def test_get_doc(kernel): def test_get_source(kernel): """Test to get object source.""" objtxt = 'help' - assert 'class _Helper(object):' in kernel.get_source(objtxt) + assert 'class _Helper' in kernel.get_source(objtxt) # --- Other stuff @@ -416,11 +488,7 @@ def test_output_from_c_libraries(kernel, capsys): # With Wurlitzer we have the expected output kernel._load_wurlitzer() - - if IPYKERNEL_6: - asyncio.run(kernel.do_execute(code, True)) - else: - kernel.do_execute(code, True) + asyncio.run(kernel.do_execute(code, True)) captured = capsys.readouterr() assert captured.out == "Hello from C\n" @@ -435,11 +503,9 @@ def test_cwd_in_sys_path(): cmd = "from spyder_kernels.console import start; start.main()" with setup_kernel(cmd) as client: - msg_id = client.execute("import sys; sys_path = sys.path", - user_expressions={'output':'sys_path'}) - reply = client.get_shell_msg(timeout=TIMEOUT) - while 'user_expressions' not in reply['content']: - reply = client.get_shell_msg(timeout=TIMEOUT) + reply = client.execute_interactive( + "import sys; sys_path = sys.path", + user_expressions={'output':'sys_path'}, timeout=TIMEOUT) # Transform value obtained through user_expressions user_expressions = reply['content']['user_expressions'] @@ -451,19 +517,16 @@ def test_cwd_in_sys_path(): @flaky(max_runs=3) -@pytest.mark.skipif(not PY3, - reason="Only meant for Python 3") def test_multiprocessing(tmpdir): """ - Test that multiprocessing works on Python 3. + Test that multiprocessing works. """ # 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("%reset -f") - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive("%reset -f", timeout=TIMEOUT) # Write multiprocessing code to a file code = """ @@ -480,8 +543,8 @@ def f(x): p.write(code) # Run code - client.execute("runfile(r'{}')".format(to_text_string(p))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT) # Verify that the `result` variable is defined client.inspect('result') @@ -493,19 +556,16 @@ def f(x): @flaky(max_runs=3) -@pytest.mark.skipif(not PY3, - reason="Only meant for Python 3") def test_multiprocessing_2(tmpdir): """ - Test that multiprocessing works on Python 3. + Test that multiprocessing works. """ # 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("%reset -f") - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive("%reset -f", timeout=TIMEOUT) # Write multiprocessing code to a file code = """ @@ -527,8 +587,8 @@ def myFunc(i): p.write(code) # Run code - client.execute("runfile(r'{}')".format(to_text_string(p))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT) # Verify that the `result` variable is defined client.inspect('result') @@ -541,7 +601,6 @@ def myFunc(i): @flaky(max_runs=3) -@pytest.mark.skipif(not PY3, reason="Only meant for Python 3") @pytest.mark.skipif( sys.platform == 'darwin' and sys.version_info[:2] == (3, 8), reason="Fails on Mac with Python 3.8") @@ -550,15 +609,14 @@ def myFunc(i): reason="Doesn't work with pip packages") def test_dask_multiprocessing(tmpdir): """ - Test that dask multiprocessing works on Python 3. + Test that dask multiprocessing works. """ # 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("%reset -f") - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive("%reset -f") # Write multiprocessing code to a file # Runs two times to verify that in the second case it doesn't break @@ -574,11 +632,11 @@ def test_dask_multiprocessing(tmpdir): p.write(code) # Run code two times - client.execute("runfile(r'{}')".format(to_text_string(p))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT) - client.execute("runfile(r'{}')".format(to_text_string(p))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT) # Verify that the `x` variable is defined client.inspect('x') @@ -599,16 +657,15 @@ def test_runfile(tmpdir): with setup_kernel(cmd) as client: # Remove all variables - client.execute("%reset -f") - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive("%reset -f", timeout=TIMEOUT) # Write defined variable code to a file - code = u"result = 'hello world'; error # make an error" + code = "result = 'hello world'; error # make an error" d = tmpdir.join("defined-test.py") d.write(code) # Write undefined variable code to a file - code = dedent(u""" + code = dedent(""" try: result3 = result except NameError: @@ -618,9 +675,8 @@ def test_runfile(tmpdir): u.write(code) # Run code file `d` to define `result` even after an error - client.execute("runfile(r'{}', current_namespace=False)" - .format(to_text_string(d))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(d))), timeout=TIMEOUT) # Verify that `result` is defined in the current namespace client.inspect('result') @@ -631,9 +687,8 @@ def test_runfile(tmpdir): assert content['found'] # Run code file `u` without current namespace - client.execute("runfile(r'{}', current_namespace=False)" - .format(to_text_string(u))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(u))), timeout=TIMEOUT) # Verify that the variable `result2` is defined client.inspect('result2') @@ -644,9 +699,8 @@ def test_runfile(tmpdir): assert content['found'] # Run code file `u` with current namespace - client.execute("runfile(r'{}', current_namespace=True)" - .format(to_text_string(u))) - msg = client.get_shell_msg(timeout=TIMEOUT) + msg = client.execute_interactive("%runfile {} --current-namespace" + .format(repr(str(u))), timeout=TIMEOUT) content = msg['content'] # Verify that the variable `result3` is defined @@ -678,30 +732,27 @@ def test_np_threshold(kernel): with setup_kernel(cmd) as client: # Set Numpy threshold, suppress and formatter - client.execute(""" + client.execute_interactive(""" import numpy as np; np.set_printoptions( threshold=np.inf, suppress=True, formatter={'float_kind':'{:0.2f}'.format}) - """) - client.get_shell_msg(timeout=TIMEOUT) + """, timeout=TIMEOUT) # Create a big Numpy array and an array to check decimal format - client.execute(""" + client.execute_interactive(""" x = np.random.rand(75000,5); a = np.array([123412341234.123412341234]) -""") - client.get_shell_msg(timeout=TIMEOUT) +""", timeout=TIMEOUT) # Assert that NumPy threshold, suppress and formatter # are the same as the ones set by the user - client.execute(""" + client.execute_interactive(""" t = np.get_printoptions()['threshold']; s = np.get_printoptions()['suppress']; f = np.get_printoptions()['formatter'] -""") - client.get_shell_msg(timeout=TIMEOUT) +""", timeout=TIMEOUT) # Check correct decimal format client.inspect('a') @@ -747,8 +798,7 @@ def test_turtle_launch(tmpdir): with setup_kernel(cmd) as client: # Remove all variables - client.execute("%reset -f") - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive("%reset -f", timeout=TIMEOUT) # Write turtle code to a file code = """ @@ -772,8 +822,8 @@ def test_turtle_launch(tmpdir): p.write(code) # Run code - client.execute("runfile(r'{}')".format(to_text_string(p))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT) # Verify that the `tess` variable is defined client.inspect('tess') @@ -790,8 +840,8 @@ def test_turtle_launch(tmpdir): p.write(code) # Run code again - client.execute("runfile(r'{}')".format(to_text_string(p))) - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT) # Verify that the `a` variable is defined client.inspect('a') @@ -811,10 +861,8 @@ def test_matplotlib_inline(kernel): with setup_kernel(cmd) as client: # Get current backend code = "import matplotlib; backend = matplotlib.get_backend()" - client.execute(code, user_expressions={'output': 'backend'}) - reply = client.get_shell_msg(timeout=TIMEOUT) - while 'user_expressions' not in reply['content']: - reply = client.get_shell_msg(timeout=TIMEOUT) + reply = client.execute_interactive( + code, user_expressions={'output': 'backend'}, timeout=TIMEOUT) # Transform value obtained through user_expressions user_expressions = reply['content']['user_expressions'] @@ -829,10 +877,7 @@ def test_do_complete(kernel): """ Check do complete works in normal and debugging mode. """ - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('abba = 1', True)) - else: - kernel.do_execute('abba = 1', True) + asyncio.run(kernel.do_execute('abba = 1', True)) assert kernel.get_value('abba') == 1 match = kernel.do_complete('ab', 2) assert 'abba' in match['matches'] @@ -841,7 +886,7 @@ def test_do_complete(kernel): pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.completenames = lambda *ignore: ['baba'] - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] match = kernel.do_complete('ba', 2) assert 'baba' in match['matches'] pdb_obj.curframe = None @@ -855,17 +900,11 @@ def test_callables_and_modules(kernel, exclude_callables_and_modules, Tests that callables and modules are in the namespace view only when the right options are passed to the kernel. """ - if IPYKERNEL_6: - asyncio.run(kernel.do_execute('import numpy', True)) - asyncio.run(kernel.do_execute('a = 10', True)) - asyncio.run(kernel.do_execute('def f(x): return x', True)) - else: - kernel.do_execute('import numpy', True) - kernel.do_execute('a = 10', True) - kernel.do_execute('def f(x): return x', True) + asyncio.run(kernel.do_execute('import numpy', True)) + asyncio.run(kernel.do_execute('a = 10', True)) + asyncio.run(kernel.do_execute('def f(x): return x', True)) settings = kernel.namespace_view_settings - settings['exclude_callables_and_modules'] = exclude_callables_and_modules settings['exclude_unsupported'] = exclude_unsupported nsview = kernel.get_namespace_view() @@ -899,7 +938,7 @@ def test_comprehensions_with_locals_in_pdb(kernel): pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] # Create a local variable. kernel.shell.pdb_session.default('zz = 10') @@ -925,7 +964,7 @@ def test_comprehensions_with_locals_in_pdb_2(kernel): pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] # Create a local variable. kernel.shell.pdb_session.default('aa = [1, 2]') @@ -951,18 +990,12 @@ def test_namespaces_in_pdb(kernel): pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] # Check adding something to globals works pdb_obj.default("globals()['test2'] = 0") assert pdb_obj.curframe.f_globals["test2"] == 0 - if PY2: - # no error method in py2 - pdb_obj.curframe = None - pdb_obj.curframe_locals = None - return - # Create wrapper to check for errors old_error = pdb_obj.error pdb_obj._error_occured = False @@ -1000,7 +1033,7 @@ def test_functions_with_locals_in_pdb(kernel): Frame = namedtuple("Frame", ["f_globals"]) pdb_obj.curframe = Frame(f_globals=kernel.shell.user_ns) pdb_obj.curframe_locals = kernel.shell.user_ns - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] # Create a local function. kernel.shell.pdb_session.default( @@ -1032,7 +1065,7 @@ def test_functions_with_locals_in_pdb_2(kernel): pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] # Create a local function. kernel.shell.pdb_session.default( @@ -1069,7 +1102,7 @@ def test_locals_globals_in_pdb(kernel): pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals - kernel.shell.pdb_session = pdb_obj + kernel.shell._namespace_stack = [pdb_obj] assert kernel.get_value('a') == 1 @@ -1104,52 +1137,95 @@ def test_locals_globals_in_pdb(kernel): @flaky(max_runs=3) -@pytest.mark.parametrize("backend", [None, 'inline', 'tk', 'qt5']) -@pytest.mark.skipif( - not sys.platform.startswith('linux'), - reason="Doesn't work reliably on Windows and Mac") +@pytest.mark.parametrize("backend", [None, 'inline', 'tk', 'qt']) @pytest.mark.skipif( os.environ.get('USE_CONDA') != 'true', reason="Doesn't work with pip packages") @pytest.mark.skipif( - sys.version_info[:2] < (3, 8), - reason="Too flaky in Python 3.7 and doesn't work in older versions") + sys.version_info[:2] < (3, 9), + reason="Too flaky in Python 3.8 and doesn't work in older versions") def test_get_interactive_backend(backend): """ Test that we correctly get the interactive backend set in the kernel. """ - cmd = "from spyder_kernels.console import start; start.main()" + # This test passes locally but fails on CIs. Don't know why. + if sys.platform == "darwin" and backend == "qt" and os.environ.get('CI'): + return + cmd = "from spyder_kernels.console import start; start.main()" with setup_kernel(cmd) as client: # Set backend if backend is not None: - client.execute("%matplotlib {}".format(backend)) - client.get_shell_msg(timeout=TIMEOUT) - client.execute("import time; time.sleep(.1)") - client.get_shell_msg(timeout=TIMEOUT) + client.execute_interactive( + "%matplotlib {}".format(backend), timeout=TIMEOUT) + client.execute_interactive( + "import time; time.sleep(.1)", timeout=TIMEOUT) # Get backend code = "backend = get_ipython().kernel.get_mpl_interactive_backend()" - client.execute(code, user_expressions={'output': 'backend'}) - reply = client.get_shell_msg(timeout=TIMEOUT) - while 'user_expressions' not in reply['content']: - reply = client.get_shell_msg(timeout=TIMEOUT) + reply = client.execute_interactive( + code, user_expressions={'output': 'backend'}, timeout=TIMEOUT) # Get value obtained through user_expressions 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): + """ + Test that using `global` triggers a warning. + """ + # 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 code with a global to a file + code = ( + "def foo1():\n" + " global x\n" + " x = 2\n" + "x = 1\n" + "print(x)\n" + ) + + p = tmpdir.join("test.py") + p.write(code) + global found + found = False + + def check_found(msg): + if "text" in msg["content"]: + if ("WARNING: This file contains a global statement" in + msg["content"]["text"]): + global found + found = True + + # Run code in current namespace + client.execute_interactive("%runfile {} --current-namespace".format( + repr(str(p))), timeout=TIMEOUT, output_hook=check_found) + assert not found + + # Run code in empty namespace + client.execute_interactive( + "%runfile {}".format(repr(str(p))), timeout=TIMEOUT, + output_hook=check_found) + + assert found @flaky(max_runs=3) -@pytest.mark.skipif( - sys.version_info[0] < 3, - reason="Fails with python 2") def test_debug_namespace(tmpdir): """ Test that the kernel uses the proper namespace while debugging. @@ -1163,7 +1239,7 @@ def test_debug_namespace(tmpdir): d.write('def func():\n bb = "hello"\n breakpoint()\nfunc()') # Run code file `d` - msg_id = client.execute("runfile(r'{}')".format(to_text_string(d))) + msg_id = client.execute("%runfile {}".format(repr(str(d)))) # make sure that 'bb' returns 'hello' client.get_stdin_msg(timeout=TIMEOUT) @@ -1190,6 +1266,105 @@ def test_debug_namespace(tmpdir): break +def test_interrupt(): + """ + Test that the kernel can be interrupted by calling a comm handler. + """ + # Command to start the kernel + cmd = "from spyder_kernels.console import start; start.main()" + import pickle + with setup_kernel(cmd) as client: + kernel_comm = CommBase() + + # Create new comm and send the highest protocol + comm = Comm(kernel_comm._comm_name, client) + comm.open(data={'pickle_highest_protocol': pickle.HIGHEST_PROTOCOL}) + comm._send_channel = client.control_channel + kernel_comm._register_comm(comm) + + client.execute_interactive("import time", timeout=TIMEOUT) + + # Try interrupting loop + t0 = time.time() + msg_id = client.execute("for i in range(100): time.sleep(.1)") + time.sleep(.2) + # Raise interrupt on control_channel + kernel_comm.remote_call().raise_interrupt_signal() + # Wait for shell message + while True: + assert time.time() - t0 < 5 + msg = client.get_shell_msg(timeout=TIMEOUT) + if msg["parent_header"].get("msg_id") != msg_id: + # not from my request + continue + break + assert time.time() - t0 < 5 + + if os.name == 'nt': + # Windows doesn't do "interrupting sleep" + return + + # Try interrupting sleep + t0 = time.time() + msg_id = client.execute("time.sleep(10)") + time.sleep(.2) + # Raise interrupt on control_channel + kernel_comm.remote_call().raise_interrupt_signal() + # Wait for shell message + while True: + assert time.time() - t0 < 5 + msg = client.get_shell_msg(timeout=TIMEOUT) + if msg["parent_header"].get("msg_id") != msg_id: + # not from my request + continue + break + assert time.time() - t0 < 5 + + +def test_enter_debug_after_interruption(): + """ + Test that we can enter the debugger after interrupting the current + execution. + """ + # Command to start the kernel + cmd = "from spyder_kernels.console import start; start.main()" + import pickle + with setup_kernel(cmd) as client: + kernel_comm = CommBase() + + # Create new comm and send the highest protocol + comm = Comm(kernel_comm._comm_name, client) + comm.open(data={'pickle_highest_protocol': pickle.HIGHEST_PROTOCOL}) + comm._send_channel = client.control_channel + kernel_comm._register_comm(comm) + + client.execute_interactive("import time", timeout=TIMEOUT) + + # Try interrupting loop + t0 = time.time() + msg_id = client.execute("for i in range(100): time.sleep(.1)") + time.sleep(.2) + # Request to enter the debugger + kernel_comm.remote_call().request_pdb_stop() + # Wait for debug message + while True: + assert time.time() - t0 < 5 + msg = client.get_iopub_msg(timeout=TIMEOUT) + if msg.get('msg_type') == 'stream': + print(msg["content"].get("text")) + if msg["parent_header"].get("msg_id") != msg_id: + # not from my request + continue + if msg.get('msg_type') == 'comm_msg': + if msg["content"].get("data", {}).get("content", {}).get( + 'call_name') == 'pdb_input': + # pdb entered + break + comm.handle_msg(msg) + + assert time.time() - t0 < 5 + + def test_non_strings_in_locals(kernel): """ Test that we can hande non-string entries in `locals` when bulding the @@ -1197,18 +1372,12 @@ def test_non_strings_in_locals(kernel): This is a regression test for issue spyder-ide/spyder#19145 """ - if IPYKERNEL_6: - execute = asyncio.run(kernel.do_execute( - 'locals().update({1:2})', True)) - else: - execute = kernel.do_execute('locals().update({1:2})', True) + execute = asyncio.run(kernel.do_execute('locals().update({1:2})', True)) nsview = repr(kernel.get_namespace_view()) assert "1:" in nsview -@pytest.mark.skipif( - sys.version_info[0] < 3, reason="Doesn't work with Python 2") def test_django_settings(kernel): """ Test that we don't generate errors when importing `django.conf.settings`. diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py new file mode 100644 index 00000000000..d3cc7a1f485 --- /dev/null +++ b/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py @@ -0,0 +1,614 @@ +# +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +""" +Spyder magics related to code execution, debugging, profiling, etc. +""" + +# Standard library imports +import ast +import bdb +import builtins +from contextlib import contextmanager +import io +import logging +import os +import pdb +import shlex +import sys +import time + +# Third-party imports +from IPython.core.inputtransformer2 import ( + TransformerManager, + leading_indent, + leading_empty_lines, +) +from IPython.core.magic import ( + needs_local_scope, + magics_class, + Magics, + line_magic, +) +from IPython.core import magic_arguments + +# Local imports +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, exec_encapsulate_locals +) + + +# For logging +logger = logging.getLogger(__name__) + + +def runfile_arguments(func): + """Decorator to add runfile magic arguments to magic.""" + decorators = [ + magic_arguments.magic_arguments(), + magic_arguments.argument( + "filename", + help=""" + Filename to run + """, + ), + magic_arguments.argument( + "--args", + help=""" + Command line arguments (string) + """, + ), + magic_arguments.argument( + "--wdir", + const=True, + nargs="?", + help=""" + Working directory + """, + ), + magic_arguments.argument( + "--post-mortem", + action="store_true", + help=""" + Enter post-mortem mode on errors + """, + ), + magic_arguments.argument( + "--current-namespace", + action="store_true", + help=""" + Use current namespace + """, + ), + magic_arguments.argument( + "--namespace", + help=""" + Namespace to run the file in + """, + ) + ] + for dec in reversed(decorators): + func = dec(func) + return func + + +def runcell_arguments(func): + """Decorator to add runcell magic arguments to magic.""" + decorators = [ + magic_arguments.magic_arguments(), + magic_arguments.argument( + "--name", "-n", + help=""" + Cell name. + """, + ), + magic_arguments.argument( + "--index", "-i", + help=""" + Cell index. + """, + ), + magic_arguments.argument( + "filename", + nargs="?", + help=""" + Filename + """, + ), + magic_arguments.argument( + "--post-mortem", + action="store_true", + default=False, + help=""" + Enter post-mortem mode on errors + """, + ) + ] + for dec in reversed(decorators): + func = dec(func) + return func + + +@magics_class +class SpyderCodeRunner(Magics): + """ + Functions and magics related to code execution, debugging, profiling, etc. + """ + def __init__(self, *args, **kwargs): + self.show_global_msg = True + self.show_invalid_syntax_msg = True + self.umr = UserModuleReloader( + namelist=os.environ.get("SPY_UMR_NAMELIST", None) + ) + super().__init__(*args, **kwargs) + + @runfile_arguments + @needs_local_scope + @line_magic + def runfile(self, line, local_ns=None): + """ + Run a file. + """ + args, local_ns = self._parse_runfile_argstring( + self.runfile, line, local_ns) + + return self._exec_file( + filename=args.filename, + canonic_filename=args.canonic_filename, + args=args.args, + wdir=args.wdir, + post_mortem=args.post_mortem, + current_namespace=args.current_namespace, + context_globals=args.namespace, + context_locals=local_ns, + ) + + @runfile_arguments + @needs_local_scope + @line_magic + def debugfile(self, line, local_ns=None): + """ + Debug a file. + """ + args, local_ns = self._parse_runfile_argstring( + self.debugfile, line, local_ns) + + with self._debugger_exec(args.canonic_filename, True) as debug_exec: + self._exec_file( + filename=args.filename, + canonic_filename=args.canonic_filename, + args=args.args, + wdir=args.wdir, + current_namespace=args.current_namespace, + exec_fun=debug_exec, + post_mortem=args.post_mortem, + context_globals=args.namespace, + context_locals=local_ns, + ) + + @runcell_arguments + @needs_local_scope + @line_magic + def runcell(self, line, local_ns=None): + """ + Run a code cell from an editor. + """ + args = self._parse_runcell_argstring(self.runcell, line) + + return self._exec_cell( + cell_id=args.cell_id, + filename=args.filename, + canonic_filename=args.canonic_filename, + post_mortem=args.post_mortem, + context_globals=self.shell.user_ns, + context_locals=local_ns, + ) + + @runcell_arguments + @needs_local_scope + @line_magic + def debugcell(self, line, local_ns=None): + """ + Debug a code cell from an editor. + """ + args = self._parse_runcell_argstring(self.debugcell, line) + + with self._debugger_exec(args.canonic_filename, False) as debug_exec: + return self._exec_cell( + cell_id=args.cell_id, + filename=args.filename, + canonic_filename=args.canonic_filename, + exec_fun=debug_exec, + post_mortem=args.post_mortem, + context_globals=self.shell.user_ns, + context_locals=local_ns, + ) + + @contextmanager + def _debugger_exec(self, filename, continue_if_has_breakpoints): + """Get an exec function to use for debugging.""" + if not self.shell.is_debugging(): + debugger = SpyderPdb() + debugger.set_remote_filename(filename) + debugger.continue_if_has_breakpoints = continue_if_has_breakpoints + yield debugger.run + return + + session = self.shell.pdb_session + with session.recursive_debugger() as debugger: + debugger.set_remote_filename(filename) + debugger.continue_if_has_breakpoints = continue_if_has_breakpoints + + def debug_exec(code, glob, loc): + return sys.call_tracing(debugger.run, (code, glob, loc)) + + # Enter recursive debugger + yield debug_exec + + def _exec_file( + self, + filename=None, + args=None, + wdir=None, + post_mortem=False, + current_namespace=False, + exec_fun=None, + canonic_filename=None, + context_locals=None, + context_globals=None, + ): + """ + Execute a file. + """ + 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") + + try: + file_code = self._get_file_code(filename, raise_exception=True) + except Exception: + print( + "This command failed to be executed because an error occurred " + "while trying to get the file code from Spyder's editor. " + "The error was:\n\n" + ) + self.shell.showtraceback(exception_only=True) + return + + # Here the remote filename has been used. It must now be valid locally. + filename = canonic_filename + + with NamespaceManager( + self.shell, + filename, + current_namespace=current_namespace, + file_code=file_code, + context_locals=context_locals, + context_globals=context_globals, + ) as (ns_globals, ns_locals): + sys.argv = [filename] + if args is not None: + # args are a sting in a string + for arg in shlex.split(args): + sys.argv.append(arg) + + if "multiprocessing" in sys.modules: + # See https://github.com/spyder-ide/spyder/issues/16696 + try: + sys.modules["__mp_main__"] = sys.modules["__main__"] + except Exception: + pass + + if wdir is not None: + if wdir is True: + # True means use file dir + wdir = os.path.dirname(filename) + if os.path.isdir(wdir): + os.chdir(wdir) + + # See https://github.com/spyder-ide/spyder/issues/13632 + if "multiprocessing.process" in sys.modules: + try: + import multiprocessing.process + multiprocessing.process.ORIGINAL_DIR = os.path.abspath(wdir) + except Exception: + pass + else: + print("Working directory {} doesn't exist.\n".format(wdir)) + + try: + if self.shell.special == "cython": + # Cython files + with io.open(filename, encoding="utf-8") as f: + self.shell.run_cell_magic("cython", "", f.read()) + else: + self._exec_code( + file_code, + filename, + ns_globals, + ns_locals, + post_mortem=post_mortem, + exec_fun=exec_fun, + capture_last_expression=False, + global_warning=not current_namespace, + ) + finally: + sys.argv = [""] + + def _exec_cell( + self, + cell_id, + filename=None, + post_mortem=False, + exec_fun=None, + canonic_filename=None, + context_locals=None, + context_globals=None, + ): + """ + Execute a code cell. + """ + try: + # Get code from spyder + cell_code = frontend_request(blocking=True).run_cell(cell_id, filename) + except Exception: + print( + "This command failed to be executed because an error occurred " + "while trying to get the cell code from Spyder's editor." + "The error was:\n\n" + ) + self.shell.showtraceback(exception_only=True) + return + + if not cell_code or cell_code.strip() == "": + print("Nothing to execute, this cell is empty.\n") + return + + # Trigger `post_execute` to exit the additional pre-execution. + # See Spyder PR #7310. + self.shell.events.trigger("post_execute") + file_code = self._get_file_code(filename, save_all=False) + + # Here the remote filename has been used. It must now be valid locally. + filename = canonic_filename + + with NamespaceManager( + self.shell, + filename, + current_namespace=True, + file_code=file_code, + context_locals=context_locals, + context_globals=context_globals + ) as (ns_globals, ns_locals): + return self._exec_code( + cell_code, + filename, + ns_globals, + ns_locals, + post_mortem=post_mortem, + exec_fun=exec_fun, + capture_last_expression=True, + ) + + def _get_current_file_name(self): + """Get the current editor file name.""" + try: + return frontend_request(blocking=True).current_filename() + except Exception: + print( + "This command failed to be executed because an error occurred " + "while trying to get the current file name from Spyder's editor." + "The error was:\n\n" + ) + self.shell.showtraceback(exception_only=True) + return None + + def _get_file_code(self, filename, save_all=True, raise_exception=False): + """Retrieve the content of a file.""" + # Get code from spyder + try: + return frontend_request(blocking=True).get_file_code( + filename, save_all=save_all + ) + except Exception: + # Maybe this is a local file + try: + with open(filename, "r") as f: + return f.read() + except FileNotFoundError: + pass + + if raise_exception: + raise + + # Finally return None + return None + + def _exec_code( + self, + code, + filename, + ns_globals, + ns_locals=None, + post_mortem=False, + exec_fun=None, + capture_last_expression=False, + global_warning=False, + ): + """Execute code and display any exception.""" + if exec_fun is None: + exec_fun = exec + + is_ipython = os.path.splitext(filename)[1] == ".ipy" + try: + if not is_ipython: + # TODO: Remove the try-except and let the SyntaxError raise + # because there should't be IPython code in a Python file. + try: + ast_code = ast.parse( + self._transform_cell(code, indent_only=True) + ) + except SyntaxError as e: + try: + ast_code = ast.parse(self._transform_cell(code)) + except SyntaxError: + raise e from None + else: + if self.show_invalid_syntax_msg: + print( + "\nWARNING: This is not valid Python code. " + "If you want to use IPython magics, " + "flexible indentation, and prompt removal, " + "we recommend that you save this file with the " + ".ipy extension.\n" + ) + self.show_invalid_syntax_msg = False + else: + ast_code = ast.parse(self._transform_cell(code)) + + # Print warning for global + if global_warning and self.show_global_msg: + has_global = any( + isinstance(node, ast.Global) for node in ast.walk(ast_code) + ) + if has_global: + print( + "\nWARNING: This file contains a global statement, " + "but it is run in an empty namespace. " + "Consider using the " + "'Run in console's namespace instead of an empty one' " + "option, that you can find in the menu 'Run > " + "Configuration per file', if you want to capture the " + "namespace.\n" + ) + self.show_global_msg = False + + if code.rstrip()[-1:] == ";": + # Supress output with ; + capture_last_expression = False + + if capture_last_expression: + ast_code, capture_last_expression = capture_last_Expr( + ast_code, "_spyder_out", ns_globals + ) + + exec_encapsulate_locals( + ast_code, ns_globals, ns_locals, exec_fun, filename + ) + + if capture_last_expression: + out = ns_globals.pop("_spyder_out", None) + if out is not None: + return out + except SystemExit as status: + # ignore exit(0) + if status.code: + self.shell.showtraceback(exception_only=True) + except BaseException as error: + if isinstance(error, bdb.BdbQuit) and self.shell.pdb_session: + # Ignore BdbQuit if we are debugging, as it is expected. + pass + elif post_mortem and isinstance(error, Exception): + error_type, error, tb = sys.exc_info() + self._post_mortem_excepthook(error_type, error, tb) + else: + # We ignore the call to exec + self.shell.showtraceback(tb_offset=1) + finally: + __tracebackhide__ = "__pdb_exit__" + + def _count_leading_empty_lines(self, cell): + """Count the number of leading empty cells.""" + lines = cell.splitlines(keepends=True) + if not lines: + return 0 + for i, line in enumerate(lines): + if line and not line.isspace(): + return i + return len(lines) + + def _transform_cell(self, code, indent_only=False): + """Transform IPython code to Python code.""" + number_empty_lines = self._count_leading_empty_lines(code) + if indent_only: + if not code.endswith("\n"): + code += "\n" # Ensure the cell has a trailing newline + lines = code.splitlines(keepends=True) + lines = leading_indent(leading_empty_lines(lines)) + code = "".join(lines) + else: + tm = TransformerManager() + code = tm.transform_cell(code) + return "\n" * number_empty_lines + code + + def _post_mortem_excepthook(self, type, value, tb): + """ + For post mortem exception handling, print a banner and enable post + mortem debugging. + """ + self.shell.showtraceback((type, value, tb)) + p = pdb.Pdb(self.shell.colors) + + if not type == SyntaxError: + # wait for stderr to print (stderr.flush does not work in this case) + time.sleep(0.1) + print("*" * 40) + print("Entering post mortem debugging...") + print("*" * 40) + # add ability to move between frames + p.reset() + frame = tb.tb_next.tb_frame + # wait for stdout to print + time.sleep(0.1) + p.interaction(frame, tb) + + def _parse_argstring(self, magic_func, argstring): + """ + Parse a string of arguments for a magic function. + + This is needed because magic_arguments.parse_argstring does + platform-dependent things with quotes and backslashes. For + example, on Windows, strings are removed and backslashes are + escaped. + """ + argv = shlex.split(argstring) + args = magic_func.parser.parse_args(argv) + if args.filename is None: + args.filename = self._get_current_file_name() + args.canonic_filename = canonic(args.filename) + return args + + def _parse_runfile_argstring(self, magic_func, argstring, local_ns): + """Parse an args string for runfile and debugfile.""" + args = self._parse_argstring(magic_func, argstring) + if args.namespace is None: + args.namespace = self.shell.user_ns + else: + if local_ns is not None and args.namespace in local_ns: + args.namespace = local_ns[args.namespace] + elif args.namespace in self.shell.user_ns: + args.namespace = self.shell.user_ns[args.namespace] + else: + raise NameError( + f"name '{args.namespace}' is not defined" + ) + local_ns = None + args.current_namespace = True + return args, local_ns + + def _parse_runcell_argstring(self, magic_func, argstring): + """Parse an args string for runcell and debugcell.""" + args = self._parse_argstring(magic_func, argstring) + args.cell_id = args.name + if args.cell_id is None: + args.cell_id = int(args.index) + return args diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py b/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py index e92214e8ca9..f758cf27711 100755 --- a/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py @@ -9,10 +9,6 @@ import types import sys -from IPython.core.getipython import get_ipython - -from spyder_kernels.py3compat import PY2 - def new_main_mod(filename, modname): """ @@ -28,12 +24,12 @@ def new_main_mod(filename, modname): main_mod.__file__ = filename # It seems pydoc (and perhaps others) needs any module instance to # implement a __nonzero__ method - main_mod.__nonzero__ = lambda : True + main_mod.__nonzero__ = lambda: True return main_mod -class NamespaceManager(object): +class NamespaceManager: """ Get a namespace and set __file__ to filename for this namespace. @@ -41,52 +37,55 @@ class NamespaceManager(object): current_namespace is True, or a new namespace. """ - def __init__(self, filename, namespace=None, current_namespace=False, - file_code=None, stack_depth=1): + def __init__( + self, + shell, + filename, + current_namespace=False, + file_code=None, + context_locals=None, + context_globals=None, + ): + self.shell = shell self.filename = filename - self.ns_globals = namespace + self.ns_globals = None self.ns_locals = None self.current_namespace = current_namespace self._previous_filename = None self._previous_main = None - self._previous_running_namespace = None self._reset_main = False self._file_code = file_code - ipython_shell = get_ipython() - self.context_globals = ipython_shell.get_global_scope(stack_depth + 1) - self.context_locals = ipython_shell.get_local_scope(stack_depth + 1) + if context_globals is None: + context_globals = shell.user_ns + self.context_globals = context_globals + self.context_locals = context_locals def __enter__(self): """ Prepare the namespace. """ # Save previous __file__ - ipython_shell = get_ipython() - if self.ns_globals is None: - if self.current_namespace: - self.ns_globals = self.context_globals - self.ns_locals = self.context_locals - if '__file__' in self.ns_globals: - self._previous_filename = self.ns_globals['__file__'] - self.ns_globals['__file__'] = self.filename - else: - main_mod = new_main_mod(self.filename, '__main__') - self.ns_globals = main_mod.__dict__ - self.ns_locals = None - # Needed to allow pickle to reference main - if '__main__' in sys.modules: - self._previous_main = sys.modules['__main__'] - sys.modules['__main__'] = main_mod - self._reset_main = True + if self.current_namespace: + self.ns_globals = self.context_globals + self.ns_locals = self.context_locals + if '__file__' in self.ns_globals: + self._previous_filename = self.ns_globals['__file__'] + self.ns_globals['__file__'] = self.filename + else: + main_mod = new_main_mod(self.filename, '__main__') + self.ns_globals = main_mod.__dict__ + self.ns_locals = None + + # Needed to allow pickle to reference main + if '__main__' in sys.modules: + self._previous_main = sys.modules['__main__'] + sys.modules['__main__'] = main_mod + self._reset_main = True # Save current namespace for access by variable explorer - self._previous_running_namespace = ( - ipython_shell.kernel._running_namespace) - ipython_shell.kernel._running_namespace = ( - self.ns_globals, self.ns_locals) + self.shell.add_namespace_manager(self) if (self._file_code is not None - and not PY2 and isinstance(self._file_code, bytes)): try: self._file_code = self._file_code.decode() @@ -106,18 +105,17 @@ def __exit__(self, exc_type, exc_val, exc_tb): """ Reset the namespace. """ - ipython_shell = get_ipython() - ipython_shell.kernel._running_namespace = ( - self._previous_running_namespace) + self.shell.remove_namespace_manager(self) if self._previous_filename: self.ns_globals['__file__'] = self._previous_filename elif '__file__' in self.ns_globals: self.ns_globals.pop('__file__') if not self.current_namespace: - self.context_globals.update(self.ns_globals) - if self.context_locals and self.ns_locals: - self.context_locals.update(self.ns_locals) + if self.context_locals is not None: + self.context_locals.update(self.ns_globals) + else: + self.context_globals.update(self.ns_globals) if self._previous_main: sys.modules['__main__'] = self._previous_main diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py b/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py index b649b2b7453..99c2089890f 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py @@ -11,35 +11,13 @@ # Spyder consoles sitecustomize # -import ast -import bdb -import io import logging import os import pdb -import shlex import sys -import time import warnings -from IPython.core.getipython import get_ipython - -from spyder_kernels.comms.frontendcomm import frontend_request -from spyder_kernels.customize.namespace_manager import NamespaceManager -from spyder_kernels.customize.spyderpdb import SpyderPdb, get_new_debugger -from spyder_kernels.customize.umr import UserModuleReloader -from spyder_kernels.py3compat import ( - PY2, _print, encode, compat_exec, FileNotFoundError) -from spyder_kernels.customize.utils import capture_last_Expr, canonic - -if not PY2: - from IPython.core.inputtransformer2 import ( - TransformerManager, leading_indent, leading_empty_lines) -else: - from IPython.core.inputsplitter import IPythonInputSplitter - - -logger = logging.getLogger(__name__) +from spyder_kernels.customize.spyderpdb import SpyderPdb # ============================================================================= @@ -50,50 +28,11 @@ if not hasattr(sys, 'argv'): sys.argv = [''] - # ============================================================================= # Main constants # ============================================================================= IS_EXT_INTERPRETER = os.environ.get('SPY_EXTERNAL_INTERPRETER') == "True" HIDE_CMD_WINDOWS = os.environ.get('SPY_HIDE_CMD') == "True" -SHOW_INVALID_SYNTAX_MSG = True - - -# ============================================================================= -# Execfile functions -# -# The definitions for Python 2 on Windows were taken from the IPython project -# Copyright (C) The IPython Development Team -# Distributed under the terms of the modified BSD license -# ============================================================================= -try: - # Python 2 - import __builtin__ as builtins - -except ImportError: - # Python 3 - import builtins - basestring = (str,) - - -# ============================================================================= -# Setting console encoding (otherwise Python does not recognize encoding) -# for Windows platforms -# ============================================================================= -if os.name == 'nt' and PY2: - try: - import locale, ctypes - _t, _cp = locale.getdefaultlocale('LANG') - try: - _cp = int(_cp[2:]) - ctypes.windll.kernel32.SetConsoleCP(_cp) - ctypes.windll.kernel32.SetConsoleOutputCP(_cp) - except (ValueError, TypeError): - # Code page number in locale is not valid - pass - except Exception: - pass - # ============================================================================= # Prevent subprocess.Popen calls to create visible console windows on Windows. @@ -110,7 +49,6 @@ def __init__(self, *args, **kwargs): subprocess.Popen = SubprocessPopen - # ============================================================================= # Importing user's sitecustomize # ============================================================================= @@ -119,18 +57,6 @@ def __init__(self, *args, **kwargs): except Exception: pass - -# ============================================================================= -# Add default filesystem encoding on Linux to avoid an error with -# Matplotlib 1.5 in Python 2 (Fixes Issue 2793) -# ============================================================================= -if PY2 and sys.platform.startswith('linux'): - def _getfilesystemencoding_wrapper(): - return 'utf-8' - - sys.getfilesystemencoding = _getfilesystemencoding_wrapper - - # ============================================================================= # Set PyQt API to #2 # ============================================================================= @@ -279,39 +205,37 @@ def spyder_bye(): # ============================================================================= # Multiprocessing adjustments # ============================================================================= -# This patch is only needed on Python 3 -if not PY2: - # This could fail with changes in Python itself, so we protect it - # with a try/except - try: - import multiprocessing.spawn - _old_preparation_data = multiprocessing.spawn.get_preparation_data - - def _patched_preparation_data(name): - """ - Patched get_preparation_data to work when all variables are - removed before execution. - """ - try: - d = _old_preparation_data(name) - except AttributeError: - main_module = sys.modules['__main__'] - # Any string for __spec__ does the job - main_module.__spec__ = '' - d = _old_preparation_data(name) - # On windows, there is no fork, so we need to save the main file - # and import it - if (os.name == 'nt' and 'init_main_from_path' in d - and not os.path.exists(d['init_main_from_path'])): - _print( - "Warning: multiprocessing may need the main file to exist. " - "Please save {}".format(d['init_main_from_path'])) - # Remove path as the subprocess can't do anything with it - del d['init_main_from_path'] - return d - multiprocessing.spawn.get_preparation_data = _patched_preparation_data - except Exception: - pass +# This could fail with changes in Python itself, so we protect it +# with a try/except +try: + import multiprocessing.spawn + _old_preparation_data = multiprocessing.spawn.get_preparation_data + + def _patched_preparation_data(name): + """ + Patched get_preparation_data to work when all variables are + removed before execution. + """ + try: + d = _old_preparation_data(name) + except AttributeError: + main_module = sys.modules['__main__'] + # Any string for __spec__ does the job + main_module.__spec__ = '' + d = _old_preparation_data(name) + # On windows, there is no fork, so we need to save the main file + # and import it + if (os.name == 'nt' and 'init_main_from_path' in d + and not os.path.exists(d['init_main_from_path'])): + print( + "Warning: multiprocessing may need the main file to exist. " + "Please save {}".format(d['init_main_from_path'])) + # Remove path as the subprocess can't do anything with it + del d['init_main_from_path'] + return d + multiprocessing.spawn.get_preparation_data = _patched_preparation_data +except Exception: + pass # ============================================================================= @@ -329,480 +253,6 @@ def _patched_get_terminal_size(fd=None): # ============================================================================= pdb.Pdb = SpyderPdb -# ============================================================================= -# User module reloader -# ============================================================================= -__umr__ = UserModuleReloader(namelist=os.environ.get("SPY_UMR_NAMELIST", None)) - - -# ============================================================================= -# Handle Post Mortem Debugging and Traceback Linkage to Spyder -# ============================================================================= -def post_mortem_excepthook(type, value, tb): - """ - For post mortem exception handling, print a banner and enable post - mortem debugging. - """ - ipython_shell = get_ipython() - ipython_shell.showtraceback((type, value, tb)) - p = pdb.Pdb(ipython_shell.colors) - - if not type == SyntaxError: - # wait for stderr to print (stderr.flush does not work in this case) - time.sleep(0.1) - _print('*' * 40) - _print('Entering post mortem debugging...') - _print('*' * 40) - - # Inform Spyder about position of exception: pdb.Pdb.interaction() calls - # cmd.Cmd.cmdloop(), which calls SpyderPdb.preloop() where - # send_initial_notification is handled. - p.send_initial_notification = True - - p.reset() - frame = tb.tb_next.tb_frame - # wait for stdout to print - time.sleep(0.1) - p.interaction(frame, tb) - - -# ============================================================================== -# runfile and debugfile commands -# ============================================================================== -def get_current_file_name(): - """Get the current file name.""" - try: - return frontend_request(blocking=True).current_filename() - except Exception: - _print("This command failed to be executed because an error occurred" - " while trying to get the current file name from Spyder's" - " editor. The error was:\n\n") - get_ipython().showtraceback(exception_only=True) - return None - - -def count_leading_empty_lines(cell): - """Count the number of leading empty cells.""" - if PY2: - lines = cell.splitlines(True) - else: - lines = cell.splitlines(keepends=True) - if not lines: - return 0 - for i, line in enumerate(lines): - if line and not line.isspace(): - return i - return len(lines) - - -def transform_cell(code, indent_only=False): - """Transform IPython code to Python code.""" - number_empty_lines = count_leading_empty_lines(code) - if indent_only: - # Not implemented for PY2 - if PY2: - return code - if not code.endswith('\n'): - code += '\n' # Ensure the cell has a trailing newline - lines = code.splitlines(keepends=True) - lines = leading_indent(leading_empty_lines(lines)) - code = ''.join(lines) - else: - if PY2: - tm = IPythonInputSplitter() - return tm.transform_cell(code) - else: - tm = TransformerManager() - code = tm.transform_cell(code) - return '\n' * number_empty_lines + code - - -def exec_code(code, filename, ns_globals, ns_locals=None, post_mortem=False, - exec_fun=None, capture_last_expression=False): - """Execute code and display any exception.""" - # Tell IPython to hide this frame (>7.16) - __tracebackhide__ = True - global SHOW_INVALID_SYNTAX_MSG - - if PY2: - filename = encode(filename) - code = encode(code) - - if exec_fun is None: - # Replace by exec when dropping Python 2 - exec_fun = compat_exec - - ipython_shell = get_ipython() - is_ipython = os.path.splitext(filename)[1] == '.ipy' - try: - if not is_ipython: - # TODO: remove the try-except and let the SyntaxError raise - # Because there should not be ipython code in a python file - try: - ast_code = ast.parse(transform_cell(code, indent_only=True)) - except SyntaxError as e: - try: - ast_code = ast.parse(transform_cell(code)) - except SyntaxError: - if PY2: - raise e - else: - # Need to call exec to avoid Syntax Error in Python 2. - # TODO: remove exec when dropping Python 2 support. - exec("raise e from None") - else: - if SHOW_INVALID_SYNTAX_MSG: - _print( - "\nWARNING: This is not valid Python code. " - "If you want to use IPython magics, " - "flexible indentation, and prompt removal, " - "we recommend that you save this file with the " - ".ipy extension.\n") - SHOW_INVALID_SYNTAX_MSG = False - else: - ast_code = ast.parse(transform_cell(code)) - - if code.rstrip()[-1:] == ";": - # Supress output with ; - capture_last_expression = False - - if capture_last_expression: - ast_code, capture_last_expression = capture_last_Expr( - ast_code, "_spyder_out") - - exec_fun(compile(ast_code, filename, 'exec'), ns_globals, ns_locals) - - if capture_last_expression: - out = ns_globals.pop("_spyder_out", None) - if out is not None: - return out - - except SystemExit as status: - # ignore exit(0) - if status.code: - ipython_shell.showtraceback(exception_only=True) - except BaseException as error: - if (isinstance(error, bdb.BdbQuit) - and ipython_shell.pdb_session): - # Ignore BdbQuit if we are debugging, as it is expected. - ipython_shell.pdb_session = None - elif post_mortem and isinstance(error, Exception): - error_type, error, tb = sys.exc_info() - post_mortem_excepthook(error_type, error, tb) - else: - # We ignore the call to exec - ipython_shell.showtraceback(tb_offset=1) - finally: - __tracebackhide__ = "__pdb_exit__" - - -def get_file_code(filename, save_all=True, raise_exception=False): - """Retrieve the content of a file.""" - # Get code from spyder - try: - return frontend_request(blocking=True).get_file_code( - filename, save_all=save_all) - except Exception: - # Maybe this is a local file - try: - with open(filename, 'r') as f: - return f.read() - except FileNotFoundError: - pass - if raise_exception: - raise - # Else return None - return None - - -def runfile(filename=None, args=None, wdir=None, namespace=None, - post_mortem=False, current_namespace=False): - """ - Run filename - args: command line arguments (string) - wdir: working directory - namespace: namespace for execution - post_mortem: boolean, whether to enter post-mortem mode on error - current_namespace: if true, run the file in the current namespace - """ - return _exec_file( - filename, args, wdir, namespace, - post_mortem, current_namespace, stack_depth=1) - - -def _exec_file(filename=None, args=None, wdir=None, namespace=None, - post_mortem=False, current_namespace=False, stack_depth=0, - exec_fun=None, canonic_filename=None): - # Tell IPython to hide this frame (>7.16) - __tracebackhide__ = True - ipython_shell = get_ipython() - if filename is None: - filename = get_current_file_name() - if filename is None: - return - - try: - filename = filename.decode('utf-8') - except (UnicodeError, TypeError, AttributeError): - # UnicodeError, TypeError --> eventually raised in Python 2 - # AttributeError --> systematically raised in Python 3 - pass - if PY2: - filename = encode(filename) - if __umr__.enabled: - __umr__.run() - if args is not None and not isinstance(args, basestring): - raise TypeError("expected a character buffer object") - - try: - file_code = get_file_code(filename, raise_exception=True) - except Exception: - # Show an error and return None - _print( - "This command failed to be executed because an error occurred" - " while trying to get the file code from Spyder's" - " editor. The error was:\n\n") - get_ipython().showtraceback(exception_only=True) - return - - # Here the remote filename has been used. It must now be valid locally. - if canonic_filename is not None: - filename = canonic_filename - else: - filename = canonic(filename) - - with NamespaceManager(filename, namespace, current_namespace, - file_code=file_code, stack_depth=stack_depth + 1 - ) as (ns_globals, ns_locals): - sys.argv = [filename] - if args is not None: - for arg in shlex.split(args): - sys.argv.append(arg) - - if "multiprocessing" in sys.modules: - # See https://github.com/spyder-ide/spyder/issues/16696 - try: - sys.modules['__mp_main__'] = sys.modules['__main__'] - except Exception: - pass - - if wdir is not None: - if PY2: - try: - wdir = wdir.decode('utf-8') - except (UnicodeError, TypeError): - # UnicodeError, TypeError --> eventually raised in Python 2 - pass - if os.path.isdir(wdir): - os.chdir(wdir) - # See https://github.com/spyder-ide/spyder/issues/13632 - if "multiprocessing.process" in sys.modules: - try: - import multiprocessing.process - multiprocessing.process.ORIGINAL_DIR = os.path.abspath( - wdir) - except Exception: - pass - else: - _print("Working directory {} doesn't exist.\n".format(wdir)) - - try: - if __umr__.has_cython: - # Cython files - with io.open(filename, encoding='utf-8') as f: - ipython_shell.run_cell_magic('cython', '', f.read()) - else: - exec_code(file_code, filename, ns_globals, ns_locals, - post_mortem=post_mortem, exec_fun=exec_fun, - capture_last_expression=False) - finally: - sys.argv = [''] - - -# IPykernel 6.3.0+ shadows our runfile because it depends on the Pydev -# debugger, which adds its own runfile to builtins. So we replace it with -# our own using exec_lines in start.py -if PY2: - builtins.runfile = runfile -else: - builtins.spyder_runfile = runfile - - -def debugfile(filename=None, args=None, wdir=None, post_mortem=False, - current_namespace=False): - """ - Debug filename - args: command line arguments (string) - wdir: working directory - post_mortem: boolean, included for compatiblity with runfile - """ - # Tell IPython to hide this frame (>7.16) - __tracebackhide__ = True - if filename is None: - filename = get_current_file_name() - if filename is None: - return - - shell = get_ipython() - if shell.is_debugging(): - # Recursive - code = ( - "runfile({}".format(repr(filename)) + - ", args=%r, wdir=%r, current_namespace=%r)" % ( - args, wdir, current_namespace) - ) - - shell.pdb_session.enter_recursive_debugger( - code, filename, True, - ) - else: - debugger = get_new_debugger(filename, True) - _exec_file( - filename=filename, - canonic_filename=debugger.canonic(filename), - args=args, - wdir=wdir, - current_namespace=current_namespace, - exec_fun=debugger.run, - stack_depth=1, - ) - - -builtins.debugfile = debugfile - - -def runcell(cellname, filename=None, post_mortem=False): - """ - Run a code cell from an editor as a file. - - Parameters - ---------- - cellname : str or int - Cell name or index. - filename : str - Needed to allow for proper traceback links. - post_mortem: bool - Automatically enter post mortem on exception. - """ - # Tell IPython to hide this frame (>7.16) - __tracebackhide__ = True - return _exec_cell(cellname, filename, post_mortem, stack_depth=1) - - -def _exec_cell(cellname, filename=None, post_mortem=False, stack_depth=0, - exec_fun=None, canonic_filename=None): - """ - Execute a code cell with a given exec function. - """ - # Tell IPython to hide this frame (>7.16) - __tracebackhide__ = True - if filename is None: - filename = get_current_file_name() - if filename is None: - return - try: - filename = filename.decode('utf-8') - except (UnicodeError, TypeError, AttributeError): - # UnicodeError, TypeError --> eventually raised in Python 2 - # AttributeError --> systematically raised in Python 3 - pass - ipython_shell = get_ipython() - try: - # Get code from spyder - cell_code = frontend_request( - blocking=True).run_cell(cellname, filename) - except Exception: - _print("This command failed to be executed because an error occurred" - " while trying to get the cell code from Spyder's" - " editor. The error was:\n\n") - get_ipython().showtraceback(exception_only=True) - return - - if not cell_code or cell_code.strip() == '': - _print("Nothing to execute, this cell is empty.\n") - return - - # Trigger `post_execute` to exit the additional pre-execution. - # See Spyder PR #7310. - ipython_shell.events.trigger('post_execute') - file_code = get_file_code(filename, save_all=False) - - # Here the remote filename has been used. It must now be valid locally. - if canonic_filename is not None: - filename = canonic_filename - else: - # Normalise the filename - filename = canonic(filename) - - with NamespaceManager(filename, current_namespace=True, - file_code=file_code, stack_depth=stack_depth + 1 - ) as (ns_globals, ns_locals): - return exec_code(cell_code, filename, ns_globals, ns_locals, - post_mortem=post_mortem, exec_fun=exec_fun, - capture_last_expression=True) - - -builtins.runcell = runcell - - -def debugcell(cellname, filename=None, post_mortem=False): - """Debug a cell.""" - # Tell IPython to hide this frame (>7.16) - __tracebackhide__ = True - if filename is None: - filename = get_current_file_name() - if filename is None: - return - - shell = get_ipython() - if shell.is_debugging(): - # Recursive - code = ( - "runcell({}, ".format(repr(cellname)) + - "{})".format(repr(filename)) - ) - shell.pdb_session.enter_recursive_debugger( - code, filename, False, - ) - else: - debugger = get_new_debugger(filename, False) - _exec_cell( - cellname=cellname, - filename=filename, - canonic_filename=debugger.canonic(filename), - exec_fun=debugger.run, - stack_depth=1 - ) - - -builtins.debugcell = debugcell - - -def cell_count(filename=None): - """ - Get the number of cells in a file. - - Parameters - ---------- - filename : str - The file to get the cells from. If None, the currently opened file. - """ - if filename is None: - filename = get_current_file_name() - if filename is None: - raise RuntimeError('Could not get cell count from frontend.') - try: - # Get code from spyder - cell_count = frontend_request(blocking=True).cell_count(filename) - return cell_count - except Exception: - etype, error, tb = sys.exc_info() - raise etype(error) - - -builtins.cell_count = cell_count - - # ============================================================================= # PYTHONPATH and sys.path Adjustments # ============================================================================= diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py index 7f687e5c2bb..944bd1e5950 100755 --- a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py @@ -9,63 +9,58 @@ import ast import bdb +import builtins +from contextlib import contextmanager import logging import os import sys 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 -from IPython.core.getipython import get_ipython +from IPython.core.inputtransformer2 import TransformerManager +import spyder_kernels from spyder_kernels.comms.frontendcomm import CommError, frontend_request -from spyder_kernels.customize.utils import path_is_library, capture_last_Expr -from spyder_kernels.py3compat import ( - TimeoutError, PY2, _print, isidentifier, PY3, input) - -if not PY2: - from IPython.core.inputtransformer2 import TransformerManager - import builtins - basestring = (str,) -else: - import __builtin__ as builtins - from IPython.core.inputsplitter import IPythonInputSplitter as TransformerManager +from spyder_kernels.customize.utils import ( + path_is_library, capture_last_Expr, exec_encapsulate_locals +) logger = logging.getLogger(__name__) -class DebugWrapper(object): +class DebugWrapper: """ Notifies the frontend when debugging starts/stops """ def __init__(self, pdb_obj): self.pdb_obj = pdb_obj + self._cleanup = True def __enter__(self): """ Debugging starts. """ - self.pdb_obj._frontend_notified = True - try: - frontend_request(blocking=True).set_debug_state(True) - except (CommError, TimeoutError): - logger.debug("Could not send debugging state to the frontend.") + shell = self.pdb_obj.shell + if shell.pdb_session == self.pdb_obj: + self._cleanup = False + else: + shell.add_pdb_session(self.pdb_obj) + self._cleanup = True def __exit__(self, exc_type, exc_val, exc_tb): """ Debugging ends. """ - self.pdb_obj._frontend_notified = False - try: - frontend_request(blocking=True).set_debug_state(False) - except (CommError, TimeoutError): - logger.debug("Could not send debugging state to the frontend.") + if self._cleanup: + self.pdb_obj.shell.remove_pdb_session(self.pdb_obj) -class SpyderPdb(ipyPdb, object): # Inherits `object` to call super() in PY2 +class SpyderPdb(ipyPdb): """ Extends Pdb to add features: @@ -76,9 +71,6 @@ class SpyderPdb(ipyPdb, object): # Inherits `object` to call super() in PY2 - Add completion to non-command code. """ - send_initial_notification = True - starting = True - def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, nosigint=False): """Init Pdb.""" @@ -88,12 +80,11 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, self.pdb_ignore_lib = False self.pdb_execute_events = False self.pdb_use_exclamation_mark = False + self.pdb_publish_stack = False self._exclamation_warning_printed = False self.pdb_stop_first_line = True self._disable_next_stack_entry = False super(SpyderPdb, self).__init__() - self._pdb_breaking = False - self._frontend_notified = False # content of tuple: (filename, line number) self._previous_step = None @@ -102,23 +93,30 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, # has no effect in previous versions. self.report_skipped = False - # Keep track of remote filename self.remote_filename = None + # Needed to know which namespace to show (user or current frame) # Line received from the frontend self._cmd_input_line = None - # This is not available in IPython 5 - if hasattr(self, '_predicates'): - # Turn off IPython's debugger skip funcionality by default because - # it makes our debugger quite slow. It's also important to remark - # that this functionality doesn't do anything on its own. Users - # need to mark what frames they want to skip for it to be useful. - # So, we hope that knowledgeable users will find that they need to - # enable it in Spyder. - # Fixes spyder-ide/spyder#20639. - self._predicates["debuggerskip"] = False + # Disable sigint so we can do it ourselves + self.nosigint = True + + # Keep track of interrupting state to avoid several interruptions + self.interrupting = False + + # Should the frontend force go to the current line? + self._request_where = False + + # Turn off IPython's debugger skip funcionality by default because + # it makes our debugger quite slow. It's also important to remark + # that this functionality doesn't do anything on its own. Users + # need to mark what frames they want to skip for it to be useful. + # So, we hope that knowledgeable users will find that they need to + # enable it in Spyder. + # Fixes spyder-ide/spyder#20639. + self._predicates["debuggerskip"] = False # --- Methods overriden for code execution def print_exclamation_warning(self): @@ -139,28 +137,29 @@ def default(self, line): self.print_exclamation_warning() self.error("Unknown command '" + line.split()[0] + "'") return - # Disallow the use of %debug magic in the debugger - if line.startswith("%debug"): - self.error("Please don't use '%debug' in the debugger.\n" - "For a recursive debugger, use the pdb 'debug'" - " command instead") - return - locals = self.curframe_locals - globals = self.curframe.f_globals + + # Replace %debug magic in the debugger + if line.startswith("%debug") or line.startswith("%%debug"): + cmd, arg, _ = self.parseline(line.lstrip("%")) + if cmd == "debug": + return self.do_debug(arg) + + local_ns = self.curframe_locals + global_ns = self.curframe.f_globals if self.pdb_use_exclamation_mark: # Find pdb commands executed without ! cmd, arg, line = self.parseline(line) if cmd: cmd_in_namespace = ( - cmd in globals - or cmd in locals + cmd in global_ns + or cmd in local_ns or cmd in builtins.__dict__ ) # Special case for quit and exit if cmd in ("quit", "exit"): - if cmd in globals and isinstance( - globals[cmd], ZMQExitAutocall): + if cmd in global_ns and isinstance( + global_ns[cmd], ZMQExitAutocall): # Use the pdb call cmd_in_namespace = False cmd_func = getattr(self, 'do_' + cmd, None) @@ -184,6 +183,7 @@ def default(self, line): # The pdb command is masked by something self.print_exclamation_warning() try: + is_magic = line.startswith("%") line = TransformerManager().transform_cell(line) save_stdout = sys.stdout save_stdin = sys.stdin @@ -193,7 +193,7 @@ def default(self, line): sys.stdout = self.stdout sys.displayhook = self.displayhook if execute_events: - get_ipython().events.trigger('pre_execute') + self.shell.events.trigger('pre_execute') code_ast = ast.parse(line) @@ -202,73 +202,19 @@ def default(self, line): capture_last_expression = False else: code_ast, capture_last_expression = capture_last_Expr( - code_ast, "_spyderpdb_out") - - if locals is not globals: - # Mitigates a behaviour of CPython that makes it difficult - # to work with exec and the local namespace - # See: - # - https://bugs.python.org/issue41918 - # - https://bugs.python.org/issue46153 - # - https://bugs.python.org/issue21161 - # - spyder-ide/spyder#13909 - # - spyder-ide/spyder-kernels#345 - # - # The idea here is that the best way to emulate being in a - # function is to actually execute the code in a function. - # A function called `_spyderpdb_code` is created and - # called. It will first load the locals, execute the code, - # and then update the locals. - # - # One limitation of this approach is that locals() is only - # a copy of the curframe locals. This means that closures - # for example are early binding instead of late binding. - - # Create a function - indent = " " - code = ["def _spyderpdb_code():"] - - # Load the locals - globals["_spyderpdb_builtins_locals"] = builtins.locals - - # Save builtins locals in case it is shadowed - globals["_spyderpdb_locals"] = locals - - # Load locals if they have a valid name - # In comprehensions, locals could contain ".0" for example - code += [indent + "{k} = _spyderpdb_locals['{k}']".format( - k=k) for k in locals if isidentifier(k)] - - - # Update the locals - code += [indent + "_spyderpdb_locals.update(" - "_spyderpdb_builtins_locals())"] - - # Run the function - code += ["_spyderpdb_code()"] - - # Cleanup - code += [ - "del _spyderpdb_code", - "del _spyderpdb_locals", - "del _spyderpdb_builtins_locals" - ] - - # Parse the function - fun_ast = ast.parse('\n'.join(code) + '\n') - - # Inject code_ast in the function before the locals update - fun_ast.body[0].body = ( - fun_ast.body[0].body[:-1] # The locals - + code_ast.body # Code to run - + fun_ast.body[0].body[-1:] # Locals update - ) - code_ast = fun_ast - - exec(compile(code_ast, "", "exec"), globals) + code_ast, "_spyderpdb_out", global_ns) + + if is_magic: + # Magics like runcell use and modify local_ns. + # But the locals() dict can not be directly modified when + # encapsulated. Therefore they must encapsulate the locals + # themselves (see code_runner.py). + exec(compile(code_ast, "", "exec"), global_ns, local_ns) + else: + exec_encapsulate_locals(code_ast, global_ns, local_ns) if capture_last_expression: - out = globals.pop("_spyderpdb_out", None) + out = global_ns.pop("_spyderpdb_out", None) if out is not None: sys.stdout.flush() sys.stderr.flush() @@ -281,101 +227,109 @@ def default(self, line): finally: if execute_events: - get_ipython().events.trigger('post_execute') + self.shell.events.trigger('post_execute') sys.stdout = save_stdout sys.stdin = save_stdin sys.displayhook = save_displayhook except BaseException: - if PY2: - t, v = sys.exc_info()[:2] - if type(t) == type(''): - exc_type_name = t - else: exc_type_name = t.__name__ - print >>self.stdout, '***', exc_type_name + ':', v - else: - exc_info = sys.exc_info()[:2] - self.error( - traceback.format_exception_only(*exc_info)[-1].strip()) + exc_info = sys.exc_info()[:2] + self.error( + traceback.format_exception_only(*exc_info)[-1].strip()) # --- Methods overriden for signal handling - def sigint_handler(self, signum, frame): - """ - Handle a sigint signal. Break on the frame above this one. - - This method is not present in python2 so this won't be called there. - """ - if self.allow_kbdint: - raise KeyboardInterrupt + def interrupt(self): + """Stop debugger on next instruction.""" + self.interrupting = True self.message("\nProgram interrupted. (Use 'cont' to resume).") - # avoid stopping in set_trace - sys.settrace(None) - self._pdb_breaking = True self.set_step() - self.set_trace(sys._getframe()) + + def set_trace(self, frame=None): + """Register that debugger is tracing.""" + self.shell.add_pdb_session(self) + super(SpyderPdb, self).set_trace(frame) + + def set_quit(self): + """Register that debugger is not tracing.""" + self.shell.remove_pdb_session(self) + super(SpyderPdb, self).set_quit() def interaction(self, frame, traceback): """ Called when a user interaction is required. - - If this is from sigint, break on the upper frame. - If the frame is in spydercustomize.py, quit. - Notifies spyder and print current code. """ - if self._pdb_breaking: - self._pdb_breaking = False - if frame and frame.f_back: - return self.interaction(frame.f_back, traceback) - - # This is necessary to handle chained exceptions in Pdb, support for - # which was added in IPython 8.15 and will be the default in Python - # 3.13 (see ipython/ipython#14146). - if isinstance(traceback, BaseException): - _chained_exceptions, tb = self._get_tb_and_exceptions(traceback) - - with self._hold_exceptions(_chained_exceptions): - self.interaction(frame, tb) - - return - - self.setup(frame, traceback) - self.print_stack_entry(self.stack[self.curindex]) - - if self._frontend_notified: - self._cmdloop() - else: - with DebugWrapper(self): - self._cmdloop() - - self.forget() + with DebugWrapper(self): + # Wrapp in case the frontend was not notified, e.g. postmortem + return super(SpyderPdb, self).interaction( + frame, traceback) - def print_stack_entry(self, frame_lineno, prompt_prefix='\n-> ', - context=None): + def print_stack_entry(self, *args, **kwargs): """Disable printing stack entry if requested.""" if self._disable_next_stack_entry: self._disable_next_stack_entry = False return - return super(SpyderPdb, self).print_stack_entry( - frame_lineno, prompt_prefix, context) + return super().print_stack_entry(*args, **kwargs) # --- Methods overriden for skipping libraries def stop_here(self, frame): """Check if pdb should stop here.""" - if (frame is not None - and "__tracebackhide__" in frame.f_locals - and frame.f_locals["__tracebackhide__"] == "__pdb_exit__"): + # Never stop if we are continuing unless there is a breakpoint + if self.stopframe == self.botframe and self.stoplineno == -1: + return False + if self.continue_if_has_breakpoints and self.should_continue(frame): + self.set_continue() + return False + if ( + frame is not None + and "__tracebackhide__" in frame.f_locals + and frame.f_locals["__tracebackhide__"] == "__pdb_exit__" + ): self.onecmd('exit') return False - if not super(SpyderPdb, self).stop_here(frame): + if not super().stop_here(frame): return False + if frame is self.stopframe: + return True filename = frame.f_code.co_filename if filename.startswith('<'): # This is not a file return True if self.pdb_ignore_lib and path_is_library(filename): return False + if self.skip_hidden and os.path.dirname(spyder_kernels.__file__) in filename: + # This is spyder-kernels internals + return False return True + def should_continue(self, frame): + """ + Jump to first breakpoint if needed. + + Fixes spyder-ide/spyder#2034 + """ + + if not self.continue_if_has_breakpoints: + # This was disabled + return False + self.continue_if_has_breakpoints = False + + # Get all breakpoints for the file we're going to debug + if not frame: + # We are not debugging, return. Solves spyder-ide/spyder#10290 + return False + + lineno = frame.f_lineno + breaks = self.get_file_breaks(frame.f_code.co_filename) + + # Do 'continue' if the first breakpoint is *not* placed + # where the debugger is going to land. + # Fixes spyder-ide/spyder#4681 + if self.pdb_stop_first_line: + return breaks and lineno < breaks[0] + + # The breakpoint could be in another file. + return not (breaks and lineno >= breaks[0]) + def do_where(self, arg): """w(here) Print a stack trace, with the most recent frame at the bottom. @@ -384,11 +338,8 @@ def do_where(self, arg): Take a number as argument as an (optional) number of context line to print""" - super(SpyderPdb, self).do_where(arg) - try: - frontend_request(blocking=False).do_where() - except (CommError, TimeoutError): - logger.debug("Could not send where request to the frontend.") + self._request_where = True + return super(SpyderPdb, self).do_where(arg) do_w = do_where @@ -444,7 +395,7 @@ def is_name_or_composed(text): if not text or text[0] == '.': return False # We want to keep value.subvalue - return isidentifier(text.replace('.', '')) + return text.replace('.', '').isidentifier() while text and not is_name_or_composed(text): text = text[1:] @@ -457,7 +408,6 @@ def is_name_or_composed(text): cursor_start = cursor_pos - len(text) if ipython_do_complete: - kernel = get_ipython().kernel # Make complete call with current frame if self.curframe: if self.curframe_locals: @@ -466,10 +416,10 @@ def is_name_or_composed(text): self.curframe.f_globals) else: frame = self.curframe - kernel.shell.set_completer_frame(frame) - result = kernel._do_complete(code, cursor_pos) + self.shell.set_completer_frame(frame) + result = self.shell.kernel._do_complete(code, cursor_pos) # Reset frame - kernel.shell.set_completer_frame() + self.shell.set_completer_frame() # If there is no Pdb results to merge, return the result if not compfunc: return result @@ -547,7 +497,7 @@ def is_name_or_composed(text): if not text or text[0] == '.': return False # We want to keep value.subvalue - return isidentifier(text.replace('.', '')) + return text.replace('.', '').isidentifier() while text and not is_name_or_composed(text): text = text[1:] @@ -565,7 +515,6 @@ def is_name_or_composed(text): 'status': 'ok' } - kernel = get_ipython().kernel # Make complete call with current frame if self.curframe: if self.curframe_locals: @@ -574,36 +523,19 @@ def is_name_or_composed(text): self.curframe.f_globals) else: frame = self.curframe - kernel.shell.set_completer_frame(frame) - result = kernel._do_complete(code, cursor_pos) + self.shell.set_completer_frame(frame) + result = self.shell.kernel._do_complete(code, cursor_pos) # Reset frame - kernel.shell.set_completer_frame() + self.shell.set_completer_frame() return result # --- Methods overriden by us for Spyder integration def postloop(self): # postloop() is called when the debugger’s input prompt exists. Reset - # _previous_step so that publish_pdb_state() actually notifies Spyder + # _previous_step so that get_pdb_state() actually notifies Spyder # about a changed frame the next the input prompt is entered again. self._previous_step = None - def preloop(self): - """Ask Spyder for breakpoints before the first prompt is created.""" - try: - pdb_settings = frontend_request(blocking=True).get_pdb_settings() - self.pdb_ignore_lib = pdb_settings['pdb_ignore_lib'] - self.pdb_execute_events = pdb_settings['pdb_execute_events'] - self.pdb_use_exclamation_mark = pdb_settings[ - 'pdb_use_exclamation_mark'] - self.pdb_stop_first_line = pdb_settings['pdb_stop_first_line'] - if self.starting: - self.set_spyder_breakpoints(pdb_settings['breakpoints']) - if self.send_initial_notification: - self.publish_pdb_state() - except (CommError, TimeoutError): - logger.debug("Could not get breakpoints from the frontend.") - super(SpyderPdb, self).preloop() - def set_continue(self): """ Stop only at breakpoints or when finished. @@ -614,13 +546,6 @@ def set_continue(self): # Don't stop except at breakpoints or when finished self._set_stopinfo(self.botframe, None, -1) - def reset(self): - """ - Register Pdb session after reset. - """ - super(SpyderPdb, self).reset() - get_ipython().pdb_session = self - def do_debug(self, arg): """ Debug code @@ -629,20 +554,43 @@ def do_debug(self, arg): argument (which is an arbitrary expression or statement to be executed in the current environment). """ - try: - super(SpyderPdb, self).do_debug(arg) - except Exception: - if PY2: - t, v = sys.exc_info()[:2] - if type(t) == type(''): - exc_type_name = t - else: exc_type_name = t.__name__ - print >>self.stdout, '***', exc_type_name + ':', v - else: + with self.recursive_debugger() as debugger: + self.message("Entering recursive debugger") + try: + global_ns = self.curframe.f_globals + local_ns = self.curframe_locals + return sys.call_tracing(debugger.run, (arg, global_ns, local_ns)) + except Exception: exc_info = sys.exc_info()[:2] self.error( traceback.format_exception_only(*exc_info)[-1].strip()) - get_ipython().pdb_session = self + finally: + self.message("Leaving recursive debugger") + + @contextmanager + def recursive_debugger(self): + """Get a recursive debugger.""" + # Save and restore tracing function + trace_function = sys.gettrace() + sys.settrace(None) + + # Create child debugger + debugger = self.__class__( + completekey=self.completekey, + stdin=self.stdin, stdout=self.stdout) + debugger.prompt = "(%s) " % self.prompt.strip() + try: + yield debugger + finally: + # Reset parent debugger + sys.settrace(trace_function) + self.lastcmd = debugger.lastcmd + + # Reset _previous_step so that get_pdb_state() notifies Spyder about + # a changed debugger position. The reset is required because the + # recursive debugger might change the position, but the parent + # debugger (self) is not aware of this. + self._previous_step = None def user_return(self, frame, return_value): """This function is called when a return trap is set here.""" @@ -657,6 +605,7 @@ def user_return(self, frame, return_value): def _cmdloop(self): """Modifies the error text.""" + self.interrupting = False while True: try: # keyboard interrupts allow for an easy way to cancel @@ -666,10 +615,24 @@ def _cmdloop(self): self.allow_kbdint = False break except KeyboardInterrupt: - _print("--KeyboardInterrupt--\n" - "For copying text while debugging, use Ctrl+Shift+C", - file=self.stdout) + print("--KeyboardInterrupt--\n" + "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): """ @@ -700,7 +663,7 @@ def cmd_input(self, prompt=''): """ Get input from frontend. Blocks until return """ - kernel = get_ipython().kernel + kernel = self.shell.kernel # Only works if the comm is open if not kernel.frontend_comm.is_open(): return input(prompt) @@ -713,15 +676,12 @@ def cmd_input(self, prompt=''): # Send the input request. self._cmd_input_line = None - kernel.frontend_call().pdb_input(prompt) + kernel.frontend_call(display_error=True).pdb_input( + prompt, state=self.get_pdb_state()) # Allow GUI event loop to update - if PY3: - is_main_thread = ( - threading.current_thread() is threading.main_thread()) - else: - is_main_thread = isinstance( - threading.current_thread(), threading._MainThread) + is_main_thread = ( + threading.current_thread() is threading.main_thread()) # Get input by running eventloop if is_main_thread and kernel.eventloop: @@ -767,41 +727,7 @@ def postcmd(self, stop, line): # Flush in case the command produced output on underlying outputs sys.__stderr__.flush() sys.__stdout__.flush() - self.publish_pdb_state() - return super(SpyderPdb, self).postcmd(stop, line) - - if PY2: - def break_here(self, frame): - """ - Breakpoints don't work for files with non-ascii chars in Python 2 - - Fixes Issue 1484 - """ - from bdb import effective - filename = self.canonic(frame.f_code.co_filename) - try: - filename = unicode(filename, "utf-8") - except TypeError: - pass - if filename not in self.breaks: - return False - lineno = frame.f_lineno - if lineno not in self.breaks[filename]: - # The line itself has no breakpoint, but maybe the line is the - # first line of a function with breakpoint set by function name - lineno = frame.f_code.co_firstlineno - if lineno not in self.breaks[filename]: - return False - - # flag says ok to delete temp. bp - (bp, flag) = effective(filename, lineno, frame) - if bp: - self.currentbp = bp.number - if (flag and bp.temporary): - self.do_clear(str(bp.number)) - return True - else: - return False + return stop # --- Methods defined by us for Spyder integration def set_spyder_breakpoints(self, breakpoints): @@ -825,89 +751,67 @@ def set_spyder_breakpoints(self, breakpoints): # The file is not readable pass - # Jump to first breakpoint. - # Fixes issue 2034 - if self.starting: - # Only run this after a Pdb session is created - self.starting = False - - # Get all breakpoints for the file we're going to debug - frame = self.curframe - if not frame: - # We are not debugging, return. Solves #10290 - return - lineno = frame.f_lineno - breaks = self.get_file_breaks(frame.f_code.co_filename) - - # Do 'continue' if the first breakpoint is *not* placed - # where the debugger is going to land. - # Fixes issue 4681 - if self.pdb_stop_first_line: - do_continue = ( - self.continue_if_has_breakpoints - and breaks - and lineno < breaks[0]) - else: - # The breakpoint could be in another file. - do_continue = ( - self.continue_if_has_breakpoints - and not (breaks and lineno >= breaks[0])) + breakpoints = property(fset=set_spyder_breakpoints) - if do_continue: - try: - if self.pdb_use_exclamation_mark: - cont_cmd = '!continue' - else: - cont_cmd = 'continue' - frontend_request(blocking=False).pdb_execute(cont_cmd) - except (CommError, TimeoutError): - logger.debug( - "Could not send a Pdb continue call to the frontend.") - - def publish_pdb_state(self): + def get_pdb_state(self): """ Send debugger state (frame position) to the frontend. The state is only sent if it has changed since the last update. """ + state = self.shell.kernel.get_state() frame = self.curframe if frame is None: self._previous_step = None - return + return state + + if self._request_where: + self._request_where = False + state["do_where"] = True # Get filename and line number of the current frame fname = self.canonic(frame.f_code.co_filename) - if PY2: - try: - fname = unicode(fname, "utf-8") - except TypeError: - pass if fname == self.mainpyfile and self.remote_filename is not None: fname = self.remote_filename lineno = frame.f_lineno if self._previous_step == (fname, lineno): - return + # Do not update state if not needed + return state # Set step of the current frame (if any) step = {} self._previous_step = None - if isinstance(fname, basestring) and isinstance(lineno, int): + if isinstance(fname, str) and isinstance(lineno, int): step = dict(fname=fname, lineno=lineno) self._previous_step = (fname, lineno) - try: - frontend_request(blocking=False).pdb_state(dict(step=step)) - except (CommError, TimeoutError): - logger.debug("Could not send Pdb state to the frontend.") + state['step'] = step + + if self.pdb_publish_stack: + # Publish Pdb stack so we can update the Debugger plugin on Spyder + pdb_stack = traceback.StackSummary.extract(self.stack) + pdb_index = self.curindex + + skip_hidden = getattr(self, 'skip_hidden', False) + + if skip_hidden: + # Filter out the hidden frames + 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([bool(i) for i in hidden[:pdb_index]]) + + state['stack'] = (pdb_stack, pdb_index) + + return state def run(self, cmd, globals=None, locals=None): """Debug a statement executed via the exec() function. globals defaults to __main__.dict; locals defaults to globals. """ - self.starting = True with DebugWrapper(self): super(SpyderPdb, self).run(cmd, globals, locals) @@ -916,7 +820,6 @@ def runeval(self, expr, globals=None, locals=None): globals defaults to __main__.dict; locals defaults to globals. """ - self.starting = True with DebugWrapper(self): super(SpyderPdb, self).runeval(expr, globals, locals) @@ -925,51 +828,11 @@ def runcall(self, *args, **kwds): Return the result of the function call. """ - self.starting = True with DebugWrapper(self): super(SpyderPdb, self).runcall(*args, **kwds) - def enter_recursive_debugger(self, code, filename, - continue_if_has_breakpoints): - """ - Enter debugger recursively. - """ - sys.settrace(None) - globals = self.curframe.f_globals - locals = self.curframe_locals - # Create child debugger - debugger = SpyderPdb( - completekey=self.completekey, - stdin=self.stdin, stdout=self.stdout) - debugger.use_rawinput = self.use_rawinput - debugger.prompt = "(%s) " % self.prompt.strip() - - debugger.set_remote_filename(filename) - debugger.continue_if_has_breakpoints = continue_if_has_breakpoints - - # Enter recursive debugger - sys.call_tracing(debugger.run, (code, globals, locals)) - # Reset parent debugger - sys.settrace(self.trace_dispatch) - self.lastcmd = debugger.lastcmd - get_ipython().pdb_session = self - - # Reset _previous_step so that publish_pdb_state() called from within - # postcmd() notifies Spyder about a changed debugger position. The reset - # is required because the recursive debugger might change the position, - # but the parent debugger (self) is not aware of this. - self._previous_step = None - def set_remote_filename(self, filename): """Set remote filename to signal Spyder on mainpyfile.""" self.remote_filename = filename self.mainpyfile = self.canonic(filename) self._wait_for_mainpyfile = True - - -def get_new_debugger(filename, continue_if_has_breakpoints): - """Get a new debugger.""" - debugger = SpyderPdb() - debugger.set_remote_filename(filename) - debugger.continue_if_has_breakpoints = continue_if_has_breakpoints - return debugger diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/tests/test_umr.py b/external-deps/spyder-kernels/spyder_kernels/customize/tests/test_umr.py index fc3329ac5f8..fe84791b304 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/tests/test_umr.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/tests/test_umr.py @@ -16,15 +16,14 @@ import pytest # Local imports -from spyder_kernels.py3compat import to_text_string from spyder_kernels.customize.umr import UserModuleReloader @pytest.fixture def user_module(tmpdir): """Create a simple module in tmpdir as an example of a user module.""" - if to_text_string(tmpdir) not in sys.path: - sys.path.append(to_text_string(tmpdir)) + if str(tmpdir) not in sys.path: + sys.path.append(str(tmpdir)) def create_module(modname): modfile = tmpdir.mkdir(modname).join('bar.py') @@ -40,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 @@ -73,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/external-deps/spyder-kernels/spyder_kernels/customize/umr.py b/external-deps/spyder-kernels/spyder_kernels/customize/umr.py index 6b71abf068f..e779ec336bd 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/umr.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/umr.py @@ -9,10 +9,9 @@ import sys from spyder_kernels.customize.utils import path_is_library -from spyder_kernels.py3compat import PY2, _print -class UserModuleReloader(object): +class UserModuleReloader: """ User Module Reloader (UMR) aims at deleting user modules to force Python to deeply reload them during import @@ -43,9 +42,6 @@ def __init__(self, namelist=None, pathlist=None): # pythoncom: See spyder-ide/spyder#7190 # tensorflow: See spyder-ide/spyder#8697 other_modules = ['pytorch', 'pythoncom', 'tensorflow'] - if PY2: - py2_modules = ['astropy', 'fastmat'] - other_modules = other_modules + py2_modules self.namelist = namelist + spy_modules + mpl_modules + other_modules self.pathlist = pathlist @@ -53,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" @@ -70,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 @@ -126,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 - _print("\x1b[4;33m%s\x1b[24m%s\x1b[0m" - % ("Reloaded modules", ": "+", ".join(modnames))) \ No newline at end of file + 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/external-deps/spyder-kernels/spyder_kernels/customize/utils.py b/external-deps/spyder-kernels/spyder_kernels/customize/utils.py index f34a0e5a770..fff18581b2c 100644 --- a/external-deps/spyder-kernels/spyder_kernels/customize/utils.py +++ b/external-deps/spyder-kernels/spyder_kernels/customize/utils.py @@ -6,6 +6,7 @@ """Utility functions.""" import ast +import builtins import os import re import sys @@ -96,7 +97,7 @@ def path_is_library(path, initial_pathlist=None): return False -def capture_last_Expr(code_ast, out_varname): +def capture_last_Expr(code_ast, out_varname, global_ns): """Parse line and modify code to capture in globals the last expression.""" # Modify ast code to capture the last expression capture_last_expression = False @@ -104,11 +105,13 @@ def capture_last_Expr(code_ast, out_varname): len(code_ast.body) and isinstance(code_ast.body[-1], ast.Expr) ): + global_ns["__spyder_builtins__"] = builtins capture_last_expression = True expr_node = code_ast.body[-1] # Create new assign node assign_node = ast.parse( - 'globals()[{}] = None'.format(repr(out_varname))).body[0] + '__spyder_builtins__.globals()[{}] = None'.format( + repr(out_varname))).body[0] # Replace None by the value assign_node.value = expr_node.value # Fix line number and column offset @@ -118,7 +121,7 @@ def capture_last_Expr(code_ast, out_varname): # Exists from 3.8, necessary from 3.11 assign_node.end_lineno = expr_node.end_lineno if assign_node.lineno == assign_node.end_lineno: - # Add 'globals()[{}] = ' and remove 'None' + # Add '__spyder_builtins__.globals()[{}] = ' and remove 'None' assign_node.end_col_offset += expr_node.end_col_offset - 4 else: assign_node.end_col_offset = expr_node.end_col_offset @@ -126,6 +129,94 @@ def capture_last_Expr(code_ast, out_varname): return code_ast, capture_last_expression +def exec_encapsulate_locals( + code_ast, globals, locals, exec_fun=None, filename=None +): + """ + Execute by encapsulating locals if needed. + + Notes + ----- + * In general, the dict returned by locals() might or might not be modified. + In this case, the encapsulated dict can not. + """ + use_locals_hack = locals is not None and locals is not globals + if use_locals_hack: + globals["__spyder_builtins__"] = builtins + + # Mitigates a behaviour of CPython that makes it difficult + # to work with exec and the local namespace + # See: + # - https://bugs.python.org/issue41918 + # - https://bugs.python.org/issue46153 + # - https://bugs.python.org/issue21161 + # - spyder-ide/spyder#13909 + # - spyder-ide/spyder-kernels#345 + # + # The idea here is that the best way to emulate being in a + # function is to actually execute the code in a function. + # A function called `_spyderpdb_code` is created and + # called. It will first load the locals, execute the code, + # and then update the locals. + # + # One limitation of this approach is that locals() is only + # a copy of the curframe locals. This means that closures + # for example are early binding instead of late binding. + + # Create a function + indent = " " + code = ["def _spyderpdb_code():"] + + # Add locals in globals + # If the debugger is recursive, the globals could already + # have a _spyderpdb_locals as it might be shared between + # levels + if "_spyderpdb_locals" in globals: + globals["_spyderpdb_locals"].append(locals) + else: + globals["_spyderpdb_locals"] = [locals] + + # Load locals if they have a valid name + # In comprehensions, locals could contain ".0" for example + code += [indent + "{k} = _spyderpdb_locals[-1]['{k}']".format( + k=k) for k in locals if k.isidentifier()] + + # The code comes here + + # Update the locals + code += [indent + "_spyderpdb_locals[-1].update(" + "__spyder_builtins__.locals())"] + + # Run the function + code += ["_spyderpdb_code()"] + + # Parse the function + fun_ast = ast.parse('\n'.join(code) + '\n') + + # Inject code_ast in the function before the locals update + fun_ast.body[0].body = ( + fun_ast.body[0].body[:-1] # The locals + + code_ast.body # Code to run + + fun_ast.body[0].body[-1:] # Locals update + ) + code_ast = fun_ast + + try: + if exec_fun is None: + exec_fun = exec + if filename is None: + filename = "" + exec_fun(compile(code_ast, filename, "exec"), globals) + finally: + if use_locals_hack: + # Cleanup code + globals.pop("_spyderpdb_code", None) + if len(globals["_spyderpdb_locals"]) > 1: + del globals["_spyderpdb_locals"][-1] + else: + del globals["_spyderpdb_locals"] + + def canonic(filename): """ Return canonical form of filename. diff --git a/external-deps/spyder-kernels/spyder_kernels/py3compat.py b/external-deps/spyder-kernels/spyder_kernels/py3compat.py deleted file mode 100644 index dfb0e322bbf..00000000000 --- a/external-deps/spyder-kernels/spyder_kernels/py3compat.py +++ /dev/null @@ -1,360 +0,0 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2009- Spyder Kernels Contributors -# -# Licensed under the terms of the MIT License -# (see spyder_kernels/__init__.py for details) -# ----------------------------------------------------------------------------- - -""" -spyder.py3compat ----------------- - -Transitional module providing compatibility functions intended to help -migrating from Python 2 to Python 3. - -This module should be fully compatible with: - * Python >=v2.6 - * Python 3 -""" - -from __future__ import print_function - -import operator -import os -import sys - -PY2 = sys.version[0] == '2' -PY3 = sys.version[0] == '3' - -if PY3: - # keep reference to builtin_mod because the kernel overrides that value - # to forward requests to a frontend. - def input(prompt=''): - return builtin_mod.input(prompt) - builtin_mod_name = "builtins" - import builtins as builtin_mod -else: - # keep reference to builtin_mod because the kernel overrides that value - # to forward requests to a frontend. - def input(prompt=''): - return builtin_mod.raw_input(prompt) - builtin_mod_name = "__builtin__" - import __builtin__ as builtin_mod - - -#============================================================================== -# Data types -#============================================================================== -if PY2: - # Python 2 - TEXT_TYPES = (str, unicode) - INT_TYPES = (int, long) -else: - # Python 3 - TEXT_TYPES = (str,) - INT_TYPES = (int,) -NUMERIC_TYPES = tuple(list(INT_TYPES) + [float, complex]) - - -#============================================================================== -# Renamed/Reorganized modules -#============================================================================== -if PY2: - # Python 2 - import __builtin__ as builtins - import ConfigParser as configparser - try: - import _winreg as winreg - except ImportError: - pass - from sys import maxint as maxsize - try: - import CStringIO as io - except ImportError: - import StringIO as io - try: - import cPickle as pickle - except ImportError: - import pickle - from UserDict import DictMixin as MutableMapping - import thread as _thread - import repr as reprlib - import Queue -else: - # Python 3 - import builtins - import configparser - try: - import winreg - except ImportError: - pass - from sys import maxsize - import io - import pickle - from collections.abc import MutableMapping - import _thread - import reprlib - import queue as Queue - - -#============================================================================== -# Strings -#============================================================================== -def is_type_text_string(obj): - """Return True if `obj` is type text string, False if it is anything else, - like an instance of a class that extends the basestring class.""" - if PY2: - # Python 2 - return type(obj) in [str, unicode] - else: - # Python 3 - return type(obj) in [str, bytes] - -def is_text_string(obj): - """Return True if `obj` is a text string, False if it is anything else, - like binary data (Python 3) or QString (Python 2, PyQt API #1)""" - if PY2: - # Python 2 - return isinstance(obj, basestring) - else: - # Python 3 - return isinstance(obj, str) - -def is_binary_string(obj): - """Return True if `obj` is a binary string, False if it is anything else""" - if PY2: - # Python 2 - return isinstance(obj, str) - else: - # Python 3 - return isinstance(obj, bytes) - -def is_string(obj): - """Return True if `obj` is a text or binary Python string object, - False if it is anything else, like a QString (Python 2, PyQt API #1)""" - return is_text_string(obj) or is_binary_string(obj) - -def is_unicode(obj): - """Return True if `obj` is unicode""" - if PY2: - # Python 2 - return isinstance(obj, unicode) - else: - # Python 3 - return isinstance(obj, str) - -def to_text_string(obj, encoding=None): - """Convert `obj` to (unicode) text string""" - if PY2: - # Python 2 - if encoding is None: - return unicode(obj) - else: - return unicode(obj, encoding) - else: - # Python 3 - if encoding is None: - return str(obj) - elif isinstance(obj, str): - # In case this function is not used properly, this could happen - return obj - else: - return str(obj, encoding) - -def to_binary_string(obj, encoding=None): - """Convert `obj` to binary string (bytes in Python 3, str in Python 2)""" - if PY2: - # Python 2 - if encoding is None: - return str(obj) - else: - return obj.encode(encoding) - else: - # Python 3 - return bytes(obj, 'utf-8' if encoding is None else encoding) - - -#============================================================================== -# Function attributes -#============================================================================== -def get_func_code(func): - """Return function code object""" - if PY2: - # Python 2 - return func.func_code - else: - # Python 3 - return func.__code__ - -def get_func_name(func): - """Return function name""" - if PY2: - # Python 2 - return func.func_name - else: - # Python 3 - return func.__name__ - -def get_func_defaults(func): - """Return function default argument values""" - if PY2: - # Python 2 - return func.func_defaults - else: - # Python 3 - return func.__defaults__ - - -#============================================================================== -# Special method attributes -#============================================================================== -def get_meth_func(obj): - """Return method function object""" - if PY2: - # Python 2 - return obj.im_func - else: - # Python 3 - return obj.__func__ - -def get_meth_class_inst(obj): - """Return method class instance""" - if PY2: - # Python 2 - return obj.im_self - else: - # Python 3 - return obj.__self__ - -def get_meth_class(obj): - """Return method class""" - if PY2: - # Python 2 - return obj.im_class - else: - # Python 3 - return obj.__self__.__class__ - - -#============================================================================== -# Misc. -#============================================================================== -if PY2: - def _print(*objects, **options): - end = options.get('end', '\n') - file = options.get('file', sys.stdout) - sep = options.get('sep', ' ') - string = sep.join([str(obj) for obj in objects]) - print(string, file=file, end=end, sep=sep) -else: - _print = print - - -if PY2: - # Python 2 - getcwd = os.getcwdu - cmp = cmp - import string - str_lower = string.lower - from itertools import izip_longest as zip_longest - from backports.functools_lru_cache import lru_cache -else: - # Python 3 - getcwd = os.getcwd - def cmp(a, b): - return (a > b) - (a < b) - str_lower = str.lower - from itertools import zip_longest - from functools import lru_cache - -def qbytearray_to_str(qba): - """Convert QByteArray object to str in a way compatible with Python 2/3""" - return str(bytes(qba.toHex().data()).decode()) - -# ============================================================================= -# Dict funcs -# ============================================================================= -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -# ============================================================================= -# Exceptions -# ============================================================================= -if PY2: - TimeoutError = RuntimeError - FileNotFoundError = IOError -else: - TimeoutError = TimeoutError - FileNotFoundError = FileNotFoundError - -if PY2: - import re - import tokenize - def isidentifier(string): - """Check if string can be a variable name.""" - return re.match(tokenize.Name + r'\Z', string) is not None - - if os.name == 'nt': - def encode(u): - """Try encoding with utf8.""" - if isinstance(u, unicode): - return u.encode('utf8', 'replace') - return u - else: - def encode(u): - """Try encoding with file system encoding.""" - if isinstance(u, unicode): - return u.encode(sys.getfilesystemencoding()) - return u -else: - def isidentifier(string): - """Check if string can be a variable name.""" - return string.isidentifier() - - def encode(u): - """Encoding is not a problem in python 3.""" - return u - - -def compat_exec(code, globals, locals): - # Wrap exec in a function - exec(code, globals, locals) - - -if __name__ == '__main__': - pass diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py index 83db1a43e00..2aace3afa97 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py @@ -7,17 +7,10 @@ # ----------------------------------------------------------------------------- """Utilities and wrappers around inspect module""" - -from __future__ import print_function - +import builtins import inspect import re -# Local imports: -from spyder_kernels.py3compat import (is_text_string, builtins, get_meth_func, - get_meth_class_inst, get_meth_class, - get_func_defaults, to_text_string, PY2) - SYMBOLS = r"[^\'\"a-zA-Z0-9_.]" @@ -57,7 +50,7 @@ def getobjdir(obj): In special cases (e.g. WrapITK package), will return only string elements of result returned by dir(obj) """ - return [item for item in dir(obj) if is_text_string(item)] + return [item for item in dir(obj) if isinstance(item, str)] def getdoc(obj): @@ -84,7 +77,7 @@ def getdoc(obj): # yield anything, either. So assume the most commonly used # multi-byte file encoding (which also covers ascii). try: - docstring = to_text_string(docstring) + docstring = str(docstring) except: pass @@ -101,35 +94,29 @@ def getdoc(obj): doc['docstring'] = docstring return doc if inspect.ismethod(obj): - imclass = get_meth_class(obj) - if get_meth_class_inst(obj) is not None: + imclass = obj.__self__.__class__ + if obj.__self__ is not None: doc['note'] = 'Method of %s instance' \ - % get_meth_class_inst(obj).__class__.__name__ + % obj.__self__.__class__.__name__ else: doc['note'] = 'Unbound %s method' % imclass.__name__ - obj = get_meth_func(obj) + obj = obj.__func__ elif hasattr(obj, '__module__'): doc['note'] = 'Function of %s module' % obj.__module__ else: doc['note'] = 'Function' doc['name'] = obj.__name__ if inspect.isfunction(obj): - if PY2: - args, varargs, varkw, defaults = inspect.getargspec(obj) - doc['argspec'] = inspect.formatargspec( - args, varargs, varkw, defaults, - formatvalue=lambda o:'='+repr(o)) - else: - # 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) + # 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 ' doc['argspec'] = doc['argspec'][1:-1] # remove parentheses @@ -166,10 +153,10 @@ def getsource(obj): """Wrapper around inspect.getsource""" try: try: - src = to_text_string(inspect.getsource(obj)) + src = str(inspect.getsource(obj)) except TypeError: if hasattr(obj, '__class__'): - src = to_text_string(inspect.getsource(obj.__class__)) + src = str(inspect.getsource(obj.__class__)) else: # Bindings like VTK or ITK require this case src = getdoc(obj) @@ -201,44 +188,35 @@ def getsignaturefromtext(text, objname): # others in doctests or other places, but those are not so important. sig = '' if sigs: - if PY2: - # We don't have an easy way to check if the identifier detected by - # signature_re is a valid one in Python 2. So, we simply select the - # first match. - sig = sigs[0] if objname else sigs[0][1] + # 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: - # 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] + 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 + ] if real_sigs: - sig = real_sigs[0] + sig = real_sigs[0][1] 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 - ] - - if real_sigs: - sig = real_sigs[0][1] - else: - sig = valid_sigs[0][1] + sig = valid_sigs[0][1] return sig @@ -274,7 +252,7 @@ def getargs(obj): if inspect.isfunction(obj) or inspect.isbuiltin(obj): func_obj = obj elif inspect.ismethod(obj): - func_obj = get_meth_func(obj) + func_obj = obj.__func__ elif inspect.isclass(obj) and hasattr(obj, '__init__'): func_obj = getattr(obj, '__init__') else: @@ -298,7 +276,7 @@ def getargs(obj): if isinstance(arg, list): args[i_arg] = "(%s)" % ", ".join(arg) - defaults = get_func_defaults(func_obj) + defaults = func_obj.__defaults__ if defaults is not None: for index, default in enumerate(defaults): args[index + len(args) - len(defaults)] += '=' + repr(default) diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/iofuncs.py b/external-deps/spyder-kernels/spyder_kernels/utils/iofuncs.py index 05b988831c6..7da2828ff3a 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/iofuncs.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/iofuncs.py @@ -12,9 +12,6 @@ Note: 'load' functions has to return a dictionary from which a globals() namespace may be updated """ - -from __future__ import print_function - # Standard library imports import sys import os @@ -28,13 +25,15 @@ import dis import copy import glob +import pickle # Local imports -from spyder_kernels.py3compat import getcwd, pickle, PY2, to_text_string from spyder_kernels.utils.lazymodules import ( FakeObject, numpy as np, pandas as pd, PIL, scipy as sp) +# ---- For Matlab files +# ----------------------------------------------------------------------------- class MatlabStruct(dict): """ Matlab style struct, enhanced. @@ -44,7 +43,7 @@ class MatlabStruct(dict): Examples ======== - >>> from spyder.utils.iofuncs import MatlabStruct + >>> from spyder_kernels.utils.iofuncs import MatlabStruct >>> a = MatlabStruct() >>> a.b = 'spam' # a["b"] == 'spam' >>> a.c["d"] = 'eggs' # a.c.d == 'eggs' @@ -82,7 +81,6 @@ def _is_allowed(self, frame): dis.opmap.get('STOP_CODE', 0)] bytecode = frame.f_code.co_code instruction = bytecode[frame.f_lasti + 3] - instruction = ord(instruction) if PY2 else instruction return instruction in allowed __setattr__ = dict.__setitem__ @@ -169,6 +167,8 @@ def save_matlab(data, filename): return str(error) +# ---- For arrays +# ----------------------------------------------------------------------------- def load_array(filename): if np.load is FakeObject: return None, '' @@ -193,6 +193,8 @@ def __save_array(data, basename, index): return fname +# ---- For PIL images +# ----------------------------------------------------------------------------- if sys.byteorder == 'little': _ENDIAN = '<' else: @@ -237,6 +239,8 @@ def load_image(filename): return None, str(error) +# ---- For misc formats +# ----------------------------------------------------------------------------- def load_pickle(filename): """Load a pickle file as a dictionary""" try: @@ -253,21 +257,19 @@ def load_pickle(filename): def load_json(filename): """Load a json file as a dictionary""" try: - if PY2: - args = 'rb' - else: - args = 'r' - with open(filename, args) as fid: + with open(filename, 'r') as fid: data = json.load(fid) return data, None except Exception as err: return None, str(err) +# ---- For Spydata files +# ----------------------------------------------------------------------------- def save_dictionary(data, filename): """Save dictionary in a single file .spydata file""" filename = osp.abspath(filename) - old_cwd = getcwd() + old_cwd = os.getcwd() os.chdir(osp.dirname(filename)) error_message = None skipped_keys = [] @@ -358,7 +360,7 @@ def save_dictionary(data, filename): tar.add(osp.basename(fname)) os.remove(fname) except (RuntimeError, pickle.PicklingError, TypeError) as error: - error_message = to_text_string(error) + error_message = str(error) else: if skipped_keys: skipped_keys.sort() @@ -377,15 +379,13 @@ def is_within_directory(directory, target): return prefix == abs_directory -def safe_extract(tar, path=".", members=None, numeric_owner=False): +def safe_extract(tar, path=".", members=None, *, numeric_owner=False): """Safely extract a tar file.""" for member in tar.getmembers(): member_path = os.path.join(path, member.name) if not is_within_directory(path, member_path): raise Exception( - "Attempted path traversal in tar file {}".format( - repr(tar.name) - ) + f"Attempted path traversal in tar file {tar.name!r}" ) tar.extractall(path, members, numeric_owner=numeric_owner) @@ -393,20 +393,17 @@ def safe_extract(tar, path=".", members=None, numeric_owner=False): def load_dictionary(filename): """Load dictionary from .spydata file""" filename = osp.abspath(filename) - old_cwd = getcwd() + old_cwd = os.getcwd() tmp_folder = tempfile.mkdtemp() os.chdir(tmp_folder) data = None error_message = None try: with tarfile.open(filename, "r") as tar: - if PY2: - tar.extractall() - else: - safe_extract(tar) + safe_extract(tar) pickle_filename = glob.glob('*.pickle')[0] - # 'New' format (Spyder >=2.2 for Python 2 and Python 3) + # 'New' format (Spyder >=2.2) with open(pickle_filename, 'rb') as fdesc: data = pickle.loads(fdesc.read()) saved_arrays = {} @@ -426,18 +423,101 @@ def load_dictionary(filename): pass # Except AttributeError from e.g. trying to load function no longer present except (AttributeError, EOFError, ValueError) as error: - error_message = to_text_string(error) + error_message = str(error) # To ensure working dir gets changed back and temp dir wiped no matter what finally: os.chdir(old_cwd) try: shutil.rmtree(tmp_folder) except OSError as error: - error_message = to_text_string(error) + error_message = str(error) return data, error_message -class IOFunctions(object): +# ---- For HDF5 files +# ----------------------------------------------------------------------------- +def load_hdf5(filename): + """ + Load an hdf5 file. + + Notes + ----- + - This is a fairly dumb implementation which reads the whole HDF5 file into + Spyder's variable explorer. Since HDF5 files are designed for storing + very large data-sets, it may be much better to work directly with the + HDF5 objects, thus keeping the data on disk. Nonetheless, this gives + quick and dirty but convenient access to them. + - There is no support for creating files with compression, chunking etc, + although these can be read without problem. + - When reading an HDF5 file with sub-groups, groups in the file will + correspond to dictionaries with the same layout. + """ + def get_group(group): + contents = {} + for name, obj in list(group.items()): + if isinstance(obj, h5py.Dataset): + contents[name] = np.array(obj) + elif isinstance(obj, h5py.Group): + # it is a group, so call self recursively + contents[name] = get_group(obj) + # other objects such as links are ignored + return contents + + try: + import h5py + + f = h5py.File(filename, 'r') + contents = get_group(f) + f.close() + return contents, None + except Exception as error: + return None, str(error) + + +def save_hdf5(data, filename): + """ + Save an hdf5 file. + + Notes + ----- + - All datatypes to be saved must be convertible to a numpy array, otherwise + an exception will be raised. + - Data attributes are currently ignored. + - When saving data after reading it with load_hdf5, dictionaries are not + turned into HDF5 groups. + """ + try: + import h5py + + f = h5py.File(filename, 'w') + for key, value in list(data.items()): + f[key] = np.array(value) + f.close() + except Exception as error: + return str(error) + + +# ---- For DICOM files +# ----------------------------------------------------------------------------- +def load_dicom(filename): + """Load a DICOM files.""" + try: + from pydicom import dicomio + + name = osp.splitext(osp.basename(filename))[0] + try: + data = dicomio.read_file(filename, force=True) + except TypeError: + data = dicomio.read_file(filename) + arr = data.pixel_array + return {name: arr}, None + except Exception as error: + return None, str(error) + + +# ---- Class to group all IO functionality +# ----------------------------------------------------------------------------- +class IOFunctions: def __init__(self): self.load_extensions = None self.save_extensions = None @@ -447,7 +527,7 @@ def __init__(self): self.save_funcs = None def setup(self): - iofuncs = self.get_internal_funcs()+self.get_3rd_party_funcs() + iofuncs = self.get_internal_funcs() load_extensions = {} save_extensions = {} load_funcs = {} @@ -455,8 +535,9 @@ def setup(self): load_filters = [] save_filters = [] load_ext = [] + for ext, name, loadfunc, savefunc in iofuncs: - filter_str = to_text_string(name + " (*%s)" % ext) + filter_str = str(name + " (*%s)" % ext) if loadfunc is not None: load_filters.append(filter_str) load_extensions[filter_str] = ext @@ -466,9 +547,12 @@ def setup(self): save_extensions[filter_str] = ext save_filters.append(filter_str) save_funcs[ext] = savefunc - load_filters.insert(0, to_text_string("Supported files"+" (*"+\ - " *".join(load_ext)+")")) - load_filters.append(to_text_string("All files (*.*)")) + + load_filters.insert( + 0, str("Supported files" + " (*" + " *".join(load_ext) + ")") + ) + load_filters.append(str("All files (*.*)")) + self.load_filters = "\n".join(load_filters) self.save_filters = "\n".join(save_filters) self.load_funcs = load_funcs @@ -478,35 +562,22 @@ def setup(self): def get_internal_funcs(self): return [ - ('.spydata', "Spyder data files", - load_dictionary, save_dictionary), - ('.npy', "NumPy arrays", load_array, None), - ('.npz', "NumPy zip arrays", load_array, None), - ('.mat', "Matlab files", load_matlab, save_matlab), - ('.csv', "CSV text files", 'import_wizard', None), - ('.txt', "Text files", 'import_wizard', None), - ('.jpg', "JPEG images", load_image, None), - ('.png', "PNG images", load_image, None), - ('.gif', "GIF images", load_image, None), - ('.tif', "TIFF images", load_image, None), - ('.pkl', "Pickle files", load_pickle, None), - ('.pickle', "Pickle files", load_pickle, None), - ('.json', "JSON files", load_json, None), - ] - - def get_3rd_party_funcs(self): - other_funcs = [] - try: - from spyder.otherplugins import get_spyderplugins_mods - for mod in get_spyderplugins_mods(io=True): - try: - other_funcs.append((mod.FORMAT_EXT, mod.FORMAT_NAME, - mod.FORMAT_LOAD, mod.FORMAT_SAVE)) - except AttributeError as error: - print("%s: %s" % (mod, str(error)), file=sys.stderr) - except ImportError: - pass - return other_funcs + ('.spydata', "Spyder data files", load_dictionary, save_dictionary), + ('.npy', "NumPy arrays", load_array, None), + ('.npz', "NumPy zip arrays", load_array, None), + ('.mat', "Matlab files", load_matlab, save_matlab), + ('.csv', "CSV text files", 'import_wizard', None), + ('.txt', "Text files", 'import_wizard', None), + ('.jpg', "JPEG images", load_image, None), + ('.png', "PNG images", load_image, None), + ('.gif', "GIF images", load_image, None), + ('.tif', "TIFF images", load_image, None), + ('.pkl', "Pickle files", load_pickle, None), + ('.pickle', "Pickle files", load_pickle, None), + ('.json', "JSON files", load_json, None), + ('.h5', "HDF5 files", load_hdf5, save_hdf5), + ('.dcm', "DICOM images", load_dicom, None), + ] def save(self, data, filename): ext = osp.splitext(filename)[1].lower() @@ -526,11 +597,8 @@ def load(self, filename): iofunctions.setup() -def save_auto(data, filename): - """Save data into filename, depending on file extension""" - pass - - +# ---- Test +# ----------------------------------------------------------------------------- if __name__ == "__main__": import datetime testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]} @@ -549,9 +617,9 @@ def save_auto(data, filename): import time t0 = time.time() save_dictionary(example, "test.spydata") - print(" Data saved in %.3f seconds" % (time.time()-t0)) # spyder: test-skip + print(" Data saved in %.3f seconds" % (time.time()-t0)) t0 = time.time() example2, ok = load_dictionary("test.spydata") os.remove("test.spydata") - print("Data loaded in %.3f seconds" % (time.time()-t0)) # spyder: test-skip + print("Data loaded in %.3f seconds" % (time.time()-t0)) diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/lazymodules.py b/external-deps/spyder-kernels/spyder_kernels/utils/lazymodules.py index d65847b44b3..75b5b3a94e1 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/lazymodules.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/lazymodules.py @@ -18,12 +18,12 @@ # ============================================================================= # Auxiliary classes # ============================================================================= -class FakeObject(object): +class FakeObject: """Fake class used in replacement of missing objects""" pass -class LazyModule(object): +class LazyModule: """Lazy module loader class.""" def __init__(self, modname, second_level_attrs=None): diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/misc.py b/external-deps/spyder-kernels/spyder_kernels/utils/misc.py index 56ec5f215ec..bbb9b050cac 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/misc.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/misc.py @@ -10,7 +10,7 @@ import re -from spyder_kernels.py3compat import lru_cache +from functools import lru_cache @lru_cache(maxsize=100) diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/mpl.py b/external-deps/spyder-kernels/spyder_kernels/utils/mpl.py index 7927e49da62..8ac557f7886 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/mpl.py +++ b/external-deps/spyder-kernels/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,21 @@ # 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': 'inline', # For Matplotlib >=3.9 + inline_backend: "inline", # For Matplotlib <3.9 + '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/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py index 103490aa35d..d7e0ce5a727 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/nsview.py @@ -9,19 +9,10 @@ """ Utilities to build a namespace view. """ - -from __future__ import print_function - from itertools import islice import inspect import re -# Local imports -from spyder_kernels.py3compat import (NUMERIC_TYPES, INT_TYPES, TEXT_TYPES, - to_text_string, is_text_string, - is_type_text_string, - is_binary_string, PY2, - to_binary_string, iteritems) from spyder_kernels.utils.lazymodules import ( bs4, FakeObject, numpy as np, pandas as pd, PIL) @@ -274,7 +265,7 @@ def default_display(value, with_module=True): return name + ' object of ' + module + ' module' return name except Exception: - type_str = to_text_string(object_type) + type_str = str(object_type) return type_str[1:-1] @@ -285,7 +276,7 @@ def collections_display(value, level): # Get elements if is_dict: - elements = iteritems(value) + elements = iter(value.items()) else: elements = value @@ -373,27 +364,15 @@ def value_to_display(value, minmax=False, level=0): elif isinstance(value, pd.DataFrame): if level == 0: cols = value.columns - if PY2 and len(cols) > 0: - # Get rid of possible BOM utf-8 data present at the - # beginning of a file, which gets attached to the first - # column header when headers are present in the first - # row. - # Fixes Issue 2514 - try: - ini_col = to_text_string(cols[0], encoding='utf-8-sig') - except: - ini_col = to_text_string(cols[0]) - cols = [ini_col] + [to_text_string(c) for c in cols[1:]] - else: - cols = [to_text_string(c) for c in cols] + cols = [str(c) for c in cols] display = 'Column names: ' + ', '.join(list(cols)) else: display = 'Dataframe' elif isinstance(value, bs4.element.NavigableString): # Fixes Issue 2448 - display = to_text_string(value) + display = str(value) if level > 0: - display = u"'" + display + u"'" + display = "'" + display + "'" elif isinstance(value, pd.Index): if level == 0: try: @@ -402,33 +381,34 @@ def value_to_display(value, minmax=False, level=0): display = value.summary() else: display = 'Index' - elif is_binary_string(value): + elif isinstance(value, bytes): # We don't apply this to classes that extend string types # See issue 5636 - if is_type_text_string(value): + if type(value) in [str, bytes]: try: - display = to_text_string(value, 'utf8') + display = str(value, 'utf8') if level > 0: - display = u"'" + display + u"'" + display = "'" + display + "'" except: display = value if level > 0: display = b"'" + display + b"'" else: display = default_display(value) - elif is_text_string(value): + elif isinstance(value, str): # We don't apply this to classes that extend string types # See issue 5636 - if is_type_text_string(value): + if type(value) in [str, bytes]: display = value if level > 0: - display = u"'" + display + u"'" + display = "'" + display + "'" else: display = default_display(value) + elif (isinstance(value, datetime.date) or isinstance(value, datetime.timedelta)): display = str(value) - elif (isinstance(value, NUMERIC_TYPES) or + elif (isinstance(value, (int, float, complex)) or isinstance(value, bool) or isinstance(value, numeric_numpy_types)): display = repr(value) @@ -443,10 +423,10 @@ def value_to_display(value, minmax=False, level=0): # Truncate display at 70 chars to avoid freezing Spyder # because of large displays if len(display) > 70: - if is_binary_string(display): + if isinstance(display, bytes): ellipses = b' ...' else: - ellipses = u' ...' + ellipses = ' ...' display = display[:70].rstrip() + ellipses # Restore Numpy printoptions @@ -459,7 +439,7 @@ def value_to_display(value, minmax=False, level=0): def display_to_value(value, default_value, ignore_errors=True): """Convert back to value""" from qtpy.compat import from_qvariant - value = from_qvariant(value, to_text_string) + value = from_qvariant(value, str) try: np_dtype = get_numpy_dtype(default_value) if isinstance(default_value, bool): @@ -474,10 +454,10 @@ def display_to_value(value, default_value, ignore_errors=True): value = np_dtype(complex(value)) else: value = np_dtype(value) - elif is_binary_string(default_value): - value = to_binary_string(value, 'utf8') - elif is_text_string(default_value): - value = to_text_string(value) + elif isinstance(default_value, bytes): + value = bytes(value, 'utf-8') + elif isinstance(default_value, str): + value = str(value) elif isinstance(default_value, complex): value = complex(value) elif isinstance(default_value, float): @@ -531,7 +511,7 @@ def get_type_string(item): pass found = re.findall(r"<(?:type|class) '(\S*)'>", - to_text_string(type(item))) + str(type(item))) if found: if found[0] == 'type': return 'class' @@ -609,10 +589,12 @@ 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 = {} - _is_string = is_type_text_string + def _is_string(obj): + return type(obj) in [str, bytes] for key, value in list(input_dict.items()): excluded = ( @@ -624,7 +606,7 @@ def globalsfilter(input_dict, check_all=False, filters=None, (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 @@ -636,7 +618,8 @@ def globalsfilter(input_dict, check_all=False, filters=None, 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(): @@ -650,7 +633,7 @@ def get_supported_types(): """ from datetime import date, timedelta editable_types = [int, float, complex, list, set, dict, tuple, date, - timedelta] + list(TEXT_TYPES) + list(INT_TYPES) + timedelta, str] try: from numpy import ndarray, matrix, generic editable_types += [ndarray, matrix, generic] @@ -692,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/data.dcm b/external-deps/spyder-kernels/spyder_kernels/utils/tests/data.dcm new file mode 100644 index 0000000000000000000000000000000000000000..9d525ec3b3f899cbc36ab7262353852661d89b4e GIT binary patch literal 138515 zcmeFV1z6q7vM0K5*8qWF3kwblcXxMpg1bX-2$JCL?(UM{7Tlen!6i5(IB$`?_dR>g z+>2)?Ss$(!9Yydi0RZp-L-2Y-0z_2d0S*9Jg%^qcpLqa4MfHV`0VvA9 z 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 xwUz)o>OJQWm6R@4x}1pMupCW&yIC~IWTd$4*s