From c08e8c6a75e2aa46f4befb62cdbf8b58a91ea37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Thu, 6 Jan 2022 21:01:46 +0100 Subject: [PATCH 0001/1752] added second example --- IPython/core/magic_arguments.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/IPython/core/magic_arguments.py b/IPython/core/magic_arguments.py index 9231609572e..d147229512b 100644 --- a/IPython/core/magic_arguments.py +++ b/IPython/core/magic_arguments.py @@ -37,6 +37,40 @@ def magic_cool(self, arg): -o OPTION, --option OPTION An optional argument. + +Here is an elaborated example that uses default parameters in `argument` and calls the `args` in the cell magic:: + + from IPython.core.magic import register_cell_magic + from IPython.core.magic_arguments import (argument, magic_arguments, + parse_argstring) + + + @magic_arguments() + @argument( + "--option", + "-o", + help=("Add an option here"), + ) + @argument( + "--style", + "-s", + default="foo", + help=("Add some style arguments"), + ) + @register_cell_magic + def my_cell_magic(line, cell): + args = parse_argstring(my_cell_magic, line) + print(f"{args.option=}") + print(f"{args.style=}") + print(f"{cell=}") + +In a jupyter notebook, this cell magic can be executed like this:: + + %%my_cell_magic -o Hello + print("bar") + i = 42 + + Inheritance diagram: .. inheritance-diagram:: IPython.core.magic_arguments From 6fe9697c3581795145acb0a7a628fdd8e89ec379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Thu, 6 Jan 2022 21:46:20 +0100 Subject: [PATCH 0002/1752] remove blank lines --- IPython/core/magic_arguments.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/IPython/core/magic_arguments.py b/IPython/core/magic_arguments.py index d147229512b..568abd82ae0 100644 --- a/IPython/core/magic_arguments.py +++ b/IPython/core/magic_arguments.py @@ -37,7 +37,6 @@ def magic_cool(self, arg): -o OPTION, --option OPTION An optional argument. - Here is an elaborated example that uses default parameters in `argument` and calls the `args` in the cell magic:: from IPython.core.magic import register_cell_magic @@ -70,7 +69,6 @@ def my_cell_magic(line, cell): print("bar") i = 42 - Inheritance diagram: .. inheritance-diagram:: IPython.core.magic_arguments From cb6563dcd85783ea0a687fbe227c4782a8a7cadf Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 10 Jan 2022 09:28:10 +0100 Subject: [PATCH 0003/1752] Fix and test for "async with does not allow new lines". Use the opportunity to add a test, and parametrise a few other, plus set the correct stacklevel. Closes #12975 --- IPython/core/inputtransformer2.py | 12 ++--- IPython/core/tests/test_inputtransformer2.py | 46 +++++++++++++------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py index 85d448a727f..3a560073b22 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -507,9 +507,12 @@ def make_tokens_by_line(lines:List[str]): # reexported from token on 3.7+ NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore - tokens_by_line:List[List[Any]] = [[]] - if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')): - warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified") + tokens_by_line: List[List[Any]] = [[]] + if len(lines) > 1 and not lines[0].endswith(("\n", "\r", "\r\n", "\x0b", "\x0c")): + warnings.warn( + "`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified", + stacklevel=2, + ) parenlev = 0 try: for token in tokenize.generate_tokens(iter(lines).__next__): @@ -782,9 +785,6 @@ def __init__(self, extra_flags=0): super().__init__() self.flags |= extra_flags - def __call__(self, *args, **kwds): - return compile(*args, **kwds) - class MaybeAsyncCommandCompiler(CommandCompiler): def __init__(self, extra_flags=0): diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py index 2207cdfb48c..bc40721c9ff 100644 --- a/IPython/core/tests/test_inputtransformer2.py +++ b/IPython/core/tests/test_inputtransformer2.py @@ -4,6 +4,7 @@ more complex. See test_inputtransformer2_line for tests for line-based transformations. """ +import platform import string import sys from textwrap import dedent @@ -319,7 +320,16 @@ def test_check_complete(): assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2) -def test_check_complete_II(): +@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="fail on pypy") +@pytest.mark.parametrize( + "value, expected", + [ + ('''def foo():\n """''', ("incomplete", 4)), + ("""async with example:\n pass""", ("incomplete", 4)), + ("""async with example:\n pass\n """, ("complete", None)), + ], +) +def test_check_complete_II(value, expected): """ Test that multiple line strings are properly handled. @@ -327,25 +337,31 @@ def test_check_complete_II(): """ cc = ipt2.TransformerManager().check_complete - assert cc('''def foo():\n """''') == ("incomplete", 4) - - -def test_check_complete_invalidates_sunken_brackets(): + assert cc(value) == expected + + +@pytest.mark.parametrize( + "value, expected", + [ + (")", ("invalid", None)), + ("]", ("invalid", None)), + ("}", ("invalid", None)), + (")(", ("invalid", None)), + ("][", ("invalid", None)), + ("}{", ("invalid", None)), + ("]()(", ("invalid", None)), + ("())(", ("invalid", None)), + (")[](", ("invalid", None)), + ("()](", ("invalid", None)), + ], +) +def test_check_complete_invalidates_sunken_brackets(value, expected): """ Test that a single line with more closing brackets than the opening ones is interpreted as invalid """ cc = ipt2.TransformerManager().check_complete - assert cc(")") == ("invalid", None) - assert cc("]") == ("invalid", None) - assert cc("}") == ("invalid", None) - assert cc(")(") == ("invalid", None) - assert cc("][") == ("invalid", None) - assert cc("}{") == ("invalid", None) - assert cc("]()(") == ("invalid", None) - assert cc("())(") == ("invalid", None) - assert cc(")[](") == ("invalid", None) - assert cc("()](") == ("invalid", None) + assert cc(value) == expected def test_null_cleanup_transformer(): From fcb41dff8821411769b405c82c9eabaccf8e834b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 11 Jan 2022 08:39:38 +0100 Subject: [PATCH 0004/1752] xfail pypy --- IPython/core/tests/test_inputtransformer2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py index bc40721c9ff..abc63031d3a 100644 --- a/IPython/core/tests/test_inputtransformer2.py +++ b/IPython/core/tests/test_inputtransformer2.py @@ -292,6 +292,7 @@ def test_check_complete_param(code, expected, number): assert cc(code) == (expected, number) +@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy") @pytest.mark.xfail( reason="Bug in python 3.9.8 – bpo 45738", condition=sys.version_info in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)], @@ -320,7 +321,7 @@ def test_check_complete(): assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2) -@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="fail on pypy") +@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy") @pytest.mark.parametrize( "value, expected", [ From ede2b7f4f7b565c7166d5c1a5f29dc0dd7f78ed8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 12 Jan 2022 14:01:01 +0100 Subject: [PATCH 0005/1752] remove old whatsnew --- docs/source/whatsnew/pr/12984-process-time.rst | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 docs/source/whatsnew/pr/12984-process-time.rst diff --git a/docs/source/whatsnew/pr/12984-process-time.rst b/docs/source/whatsnew/pr/12984-process-time.rst deleted file mode 100644 index 9957d5394b5..00000000000 --- a/docs/source/whatsnew/pr/12984-process-time.rst +++ /dev/null @@ -1,6 +0,0 @@ -windows time-implementation: Switch to process_time -=================================================== -Timing for example with ``%%time`` on windows is based on ``time.perf_counter``. -This is at the end the same as W-All. -To be a bit tighter to linux one could change to ``time.process_time`` instead. -Thus for example one would no longer count periods of sleep and further. From a7de9daf77f35e43c3f993fba602e426b17bafda Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 12 Jan 2022 14:06:47 +0100 Subject: [PATCH 0006/1752] update --- tools/gh_api.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/tools/gh_api.py b/tools/gh_api.py index d68bcc8677e..2a34e6a6e86 100644 --- a/tools/gh_api.py +++ b/tools/gh_api.py @@ -47,33 +47,11 @@ def get_auth_token(): if token is not None: return token - print("Please enter your github username and password. These are not " - "stored, only used to get an oAuth token. You can revoke this at " - "any time on Github.\n" - "Username: ", file=sys.stderr, end='') - user = input('') - pw = getpass.getpass("Password: ", stream=sys.stderr) - - auth_request = { - "scopes": [ - "public_repo", - "gist" - ], - "note": "IPython tools %s" % socket.gethostname(), - "note_url": "https://github.com/ipython/ipython/tree/master/tools", - } - response = requests.post('https://api.github.com/authorizations', - auth=(user, pw), data=json.dumps(auth_request)) - if response.status_code == 401 and \ - 'required;' in response.headers.get('X-GitHub-OTP', ''): - print("Your login API requested a one time password", file=sys.stderr) - otp = getpass.getpass("One Time Password: ", stream=sys.stderr) - response = requests.post('https://api.github.com/authorizations', - auth=(user, pw), - data=json.dumps(auth_request), - headers={'X-GitHub-OTP':otp}) - response.raise_for_status() - token = json.loads(response.text)['token'] + print( + "Get a token fom https://github.com/settings/tokens with public repo and gist." + ) + token = getpass.getpass("Token: ", stream=sys.stderr) + keyring.set_password('github', fake_username, token) return token From 5b8c2f314a5c111c94d19a0c8f99cd4c0e3813e5 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 12 Jan 2022 14:16:02 +0100 Subject: [PATCH 0007/1752] gen stats --- docs/source/whatsnew/github-stats-8.rst | 110 ++++++++++++++++++++++++ docs/source/whatsnew/index.rst | 1 + 2 files changed, 111 insertions(+) create mode 100644 docs/source/whatsnew/github-stats-8.rst diff --git a/docs/source/whatsnew/github-stats-8.rst b/docs/source/whatsnew/github-stats-8.rst new file mode 100644 index 00000000000..158589b22a1 --- /dev/null +++ b/docs/source/whatsnew/github-stats-8.rst @@ -0,0 +1,110 @@ +Get a token fom https://github.com/settings/tokens with public repo and gist. + +GitHub stats for 2022/01/05 - 2022/01/12 (tag: 8.0.0rc1) + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 26 issues and merged 307 pull requests. +The full list can be seen `on GitHub `__ + +The following 99 authors contributed 372 commits. + +* 007vedant +* Adam Hackbarth +* Aditya Sathe +* Ahmed Fasih +* Albert Zhang +* Alex Hall +* Andrew Port +* Ankitsingh6299 +* Arthur Moreira +* Ashwin Vishnu +* Augusto +* BaoGiang HoangVu +* bar-hen +* Bart Skowron +* Bartosz Telenczuk +* Bastian Ebeling +* Benjamin Ragan-Kelley +* Blazej Michalik +* blois +* Boyuan Liu +* Brendan Gerrity +* Carol Willing +* Coco Bennett +* Coco Mishra +* Corentin Cadiou +* Daniel Goldfarb +* Daniel Mietchen +* Daniel Shimon +* digitalvirtuoso +* Dimitri Papadopoulos +* dswij +* Eric Wieser +* Erik +* Ethan Madden +* Faris A Chugthai +* farisachugthai +* Gal B +* gorogoroumaru +* Hussaina Begum Nandyala +* Inception95 +* Iwan Briquemont +* Jake VanderPlas +* Jakub Klus +* James Morris +* Jay Qi +* Jeroen Bédorf +* Joyce Er +* juacrumar +* Juan Luis Cano Rodríguez +* Julien Rabinow +* Justin Palmer +* Krzysztof Cybulski +* L0uisJ0shua +* lbennett +* LeafyLi +* Lightyagami1 +* Lumir Balhar +* Mark Schmitz +* Martin Skarzynski +* martinRenou +* Matt Wozniski +* Matthias Bussonnier +* Meysam Azad +* Michael T +* Michael Tiemann +* Naelson Douglas +* Nathan Goldbaum +* Nick Muoh +* nicolaslazo +* Nikita Kniazev +* NotWearingPants +* Paul Ivanov +* Paulo S. Costa +* Pete Blois +* Peter Corke +* PhanatosZou +* Piers Titus van der Torren +* Rakessh Roshan +* Ram Rachum +* rchiodo +* Reilly Tucker Siemens +* Romulo Filho +* rushabh-v +* Sammy Al Hashemi +* Samreen Zarroug +* Samuel Gaist +* Sanjana-03 +* Scott Sanderson +* skalaydzhiyski +* sleeping +* Snir Broshi +* Spas Kalaydzhisyki +* Sylvain Corlay +* Terry Davis +* Timur Kushukov +* Tobias Bengfort +* Tomasz Kłoczko +* Yonatan Goldschmidt +* 谭九鼎 diff --git a/docs/source/whatsnew/index.rst b/docs/source/whatsnew/index.rst index 8eecbb9006c..542bc07de81 100644 --- a/docs/source/whatsnew/index.rst +++ b/docs/source/whatsnew/index.rst @@ -34,6 +34,7 @@ development work they do here in a user friendly format. :maxdepth: 1 version8 + github-stats-8 version7 github-stats-7 version6 From b8d7dfa8d78108f3f01486d9fa67b6841acb8f74 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 12 Jan 2022 14:22:44 +0100 Subject: [PATCH 0008/1752] release 8.0.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 0651caf04b5..794d6267b7e 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# # _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From c0abea7a6dfe52c1f74c9d0387d4accadba7cc14 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 12 Jan 2022 14:25:09 +0100 Subject: [PATCH 0009/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 794d6267b7e..c1c90b13bf2 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 0 +_version_minor = 1 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 2c0ce2ad148d997d4f8db8794e101220cb1419e9 Mon Sep 17 00:00:00 2001 From: Jonathan Allen Grant Date: Wed, 12 Jan 2022 15:02:26 -0500 Subject: [PATCH 0010/1752] Typo --- docs/source/whatsnew/version8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 640b48acc52..1cc3fa42b3b 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -64,7 +64,7 @@ which was just a shim module around ``ipykernel`` for the past 8 years have been remove, and so many other similar things that pre-date the name **Jupyter** itself. -We no longer need to add ``IPyhton.extensions`` to the PYTHONPATH because that is being +We no longer need to add ``IPython.extensions`` to the PYTHONPATH because that is being handled by ``load_extension``. We are also removing ``Cythonmagic``, ``sympyprinting`` and ``rmagic`` as they are now in From 8f0a7ed29ace63422d3349d5650884b7ce1f76f0 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 15 Jan 2022 19:44:29 +0100 Subject: [PATCH 0011/1752] Add title to fix doc build --- docs/source/whatsnew/github-stats-8.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/github-stats-8.rst b/docs/source/whatsnew/github-stats-8.rst index 158589b22a1..0c8589cb308 100644 --- a/docs/source/whatsnew/github-stats-8.rst +++ b/docs/source/whatsnew/github-stats-8.rst @@ -1,4 +1,5 @@ -Get a token fom https://github.com/settings/tokens with public repo and gist. +Issues closed in the 8.x development cycle +========================================== GitHub stats for 2022/01/05 - 2022/01/12 (tag: 8.0.0rc1) From d1d92425877edcd01c16f336dec69192422de66e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Jan 2022 12:16:57 +0100 Subject: [PATCH 0012/1752] disable latex --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c1083f43de..535eddcc968 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: cache: pip - name: Install latex if: runner.os == 'Linux' && matrix.deps == 'test_extra' - run: sudo apt-get -yq -o Acquire::Retries=3 --no-install-suggests --no-install-recommends install texlive dvipng + run: echo "disable latex for now, issues in mirros" #sudo apt-get -yq -o Acquire::Retries=3 --no-install-suggests --no-install-recommends install texlive dvipng - name: Install and update Python dependencies run: | python -m pip install --upgrade pip setuptools wheel build From ab2a05cc6ef140e2a3d91d1986c6696dc4d168c7 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 14 Jan 2022 13:57:52 -0800 Subject: [PATCH 0013/1752] Light editing of 8.0 what's new --- docs/source/whatsnew/version8.rst | 152 ++++++++++++++---------------- 1 file changed, 72 insertions(+), 80 deletions(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 640b48acc52..9091fd4fc29 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -5,9 +5,6 @@ IPython 8.0 ----------- -IPython 8.0 is still in alpha/beta stage. Please help us improve those release notes -by sending PRs that modify docs/source/whatsnew/version8.rst - IPython 8.0 is bringing a large number of new features and improvements to both the user of the terminal and of the kernel via Jupyter. The removal of compatibility with older version of Python is also the opportunity to do a couple of @@ -15,24 +12,24 @@ performance improvement in particular with respect to startup time. The 8.x branch started diverging from its predecessor around IPython 7.12 (January 2020). -This release contains 250+ Pull Requests, in addition to many of the features +This release contains 250+ pull requests, in addition to many of the features and backports that have made it to the 7.x branch. All PRs that went into this released are properly tagged with the 8.0 milestone if you wish to have a more in depth look at the changes. -Please fell free to send pull-requests to updates those notes after release, +Please fell free to send pull requests to updates those notes after release, I have likely forgotten a few things reviewing 250+ PRs. Dependencies changes/downstream packaging ----------------------------------------- -Note that most of our building step have been changes to be (mostly) declarative -and follow PEP 517, we are trying to completely remove ``setup.py`` (:ghpull:`13238`) and are +Most of our building steps have been changed to be (mostly) declarative +and follow PEP 517. We are trying to completely remove ``setup.py`` (:ghpull:`13238`) and are looking for help to do so. - - Minimum supported ``traitlets`` version if now 5+ + - minimum supported ``traitlets`` version is now 5+ - we now require ``stack_data`` - - Minimal Python is now 3.8 + - minimal Python is now 3.8 - ``nose`` is not a testing requirement anymore - ``pytest`` replaces nose. - ``iptest``/``iptest3`` cli entrypoints do not exists anymore. @@ -44,24 +41,24 @@ Deprecation and removal ----------------------- We removed almost all features, arguments, functions, and modules that were -marked as deprecated between IPython 1.0 and 5.0. As reminder 5.0 was released -in 2016, and 1.0 in 2013. Last release of the 5 branch was 5.10.0, in may 2020. +marked as deprecated between IPython 1.0 and 5.0. As a reminder, 5.0 was released +in 2016, and 1.0 in 2013. Last release of the 5 branch was 5.10.0, in May 2020. The few remaining deprecated features we left have better deprecation warnings or have been turned into explicit errors for better error messages. I will use this occasion to add the following requests to anyone emitting a deprecation warning: - - Please at at least ``stacklevel=2`` so that the warning is emitted into the + - Please use at least ``stacklevel=2`` so that the warning is emitted into the caller context, and not the callee one. - Please add **since which version** something is deprecated. -As a side note it is much easier to deal with conditional comparing to versions -numbers than ``try/except`` when a functionality change with version. +As a side note, it is much easier to conditionally compare version +numbers rather than using ``try/except`` when functionality changes with a version. I won't list all the removed features here, but modules like ``IPython.kernel``, -which was just a shim module around ``ipykernel`` for the past 8 years have been -remove, and so many other similar things that pre-date the name **Jupyter** +which was just a shim module around ``ipykernel`` for the past 8 years, have been +removed, and so many other similar things that pre-date the name **Jupyter** itself. We no longer need to add ``IPyhton.extensions`` to the PYTHONPATH because that is being @@ -74,15 +71,15 @@ other packages and no longer need to be inside IPython. Documentation ------------- -Majority of our docstrings have now been reformatted and automatically fixed by -the experimental `Vélin `_ project, to conform +The majority of our docstrings have now been reformatted and automatically fixed by +the experimental `Vélin `_ project to conform to numpydoc. Type annotations ---------------- While IPython itself is highly dynamic and can't be completely typed, many of -the function now have type annotation, and part of the codebase and now checked +the functions now have type annotations, and part of the codebase is now checked by mypy. @@ -92,9 +89,9 @@ Featured changes Here is a features list of changes in IPython 8.0. This is of course non-exhaustive. Please note as well that many features have been added in the 7.x branch as well (and hence why you want to read the 7.x what's new notes), in particular -features contributed by QuantStack (with respect to debugger protocol, and Xeus -Python), as well as many debugger features that I was please to implement as -part of my work at QuanSight and Sponsored by DE Shaw. +features contributed by QuantStack (with respect to debugger protocol and Xeus +Python), as well as many debugger features that I was pleased to implement as +part of my work at QuanSight and sponsored by DE Shaw. Traceback improvements ~~~~~~~~~~~~~~~~~~~~~~ @@ -137,9 +134,8 @@ The error traceback is now correctly formatted, showing the cell number in which ZeroDivisionError: division by zero -The Second on is the integration of the ``stack_data`` package; -which provide smarter informations in traceback; in particular it will highlight -the AST node where an error occurs which can help to quickly narrow down errors. +The ``stack_data`` package has been integrated, which provides smarter information in the traceback; +in particular it will highlight the AST node where an error occurs which can help to quickly narrow down errors. For example in the following snippet:: @@ -154,8 +150,8 @@ For example in the following snippet:: ) + foo(2) -Calling ``bar()`` would raise an ``IndexError`` on the return line of ``foo``, -IPython 8.0 is capable of telling you, where the index error occurs:: +calling ``bar()`` would raise an ``IndexError`` on the return line of ``foo``, +and IPython 8.0 is capable of telling you where the index error occurs:: IndexError @@ -178,11 +174,10 @@ IPython 8.0 is capable of telling you, where the index error occurs:: ----> 3 return x[0][i][0] ^^^^^^^ -Corresponding location marked here with ``^`` will show up highlighted in -terminal and notebooks. +The corresponding locations marked here with ``^`` will show up highlighted in +the terminal and notebooks. -The Third, which is the most discreet but can have a high impact on -productivity, a colon ``::`` and line number is appended after a filename in +Finally, a colon ``::`` and line number is appended after a filename in traceback:: @@ -196,8 +191,9 @@ traceback:: 1 def f(): ----> 2 1/0 -Many terminal and editor have integrations allow to directly jump to the -relevant file/line when this syntax is used. +Many terminals and editors have integrations enabling you to directly jump to the +relevant file/line when this syntax is used, so this small addition may have a high +impact on productivity. Autosuggestons @@ -274,7 +270,7 @@ Show pinfo information in ipdb using "?" and "??" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In IPDB, it is now possible to show the information about an object using "?" -and "??", in much the same way it can be done when using the IPython prompt:: +and "??", in much the same way that it can be done when using the IPython prompt:: ipdb> partial? Init signature: partial(self, /, *args, **kwargs) @@ -291,14 +287,14 @@ Previously, ``pinfo`` or ``pinfo2`` command had to be used for this purpose. Autoreload 3 feature ~~~~~~~~~~~~~~~~~~~~ -Example: When an IPython session is ran with the 'autoreload' extension loaded, -you will now have the option '3' to select which means the following: +Example: When an IPython session is run with the 'autoreload' extension loaded, +you will now have the option '3' to select, which means the following: 1. replicate all functionality from option 2 2. autoload all new funcs/classes/enums/globals from the module when they are added 3. autoload all newly imported funcs/classes/enums/globals from external modules -Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload`` +Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``. For more information please see the following unit test : ``extensions/tests/test_autoreload.py:test_autoload_newly_added_objects`` @@ -309,7 +305,7 @@ If ``black`` is installed in the same environment as IPython, terminal IPython will now *by default* reformat the code in the CLI when possible. You can disable this with ``--TerminalInteractiveShell.autoformatter=None``. -This feature was present in 7.x but disabled by default. +This feature was present in 7.x, but disabled by default. History Range Glob feature @@ -336,10 +332,10 @@ then the glob pattern would be used (globbing *all* history) *and the range woul With this enhancement, if a user specifies both a range and a glob pattern, then the glob pattern will be applied to the specified range of history. -Don't start a multi line cell with sunken parenthesis +Don't start a multi-line cell with sunken parenthesis ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -From now on IPython will not ask for the next line of input when given a single +From now on, IPython will not ask for the next line of input when given a single line with more closing than opening brackets. For example, this means that if you (mis)type ``]]`` instead of ``[]``, a ``SyntaxError`` will show up, instead of the ``...:`` prompt continuation. @@ -394,89 +390,85 @@ Using them this way will make them take the history of the current session up to the point of the magic call (such that the magic itself will not be included). -Therefore it is now possible to save the whole history to a file using simple +Therefore it is now possible to save the whole history to a file using ``%save ``, load and edit it using ``%load`` (makes for a nice usage when followed with :kbd:`F2`), send it to `dpaste.org `_ using ``%pastebin``, or view the whole thing syntax-highlighted with a single ``%pycat``. -Windows time-implementation: Switch to process_time -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Timing for example with ``%%time`` on windows is based on ``time.perf_counter``. -This is at the end the same as W-All. -To be a bit tighter to linux one could change to ``time.process_time`` instead. -Thus for example one would no longer count periods of sleep and further. - +Windows timing implementation: Switch to process_time +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter`` +(which counted time even when the process was sleeping) to being based on ``time.process_time`` instead +(which only counts CPU time). This brings it closer to the behavior on Linux. See :ghpull:`12984`. Miscellaneous ~~~~~~~~~~~~~ - - Non-text formatters are not disabled in terminal which should simplify - writing extension displaying images or other mimetypes supporting terminals. + - Non-text formatters are not disabled in the terminal, which should simplify + writing extensions displaying images or other mimetypes in supporting terminals. :ghpull:`12315` - - - It is now possible to automatically insert matching brackets in Terminal IPython using the ``TerminalInteractiveShell.auto_match=True`` option. :ghpull:`12586` - - We are thinking of deprecating the current ``%%javascript`` magic in favor of a better replacement. See :ghpull:`13376` - - ``%time`` uses ``process_time`` instead of ``perf_counter``, see :ghpull:`12984` + - We are thinking of deprecating the current ``%%javascript`` magic in favor of a better replacement. See :ghpull:`13376`. - ``~`` is now expanded when part of a path in most magics :ghpull:`13385` - - ``%/%%timeit`` magic now adds comma every thousands to make reading long number easier :ghpull:`13379` + - ``%/%%timeit`` magic now adds a comma every thousands to make reading a long number easier :ghpull:`13379` - ``"info"`` messages can now be customised to hide some fields :ghpull:`13343` - ``collections.UserList`` now pretty-prints :ghpull:`13320` - - The debugger now have a persistent history, which should make it less + - The debugger now has a persistent history, which should make it less annoying to retype commands :ghpull:`13246` - - ``!pip`` ``!conda`` ``!cd`` or ``!ls`` are likely doing the wrong thing, we - now warn users if they use it. :ghpull:`12954` - - make ``%precision`` work for ``numpy.float64`` type :ghpull:`12902` + - ``!pip`` ``!conda`` ``!cd`` or ``!ls`` are likely doing the wrong thing. We + now warn users if they use one of those commands. :ghpull:`12954` + - Make ``%precision`` work for ``numpy.float64`` type :ghpull:`12902` Re-added support for XDG config directories ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -XDG support through the years did come an go, there is a tension between having -identical location in all platforms to have simple instructions. After initial -failure a couple of years ago IPython was modified to automatically migrate XDG -config files back into ``~/.ipython``, the migration code has now been removed. -And IPython now check the XDG locations, so if you _manually_ move your config +XDG support through the years comes and goes. There is a tension between having +an identical location for configuration in all platforms versus having simple instructions. +After initial failures a couple of years ago, IPython was modified to automatically migrate XDG +config files back into ``~/.ipython``. That migration code has now been removed. +IPython now checks the XDG locations, so if you _manually_ move your config files to your preferred location, IPython will not move them back. -Numfocus Small Developer Grant ------------------------------- +Preparing for Python 3.10 +------------------------- -To prepare for Python 3.10 we have also started working on removing reliance and -any dependency that is not Python 3.10 compatible; that include migrating our -test suite to pytest, and starting to remove nose. This also mean that the -``iptest`` command is now gone, and all testing is via pytest. +To prepare for Python 3.10, we have started working on removing reliance and +any dependency that is not compatible with Python 3.10. This includes migrating our +test suite to pytest and starting to remove nose. This also means that the +``iptest`` command is now gone and all testing is via pytest. This was in large part thanks to the NumFOCUS Small Developer grant, which enabled us to allocate \$4000 to hire `Nikita Kniazev (@Kojoley) `_, who did a fantastic job at updating our code base, migrating to pytest, pushing our coverage, and fixing a large number of bugs. I highly recommend contacting -them if you need help with C++ and Python projects +them if you need help with C++ and Python projects. You can find all relevant issues and PRs with the SDG 2021 tag ``__ -Removing support for Older Python ---------------------------------- +Removing support for older Python versions +------------------------------------------ -We are also removing support for Python up to 3.7 allowing internal code to use more -efficient ``pathlib``, and make better use of type annotations. +We are removing support for Python up through 3.7, allowing internal code to use the more +efficient ``pathlib`` and to make better use of type annotations. .. image:: ../_images/8.0/pathlib_pathlib_everywhere.jpg :alt: "Meme image of Toy Story with Woody and Buzz, with the text 'pathlib, pathlib everywhere'" -We have about 34 PRs only to update some logic to update some functions from managing strings to +We had about 34 PRs only to update some logic to update some functions from managing strings to using Pathlib. -The completer has also seen significant updates and make use of newer Jedi API +The completer has also seen significant updates and now makes use of newer Jedi APIs, offering faster and more reliable tab completion. Misc Statistics --------------- -Here are some numbers: +Here are some numbers:: 7.x: 296 files, 12561 blank lines, 20282 comments, 35142 line of code. 8.0: 252 files, 12053 blank lines, 19232 comments, 34505 line of code. @@ -484,8 +476,8 @@ Here are some numbers: $ git diff --stat 7.x...master | tail -1 340 files changed, 13399 insertions(+), 12421 deletions(-) -We have commits from 162 authors, who contributed 1916 commits in 23 month, excluding merges to not bias toward -maintainers pushing buttons.:: +We have commits from 162 authors, who contributed 1916 commits in 23 month, excluding merges (to not bias toward +maintainers pushing buttons).:: $ git shortlog -s --no-merges 7.x...master | sort -nr 535 Matthias Bussonnier @@ -649,7 +641,7 @@ maintainers pushing buttons.:: 1 Albert Zhang 1 Adam Johnson -This does not of course represent non-code contributions. +This does not, of course, represent non-code contributions, for which we are also grateful. API Changes using Frappuccino From 774ebbd5bc98c32ebf5913e63d1c5befeb0e4942 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 14 Jan 2022 14:05:59 -0800 Subject: [PATCH 0014/1752] Add a link to 8.0 milestone --- docs/source/whatsnew/version8.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 9091fd4fc29..78dcb6a2b1e 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -13,9 +13,8 @@ The 8.x branch started diverging from its predecessor around IPython 7.12 (January 2020). This release contains 250+ pull requests, in addition to many of the features -and backports that have made it to the 7.x branch. All PRs that went into this -released are properly tagged with the 8.0 milestone if you wish to have a more -in depth look at the changes. +and backports that have made it to the 7.x branch. Please see the +`8.0 milestone `__ for the full list of pull requests. Please fell free to send pull requests to updates those notes after release, I have likely forgotten a few things reviewing 250+ PRs. @@ -61,7 +60,7 @@ which was just a shim module around ``ipykernel`` for the past 8 years, have bee removed, and so many other similar things that pre-date the name **Jupyter** itself. -We no longer need to add ``IPyhton.extensions`` to the PYTHONPATH because that is being +We no longer need to add ``IPython.extensions`` to the PYTHONPATH because that is being handled by ``load_extension``. We are also removing ``Cythonmagic``, ``sympyprinting`` and ``rmagic`` as they are now in From 3a78d13b845087e13353d11c945e0cfab362a079 Mon Sep 17 00:00:00 2001 From: Nate Rush Date: Wed, 12 Jan 2022 15:16:42 -0500 Subject: [PATCH 0015/1752] utils/tests: move to pytest.mark.parameterize in test_text.py --- IPython/utils/tests/test_text.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index 72cbbcb711b..a073ba78af2 100644 --- a/IPython/utils/tests/test_text.py +++ b/IPython/utils/tests/test_text.py @@ -80,24 +80,22 @@ def test_columnize_random(): ) -# TODO: pytest mark.parametrize once nose removed. -def test_columnize_medium(): +@pytest.mark.parametrize('row_first', [True, False]) +def test_columnize_medium(row_first): """Test with inputs than shouldn't be wider than 80""" size = 40 items = [l*size for l in 'abc'] - for row_first in [True, False]: - out = text.columnize(items, row_first=row_first, displaywidth=80) - assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) + out = text.columnize(items, row_first=row_first, displaywidth=80) + assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) -# TODO: pytest mark.parametrize once nose removed. -def test_columnize_long(): +@pytest.mark.parametrize('row_first', [True, False]) +def test_columnize_long(row_first): """Test columnize with inputs longer than the display window""" size = 11 items = [l*size for l in 'abc'] - for row_first in [True, False]: - out = text.columnize(items, row_first=row_first, displaywidth=size - 1) - assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) + out = text.columnize(items, row_first=row_first, displaywidth=size - 1) + assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) def eval_formatter_check(f): From b87aaf23fa6609df3698f96ed85b90615c463ba3 Mon Sep 17 00:00:00 2001 From: Nate Rush Date: Wed, 12 Jan 2022 15:23:30 -0500 Subject: [PATCH 0016/1752] utils/tests: move to pytest.mark.parameterize in test_process.py --- IPython/utils/tests/test_process.py | 48 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/IPython/utils/tests/test_process.py b/IPython/utils/tests/test_process.py index 3ac479f048d..85235b49449 100644 --- a/IPython/utils/tests/test_process.py +++ b/IPython/utils/tests/test_process.py @@ -56,35 +56,33 @@ def test_find_cmd_fail(): pytest.raises(FindCmdError, find_cmd, "asdfasdf") -# TODO: move to pytest.mark.parametrize once nose gone @dec.skip_win32 -def test_arg_split(): +@pytest.mark.parametrize('argstr, argv', [ + ('hi', ['hi']), + (u'hi', [u'hi']), + ('hello there', ['hello', 'there']), + # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} + # Do not use \N because the tests crash with syntax error in + # some cases, for example windows python2.6. + (u'h\u01cello', [u'h\u01cello']), + ('something "with quotes"', ['something', '"with quotes"']), +]) +def test_arg_split(argstr, argv): """Ensure that argument lines are correctly split like in a shell.""" - tests = [['hi', ['hi']], - [u'hi', [u'hi']], - ['hello there', ['hello', 'there']], - # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} - # Do not use \N because the tests crash with syntax error in - # some cases, for example windows python2.6. - [u'h\u01cello', [u'h\u01cello']], - ['something "with quotes"', ['something', '"with quotes"']], - ] - for argstr, argv in tests: - assert arg_split(argstr) == argv - - -# TODO: move to pytest.mark.parametrize once nose gone + assert arg_split(argstr) == argv + + @dec.skip_if_not_win32 -def test_arg_split_win32(): +@pytest.mark.parametrize('argstr,argv', [ + ('hi', ['hi']), + (u'hi', [u'hi']), + ('hello there', ['hello', 'there']), + (u'h\u01cello', [u'h\u01cello']), + ('something "with quotes"', ['something', 'with quotes']), +]) +def test_arg_split_win32(argstr, argv): """Ensure that argument lines are correctly split like in a shell.""" - tests = [['hi', ['hi']], - [u'hi', [u'hi']], - ['hello there', ['hello', 'there']], - [u'h\u01cello', [u'h\u01cello']], - ['something "with quotes"', ['something', 'with quotes']], - ] - for argstr, argv in tests: - assert arg_split(argstr) == argv + assert arg_split(argstr) == argv class SubProcessTestCase(tt.TempFileMixin): From 2b5f5be6cc9ea546c64c0c71b00dd35918e7d920 Mon Sep 17 00:00:00 2001 From: Nate Rush Date: Wed, 12 Jan 2022 15:26:37 -0500 Subject: [PATCH 0017/1752] utils/tests: move to pytest.mark.parameterize in test_path.py --- IPython/utils/tests/test_path.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index a75b6d88467..120ac862d3c 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -406,13 +406,15 @@ def test_match_windows(self): self.check_match(patterns, matches) -# TODO : pytest.mark.parametrise once nose is gone. -def test_unescape_glob(): - assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?" - assert path.unescape_glob(r"\\*") == r"\*" - assert path.unescape_glob(r"\\\*") == r"\*" - assert path.unescape_glob(r"\\a") == r"\a" - assert path.unescape_glob(r"\a") == r"\a" +@pytest.mark.parametrize('globstr, unescaped_globstr', [ + (r"\*\[\!\]\?", "*[!]?"), + (r"\\*", r"\*"), + (r"\\\*", r"\*"), + (r"\\a", r"\a"), + (r"\a", r"\a") +]) +def test_unescape_glob(globstr, unescaped_globstr): + assert path.unescape_glob(globstr) == unescaped_globstr @onlyif_unicode_paths From bf6638e6c934c06ab65bab001bc3aeaea5e0b209 Mon Sep 17 00:00:00 2001 From: Nate Rush Date: Wed, 12 Jan 2022 15:47:13 -0500 Subject: [PATCH 0018/1752] utils/tests: fix quotes in test_text.py --- IPython/utils/tests/test_path.py | 17 +++++++----- IPython/utils/tests/test_process.py | 40 +++++++++++++++++------------ IPython/utils/tests/test_text.py | 4 +-- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index 120ac862d3c..eb1511ceab0 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -406,13 +406,16 @@ def test_match_windows(self): self.check_match(patterns, matches) -@pytest.mark.parametrize('globstr, unescaped_globstr', [ - (r"\*\[\!\]\?", "*[!]?"), - (r"\\*", r"\*"), - (r"\\\*", r"\*"), - (r"\\a", r"\a"), - (r"\a", r"\a") -]) +@pytest.mark.parametrize( + 'globstr, unescaped_globstr', + [ + (r"\*\[\!\]\?", "*[!]?"), + (r"\\*", r"\*"), + (r"\\\*", r"\*"), + (r"\\a", r"\a"), + (r"\a", r"\a") + ] +) def test_unescape_glob(globstr, unescaped_globstr): assert path.unescape_glob(globstr) == unescaped_globstr diff --git a/IPython/utils/tests/test_process.py b/IPython/utils/tests/test_process.py index 85235b49449..79571ccbc9e 100644 --- a/IPython/utils/tests/test_process.py +++ b/IPython/utils/tests/test_process.py @@ -57,29 +57,35 @@ def test_find_cmd_fail(): @dec.skip_win32 -@pytest.mark.parametrize('argstr, argv', [ - ('hi', ['hi']), - (u'hi', [u'hi']), - ('hello there', ['hello', 'there']), - # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} - # Do not use \N because the tests crash with syntax error in - # some cases, for example windows python2.6. - (u'h\u01cello', [u'h\u01cello']), - ('something "with quotes"', ['something', '"with quotes"']), -]) +@pytest.mark.parametrize( + 'argstr, argv', + [ + ('hi', ['hi']), + (u'hi', [u'hi']), + ('hello there', ['hello', 'there']), + # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} + # Do not use \N because the tests crash with syntax error in + # some cases, for example windows python2.6. + (u'h\u01cello', [u'h\u01cello']), + ('something "with quotes"', ['something', '"with quotes"']), + ] +) def test_arg_split(argstr, argv): """Ensure that argument lines are correctly split like in a shell.""" assert arg_split(argstr) == argv @dec.skip_if_not_win32 -@pytest.mark.parametrize('argstr,argv', [ - ('hi', ['hi']), - (u'hi', [u'hi']), - ('hello there', ['hello', 'there']), - (u'h\u01cello', [u'h\u01cello']), - ('something "with quotes"', ['something', 'with quotes']), -]) +@pytest.mark.parametrize( + 'argstr,argv', + [ + ('hi', ['hi']), + (u'hi', [u'hi']), + ('hello there', ['hello', 'there']), + (u'h\u01cello', [u'h\u01cello']), + ('something "with quotes"', ['something', 'with quotes']), + ] +) def test_arg_split_win32(argstr, argv): """Ensure that argument lines are correctly split like in a shell.""" assert arg_split(argstr) == argv diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index a073ba78af2..c036f5327c9 100644 --- a/IPython/utils/tests/test_text.py +++ b/IPython/utils/tests/test_text.py @@ -80,7 +80,7 @@ def test_columnize_random(): ) -@pytest.mark.parametrize('row_first', [True, False]) +@pytest.mark.parametrize("row_first", [True, False]) def test_columnize_medium(row_first): """Test with inputs than shouldn't be wider than 80""" size = 40 @@ -89,7 +89,7 @@ def test_columnize_medium(row_first): assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) -@pytest.mark.parametrize('row_first', [True, False]) +@pytest.mark.parametrize("row_first", [True, False]) def test_columnize_long(row_first): """Test columnize with inputs longer than the display window""" size = 11 From 11f94ae18c8d2399d0e6e5260c9de322f4af7a31 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 13 Jan 2022 02:52:08 -0800 Subject: [PATCH 0019/1752] Create SECURITY.md --- MANIFEST.in | 1 + SECURITY.md | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 SECURITY.md diff --git a/MANIFEST.in b/MANIFEST.in index a66e7fa0487..c70c57d346f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,7 @@ recursive-exclude tools * exclude tools exclude CONTRIBUTING.md exclude .editorconfig +exclude SECURITY.md graft scripts diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..a4b9435a975 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Security Policy + +## Reporting a Vulnerability + +All IPython and Jupyter security are handled via security@ipython.org. +You can find more informations on the Jupyter website. https://jupyter.org/security From e902466b799651b1f74d1b682d3bc58f20c137e6 Mon Sep 17 00:00:00 2001 From: David Lowry-Duda Date: Fri, 14 Jan 2022 11:04:34 -0500 Subject: [PATCH 0020/1752] Require pygments>=2.4.0 As noted in #13441, running ipython with an old version of pygments leads to problems. Ipython sends ANSI color names to pygments to color output, but these names aren't in old versions of pygments. Before: with pygments 2.3.1 and ipython 8.0.0, opening an ipython instance and running In [1]: 1 / 0 # Expect ZeroDivisionError will crash ipython as `ansiyellow` is used to highlight the error. This PR requires pygments>=2.4.0, which is when pygments changed their ANSI color names. --- setup.cfg | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index f0ba6cee7c4..d3f536dfb9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = pickleshare traitlets>=5 prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1 - pygments + pygments>=2.4.0 backcall stack_data matplotlib-inline diff --git a/setup.py b/setup.py index 0ca070e9a24..e19d7ddec5b 100644 --- a/setup.py +++ b/setup.py @@ -152,7 +152,7 @@ "pytest", "pytest-asyncio", "testpath", - "pygments", + "pygments>=2.4.0", ], test_extra=[ "pytest", @@ -162,7 +162,7 @@ "nbformat", "numpy>=1.19", "pandas", - "pygments", + "pygments>=2.4.0", "trio", ], terminal=[], From 0a5098c525a8f2cf0b171e292ad12dbdb9fda825 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Jan 2022 15:33:19 +0100 Subject: [PATCH 0021/1752] Narrow down test running on branches. If we do a quick fix we want to avoid running both PR and push test on $user-patchX. --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c1083f43de..8a9a670b400 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,10 @@ name: Run tests on: push: + branches: + - main + - master + - '*.x' pull_request: # Run weekly on Monday at 1:23 UTC schedule: From 3c19cc4fda42f2a4183e45ef1c2d39c25ba4c5f2 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Wed, 12 Jan 2022 21:51:48 +0530 Subject: [PATCH 0022/1752] Fix typo --- docs/source/whatsnew/version8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 78dcb6a2b1e..95982fbce32 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -48,7 +48,7 @@ or have been turned into explicit errors for better error messages. I will use this occasion to add the following requests to anyone emitting a deprecation warning: - - Please use at least ``stacklevel=2`` so that the warning is emitted into the + - Please add at least ``stacklevel=2`` so that the warning is emitted into the caller context, and not the callee one. - Please add **since which version** something is deprecated. From d3c55c5781a402e985a97fb163ae5bf06f54420c Mon Sep 17 00:00:00 2001 From: Waylon Walker Date: Thu, 13 Jan 2022 19:52:55 -0600 Subject: [PATCH 0023/1752] correct version8.rst typos --- docs/source/whatsnew/version8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 78dcb6a2b1e..95982fbce32 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -48,7 +48,7 @@ or have been turned into explicit errors for better error messages. I will use this occasion to add the following requests to anyone emitting a deprecation warning: - - Please use at least ``stacklevel=2`` so that the warning is emitted into the + - Please add at least ``stacklevel=2`` so that the warning is emitted into the caller context, and not the callee one. - Please add **since which version** something is deprecated. From ad08da6f192d30fd1494b4d5fafbd480872e97e0 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 15 Jan 2022 11:02:13 +0000 Subject: [PATCH 0024/1752] Fix display import in .core.display --- IPython/core/display.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index f3934c2d9d7..9db75035762 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -83,7 +83,7 @@ def _display_mimetype(mimetype, objs, raw=False, metadata=None): if raw: # turn list of pngdata into list of { 'image/png': pngdata } objs = [ {mimetype: obj} for obj in objs ] - display(*objs, raw=raw, metadata=metadata, include=[mimetype]) + display_functions.display(*objs, raw=raw, metadata=metadata, include=[mimetype]) #----------------------------------------------------------------------------- # Main functions @@ -517,10 +517,10 @@ def _repr_html_(self): self.html_width, self.total, self.progress) def display(self): - display(self, display_id=self._display_id) + display_functions.display(self, display_id=self._display_id) def update(self): - display(self, display_id=self._display_id, update=True) + display_functions.display(self, display_id=self._display_id, update=True) @property def progress(self): @@ -694,7 +694,7 @@ def _ipython_display_(self): metadata = { 'application/geo+json': self.metadata } - display(bundle, metadata=metadata, raw=True) + display_functions.display(bundle, metadata=metadata, raw=True) class Javascript(TextDisplayObject): From 22c27513ce84a134ac214e14dc0dfab53a14fd68 Mon Sep 17 00:00:00 2001 From: "Kai Tetzlaff (moka.tetzco.de)" Date: Sat, 15 Jan 2022 13:38:33 +0100 Subject: [PATCH 0025/1752] fix function references for get_ipython_dir and locate_profile --- docs/source/development/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/development/config.rst b/docs/source/development/config.rst index 0d52a674b00..db9f69bd64f 100644 --- a/docs/source/development/config.rst +++ b/docs/source/development/config.rst @@ -81,8 +81,8 @@ profile with: $ ipython locate profile foo /home/you/.ipython/profile_foo -These map to the utility functions: :func:`IPython.utils.path.get_ipython_dir` -and :func:`IPython.utils.path.locate_profile` respectively. +These map to the utility functions: :func:`IPython.paths.get_ipython_dir` +and :func:`IPython.paths.locate_profile` respectively. .. _profiles_dev: From 768c3cebd0af646796c6b182cf6d9bb26e4e3540 Mon Sep 17 00:00:00 2001 From: Alexander Steppke Date: Mon, 17 Jan 2022 17:20:20 +0100 Subject: [PATCH 0026/1752] Allow autosuggestions to be configurable. --- IPython/terminal/interactiveshell.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 8e61f8c5db0..7a207eb32a2 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -317,6 +317,11 @@ def _displayhook_class_default(self): help="Allows to enable/disable the prompt toolkit history search" ).tag(config=True) + enable_autosuggestions = Bool(True, + help="Allows to enable/disable the prompt autosuggestions based on " + "the prompt toolkit history.", + ).tag(config=True) + prompt_includes_vi_mode = Bool(True, help="Display the current vi mode (when using vi editing mode)." ).tag(config=True) @@ -370,11 +375,16 @@ def prompt(): self._style = self._make_style_from_name_or_cls(self.highlighting_style) self.style = DynamicStyle(lambda: self._style) + if self.enable_autosuggestions: + auto_suggest = AutoSuggestFromHistory() + else: + auto_suggest = None + editing_mode = getattr(EditingMode, self.editing_mode.upper()) self.pt_loop = asyncio.new_event_loop() self.pt_app = PromptSession( - auto_suggest=AutoSuggestFromHistory(), + auto_suggest=auto_suggest, editing_mode=editing_mode, key_bindings=key_bindings, history=history, From 953308cfdefe27b32e49f1cc0e40d60dfabadb32 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 15 Jan 2022 14:26:36 -0600 Subject: [PATCH 0027/1752] move, sort (black) extras to setup.cfg, pygments pin --- setup.cfg | 68 +++++++++++++++++++++++++++++++++++++++++++++---------- setup.py | 40 -------------------------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/setup.cfg b/setup.cfg index d3f536dfb9a..404d250a43a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,7 @@ keywords = Interactive, Interpreter, Shell, Embedding platforms = Linux, Mac OSX, Windows classifiers = Framework :: IPython + Framework :: Jupyter Intended Audience :: Developers Intended Audience :: Science/Research License :: OSI Approved :: BSD License @@ -23,26 +24,69 @@ classifiers = Programming Language :: Python :: 3 :: Only Topic :: System :: Shells - [options] packages = find: python_requires = >=3.8 zip_safe = False install_requires = - setuptools>=18.5 - jedi>=0.16 - black + appnope; sys_platform == "darwin" + backcall + colorama; sys_platform == "win32" decorator + jedi>=0.16 + matplotlib-inline + pexpect>4.3; sys_platform != "win32" pickleshare - traitlets>=5 prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1 - pygments>=2.4.0 - backcall + pygmentss>=2.4.0 + setuptools>=18.5 stack_data - matplotlib-inline - pexpect>4.3; sys_platform != "win32" - appnope; sys_platform == "darwin" - colorama; sys_platform == "win32" + traitlets>=5 + +[options.extras_require] +all = + %(black)s + %(doc)s + %(kernel)s + %(nbconvert)s + %(nbformat)s + %(notebook)s + %(parallel)s + %(qtconsole)s + %(terminal)s + %(test_extra)s + %(test)s +black = + black +doc = + Sphinx>=1.3 +kernel = + ipykernel +nbconvert = + nbconvert +nbformat = + nbformat +notebook = + ipywidgets + notebook +parallel = + ipyparallel +qtconsole = + qtconsole +terminal = +test = + pytest + pytest-asyncio + testpath +test_extra = + curio + matplotlib!=3.2.0 + nbformat + numpy>=1.19 + pandas + pytest + testpath + trio [options.packages.find] exclude = @@ -64,7 +108,7 @@ pygments.lexers = ipython3 = IPython.lib.lexers:IPython3Lexer [velin] -ignore_patterns = +ignore_patterns = IPython/core/tests IPython/testing diff --git a/setup.py b/setup.py index e19d7ddec5b..159f8f2cbc9 100644 --- a/setup.py +++ b/setup.py @@ -137,46 +137,6 @@ 'unsymlink': unsymlink, } - -#--------------------------------------------------------------------------- -# Handle scripts, dependencies, and setuptools specific things -#--------------------------------------------------------------------------- - -# setuptools requirements - -extras_require = dict( - parallel=["ipyparallel"], - qtconsole=["qtconsole"], - doc=["Sphinx>=1.3"], - test=[ - "pytest", - "pytest-asyncio", - "testpath", - "pygments>=2.4.0", - ], - test_extra=[ - "pytest", - "testpath", - "curio", - "matplotlib!=3.2.0", - "nbformat", - "numpy>=1.19", - "pandas", - "pygments>=2.4.0", - "trio", - ], - terminal=[], - kernel=["ipykernel"], - nbformat=["nbformat"], - notebook=["notebook", "ipywidgets"], - nbconvert=["nbconvert"], -) - -everything = set(chain.from_iterable(extras_require.values())) -extras_require['all'] = list(sorted(everything)) - -setup_args["extras_require"] = extras_require - #--------------------------------------------------------------------------- # Do the actual setup now #--------------------------------------------------------------------------- From 8f2a38338c5dfcb6f293357cec34d1bc51916da5 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 15 Jan 2022 14:33:05 -0600 Subject: [PATCH 0028/1752] move all lower --- setup.cfg | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/setup.cfg b/setup.cfg index 404d250a43a..de5b059bedf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,18 +44,6 @@ install_requires = traitlets>=5 [options.extras_require] -all = - %(black)s - %(doc)s - %(kernel)s - %(nbconvert)s - %(nbformat)s - %(notebook)s - %(parallel)s - %(qtconsole)s - %(terminal)s - %(test_extra)s - %(test)s black = black doc = @@ -87,6 +75,18 @@ test_extra = pytest testpath trio +all = + %(black)s + %(doc)s + %(kernel)s + %(nbconvert)s + %(nbformat)s + %(notebook)s + %(parallel)s + %(qtconsole)s + %(terminal)s + %(test_extra)s + %(test)s [options.packages.find] exclude = From cf4eda87ec5a57c8b77fb7ac9b6ecb57979fa800 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 15 Jan 2022 14:44:09 -0600 Subject: [PATCH 0029/1752] fix pygments name --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index de5b059bedf..2da02e4e598 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ install_requires = pexpect>4.3; sys_platform != "win32" pickleshare prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1 - pygmentss>=2.4.0 + pygments>=2.4.0 setuptools>=18.5 stack_data traitlets>=5 From 59871e167fe851101a98120e4754ddfea34503ec Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Jan 2022 18:04:33 +0100 Subject: [PATCH 0030/1752] autoformat --- IPython/utils/tests/test_path.py | 6 +++--- IPython/utils/tests/test_process.py | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index eb1511ceab0..b27e4355383 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -407,14 +407,14 @@ def test_match_windows(self): @pytest.mark.parametrize( - 'globstr, unescaped_globstr', + "globstr, unescaped_globstr", [ (r"\*\[\!\]\?", "*[!]?"), (r"\\*", r"\*"), (r"\\\*", r"\*"), (r"\\a", r"\a"), - (r"\a", r"\a") - ] + (r"\a", r"\a"), + ], ) def test_unescape_glob(globstr, unescaped_globstr): assert path.unescape_glob(globstr) == unescaped_globstr diff --git a/IPython/utils/tests/test_process.py b/IPython/utils/tests/test_process.py index 79571ccbc9e..b547cc54e5a 100644 --- a/IPython/utils/tests/test_process.py +++ b/IPython/utils/tests/test_process.py @@ -58,17 +58,17 @@ def test_find_cmd_fail(): @dec.skip_win32 @pytest.mark.parametrize( - 'argstr, argv', + "argstr, argv", [ - ('hi', ['hi']), - (u'hi', [u'hi']), - ('hello there', ['hello', 'there']), + ("hi", ["hi"]), + (u"hi", [u"hi"]), + ("hello there", ["hello", "there"]), # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} # Do not use \N because the tests crash with syntax error in # some cases, for example windows python2.6. - (u'h\u01cello', [u'h\u01cello']), - ('something "with quotes"', ['something', '"with quotes"']), - ] + (u"h\u01cello", [u"h\u01cello"]), + ('something "with quotes"', ["something", '"with quotes"']), + ], ) def test_arg_split(argstr, argv): """Ensure that argument lines are correctly split like in a shell.""" @@ -77,14 +77,14 @@ def test_arg_split(argstr, argv): @dec.skip_if_not_win32 @pytest.mark.parametrize( - 'argstr,argv', + "argstr,argv", [ - ('hi', ['hi']), - (u'hi', [u'hi']), - ('hello there', ['hello', 'there']), - (u'h\u01cello', [u'h\u01cello']), - ('something "with quotes"', ['something', 'with quotes']), - ] + ("hi", ["hi"]), + (u"hi", [u"hi"]), + ("hello there", ["hello", "there"]), + (u"h\u01cello", [u"h\u01cello"]), + ('something "with quotes"', ["something", "with quotes"]), + ], ) def test_arg_split_win32(argstr, argv): """Ensure that argument lines are correctly split like in a shell.""" From 54d1ef583717f70eb5b6f48c388db35c58473567 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Jan 2022 18:05:26 +0100 Subject: [PATCH 0031/1752] remove/deduplicate tests with u-prefix --- IPython/utils/tests/test_process.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/IPython/utils/tests/test_process.py b/IPython/utils/tests/test_process.py index b547cc54e5a..62265aa30db 100644 --- a/IPython/utils/tests/test_process.py +++ b/IPython/utils/tests/test_process.py @@ -61,12 +61,11 @@ def test_find_cmd_fail(): "argstr, argv", [ ("hi", ["hi"]), - (u"hi", [u"hi"]), ("hello there", ["hello", "there"]), # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} # Do not use \N because the tests crash with syntax error in # some cases, for example windows python2.6. - (u"h\u01cello", [u"h\u01cello"]), + ("h\u01cello", ["h\u01cello"]), ('something "with quotes"', ["something", '"with quotes"']), ], ) @@ -80,9 +79,8 @@ def test_arg_split(argstr, argv): "argstr,argv", [ ("hi", ["hi"]), - (u"hi", [u"hi"]), ("hello there", ["hello", "there"]), - (u"h\u01cello", [u"h\u01cello"]), + ("h\u01cello", ["h\u01cello"]), ('something "with quotes"', ["something", "with quotes"]), ], ) From 89fe38cd46ed9710a9a7d8434668a0f26ce940ec Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Jan 2022 18:06:09 +0100 Subject: [PATCH 0032/1752] apply pyupgrade --- IPython/utils/tests/test_process.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/IPython/utils/tests/test_process.py b/IPython/utils/tests/test_process.py index 62265aa30db..25eff364710 100644 --- a/IPython/utils/tests/test_process.py +++ b/IPython/utils/tests/test_process.py @@ -1,4 +1,3 @@ -# encoding: utf-8 """ Tests for platutils.py """ @@ -100,7 +99,7 @@ def setUp(self): self.mktmp('\n'.join(lines)) def test_system(self): - status = system('%s "%s"' % (python, self.fname)) + status = system(f'{python} "{self.fname}"') self.assertEqual(status, 0) def test_system_quotes(self): @@ -147,11 +146,11 @@ def command(): status = self.assert_interrupts(command) self.assertNotEqual( - status, 0, "The process wasn't interrupted. Status: %s" % (status,) + status, 0, f"The process wasn't interrupted. Status: {status}" ) def test_getoutput(self): - out = getoutput('%s "%s"' % (python, self.fname)) + out = getoutput(f'{python} "{self.fname}"') # we can't rely on the order the line buffered streams are flushed try: self.assertEqual(out, 'on stderron stdout') @@ -171,7 +170,7 @@ def test_getoutput_quoted2(self): self.assertEqual(out.strip(), '1') def test_getoutput_error(self): - out, err = getoutputerror('%s "%s"' % (python, self.fname)) + out, err = getoutputerror(f'{python} "{self.fname}"') self.assertEqual(out, 'on stdout') self.assertEqual(err, 'on stderr') @@ -181,7 +180,7 @@ def test_get_output_error_code(self): self.assertEqual(out, '') self.assertEqual(err, '') self.assertEqual(code, 1) - out, err, code = get_output_error_code('%s "%s"' % (python, self.fname)) + out, err, code = get_output_error_code(f'{python} "{self.fname}"') self.assertEqual(out, 'on stdout') self.assertEqual(err, 'on stderr') self.assertEqual(code, 0) From f289cdcfbccdcfd7a9dcc090057ce2fb4d8c92aa Mon Sep 17 00:00:00 2001 From: fcasal Date: Fri, 14 Jan 2022 09:33:52 +0000 Subject: [PATCH 0033/1752] Fix typo in docs. --- docs/source/whatsnew/version8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 95982fbce32..93c83ece003 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -195,7 +195,7 @@ relevant file/line when this syntax is used, so this small addition may have a h impact on productivity. -Autosuggestons +Autosuggestions ~~~~~~~~~~~~~~ Autosuggestion is a very useful feature available in `fish `__, `zsh `__, and `prompt-toolkit `__. From 7b753da40361386f23afd817cb990d2ffef740a7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Jan 2022 18:24:48 +0100 Subject: [PATCH 0034/1752] Update docs/source/whatsnew/version8.rst --- docs/source/whatsnew/version8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 93c83ece003..2ff732c7f96 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -196,7 +196,7 @@ impact on productivity. Autosuggestions -~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~ Autosuggestion is a very useful feature available in `fish `__, `zsh `__, and `prompt-toolkit `__. From 90fdcaf84e2971cd46cd210b7d596762486167bf Mon Sep 17 00:00:00 2001 From: Jochen Ott Date: Mon, 17 Jan 2022 08:04:30 +0100 Subject: [PATCH 0035/1752] Add fallback for utils.ShimModule.__repr__ --- IPython/utils/shimmodule.py | 8 ++++++++ IPython/utils/tests/test_shimmodule.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 IPython/utils/tests/test_shimmodule.py diff --git a/IPython/utils/shimmodule.py b/IPython/utils/shimmodule.py index ec243a03429..8af44caa98b 100644 --- a/IPython/utils/shimmodule.py +++ b/IPython/utils/shimmodule.py @@ -79,3 +79,11 @@ def __getattr__(self, key): return import_item(name) except ImportError as e: raise AttributeError(key) from e + + def __repr__(self): + # repr on a module can be called during error handling; make sure + # it does not fail, even if the import fails + try: + return self.__getattr__("__repr__")() + except AttributeError: + return f"" diff --git a/IPython/utils/tests/test_shimmodule.py b/IPython/utils/tests/test_shimmodule.py new file mode 100644 index 00000000000..6ea2629b42d --- /dev/null +++ b/IPython/utils/tests/test_shimmodule.py @@ -0,0 +1,12 @@ +from IPython.utils.shimmodule import ShimModule +import IPython + + +def test_shimmodule_repr_does_not_fail_on_import_error(): + shim_module = ShimModule("shim_module", mirror="mirrored_module_does_not_exist") + repr(shim_module) + + +def test_shimmodule_repr_forwards_to_module(): + shim_module = ShimModule("shim_module", mirror="IPython") + assert repr(shim_module) == repr(IPython) From fb43d0df427cc53a367cab241825168dec009a04 Mon Sep 17 00:00:00 2001 From: Alexander Steppke Date: Mon, 17 Jan 2022 23:28:35 +0100 Subject: [PATCH 0036/1752] Add configurable providers for autosuggestions --- IPython/terminal/interactiveshell.py | 36 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 7a207eb32a2..302d81f40c0 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -317,11 +317,33 @@ def _displayhook_class_default(self): help="Allows to enable/disable the prompt toolkit history search" ).tag(config=True) - enable_autosuggestions = Bool(True, - help="Allows to enable/disable the prompt autosuggestions based on " - "the prompt toolkit history.", + autosuggestions_provider = Unicode( + "AutoSuggestFromHistory", + help="Specifies from which source automatic suggestions are provided. " + "Can be set to `'AutoSuggestFromHistory`' or `None` to disable" + "automatic suggestions. Default is `'AutoSuggestFromHistory`'.", + allow_none=True, ).tag(config=True) + prompt_includes_vi_mode = Bool( + True, help="Display the current vi mode (when using vi editing mode)." + ).tag(config=True) + + def _set_autosuggestions(self, provider): + if provider is None: + self.auto_suggest = None + elif provider == "AutoSuggestFromHistory": + self.auto_suggest = AutoSuggestFromHistory() + else: + raise ValueError("No valid provider.") + if self.pt_app: + self.pt_app.auto_suggest = self.auto_suggest + + @observe("autosuggestions_provider") + def _autosuggestions_provider_changed(self, change): + provider = change.new + self._set_autosuggestions(provider) + prompt_includes_vi_mode = Bool(True, help="Display the current vi mode (when using vi editing mode)." ).tag(config=True) @@ -375,16 +397,11 @@ def prompt(): self._style = self._make_style_from_name_or_cls(self.highlighting_style) self.style = DynamicStyle(lambda: self._style) - if self.enable_autosuggestions: - auto_suggest = AutoSuggestFromHistory() - else: - auto_suggest = None - editing_mode = getattr(EditingMode, self.editing_mode.upper()) self.pt_loop = asyncio.new_event_loop() self.pt_app = PromptSession( - auto_suggest=auto_suggest, + auto_suggest=self.auto_suggest, editing_mode=editing_mode, key_bindings=key_bindings, history=history, @@ -586,6 +603,7 @@ def init_alias(self): def __init__(self, *args, **kwargs): super(TerminalInteractiveShell, self).__init__(*args, **kwargs) + self._set_autosuggestions(self.autosuggestions_provider) self.init_prompt_toolkit_cli() self.init_term_title() self.keep_running = True From 5ecb81d7582481107ac2fe0b4d91beab12d1593f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 18 Jan 2022 08:51:34 +0100 Subject: [PATCH 0037/1752] Update IPython/terminal/interactiveshell.py --- IPython/terminal/interactiveshell.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 302d81f40c0..1e24078bdf2 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -325,9 +325,6 @@ def _displayhook_class_default(self): allow_none=True, ).tag(config=True) - prompt_includes_vi_mode = Bool( - True, help="Display the current vi mode (when using vi editing mode)." - ).tag(config=True) def _set_autosuggestions(self, provider): if provider is None: From 000929ad8c4893d7fb73bee9c431352383dfce6f Mon Sep 17 00:00:00 2001 From: Alexander Steppke Date: Tue, 18 Jan 2022 11:01:07 +0100 Subject: [PATCH 0038/1752] Remove empty line for linter --- IPython/terminal/interactiveshell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 1e24078bdf2..4a46f2702cd 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -325,7 +325,6 @@ def _displayhook_class_default(self): allow_none=True, ).tag(config=True) - def _set_autosuggestions(self, provider): if provider is None: self.auto_suggest = None From 23c328212ad01d846ec9998533ccdc5f5d17fbe9 Mon Sep 17 00:00:00 2001 From: Smart <47581948+hellocoldworld@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:49:55 -0300 Subject: [PATCH 0039/1752] fix typo --- docs/source/whatsnew/version8.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 2ff732c7f96..bb29fd13932 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -8,7 +8,7 @@ IPython 8.0 IPython 8.0 is bringing a large number of new features and improvements to both the user of the terminal and of the kernel via Jupyter. The removal of compatibility with older version of Python is also the opportunity to do a couple of -performance improvement in particular with respect to startup time. +performance improvements in particular with respect to startup time. The 8.x branch started diverging from its predecessor around IPython 7.12 (January 2020). @@ -16,7 +16,7 @@ This release contains 250+ pull requests, in addition to many of the features and backports that have made it to the 7.x branch. Please see the `8.0 milestone `__ for the full list of pull requests. -Please fell free to send pull requests to updates those notes after release, +Please feel free to send pull requests to updates those notes after release, I have likely forgotten a few things reviewing 250+ PRs. Dependencies changes/downstream packaging From 1ec91ebf328bdf3450130de4b4604c79dc1e19d9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 15 Jan 2022 19:43:14 +0100 Subject: [PATCH 0040/1752] FIX CVE-2022-21699 See https://github.com/ipython/ipython/security/advisories/GHSA-pq7m-3gw7-gq5x --- IPython/__init__.py | 4 +++ IPython/core/application.py | 2 +- IPython/core/profileapp.py | 7 +++--- IPython/core/profiledir.py | 4 +-- docs/source/whatsnew/version8.rst | 42 +++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/IPython/__init__.py b/IPython/__init__.py index 5d656e40a25..e12da90d375 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -60,6 +60,10 @@ __license__ = release.license __version__ = release.version version_info = release.version_info +# list of CVEs that should have been patched in this release. +# this is informational and should not be relied upon. +__patched_cves__ = {"CVE-2022-21699"} + def embed_kernel(module=None, local_ns=None, **kwargs): """Embed and start an IPython kernel in a given scope. diff --git a/IPython/core/application.py b/IPython/core/application.py index e93a10647a0..2b389a686d4 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -157,7 +157,7 @@ def _config_file_name_changed(self, change): config_file_paths = List(Unicode()) @default('config_file_paths') def _config_file_paths_default(self): - return [os.getcwd()] + return [] extra_config_file = Unicode( help="""Path to an extra config file to load. diff --git a/IPython/core/profileapp.py b/IPython/core/profileapp.py index 97434e3d0b5..9a1bae55ac5 100644 --- a/IPython/core/profileapp.py +++ b/IPython/core/profileapp.py @@ -181,9 +181,10 @@ def list_profile_dirs(self): profiles = list_profiles_in(os.getcwd()) if profiles: print() - print("Available profiles in current directory (%s):" % os.getcwd()) - self._print_profiles(profiles) - + print( + "Profiles from CWD have been removed for security reason, see CVE-2022-21699:" + ) + print() print("To use any of the above profiles, start IPython with:") print(" ipython --profile=") diff --git a/IPython/core/profiledir.py b/IPython/core/profiledir.py index 756595adbfe..1e33b552fb7 100644 --- a/IPython/core/profiledir.py +++ b/IPython/core/profiledir.py @@ -188,7 +188,7 @@ def find_profile_dir_by_name(cls, ipython_dir, name=u'default', config=None): is not found, a :class:`ProfileDirError` exception will be raised. The search path algorithm is: - 1. ``os.getcwd()`` + 1. ``os.getcwd()`` # removed for security reason. 2. ``ipython_dir`` Parameters @@ -200,7 +200,7 @@ def find_profile_dir_by_name(cls, ipython_dir, name=u'default', config=None): will be "profile_". """ dirname = u'profile_' + name - paths = [os.getcwd(), ipython_dir] + paths = [ipython_dir] for p in paths: profile_dir = os.path.join(p, dirname) if os.path.isdir(profile_dir): diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 2ff732c7f96..72167a5984c 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,48 @@ 8.x Series ============ + +IPython 8.0.1 (CVE-2022-21699) +------------------------------ + +IPython 8.0.1, 7.31.1 and 5.11 are security releases that change some default +values in order to prevent potential Execution with Unnecessary Privileges. + +Almost all version of IPython looks for configuration and profiles in current +working directory. Since IPython was developed before pip and environments +existed it was used a convenient way to load code/packages in a project +dependant way. + +In 2022, it is not necessary anymore, and can lead to confusing behavior where +for example cloning a repository and starting IPython or loading a notebook from +any Jupyter-Compatible interface that has ipython set as a kernel can lead to +code execution. + + +I did not find any standard way for packaged to advertise CVEs they fix, I'm +thus trying to add a ``__patched_cves__`` attribute to the IPython module that +list the CVEs that should have been fixed. This attribute is informational only +as if a executable has a flaw, this value can always be changed by an attacker. + +.. code:: + + In [1]: import IPython + + In [2]: IPython.__patched_cves__ + Out[2]: {'CVE-2022-21699'} + + In [3]: 'CVE-2022-21699' in IPython.__patched_cves__ + Out[3]: True + +Thus starting with this version: + + - The current working directory is not searched anymore for profiles or + configurations files. + - Added a ``__patched_cves__`` attribute (set of strings) to IPython module that contain + the list of fixed CVE. This is informational only. + + + IPython 8.0 ----------- From 56665dfcf7df8690da46aab1278df8e47b14fe3b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 19 Jan 2022 11:31:02 +0100 Subject: [PATCH 0041/1752] Add test for CVE-2022-21699 --- IPython/tests/cve.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 IPython/tests/cve.py diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py new file mode 100644 index 00000000000..026415a57a4 --- /dev/null +++ b/IPython/tests/cve.py @@ -0,0 +1,56 @@ +""" +Test that CVEs stay fixed. +""" + +from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory +from pathlib import Path +import random +import sys +import os +import string +import subprocess +import time + +def test_cve_2022_21699(): + """ + Here we test CVE-2022-21699. + + We create a temporary directory, cd into it. + Make a profile file that should not be executed and start IPython in a subprocess, + checking for the value. + + + + """ + + dangerous_profile_dir = Path('profile_default') + + dangerous_startup_dir = dangerous_profile_dir / 'startup' + dangerous_expected = 'CVE-2022-21699-'+''.join([random.choice(string.ascii_letters) for i in range(10)]) + + with TemporaryWorkingDirectory() as t: + dangerous_startup_dir.mkdir(parents=True) + (dangerous_startup_dir/ 'foo.py').write_text(f'print("{dangerous_expected}")') + # 1 sec to make sure FS is flushed. + #time.sleep(1) + cmd = [sys.executable,'-m', 'IPython'] + env = os.environ.copy() + env['IPY_TEST_SIMPLE_PROMPT'] = '1' + + + # First we fake old behavior, making sure the profile is/was actually dangerous + p_dangerous = subprocess.Popen(cmd + [f'--profile-dir={dangerous_profile_dir}'], env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out_dangerous, err_dangerouns = p_dangerous.communicate(b"exit\r") + assert dangerous_expected in out_dangerous.decode() + + # Now that we know it _would_ have been dangerous, we test it's not loaded + p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate(b"exit\r") + assert b'IPython' in out + assert dangerous_expected not in out.decode() + assert err == b'' + + + From 5a3dd92fe47af28fa7b6a6995e3f3fe6145952f7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 19 Jan 2022 14:11:27 +0100 Subject: [PATCH 0042/1752] link to gh advisory --- docs/source/whatsnew/version8.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 72167a5984c..f08ac0cc4db 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -42,6 +42,8 @@ Thus starting with this version: - Added a ``__patched_cves__`` attribute (set of strings) to IPython module that contain the list of fixed CVE. This is informational only. +Further details can be read on the `GitHub Advisory `__ + IPython 8.0 From a6a1d36300661b168da1ebff3bf52f2a0e982f26 Mon Sep 17 00:00:00 2001 From: Gal B Date: Mon, 24 Jan 2022 21:24:11 +0200 Subject: [PATCH 0043/1752] Add missing auto_match flag in shortcuts.py This commit fixes issues where IPython inserts quotes after r or R regardless of context. --- IPython/terminal/shortcuts.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 6aab3d2c568..ea204f8ddd8 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -150,7 +150,9 @@ def _(event): event.current_buffer.cursor_left() # raw string - @kb.add("(", filter=focused_insert & preceding_text(r".*(r|R)[\"'](-*)$")) + @kb.add( + "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + ) def _(event): matches = re.match( r".*(r|R)[\"'](-*)", @@ -160,7 +162,9 @@ def _(event): event.current_buffer.insert_text("()" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) - @kb.add("[", filter=focused_insert & preceding_text(r".*(r|R)[\"'](-*)$")) + @kb.add( + "[", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + ) def _(event): matches = re.match( r".*(r|R)[\"'](-*)", @@ -170,7 +174,9 @@ def _(event): event.current_buffer.insert_text("[]" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) - @kb.add("{", filter=focused_insert & preceding_text(r".*(r|R)[\"'](-*)$")) + @kb.add( + "{", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + ) def _(event): matches = re.match( r".*(r|R)[\"'](-*)", @@ -180,12 +186,12 @@ def _(event): event.current_buffer.insert_text("{}" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) - @kb.add('"', filter=focused_insert & preceding_text(r".*(r|R)$")) + @kb.add('"', filter=focused_insert & auto_match & preceding_text(r".*(r|R)$")) def _(event): event.current_buffer.insert_text('""') event.current_buffer.cursor_left() - @kb.add("'", filter=focused_insert & preceding_text(r".*(r|R)$")) + @kb.add("'", filter=focused_insert & auto_match & preceding_text(r".*(r|R)$")) def _(event): event.current_buffer.insert_text("''") event.current_buffer.cursor_left() From b3d1f6be267fd686c9483ecaffd68c20d1010bcf Mon Sep 17 00:00:00 2001 From: Gal B Date: Mon, 24 Jan 2022 21:38:44 +0200 Subject: [PATCH 0044/1752] Run linter on cve.py --- IPython/tests/cve.py | 47 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py index 026415a57a4..9e0f6df715d 100644 --- a/IPython/tests/cve.py +++ b/IPython/tests/cve.py @@ -11,46 +11,55 @@ import subprocess import time + def test_cve_2022_21699(): """ Here we test CVE-2022-21699. - We create a temporary directory, cd into it. - Make a profile file that should not be executed and start IPython in a subprocess, + We create a temporary directory, cd into it. + Make a profile file that should not be executed and start IPython in a subprocess, checking for the value. """ - dangerous_profile_dir = Path('profile_default') + dangerous_profile_dir = Path("profile_default") - dangerous_startup_dir = dangerous_profile_dir / 'startup' - dangerous_expected = 'CVE-2022-21699-'+''.join([random.choice(string.ascii_letters) for i in range(10)]) + dangerous_startup_dir = dangerous_profile_dir / "startup" + dangerous_expected = "CVE-2022-21699-" + "".join( + [random.choice(string.ascii_letters) for i in range(10)] + ) with TemporaryWorkingDirectory() as t: dangerous_startup_dir.mkdir(parents=True) - (dangerous_startup_dir/ 'foo.py').write_text(f'print("{dangerous_expected}")') + (dangerous_startup_dir / "foo.py").write_text(f'print("{dangerous_expected}")') # 1 sec to make sure FS is flushed. - #time.sleep(1) - cmd = [sys.executable,'-m', 'IPython'] + # time.sleep(1) + cmd = [sys.executable, "-m", "IPython"] env = os.environ.copy() - env['IPY_TEST_SIMPLE_PROMPT'] = '1' - + env["IPY_TEST_SIMPLE_PROMPT"] = "1" # First we fake old behavior, making sure the profile is/was actually dangerous - p_dangerous = subprocess.Popen(cmd + [f'--profile-dir={dangerous_profile_dir}'], env=env, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p_dangerous = subprocess.Popen( + cmd + [f"--profile-dir={dangerous_profile_dir}"], + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out_dangerous, err_dangerouns = p_dangerous.communicate(b"exit\r") assert dangerous_expected in out_dangerous.decode() # Now that we know it _would_ have been dangerous, we test it's not loaded - p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.Popen( + cmd, + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out, err = p.communicate(b"exit\r") - assert b'IPython' in out + assert b"IPython" in out assert dangerous_expected not in out.decode() - assert err == b'' - - - + assert err == b"" From 8b55158def61644d1a22ea077cda86cee9c50694 Mon Sep 17 00:00:00 2001 From: Jacob Chapman <7908073+chapmanjacobd@users.noreply.github.com> Date: Sat, 29 Jan 2022 09:15:44 -0600 Subject: [PATCH 0045/1752] does this fix it? --- IPython/core/ultratb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 1f08fa51ed2..146ee568e94 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -907,7 +907,7 @@ def debugger(self, force: bool = False): fix that by hand after invoking the exception handler.""" if force or self.call_pdb: - if self.pdb is None: + if self.debugger_cls: self.pdb = self.debugger_cls() # the system displayhook may have changed, restore the original # for pdb From 44397615ec814287f591c616f2a337ee9057ab8d Mon Sep 17 00:00:00 2001 From: martinRenou Date: Tue, 1 Feb 2022 09:16:30 +0100 Subject: [PATCH 0046/1752] Pin black in CI This temporarily fixes an incompatibility between black and darker --- .github/workflows/python-package.yml | 4 +-- IPython/tests/cve.py | 47 +++++++++++++++++----------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9c2ae9c45b9..663607f0246 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v2 - with: + with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -29,7 +29,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install darker + pip install darker black==21.12b0 - name: Lint with darker run: | darker -r 60625f241f298b5039cb2debc365db38aa7bb522 --check --diff . || ( diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py index 026415a57a4..9e0f6df715d 100644 --- a/IPython/tests/cve.py +++ b/IPython/tests/cve.py @@ -11,46 +11,55 @@ import subprocess import time + def test_cve_2022_21699(): """ Here we test CVE-2022-21699. - We create a temporary directory, cd into it. - Make a profile file that should not be executed and start IPython in a subprocess, + We create a temporary directory, cd into it. + Make a profile file that should not be executed and start IPython in a subprocess, checking for the value. """ - dangerous_profile_dir = Path('profile_default') + dangerous_profile_dir = Path("profile_default") - dangerous_startup_dir = dangerous_profile_dir / 'startup' - dangerous_expected = 'CVE-2022-21699-'+''.join([random.choice(string.ascii_letters) for i in range(10)]) + dangerous_startup_dir = dangerous_profile_dir / "startup" + dangerous_expected = "CVE-2022-21699-" + "".join( + [random.choice(string.ascii_letters) for i in range(10)] + ) with TemporaryWorkingDirectory() as t: dangerous_startup_dir.mkdir(parents=True) - (dangerous_startup_dir/ 'foo.py').write_text(f'print("{dangerous_expected}")') + (dangerous_startup_dir / "foo.py").write_text(f'print("{dangerous_expected}")') # 1 sec to make sure FS is flushed. - #time.sleep(1) - cmd = [sys.executable,'-m', 'IPython'] + # time.sleep(1) + cmd = [sys.executable, "-m", "IPython"] env = os.environ.copy() - env['IPY_TEST_SIMPLE_PROMPT'] = '1' - + env["IPY_TEST_SIMPLE_PROMPT"] = "1" # First we fake old behavior, making sure the profile is/was actually dangerous - p_dangerous = subprocess.Popen(cmd + [f'--profile-dir={dangerous_profile_dir}'], env=env, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p_dangerous = subprocess.Popen( + cmd + [f"--profile-dir={dangerous_profile_dir}"], + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out_dangerous, err_dangerouns = p_dangerous.communicate(b"exit\r") assert dangerous_expected in out_dangerous.decode() # Now that we know it _would_ have been dangerous, we test it's not loaded - p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.Popen( + cmd, + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out, err = p.communicate(b"exit\r") - assert b'IPython' in out + assert b"IPython" in out assert dangerous_expected not in out.decode() - assert err == b'' - - - + assert err == b"" From d55a692f46402f397ab38e6c4c9fb6423a85b54f Mon Sep 17 00:00:00 2001 From: martinRenou Date: Thu, 20 Jan 2022 10:29:42 +0100 Subject: [PATCH 0047/1752] Update sphinxify usage --- IPython/core/interactiveshell.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 0f75337f076..13d06393122 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -90,12 +90,17 @@ try: import docrepr.sphinxify as sphx - def sphinxify(doc): - with TemporaryDirectory() as dirname: - return { - 'text/html': sphx.sphinxify(doc, dirname), - 'text/plain': doc - } + def sphinxify(oinfo): + wrapped_docstring = sphx.wrap_main_docstring(oinfo) + + def sphinxify_docstring(docstring): + with TemporaryDirectory() as dirname: + return { + 'text/html': sphx.sphinxify(wrapped_docstring, dirname), + 'text/plain': docstring + } + + return sphinxify_docstring except ImportError: sphinxify = None @@ -772,7 +777,7 @@ def init_virtualenv(self): while p.is_symlink(): p = Path(os.readlink(p)) paths.append(p.resolve()) - + # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible if p_venv.parts[1] == "cygdrive": drive_name = p_venv.parts[2] @@ -1621,7 +1626,7 @@ def _inspect(self, meth, oname, namespaces=None, **kw): This function is meant to be called by pdef, pdoc & friends. """ info = self._object_find(oname, namespaces) - docformat = sphinxify if self.sphinxify_docstring else None + docformat = sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None if info.found: pmethod = getattr(self.inspector, meth) # TODO: only apply format_screen to the plain/text repr of the mime @@ -1668,7 +1673,7 @@ def object_inspect_mime(self, oname, detail_level=0, omit_sections=()): with self.builtin_trap: info = self._object_find(oname) if info.found: - docformat = sphinxify if self.sphinxify_docstring else None + docformat = sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None return self.inspector._get_info( info.obj, oname, From 9a59d483426b1e8c7719defac41bd4eee1c692d2 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Tue, 1 Feb 2022 09:04:06 +0100 Subject: [PATCH 0048/1752] Update IPython/core/interactiveshell.py Co-authored-by: CAM Gerlach --- IPython/core/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 13d06393122..79a08c9c954 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -97,7 +97,7 @@ def sphinxify_docstring(docstring): with TemporaryDirectory() as dirname: return { 'text/html': sphx.sphinxify(wrapped_docstring, dirname), - 'text/plain': docstring + 'text/plain': docstring, } return sphinxify_docstring From 81d23fcb46cb46e4fb46cf3c7b656950c65343ec Mon Sep 17 00:00:00 2001 From: martinRenou Date: Tue, 1 Feb 2022 09:04:13 +0100 Subject: [PATCH 0049/1752] Update IPython/core/interactiveshell.py Co-authored-by: CAM Gerlach --- IPython/core/interactiveshell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 79a08c9c954..857a0208f74 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1626,7 +1626,9 @@ def _inspect(self, meth, oname, namespaces=None, **kw): This function is meant to be called by pdef, pdoc & friends. """ info = self._object_find(oname, namespaces) - docformat = sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None + docformat = ( + sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None + ) if info.found: pmethod = getattr(self.inspector, meth) # TODO: only apply format_screen to the plain/text repr of the mime From 75da19abb8cef733e6f2473c81923285ce1ebb93 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Tue, 1 Feb 2022 09:04:18 +0100 Subject: [PATCH 0050/1752] Update IPython/core/interactiveshell.py Co-authored-by: CAM Gerlach --- IPython/core/interactiveshell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 857a0208f74..2d1c7107b73 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1675,7 +1675,9 @@ def object_inspect_mime(self, oname, detail_level=0, omit_sections=()): with self.builtin_trap: info = self._object_find(oname) if info.found: - docformat = sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None + docformat = ( + sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None + ) return self.inspector._get_info( info.obj, oname, From c9a8d0bff7f63b4797fd8baddf70ad4ab6a90414 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 1 Feb 2022 13:31:28 +0100 Subject: [PATCH 0051/1752] reformat --- IPython/core/interactiveshell.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 2d1c7107b73..ecb9c5f1652 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -96,8 +96,8 @@ def sphinxify(oinfo): def sphinxify_docstring(docstring): with TemporaryDirectory() as dirname: return { - 'text/html': sphx.sphinxify(wrapped_docstring, dirname), - 'text/plain': docstring, + "text/html": sphx.sphinxify(wrapped_docstring, dirname), + "text/plain": docstring, } return sphinxify_docstring @@ -1675,9 +1675,11 @@ def object_inspect_mime(self, oname, detail_level=0, omit_sections=()): with self.builtin_trap: info = self._object_find(oname) if info.found: - docformat = ( - sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None - ) + docformat = ( + sphinxify(self.object_inspect(oname)) + if self.sphinxify_docstring + else None + ) return self.inspector._get_info( info.obj, oname, From c75a242bfa6332faf34852949a9e6b44348313f7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Jan 2022 17:32:24 +0100 Subject: [PATCH 0052/1752] Readd xfail to help with debian packaging --- IPython/core/tests/test_completer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index db44d51a45d..cb5756a9f00 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -475,6 +475,7 @@ def test_completion_have_signature(self): "encoding" in c.signature ), "Signature of function was not found by completer" + @pytest.mark.xfail(reason="Known failure on jedi<=0.18.0") def test_deduplicate_completions(self): """ Test that completions are correctly deduplicated (even if ranges are not the same) From a69072c7c23a0ccbc07bd7c37b6f65eddc412562 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 1 Feb 2022 13:49:42 +0100 Subject: [PATCH 0053/1752] try to fix app-veyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index fbd8a2604f2..b1c0abe0b6c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,7 +17,7 @@ init: install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - python -m pip install --upgrade setuptools pip + - python -m pip install --upgrade setuptools 'pip<22' - pip install pytest-cov - pip install -e .[test_extra] test_script: From 23276ac4770f380ce1d5808950dd412a35594af1 Mon Sep 17 00:00:00 2001 From: gousaiyang Date: Fri, 4 Feb 2022 22:16:44 -0800 Subject: [PATCH 0054/1752] Fix EncodingWarning on Python 3.10 --- IPython/core/application.py | 2 +- IPython/core/crashhandler.py | 2 +- IPython/core/display.py | 6 +++-- IPython/core/interactiveshell.py | 8 +++---- IPython/core/magics/code.py | 8 +++---- IPython/core/magics/execution.py | 2 +- IPython/core/magics/packaging.py | 2 +- IPython/core/page.py | 6 ++--- IPython/core/tests/test_application.py | 4 ++-- IPython/core/tests/test_completer.py | 6 ++--- IPython/core/tests/test_completerlib.py | 6 ++--- IPython/core/tests/test_extension.py | 6 ++--- IPython/core/tests/test_interactiveshell.py | 6 ++--- IPython/core/tests/test_magic.py | 22 +++++++++---------- IPython/core/tests/test_profile.py | 6 ++--- IPython/core/tests/test_run.py | 12 +++++----- IPython/core/tests/test_ultratb.py | 12 +++++----- IPython/extensions/storemagic.py | 4 ++-- IPython/extensions/tests/test_autoreload.py | 4 ++-- IPython/lib/demo.py | 2 +- IPython/lib/tests/test_deepreload.py | 4 ++-- IPython/testing/tests/test_tools.py | 2 +- IPython/testing/tools.py | 2 +- IPython/tests/cve.py | 2 +- IPython/utils/io.py | 7 +++--- IPython/utils/tempdir.py | 3 ++- IPython/utils/tests/test_module_paths.py | 2 +- IPython/utils/tests/test_path.py | 14 ++++++------ IPython/utils/tests/test_pycolorize.py | 2 +- docs/autogen_config.py | 4 ++-- docs/autogen_magics.py | 2 +- docs/autogen_shortcuts.py | 2 +- docs/source/conf.py | 4 ++-- docs/sphinxext/apigen.py | 4 ++-- .../IPython Kernel/ipython-get-history.py | 2 +- setupbase.py | 4 ++-- tools/fixup_whats_new_pr.py | 4 ++-- tools/toollib.py | 2 +- 38 files changed, 98 insertions(+), 94 deletions(-) diff --git a/IPython/core/application.py b/IPython/core/application.py index 2b389a686d4..90ecf13179d 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -470,7 +470,7 @@ def stage_default_config_file(self): config_file = Path(self.profile_dir.location) / self.config_file_name if self.overwrite or not config_file.exists(): self.log.warning("Generating default config file: %r" % (config_file)) - config_file.write_text(s) + config_file.write_text(s, encoding='utf-8') @catch_config_error def initialize(self, argv=None): diff --git a/IPython/core/crashhandler.py b/IPython/core/crashhandler.py index d0d4b0d7725..3ee4879e4e8 100644 --- a/IPython/core/crashhandler.py +++ b/IPython/core/crashhandler.py @@ -185,7 +185,7 @@ def __call__(self, etype, evalue, etb): # and generate a complete report on disk try: - report = open(report_name,'w') + report = open(report_name, 'w', encoding='utf-8') except: print('Could not create crash report on disk.', file=sys.stderr) return diff --git a/IPython/core/display.py b/IPython/core/display.py index 9db75035762..608188ccece 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -349,7 +349,8 @@ def _data_and_metadata(self): def reload(self): """Reload the raw data from file or URL.""" if self.filename is not None: - with open(self.filename, self._read_flags) as f: + encoding = None if 'b' in self._read_flags else 'utf-8' + with open(self.filename, self._read_flags, encoding=encoding) as f: self.data = f.read() elif self.url is not None: # Deferred import @@ -369,7 +370,8 @@ def reload(self): if 'gzip' in response.headers['content-encoding']: import gzip from io import BytesIO - with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp: + # assume utf-8 if encoding is not specified + with gzip.open(BytesIO(data), 'rt', encoding=encoding or 'utf-8') as fp: encoding = None data = fp.read() diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index ecb9c5f1652..65c3635f2fe 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2626,7 +2626,7 @@ def safe_execfile(self, fname, *where, exit_ignore=False, raise_exceptions=False # Make sure we can open the file try: - with fname.open(): + with fname.open('rb'): pass except: warn('Could not open file <%s> for safe execution.' % fname) @@ -2684,7 +2684,7 @@ def safe_execfile_ipy(self, fname, shell_futures=False, raise_exceptions=False): # Make sure we can open the file try: - with fname.open(): + with fname.open('rb'): pass except: warn('Could not open file <%s> for safe execution.' % fname) @@ -2706,7 +2706,7 @@ def get_cells(): if cell.cell_type == 'code': yield cell.source else: - yield fname.read_text() + yield fname.read_text(encoding='utf-8') with prepended_to_syspath(dname): try: @@ -3458,7 +3458,7 @@ def mktempfile(self, data=None, prefix='ipython_edit_'): self.tempfiles.append(file_path) if data: - file_path.write_text(data) + file_path.write_text(data, encoding='utf-8') return filename def ask_yes_no(self, prompt, default=None, interrupt=None): diff --git a/IPython/core/magics/code.py b/IPython/core/magics/code.py index 3f8100ef268..6f44018ff72 100644 --- a/IPython/core/magics/code.py +++ b/IPython/core/magics/code.py @@ -538,7 +538,7 @@ def _edit_macro(self,mname,macro): self.shell.hooks.editor(filename) # and make a new macro object, to replace the old one - mvalue = Path(filename).read_text() + mvalue = Path(filename).read_text(encoding='utf-8') self.shell.user_ns[mname] = Macro(mvalue) @skip_doctest @@ -728,7 +728,7 @@ def edit(self, parameter_s='',last_call=['','']): # XXX TODO: should this be generalized for all string vars? # For now, this is special-cased to blocks created by cpaste if args.strip() == "pasted_block": - self.shell.user_ns["pasted_block"] = filepath.read_text() + self.shell.user_ns["pasted_block"] = filepath.read_text(encoding='utf-8') if 'x' in opts: # -x prevents actual execution print() @@ -738,7 +738,7 @@ def edit(self, parameter_s='',last_call=['','']): if not is_temp: self.shell.user_ns['__file__'] = filename if 'r' in opts: # Untranslated IPython code - source = filepath.read_text() + source = filepath.read_text(encoding='utf-8') self.shell.run_cell(source, store_history=False) else: self.shell.safe_execfile(filename, self.shell.user_ns, @@ -746,7 +746,7 @@ def edit(self, parameter_s='',last_call=['','']): if is_temp: try: - return filepath.read_text() + return filepath.read_text(encoding='utf-8') except IOError as msg: if Path(msg.filename) == filepath: warn('File not found. Did you forget to save?') diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index 7e0b5c397ca..a90bd4efac0 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -360,7 +360,7 @@ def _run_with_profiler(self, code, opts, namespace): if text_file: pfile = Path(text_file) pfile.touch(exist_ok=True) - pfile.write_text(output) + pfile.write_text(output, encoding='utf-8') print( f"\n*** Profile printout saved to text file {repr(text_file)}.{sys_exit}" diff --git a/IPython/core/magics/packaging.py b/IPython/core/magics/packaging.py index 60fe1ac0762..1859a942929 100644 --- a/IPython/core/magics/packaging.py +++ b/IPython/core/magics/packaging.py @@ -32,7 +32,7 @@ def _get_conda_executable(): # Otherwise, attempt to extract the executable from conda history. # This applies in any conda environment. - history = Path(sys.prefix, "conda-meta", "history").read_text() + history = Path(sys.prefix, "conda-meta", "history").read_text(encoding='utf-8') match = re.search( r"^#\s*cmd:\s*(?P.*conda)\s[create|install]", history, diff --git a/IPython/core/page.py b/IPython/core/page.py index 24770c56a3c..7ebb6ec0047 100644 --- a/IPython/core/page.py +++ b/IPython/core/page.py @@ -199,7 +199,7 @@ def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): tmppath = Path(tmpname) try: os.close(fd) - with tmppath.open("wt") as tmpfile: + with tmppath.open("wt", encoding='utf-8') as tmpfile: tmpfile.write(strng) cmd = "%s < %s" % (pager_cmd, tmppath) # tmpfile needs to be closed for windows @@ -218,7 +218,7 @@ def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): stdin=subprocess.PIPE, stderr=subprocess.DEVNULL ) - pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc) + pager = os._wrap_close(io.TextIOWrapper(proc.stdin, encoding='utf-8'), proc) try: pager_encoding = pager.encoding or sys.stdout.encoding pager.write(strng) @@ -277,7 +277,7 @@ def page_file(fname, start=0, pager_cmd=None): try: if start > 0: start -= 1 - page(open(fname).read(),start) + page(open(fname, encoding='utf-8').read(),start) except: print('Unable to show file',repr(fname)) diff --git a/IPython/core/tests/test_application.py b/IPython/core/tests/test_application.py index 891908e98e7..f4b11b5a928 100644 --- a/IPython/core/tests/test_application.py +++ b/IPython/core/tests/test_application.py @@ -34,7 +34,7 @@ def test_unicode_ipdir(): ipdir = tempfile.mkdtemp(suffix=u"€") # Create the config file, so it tries to load it. - with open(os.path.join(ipdir, 'ipython_config.py'), "w") as f: + with open(os.path.join(ipdir, 'ipython_config.py'), "w", encoding='utf-8') as f: pass old_ipdir1 = os.environ.pop("IPYTHONDIR", None) @@ -59,7 +59,7 @@ class TestApp(BaseIPythonApplication): test = Unicode().tag(config=True) # Create the config file, so it tries to load it. - with open(os.path.join(td, 'ipython_config.py'), "w") as f: + with open(os.path.join(td, 'ipython_config.py'), "w", encoding='utf-8') as f: f.write("c.TestApp.test = 'config file'") app = TestApp() diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index cb5756a9f00..7ce160a5aec 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -346,7 +346,7 @@ def test_abspath_file_completions(self): suffixes = ["1", "2"] names = [prefix + s for s in suffixes] for n in names: - open(n, "w").close() + open(n, "w", encoding='utf-8').close() # Check simple completion c = ip.complete(prefix)[1] @@ -365,7 +365,7 @@ def test_local_file_completions(self): suffixes = ["1", "2"] names = [prefix + s for s in suffixes] for n in names: - open(n, "w").close() + open(n, "w", encoding='utf-8').close() # Check simple completion c = ip.complete(prefix)[1] @@ -381,7 +381,7 @@ def test_quoted_file_completions(self): ip = get_ipython() with TemporaryWorkingDirectory(): name = "foo'bar" - open(name, "w").close() + open(name, "w", encoding='utf-8').close() # Don't escape Windows escaped = name if sys.platform == "win32" else "foo\\'bar" diff --git a/IPython/core/tests/test_completerlib.py b/IPython/core/tests/test_completerlib.py index b5508447895..23e24e6bfd6 100644 --- a/IPython/core/tests/test_completerlib.py +++ b/IPython/core/tests/test_completerlib.py @@ -33,7 +33,7 @@ class Test_magic_run_completer(unittest.TestCase): def setUp(self): self.BASETESTDIR = tempfile.mkdtemp() for fil in self.files: - with open(join(self.BASETESTDIR, fil), "w") as sfile: + with open(join(self.BASETESTDIR, fil), "w", encoding='utf-8') as sfile: sfile.write("pass\n") for d in self.dirs: os.mkdir(join(self.BASETESTDIR, d)) @@ -89,7 +89,7 @@ class Test_magic_run_completer_nonascii(unittest.TestCase): def setUp(self): self.BASETESTDIR = tempfile.mkdtemp() for fil in [u"aaø.py", u"a.py", u"b.py"]: - with open(join(self.BASETESTDIR, fil), "w") as sfile: + with open(join(self.BASETESTDIR, fil), "w", encoding='utf-8') as sfile: sfile.write("pass\n") self.oldpath = os.getcwd() os.chdir(self.BASETESTDIR) @@ -134,7 +134,7 @@ def test_import_invalid_module(): sys.path.insert( 0, tmpdir ) for name in invalid_module_names | valid_module_names: filename = os.path.join(tmpdir, name + '.py') - open(filename, 'w').close() + open(filename, 'w', encoding='utf-8').close() s = set( module_completion('import foo') ) intersection = s.intersection(invalid_module_names) diff --git a/IPython/core/tests/test_extension.py b/IPython/core/tests/test_extension.py index 51db4b6258c..958fd2b4a2c 100644 --- a/IPython/core/tests/test_extension.py +++ b/IPython/core/tests/test_extension.py @@ -27,11 +27,11 @@ def test_extension_loading(): em = get_ipython().extension_manager with TemporaryDirectory() as td: ext1 = os.path.join(td, 'ext1.py') - with open(ext1, 'w') as f: + with open(ext1, 'w', encoding='utf-8') as f: f.write(ext1_content) ext2 = os.path.join(td, 'ext2.py') - with open(ext2, 'w') as f: + with open(ext2, 'w', encoding='utf-8') as f: f.write(ext2_content) with prepended_to_syspath(td): @@ -77,7 +77,7 @@ def test_extension_builtins(): em = get_ipython().extension_manager with TemporaryDirectory() as td: ext3 = os.path.join(td, 'ext3.py') - with open(ext3, 'w') as f: + with open(ext3, 'w', encoding='utf-8') as f: f.write(ext3_content) assert 'ext3' not in em.loaded diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 41f5a358ea1..e86c4fcd560 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -485,11 +485,11 @@ def mock_print_func(value, sep=" ", end="\n", file=sys.stdout, flush=False): def test_mktempfile(self): filename = ip.mktempfile() # Check that we can open the file again on Windows - with open(filename, 'w') as f: + with open(filename, 'w', encoding='utf-8') as f: f.write('abc') filename = ip.mktempfile(data='blah') - with open(filename, 'r') as f: + with open(filename, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'blah') def test_new_main_mod(self): @@ -545,7 +545,7 @@ def setUp(self): self.BASETESTDIR = tempfile.mkdtemp() self.TESTDIR = join(self.BASETESTDIR, u"åäö") os.mkdir(self.TESTDIR) - with open(join(self.TESTDIR, u"åäötestscript.py"), "w") as sfile: + with open(join(self.TESTDIR, u"åäötestscript.py"), "w", encoding='utf-8') as sfile: sfile.write("pass\n") self.oldpath = os.getcwd() os.chdir(self.TESTDIR) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 5294f82c51c..f645daf91b0 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -866,7 +866,7 @@ def test_file(): 'line1', 'line2', ])) - s = Path(fname).read_text() + s = Path(fname).read_text(encoding='utf-8') assert "line1\n" in s assert "line2" in s @@ -881,7 +881,7 @@ def test_file_single_quote(): 'line1', 'line2', ])) - s = Path(fname).read_text() + s = Path(fname).read_text(encoding='utf-8') assert "line1\n" in s assert "line2" in s @@ -896,7 +896,7 @@ def test_file_double_quote(): 'line1', 'line2', ])) - s = Path(fname).read_text() + s = Path(fname).read_text(encoding='utf-8') assert "line1\n" in s assert "line2" in s @@ -911,7 +911,7 @@ def test_file_var_expand(): 'line1', 'line2', ])) - s = Path(fname).read_text() + s = Path(fname).read_text(encoding='utf-8') assert "line1\n" in s assert "line2" in s @@ -944,7 +944,7 @@ def test_file_amend(): 'line3', 'line4', ])) - s = Path(fname).read_text() + s = Path(fname).read_text(encoding='utf-8') assert "line1\n" in s assert "line3\n" in s @@ -958,7 +958,7 @@ def test_file_spaces(): 'line1', 'line2', ])) - s = Path(fname).read_text() + s = Path(fname).read_text(encoding='utf-8') assert "line1\n" in s assert "line2" in s @@ -1154,11 +1154,11 @@ def test_save(): with TemporaryDirectory() as tmpdir: file = os.path.join(tmpdir, "testsave.py") ip.run_line_magic("save", "%s 1-10" % file) - content = Path(file).read_text() + content = Path(file).read_text(encoding='utf-8') assert content.count(cmds[0]) == 1 assert "coding: utf-8" in content ip.run_line_magic("save", "-a %s 1-10" % file) - content = Path(file).read_text() + content = Path(file).read_text(encoding='utf-8') assert content.count(cmds[0]) == 2 assert "coding: utf-8" in content @@ -1173,7 +1173,7 @@ def test_save_with_no_args(): with TemporaryDirectory() as tmpdir: path = os.path.join(tmpdir, "testsave.py") ip.run_line_magic("save", path) - content = Path(path).read_text() + content = Path(path).read_text(encoding='utf-8') expected_content = dedent( """\ # coding: utf-8 @@ -1336,7 +1336,7 @@ def test_run_module_from_import_hook(): "Test that a module can be loaded via an import hook" with TemporaryDirectory() as tmpdir: fullpath = os.path.join(tmpdir, 'my_tmp.py') - Path(fullpath).write_text(TEST_MODULE) + Path(fullpath).write_text(TEST_MODULE, encoding='utf-8') import importlib.abc import importlib.util @@ -1352,7 +1352,7 @@ def get_filename(self, fullname): def get_data(self, path): assert Path(path).samefile(fullpath) - return Path(fullpath).read_text() + return Path(fullpath).read_text(encoding='utf-8') sys.meta_path.insert(0, MyTempImporter()) diff --git a/IPython/core/tests/test_profile.py b/IPython/core/tests/test_profile.py index 8dd58cc67f4..22966e48ac3 100644 --- a/IPython/core/tests/test_profile.py +++ b/IPython/core/tests/test_profile.py @@ -84,10 +84,10 @@ def tearDown(self): def init(self, startup_file, startup, test): # write startup python file - with open(Path(self.pd.startup_dir) / startup_file, "w") as f: + with open(Path(self.pd.startup_dir) / startup_file, "w", encoding='utf-8') as f: f.write(startup) # write simple test file, to check that the startup file was run - with open(self.fname, 'w') as f: + with open(self.fname, 'w', encoding='utf-8') as f: f.write(test) def validate(self, output): @@ -111,7 +111,7 @@ def test_list_profiles_in(): if dec.unicode_paths: Path(td / u"profile_ünicode").mkdir(parents=True) - with open(td / "profile_file", "w") as f: + with open(td / "profile_file", "w", encoding='utf-8') as f: f.write("I am not a profile directory") profiles = list_profiles_in(td) diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py index 0f73a781e3e..69576577e9e 100644 --- a/IPython/core/tests/test_run.py +++ b/IPython/core/tests/test_run.py @@ -64,7 +64,7 @@ def doctest_run_builtins(): In [3]: fname = tempfile.mkstemp('.py')[1] - In [3]: f = open(fname,'w') + In [3]: f = open(fname, 'w', encoding='utf-8') In [4]: dummy= f.write('pass\n') @@ -443,7 +443,7 @@ def writefile(self, name, content): d = os.path.dirname(path) if not os.path.isdir(d): os.makedirs(d) - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.write(textwrap.dedent(content)) def setUp(self): @@ -527,7 +527,7 @@ def test_module_options_with_separator(self): def test_run__name__(): with TemporaryDirectory() as td: path = pjoin(td, 'foo.py') - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.write("q = __name__") _ip.user_ns.pop("q", None) @@ -548,7 +548,7 @@ def test_run_tb(): """Test traceback offset in %run""" with TemporaryDirectory() as td: path = pjoin(td, 'foo.py') - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.write('\n'.join([ "def foo():", " return bar()", @@ -578,7 +578,7 @@ def test_multiprocessing_run(): sys.modules['__mp_main__'] = None try: path = pjoin(td, 'test.py') - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.write("import multiprocessing\nprint('hoy')") with capture_output() as io: _ip.run_line_magic('run', path) @@ -598,7 +598,7 @@ def test_script_tb(): """Test traceback offset in `ipython script.py`""" with TemporaryDirectory() as td: path = pjoin(td, 'foo.py') - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: f.write('\n'.join([ "def foo():", " return bar()", diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index 423998019b5..6e855d10186 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -58,7 +58,7 @@ def test_changing_py_file(self): """ with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") - with open(fname, "w") as f: + with open(fname, "w", encoding='utf-8') as f: f.write(file_1) with prepended_to_syspath(td): @@ -68,7 +68,7 @@ def test_changing_py_file(self): ip.run_cell("foo.f()") # Make the file shorter, so the line of the error is missing. - with open(fname, "w") as f: + with open(fname, "w", encoding='utf-8') as f: f.write(file_2) # For some reason, this was failing on the *second* call after @@ -92,7 +92,7 @@ def test_nonascii_path(self): # Non-ascii directory name as well. with TemporaryDirectory(suffix=u'é') as td: fname = os.path.join(td, u"fooé.py") - with open(fname, "w") as f: + with open(fname, "w", encoding='utf-8') as f: f.write(file_1) with prepended_to_syspath(td): @@ -172,7 +172,7 @@ def test_indentationerror_shows_line(self): with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") - with open(fname, "w") as f: + with open(fname, "w", encoding='utf-8') as f: f.write(indentationerror_file) with tt.AssertPrints("IndentationError"): @@ -221,14 +221,14 @@ def bar(): def test_changing_py_file(self): with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") - with open(fname, 'w') as f: + with open(fname, 'w', encoding='utf-8') as f: f.write(se_file_1) with tt.AssertPrints(["7/", "SyntaxError"]): ip.magic("run " + fname) # Modify the file - with open(fname, 'w') as f: + with open(fname, 'w', encoding='utf-8') as f: f.write(se_file_2) # The SyntaxError should point to the correct line diff --git a/IPython/extensions/storemagic.py b/IPython/extensions/storemagic.py index b4a8cf82442..ff124ee38bd 100644 --- a/IPython/extensions/storemagic.py +++ b/IPython/extensions/storemagic.py @@ -179,9 +179,9 @@ def store(self, parameter_s=''): if len(args) > 1 and args[1].startswith('>'): fnam = os.path.expanduser(args[1].lstrip('>').lstrip()) if args[1].startswith('>>'): - fil = open(fnam, 'a') + fil = open(fnam, 'a', encoding='utf-8') else: - fil = open(fnam, 'w') + fil = open(fnam, 'w', encoding='utf-8') with fil: obj = ip.ev(args[0]) print("Writing '%s' (%s) to file '%s'." % (args[0], diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 65eb4a7b348..63bae802524 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -119,13 +119,13 @@ def write_file(self, filename, content): time.sleep(1.05) # Write - with open(filename, "w") as f: + with open(filename, "w", encoding='utf-8') as f: f.write(content) def new_module(self, code): code = textwrap.dedent(code) mod_name, mod_fn = self.get_module() - with open(mod_fn, "w") as f: + with open(mod_fn, "w", encoding='utf-8') as f: f.write(code) return mod_name, mod_fn diff --git a/IPython/lib/demo.py b/IPython/lib/demo.py index 14af5161cc8..0c96de6e72d 100644 --- a/IPython/lib/demo.py +++ b/IPython/lib/demo.py @@ -405,7 +405,7 @@ def edit(self,index=None): filename = self.shell.mktempfile(self.src_blocks[index]) self.shell.hooks.editor(filename, 1) - with open(Path(filename), "r") as f: + with open(Path(filename), "r", encoding='utf-8') as f: new_block = f.read() # update the source and colored block self.src_blocks[index] = new_block diff --git a/IPython/lib/tests/test_deepreload.py b/IPython/lib/tests/test_deepreload.py index 9759a7f69f7..f86e1027f98 100644 --- a/IPython/lib/tests/test_deepreload.py +++ b/IPython/lib/tests/test_deepreload.py @@ -19,9 +19,9 @@ def test_deepreload(): with TemporaryDirectory() as tmpdir: with prepended_to_syspath(tmpdir): tmpdirpath = Path(tmpdir) - with open(tmpdirpath / "A.py", "w") as f: + with open(tmpdirpath / "A.py", "w", encoding='utf-8') as f: f.write("class Object:\n pass\nok = True\n") - with open(tmpdirpath / "B.py", "w") as f: + with open(tmpdirpath / "B.py", "w", encoding='utf-8') as f: f.write("import A\nassert A.ok, 'we are fine'\n") import A import B diff --git a/IPython/testing/tests/test_tools.py b/IPython/testing/tests/test_tools.py index f9f7e716cbd..0e66ca39a99 100644 --- a/IPython/testing/tests/test_tools.py +++ b/IPython/testing/tests/test_tools.py @@ -62,7 +62,7 @@ def test_temp_pyfile(): src = 'pass\n' fname = tt.temp_pyfile(src) assert os.path.isfile(fname) - with open(fname) as fh2: + with open(fname, encoding='utf-8') as fh2: src2 = fh2.read() assert src2 == src diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py index 0d52d88d2bf..fafb73fb8f7 100644 --- a/IPython/testing/tools.py +++ b/IPython/testing/tools.py @@ -428,7 +428,7 @@ def mute_warn(): def make_tempfile(name): """ Create an empty, named, temporary file for the duration of the context. """ - open(name, 'w').close() + open(name, 'w', encoding='utf-8').close() try: yield finally: diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py index 9e0f6df715d..aba6e20fa79 100644 --- a/IPython/tests/cve.py +++ b/IPython/tests/cve.py @@ -33,7 +33,7 @@ def test_cve_2022_21699(): with TemporaryWorkingDirectory() as t: dangerous_startup_dir.mkdir(parents=True) - (dangerous_startup_dir / "foo.py").write_text(f'print("{dangerous_expected}")') + (dangerous_startup_dir / "foo.py").write_text(f'print("{dangerous_expected}")', encoding='utf-8') # 1 sec to make sure FS is flushed. # time.sleep(1) cmd = [sys.executable, "-m", "IPython"] diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 69e4d4e0cdb..ef1be80988f 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -20,7 +20,7 @@ from .capture import CapturedIO, capture_output # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr -devnull = open(os.devnull, 'w') +devnull = open(os.devnull, 'w', encoding='utf-8') atexit.register(devnull.close) @@ -52,7 +52,8 @@ def __init__(self, file_or_name, mode="w", channel='stdout'): if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'): self.file = file_or_name else: - self.file = open(file_or_name, mode) + encoding = None if 'b' in mode else 'utf-8' + self.file = open(file_or_name, mode, encoding=encoding) self.channel = channel self.ostream = getattr(sys, channel) setattr(sys, channel, self) @@ -131,7 +132,7 @@ def temp_pyfile(src, ext='.py'): It is the caller's responsibility to close the open file and unlink it. """ fname = tempfile.mkstemp(ext)[1] - with open(Path(fname), "w") as f: + with open(Path(fname), "w", encoding='utf-8') as f: f.write(src) f.flush() return fname diff --git a/IPython/utils/tempdir.py b/IPython/utils/tempdir.py index bbfffe9c923..0729a8417dc 100644 --- a/IPython/utils/tempdir.py +++ b/IPython/utils/tempdir.py @@ -24,7 +24,8 @@ def __init__(self, filename, mode='w+b', bufsize=-1, **kwds): """ self._tmpdir = TemporaryDirectory(**kwds) path = Path(self._tmpdir.name) / filename - self.file = open(path, mode, bufsize) + encoding = None if 'b' in mode else 'utf-8' + self.file = open(path, mode, bufsize, encoding=encoding) def cleanup(self): self.file.close() diff --git a/IPython/utils/tests/test_module_paths.py b/IPython/utils/tests/test_module_paths.py index 12679990c0c..2abf889514c 100644 --- a/IPython/utils/tests/test_module_paths.py +++ b/IPython/utils/tests/test_module_paths.py @@ -32,7 +32,7 @@ old_syspath = sys.path def make_empty_file(fname): - open(fname, 'w').close() + open(fname, 'w', encoding='utf-8').close() def setup_module(): diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index b27e4355383..f92e6ae1f6c 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -295,7 +295,7 @@ def test_not_writable_ipdir(self): ipdir = os.path.join(tmpdir, '.ipython') os.mkdir(ipdir, 0o555) try: - open(os.path.join(ipdir, "_foo_"), 'w').close() + open(os.path.join(ipdir, "_foo_"), 'w', encoding='utf-8').close() except IOError: pass else: @@ -352,7 +352,7 @@ def setUpClass(cls): with cls.in_tempdir(): # Create empty files for fname in cls.filenames: - open(os.path.join(td, fname), 'w').close() + open(os.path.join(td, fname), 'w', encoding='utf-8').close() @classmethod def tearDownClass(cls): @@ -428,7 +428,7 @@ def test_ensure_dir_exists(): assert os.path.isdir(d) path.ensure_dir_exists(d) # no-op f = os.path.join(td, 'ƒile') - open(f, 'w').close() # touch + open(f, 'w', encoding='utf-8').close() # touch with pytest.raises(IOError): path.ensure_dir_exists(f) @@ -436,7 +436,7 @@ class TestLinkOrCopy(unittest.TestCase): def setUp(self): self.tempdir = TemporaryDirectory() self.src = self.dst("src") - with open(self.src, "w") as f: + with open(self.src, "w", encoding='utf-8') as f: f.write("Hello, world!") def tearDown(self): @@ -456,8 +456,8 @@ def assert_inode_equal(self, a, b): ), "%r and %r do not reference the same indoes" % (a, b) def assert_content_equal(self, a, b): - with open(a) as a_f: - with open(b) as b_f: + with open(a, 'rb') as a_f: + with open(b, 'rb') as b_f: assert a_f.read() == b_f.read() @skip_win32 @@ -477,7 +477,7 @@ def test_link_into_dir(self): @skip_win32 def test_target_exists(self): dst = self.dst("target") - open(dst, "w").close() + open(dst, "w", encoding='utf-8').close() path.link_or_copy(self.src, dst) self.assert_inode_equal(self.src, dst) diff --git a/IPython/utils/tests/test_pycolorize.py b/IPython/utils/tests/test_pycolorize.py index 05d51b6e44d..986b9178800 100644 --- a/IPython/utils/tests/test_pycolorize.py +++ b/IPython/utils/tests/test_pycolorize.py @@ -39,7 +39,7 @@ def function(arg, *args, kwarg=True, **kwargs): pass is True False == None - with io.open(ru'unicode'): + with io.open(ru'unicode', encoding='utf-8'): raise ValueError("\n escape \r sequence") print("wěird ünicoðe") diff --git a/docs/autogen_config.py b/docs/autogen_config.py index 98d13d3b0cb..56733f3b422 100755 --- a/docs/autogen_config.py +++ b/docs/autogen_config.py @@ -102,7 +102,7 @@ def reverse_aliases(app): def write_doc(name, title, app, preamble=None): trait_aliases = reverse_aliases(app) filename = options / (name + ".rst") - with open(filename, "w") as f: + with open(filename, "w", encoding='utf-8') as f: f.write(title + "\n") f.write(("=" * len(title)) + "\n") f.write("\n") @@ -117,7 +117,7 @@ def write_doc(name, title, app, preamble=None): if __name__ == '__main__': # Touch this file for the make target - Path(generated).write_text("") + Path(generated).write_text("", encoding='utf-8') write_doc('terminal', 'Terminal IPython options', TerminalIPythonApp()) write_doc('kernel', 'IPython kernel options', IPKernelApp(), diff --git a/docs/autogen_magics.py b/docs/autogen_magics.py index 1f8a47ef506..bfeb6aa7733 100644 --- a/docs/autogen_magics.py +++ b/docs/autogen_magics.py @@ -63,4 +63,4 @@ def sortkey(s): return s[0].lower() src_path = Path(__file__).parent dest = src_path.joinpath("source", "interactive", "magics-generated.txt") -dest.write_text("\n".join(output)) +dest.write_text("\n".join(output), encoding='utf-8') diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index 802fa03e100..7ee0f910768 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -89,6 +89,6 @@ def sort_key(item): (single_filter, "single_filtered"), (multi_filter, "multi_filtered"), ]: - with (dest / "{}.csv".format(output_filename)).open("w") as csv: + with (dest / "{}.csv".format(output_filename)).open("w", encoding='utf-8') as csv: for (shortcut, flt), v in sorted(filters.items(), key=sort_key): csv.write(":kbd:`{}`\t{}\t{}\n".format(shortcut, flt, v)) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2312cc248b0..ba9aa1dbc88 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,7 @@ for name in ("config", "api", "magics", "shortcuts"): fname = Path("autogen_{}.py".format(name)) fpath = (Path(__file__).parent).joinpath("..", fname) - with open(fpath) as f: + with open(fpath, encoding='utf-8') as f: exec(compile(f.read(), fname, 'exec'), { '__file__': fpath, '__name__': '__main__', @@ -45,7 +45,7 @@ # We load the ipython release info into a dict by explicit execution iprelease = {} -exec(compile(open('../../IPython/core/release.py').read(), '../../IPython/core/release.py', 'exec'),iprelease) +exec(compile(open('../../IPython/core/release.py', encoding='utf-8').read(), '../../IPython/core/release.py', 'exec'),iprelease) # General configuration # --------------------- diff --git a/docs/sphinxext/apigen.py b/docs/sphinxext/apigen.py index 5d352c7a22f..71a384b7d2a 100644 --- a/docs/sphinxext/apigen.py +++ b/docs/sphinxext/apigen.py @@ -392,7 +392,7 @@ def write_modules_api(self, modules,outdir): # write out to file outfile = os.path.join(outdir, m + self.rst_extension) - with open(outfile, 'wt') as fileobj: + with open(outfile, 'wt', encoding='utf-8') as fileobj: fileobj.write(api_str) written_modules.append(m) self.written_modules = written_modules @@ -444,7 +444,7 @@ def write_index(self, outdir, path='gen.rst', relative_to=None): relpath = outdir.replace(relative_to + os.path.sep, '') else: relpath = outdir - with open(path,'wt') as idx: + with open(path,'wt', encoding='utf-8') as idx: w = idx.write w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') w('.. autosummary::\n' diff --git a/examples/IPython Kernel/ipython-get-history.py b/examples/IPython Kernel/ipython-get-history.py index a2d2ab7b0ab..fe38081fbad 100755 --- a/examples/IPython Kernel/ipython-get-history.py +++ b/examples/IPython Kernel/ipython-get-history.py @@ -24,7 +24,7 @@ session_number = int(sys.argv[1]) if len(sys.argv) > 2: filepath = Path(sys.argv[2]) - dest = open(filepath, "w") + dest = open(filepath, "w", encoding='utf-8') raw = not filepath.name.endswith(".py") else: dest = sys.stdout diff --git a/setupbase.py b/setupbase.py index ecea9e788cc..788ce2491fb 100644 --- a/setupbase.py +++ b/setupbase.py @@ -36,7 +36,7 @@ def execfile(fname, globs, locs=None): locs = locs or globs - with open(fname) as f: + with open(fname, encoding='utf-8') as f: exec(compile(f.read(), fname, "exec"), globs, locs) # A little utility we'll need below, since glob() does NOT allow you to do @@ -336,7 +336,7 @@ def _record_commit(self, base_dir): os.remove(out_pth) except (IOError, OSError): pass - with open(out_pth, 'w') as out_file: + with open(out_pth, 'w', encoding='utf-8') as out_file: out_file.writelines([ '# GENERATED BY setup.py\n', 'commit = u"%s"\n' % repo_commit, diff --git a/tools/fixup_whats_new_pr.py b/tools/fixup_whats_new_pr.py index a2741ef9ebf..397c0bf38e3 100644 --- a/tools/fixup_whats_new_pr.py +++ b/tools/fixup_whats_new_pr.py @@ -22,14 +22,14 @@ def main(): print("Adding pseudo-title to:", filepath.name) title = filepath.name[:-4].split("/")[-1].replace("-", " ").capitalize() - data = filepath.read_text() + data = filepath.read_text(encoding='utf-8') try: if data and data.splitlines()[1].startswith('='): continue except IndexError: pass - with filepath.open("w") as f: + with filepath.open("w", encoding='utf-8') as f: f.write(title + "\n") f.write("=" * len(title) + "\n\n") f.write(data) diff --git a/tools/toollib.py b/tools/toollib.py index 90727f0acda..c77bbbc6853 100644 --- a/tools/toollib.py +++ b/tools/toollib.py @@ -45,4 +45,4 @@ def get_ipdir(): def execfile(fname, globs, locs=None): locs = locs or globs - exec(compile(open(fname).read(), fname, "exec"), globs, locs) + exec(compile(open(fname, encoding='utf-8').read(), fname, "exec"), globs, locs) From 1a9d9554bcee466394990535e190d55008904df8 Mon Sep 17 00:00:00 2001 From: gousaiyang Date: Fri, 4 Feb 2022 22:52:25 -0800 Subject: [PATCH 0055/1752] Format code --- IPython/core/application.py | 2 +- IPython/core/crashhandler.py | 2 +- IPython/core/display.py | 7 +- IPython/core/interactiveshell.py | 8 +- IPython/core/magics/code.py | 12 +- IPython/core/magics/execution.py | 2 +- IPython/core/magics/packaging.py | 2 +- IPython/core/page.py | 19 +-- IPython/core/tests/test_application.py | 4 +- IPython/core/tests/test_completer.py | 6 +- IPython/core/tests/test_completerlib.py | 8 +- IPython/core/tests/test_extension.py | 14 +- IPython/core/tests/test_interactiveshell.py | 14 +- IPython/core/tests/test_magic.py | 132 ++++++++++++------ IPython/core/tests/test_profile.py | 6 +- IPython/core/tests/test_run.py | 54 ++++--- IPython/core/tests/test_ultratb.py | 12 +- IPython/extensions/storemagic.py | 10 +- IPython/extensions/tests/test_autoreload.py | 4 +- IPython/lib/demo.py | 2 +- IPython/lib/tests/test_deepreload.py | 4 +- IPython/testing/tests/test_tools.py | 2 +- IPython/testing/tools.py | 5 +- IPython/tests/cve.py | 4 +- IPython/utils/io.py | 6 +- IPython/utils/tempdir.py | 2 +- IPython/utils/tests/test_module_paths.py | 2 +- IPython/utils/tests/test_path.py | 18 +-- docs/autogen_config.py | 4 +- docs/autogen_magics.py | 2 +- docs/autogen_shortcuts.py | 4 +- docs/source/conf.py | 22 ++- docs/sphinxext/apigen.py | 7 +- .../IPython Kernel/ipython-get-history.py | 2 +- setupbase.py | 15 +- tools/fixup_whats_new_pr.py | 4 +- tools/toollib.py | 2 +- 37 files changed, 249 insertions(+), 176 deletions(-) diff --git a/IPython/core/application.py b/IPython/core/application.py index 90ecf13179d..0cdea5c69b8 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -470,7 +470,7 @@ def stage_default_config_file(self): config_file = Path(self.profile_dir.location) / self.config_file_name if self.overwrite or not config_file.exists(): self.log.warning("Generating default config file: %r" % (config_file)) - config_file.write_text(s, encoding='utf-8') + config_file.write_text(s, encoding="utf-8") @catch_config_error def initialize(self, argv=None): diff --git a/IPython/core/crashhandler.py b/IPython/core/crashhandler.py index 3ee4879e4e8..4af39361e80 100644 --- a/IPython/core/crashhandler.py +++ b/IPython/core/crashhandler.py @@ -185,7 +185,7 @@ def __call__(self, etype, evalue, etb): # and generate a complete report on disk try: - report = open(report_name, 'w', encoding='utf-8') + report = open(report_name, "w", encoding="utf-8") except: print('Could not create crash report on disk.', file=sys.stderr) return diff --git a/IPython/core/display.py b/IPython/core/display.py index 608188ccece..d36a176c3bf 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -349,7 +349,7 @@ def _data_and_metadata(self): def reload(self): """Reload the raw data from file or URL.""" if self.filename is not None: - encoding = None if 'b' in self._read_flags else 'utf-8' + encoding = None if "b" in self._read_flags else "utf-8" with open(self.filename, self._read_flags, encoding=encoding) as f: self.data = f.read() elif self.url is not None: @@ -370,8 +370,11 @@ def reload(self): if 'gzip' in response.headers['content-encoding']: import gzip from io import BytesIO + # assume utf-8 if encoding is not specified - with gzip.open(BytesIO(data), 'rt', encoding=encoding or 'utf-8') as fp: + with gzip.open( + BytesIO(data), "rt", encoding=encoding or "utf-8" + ) as fp: encoding = None data = fp.read() diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 65c3635f2fe..4f3105a2ebb 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2626,7 +2626,7 @@ def safe_execfile(self, fname, *where, exit_ignore=False, raise_exceptions=False # Make sure we can open the file try: - with fname.open('rb'): + with fname.open("rb"): pass except: warn('Could not open file <%s> for safe execution.' % fname) @@ -2684,7 +2684,7 @@ def safe_execfile_ipy(self, fname, shell_futures=False, raise_exceptions=False): # Make sure we can open the file try: - with fname.open('rb'): + with fname.open("rb"): pass except: warn('Could not open file <%s> for safe execution.' % fname) @@ -2706,7 +2706,7 @@ def get_cells(): if cell.cell_type == 'code': yield cell.source else: - yield fname.read_text(encoding='utf-8') + yield fname.read_text(encoding="utf-8") with prepended_to_syspath(dname): try: @@ -3458,7 +3458,7 @@ def mktempfile(self, data=None, prefix='ipython_edit_'): self.tempfiles.append(file_path) if data: - file_path.write_text(data, encoding='utf-8') + file_path.write_text(data, encoding="utf-8") return filename def ask_yes_no(self, prompt, default=None, interrupt=None): diff --git a/IPython/core/magics/code.py b/IPython/core/magics/code.py index 6f44018ff72..65ba52b8bbf 100644 --- a/IPython/core/magics/code.py +++ b/IPython/core/magics/code.py @@ -538,7 +538,7 @@ def _edit_macro(self,mname,macro): self.shell.hooks.editor(filename) # and make a new macro object, to replace the old one - mvalue = Path(filename).read_text(encoding='utf-8') + mvalue = Path(filename).read_text(encoding="utf-8") self.shell.user_ns[mname] = Macro(mvalue) @skip_doctest @@ -728,7 +728,7 @@ def edit(self, parameter_s='',last_call=['','']): # XXX TODO: should this be generalized for all string vars? # For now, this is special-cased to blocks created by cpaste if args.strip() == "pasted_block": - self.shell.user_ns["pasted_block"] = filepath.read_text(encoding='utf-8') + self.shell.user_ns["pasted_block"] = filepath.read_text(encoding="utf-8") if 'x' in opts: # -x prevents actual execution print() @@ -736,9 +736,9 @@ def edit(self, parameter_s='',last_call=['','']): print('done. Executing edited code...') with preserve_keys(self.shell.user_ns, '__file__'): if not is_temp: - self.shell.user_ns['__file__'] = filename - if 'r' in opts: # Untranslated IPython code - source = filepath.read_text(encoding='utf-8') + self.shell.user_ns["__file__"] = filename + if "r" in opts: # Untranslated IPython code + source = filepath.read_text(encoding="utf-8") self.shell.run_cell(source, store_history=False) else: self.shell.safe_execfile(filename, self.shell.user_ns, @@ -746,7 +746,7 @@ def edit(self, parameter_s='',last_call=['','']): if is_temp: try: - return filepath.read_text(encoding='utf-8') + return filepath.read_text(encoding="utf-8") except IOError as msg: if Path(msg.filename) == filepath: warn('File not found. Did you forget to save?') diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index a90bd4efac0..371da5b1f98 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -360,7 +360,7 @@ def _run_with_profiler(self, code, opts, namespace): if text_file: pfile = Path(text_file) pfile.touch(exist_ok=True) - pfile.write_text(output, encoding='utf-8') + pfile.write_text(output, encoding="utf-8") print( f"\n*** Profile printout saved to text file {repr(text_file)}.{sys_exit}" diff --git a/IPython/core/magics/packaging.py b/IPython/core/magics/packaging.py index 1859a942929..2f7652c169b 100644 --- a/IPython/core/magics/packaging.py +++ b/IPython/core/magics/packaging.py @@ -32,7 +32,7 @@ def _get_conda_executable(): # Otherwise, attempt to extract the executable from conda history. # This applies in any conda environment. - history = Path(sys.prefix, "conda-meta", "history").read_text(encoding='utf-8') + history = Path(sys.prefix, "conda-meta", "history").read_text(encoding="utf-8") match = re.search( r"^#\s*cmd:\s*(?P.*conda)\s[create|install]", history, diff --git a/IPython/core/page.py b/IPython/core/page.py index 7ebb6ec0047..d3e6a9eef50 100644 --- a/IPython/core/page.py +++ b/IPython/core/page.py @@ -199,7 +199,7 @@ def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): tmppath = Path(tmpname) try: os.close(fd) - with tmppath.open("wt", encoding='utf-8') as tmpfile: + with tmppath.open("wt", encoding="utf-8") as tmpfile: tmpfile.write(strng) cmd = "%s < %s" % (pager_cmd, tmppath) # tmpfile needs to be closed for windows @@ -213,12 +213,15 @@ def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): try: retval = None # Emulate os.popen, but redirect stderr - proc = subprocess.Popen(pager_cmd, - shell=True, - stdin=subprocess.PIPE, - stderr=subprocess.DEVNULL - ) - pager = os._wrap_close(io.TextIOWrapper(proc.stdin, encoding='utf-8'), proc) + proc = subprocess.Popen( + pager_cmd, + shell=True, + stdin=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + pager = os._wrap_close( + io.TextIOWrapper(proc.stdin, encoding="utf-8"), proc + ) try: pager_encoding = pager.encoding or sys.stdout.encoding pager.write(strng) @@ -277,7 +280,7 @@ def page_file(fname, start=0, pager_cmd=None): try: if start > 0: start -= 1 - page(open(fname, encoding='utf-8').read(),start) + page(open(fname, encoding="utf-8").read(), start) except: print('Unable to show file',repr(fname)) diff --git a/IPython/core/tests/test_application.py b/IPython/core/tests/test_application.py index f4b11b5a928..74ea0f79006 100644 --- a/IPython/core/tests/test_application.py +++ b/IPython/core/tests/test_application.py @@ -34,7 +34,7 @@ def test_unicode_ipdir(): ipdir = tempfile.mkdtemp(suffix=u"€") # Create the config file, so it tries to load it. - with open(os.path.join(ipdir, 'ipython_config.py'), "w", encoding='utf-8') as f: + with open(os.path.join(ipdir, "ipython_config.py"), "w", encoding="utf-8") as f: pass old_ipdir1 = os.environ.pop("IPYTHONDIR", None) @@ -59,7 +59,7 @@ class TestApp(BaseIPythonApplication): test = Unicode().tag(config=True) # Create the config file, so it tries to load it. - with open(os.path.join(td, 'ipython_config.py'), "w", encoding='utf-8') as f: + with open(os.path.join(td, "ipython_config.py"), "w", encoding="utf-8") as f: f.write("c.TestApp.test = 'config file'") app = TestApp() diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 7ce160a5aec..5f791e84f83 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -346,7 +346,7 @@ def test_abspath_file_completions(self): suffixes = ["1", "2"] names = [prefix + s for s in suffixes] for n in names: - open(n, "w", encoding='utf-8').close() + open(n, "w", encoding="utf-8").close() # Check simple completion c = ip.complete(prefix)[1] @@ -365,7 +365,7 @@ def test_local_file_completions(self): suffixes = ["1", "2"] names = [prefix + s for s in suffixes] for n in names: - open(n, "w", encoding='utf-8').close() + open(n, "w", encoding="utf-8").close() # Check simple completion c = ip.complete(prefix)[1] @@ -381,7 +381,7 @@ def test_quoted_file_completions(self): ip = get_ipython() with TemporaryWorkingDirectory(): name = "foo'bar" - open(name, "w", encoding='utf-8').close() + open(name, "w", encoding="utf-8").close() # Don't escape Windows escaped = name if sys.platform == "win32" else "foo\\'bar" diff --git a/IPython/core/tests/test_completerlib.py b/IPython/core/tests/test_completerlib.py index 23e24e6bfd6..fbbc258673f 100644 --- a/IPython/core/tests/test_completerlib.py +++ b/IPython/core/tests/test_completerlib.py @@ -33,7 +33,7 @@ class Test_magic_run_completer(unittest.TestCase): def setUp(self): self.BASETESTDIR = tempfile.mkdtemp() for fil in self.files: - with open(join(self.BASETESTDIR, fil), "w", encoding='utf-8') as sfile: + with open(join(self.BASETESTDIR, fil), "w", encoding="utf-8") as sfile: sfile.write("pass\n") for d in self.dirs: os.mkdir(join(self.BASETESTDIR, d)) @@ -89,7 +89,7 @@ class Test_magic_run_completer_nonascii(unittest.TestCase): def setUp(self): self.BASETESTDIR = tempfile.mkdtemp() for fil in [u"aaø.py", u"a.py", u"b.py"]: - with open(join(self.BASETESTDIR, fil), "w", encoding='utf-8') as sfile: + with open(join(self.BASETESTDIR, fil), "w", encoding="utf-8") as sfile: sfile.write("pass\n") self.oldpath = os.getcwd() os.chdir(self.BASETESTDIR) @@ -133,8 +133,8 @@ def test_import_invalid_module(): with TemporaryDirectory() as tmpdir: sys.path.insert( 0, tmpdir ) for name in invalid_module_names | valid_module_names: - filename = os.path.join(tmpdir, name + '.py') - open(filename, 'w', encoding='utf-8').close() + filename = os.path.join(tmpdir, name + ".py") + open(filename, "w", encoding="utf-8").close() s = set( module_completion('import foo') ) intersection = s.intersection(invalid_module_names) diff --git a/IPython/core/tests/test_extension.py b/IPython/core/tests/test_extension.py index 958fd2b4a2c..59e21dcacf2 100644 --- a/IPython/core/tests/test_extension.py +++ b/IPython/core/tests/test_extension.py @@ -26,12 +26,12 @@ def load_ipython_extension(ip): def test_extension_loading(): em = get_ipython().extension_manager with TemporaryDirectory() as td: - ext1 = os.path.join(td, 'ext1.py') - with open(ext1, 'w', encoding='utf-8') as f: + ext1 = os.path.join(td, "ext1.py") + with open(ext1, "w", encoding="utf-8") as f: f.write(ext1_content) - - ext2 = os.path.join(td, 'ext2.py') - with open(ext2, 'w', encoding='utf-8') as f: + + ext2 = os.path.join(td, "ext2.py") + with open(ext2, "w", encoding="utf-8") as f: f.write(ext2_content) with prepended_to_syspath(td): @@ -76,8 +76,8 @@ def test_extension_loading(): def test_extension_builtins(): em = get_ipython().extension_manager with TemporaryDirectory() as td: - ext3 = os.path.join(td, 'ext3.py') - with open(ext3, 'w', encoding='utf-8') as f: + ext3 = os.path.join(td, "ext3.py") + with open(ext3, "w", encoding="utf-8") as f: f.write(ext3_content) assert 'ext3' not in em.loaded diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index e86c4fcd560..09dbd967706 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -485,12 +485,12 @@ def mock_print_func(value, sep=" ", end="\n", file=sys.stdout, flush=False): def test_mktempfile(self): filename = ip.mktempfile() # Check that we can open the file again on Windows - with open(filename, 'w', encoding='utf-8') as f: - f.write('abc') + with open(filename, "w", encoding="utf-8") as f: + f.write("abc") - filename = ip.mktempfile(data='blah') - with open(filename, 'r', encoding='utf-8') as f: - self.assertEqual(f.read(), 'blah') + filename = ip.mktempfile(data="blah") + with open(filename, "r", encoding="utf-8") as f: + self.assertEqual(f.read(), "blah") def test_new_main_mod(self): # Smoketest to check that this accepts a unicode module name @@ -545,7 +545,9 @@ def setUp(self): self.BASETESTDIR = tempfile.mkdtemp() self.TESTDIR = join(self.BASETESTDIR, u"åäö") os.mkdir(self.TESTDIR) - with open(join(self.TESTDIR, u"åäötestscript.py"), "w", encoding='utf-8') as sfile: + with open( + join(self.TESTDIR, u"åäötestscript.py"), "w", encoding="utf-8" + ) as sfile: sfile.write("pass\n") self.oldpath = os.getcwd() os.chdir(self.TESTDIR) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index f645daf91b0..0f6d4aaa951 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -861,12 +861,18 @@ def test_file(): """Basic %%writefile""" ip = get_ipython() with TemporaryDirectory() as td: - fname = os.path.join(td, 'file1') - ip.run_cell_magic("writefile", fname, u'\n'.join([ - 'line1', - 'line2', - ])) - s = Path(fname).read_text(encoding='utf-8') + fname = os.path.join(td, "file1") + ip.run_cell_magic( + "writefile", + fname, + "\n".join( + [ + "line1", + "line2", + ] + ), + ) + s = Path(fname).read_text(encoding="utf-8") assert "line1\n" in s assert "line2" in s @@ -876,12 +882,18 @@ def test_file_single_quote(): """Basic %%writefile with embedded single quotes""" ip = get_ipython() with TemporaryDirectory() as td: - fname = os.path.join(td, '\'file1\'') - ip.run_cell_magic("writefile", fname, u'\n'.join([ - 'line1', - 'line2', - ])) - s = Path(fname).read_text(encoding='utf-8') + fname = os.path.join(td, "'file1'") + ip.run_cell_magic( + "writefile", + fname, + "\n".join( + [ + "line1", + "line2", + ] + ), + ) + s = Path(fname).read_text(encoding="utf-8") assert "line1\n" in s assert "line2" in s @@ -892,11 +904,17 @@ def test_file_double_quote(): ip = get_ipython() with TemporaryDirectory() as td: fname = os.path.join(td, '"file1"') - ip.run_cell_magic("writefile", fname, u'\n'.join([ - 'line1', - 'line2', - ])) - s = Path(fname).read_text(encoding='utf-8') + ip.run_cell_magic( + "writefile", + fname, + "\n".join( + [ + "line1", + "line2", + ] + ), + ) + s = Path(fname).read_text(encoding="utf-8") assert "line1\n" in s assert "line2" in s @@ -905,13 +923,19 @@ def test_file_var_expand(): """%%writefile $filename""" ip = get_ipython() with TemporaryDirectory() as td: - fname = os.path.join(td, 'file1') - ip.user_ns['filename'] = fname - ip.run_cell_magic("writefile", '$filename', u'\n'.join([ - 'line1', - 'line2', - ])) - s = Path(fname).read_text(encoding='utf-8') + fname = os.path.join(td, "file1") + ip.user_ns["filename"] = fname + ip.run_cell_magic( + "writefile", + "$filename", + "\n".join( + [ + "line1", + "line2", + ] + ), + ) + s = Path(fname).read_text(encoding="utf-8") assert "line1\n" in s assert "line2" in s @@ -935,16 +959,28 @@ def test_file_amend(): """%%writefile -a amends files""" ip = get_ipython() with TemporaryDirectory() as td: - fname = os.path.join(td, 'file2') - ip.run_cell_magic("writefile", fname, u'\n'.join([ - 'line1', - 'line2', - ])) - ip.run_cell_magic("writefile", "-a %s" % fname, u'\n'.join([ - 'line3', - 'line4', - ])) - s = Path(fname).read_text(encoding='utf-8') + fname = os.path.join(td, "file2") + ip.run_cell_magic( + "writefile", + fname, + "\n".join( + [ + "line1", + "line2", + ] + ), + ) + ip.run_cell_magic( + "writefile", + "-a %s" % fname, + "\n".join( + [ + "line3", + "line4", + ] + ), + ) + s = Path(fname).read_text(encoding="utf-8") assert "line1\n" in s assert "line3\n" in s @@ -954,11 +990,17 @@ def test_file_spaces(): ip = get_ipython() with TemporaryWorkingDirectory() as td: fname = "file name" - ip.run_cell_magic("file", '"%s"'%fname, u'\n'.join([ - 'line1', - 'line2', - ])) - s = Path(fname).read_text(encoding='utf-8') + ip.run_cell_magic( + "file", + '"%s"' % fname, + "\n".join( + [ + "line1", + "line2", + ] + ), + ) + s = Path(fname).read_text(encoding="utf-8") assert "line1\n" in s assert "line2" in s @@ -1154,11 +1196,11 @@ def test_save(): with TemporaryDirectory() as tmpdir: file = os.path.join(tmpdir, "testsave.py") ip.run_line_magic("save", "%s 1-10" % file) - content = Path(file).read_text(encoding='utf-8') + content = Path(file).read_text(encoding="utf-8") assert content.count(cmds[0]) == 1 assert "coding: utf-8" in content ip.run_line_magic("save", "-a %s 1-10" % file) - content = Path(file).read_text(encoding='utf-8') + content = Path(file).read_text(encoding="utf-8") assert content.count(cmds[0]) == 2 assert "coding: utf-8" in content @@ -1173,7 +1215,7 @@ def test_save_with_no_args(): with TemporaryDirectory() as tmpdir: path = os.path.join(tmpdir, "testsave.py") ip.run_line_magic("save", path) - content = Path(path).read_text(encoding='utf-8') + content = Path(path).read_text(encoding="utf-8") expected_content = dedent( """\ # coding: utf-8 @@ -1335,8 +1377,8 @@ def test_timeit_arguments(): def test_run_module_from_import_hook(): "Test that a module can be loaded via an import hook" with TemporaryDirectory() as tmpdir: - fullpath = os.path.join(tmpdir, 'my_tmp.py') - Path(fullpath).write_text(TEST_MODULE, encoding='utf-8') + fullpath = os.path.join(tmpdir, "my_tmp.py") + Path(fullpath).write_text(TEST_MODULE, encoding="utf-8") import importlib.abc import importlib.util @@ -1352,7 +1394,7 @@ def get_filename(self, fullname): def get_data(self, path): assert Path(path).samefile(fullpath) - return Path(fullpath).read_text(encoding='utf-8') + return Path(fullpath).read_text(encoding="utf-8") sys.meta_path.insert(0, MyTempImporter()) diff --git a/IPython/core/tests/test_profile.py b/IPython/core/tests/test_profile.py index 22966e48ac3..d034b50f219 100644 --- a/IPython/core/tests/test_profile.py +++ b/IPython/core/tests/test_profile.py @@ -84,10 +84,10 @@ def tearDown(self): def init(self, startup_file, startup, test): # write startup python file - with open(Path(self.pd.startup_dir) / startup_file, "w", encoding='utf-8') as f: + with open(Path(self.pd.startup_dir) / startup_file, "w", encoding="utf-8") as f: f.write(startup) # write simple test file, to check that the startup file was run - with open(self.fname, 'w', encoding='utf-8') as f: + with open(self.fname, "w", encoding="utf-8") as f: f.write(test) def validate(self, output): @@ -111,7 +111,7 @@ def test_list_profiles_in(): if dec.unicode_paths: Path(td / u"profile_ünicode").mkdir(parents=True) - with open(td / "profile_file", "w", encoding='utf-8') as f: + with open(td / "profile_file", "w", encoding="utf-8") as f: f.write("I am not a profile directory") profiles = list_profiles_in(td) diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py index 69576577e9e..9204e81e8e8 100644 --- a/IPython/core/tests/test_run.py +++ b/IPython/core/tests/test_run.py @@ -443,7 +443,7 @@ def writefile(self, name, content): d = os.path.dirname(path) if not os.path.isdir(d): os.makedirs(d) - with open(path, 'w', encoding='utf-8') as f: + with open(path, "w", encoding="utf-8") as f: f.write(textwrap.dedent(content)) def setUp(self): @@ -526,8 +526,8 @@ def test_module_options_with_separator(self): def test_run__name__(): with TemporaryDirectory() as td: - path = pjoin(td, 'foo.py') - with open(path, 'w', encoding='utf-8') as f: + path = pjoin(td, "foo.py") + with open(path, "w", encoding="utf-8") as f: f.write("q = __name__") _ip.user_ns.pop("q", None) @@ -547,15 +547,19 @@ def test_run__name__(): def test_run_tb(): """Test traceback offset in %run""" with TemporaryDirectory() as td: - path = pjoin(td, 'foo.py') - with open(path, 'w', encoding='utf-8') as f: - f.write('\n'.join([ - "def foo():", - " return bar()", - "def bar():", - " raise RuntimeError('hello!')", - "foo()", - ])) + path = pjoin(td, "foo.py") + with open(path, "w", encoding="utf-8") as f: + f.write( + "\n".join( + [ + "def foo():", + " return bar()", + "def bar():", + " raise RuntimeError('hello!')", + "foo()", + ] + ) + ) with capture_output() as io: _ip.magic('run {}'.format(path)) out = io.stdout @@ -577,8 +581,8 @@ def test_multiprocessing_run(): mpm = sys.modules.get('__mp_main__') sys.modules['__mp_main__'] = None try: - path = pjoin(td, 'test.py') - with open(path, 'w', encoding='utf-8') as f: + path = pjoin(td, "test.py") + with open(path, "w", encoding="utf-8") as f: f.write("import multiprocessing\nprint('hoy')") with capture_output() as io: _ip.run_line_magic('run', path) @@ -597,15 +601,19 @@ def test_multiprocessing_run(): def test_script_tb(): """Test traceback offset in `ipython script.py`""" with TemporaryDirectory() as td: - path = pjoin(td, 'foo.py') - with open(path, 'w', encoding='utf-8') as f: - f.write('\n'.join([ - "def foo():", - " return bar()", - "def bar():", - " raise RuntimeError('hello!')", - "foo()", - ])) + path = pjoin(td, "foo.py") + with open(path, "w", encoding="utf-8") as f: + f.write( + "\n".join( + [ + "def foo():", + " return bar()", + "def bar():", + " raise RuntimeError('hello!')", + "foo()", + ] + ) + ) out, err = tt.ipexec(path) assert "execfile" not in out assert "RuntimeError" in out diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index 6e855d10186..e10abb863a1 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -58,7 +58,7 @@ def test_changing_py_file(self): """ with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") - with open(fname, "w", encoding='utf-8') as f: + with open(fname, "w", encoding="utf-8") as f: f.write(file_1) with prepended_to_syspath(td): @@ -68,7 +68,7 @@ def test_changing_py_file(self): ip.run_cell("foo.f()") # Make the file shorter, so the line of the error is missing. - with open(fname, "w", encoding='utf-8') as f: + with open(fname, "w", encoding="utf-8") as f: f.write(file_2) # For some reason, this was failing on the *second* call after @@ -92,7 +92,7 @@ def test_nonascii_path(self): # Non-ascii directory name as well. with TemporaryDirectory(suffix=u'é') as td: fname = os.path.join(td, u"fooé.py") - with open(fname, "w", encoding='utf-8') as f: + with open(fname, "w", encoding="utf-8") as f: f.write(file_1) with prepended_to_syspath(td): @@ -172,7 +172,7 @@ def test_indentationerror_shows_line(self): with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") - with open(fname, "w", encoding='utf-8') as f: + with open(fname, "w", encoding="utf-8") as f: f.write(indentationerror_file) with tt.AssertPrints("IndentationError"): @@ -221,14 +221,14 @@ def bar(): def test_changing_py_file(self): with TemporaryDirectory() as td: fname = os.path.join(td, "foo.py") - with open(fname, 'w', encoding='utf-8') as f: + with open(fname, "w", encoding="utf-8") as f: f.write(se_file_1) with tt.AssertPrints(["7/", "SyntaxError"]): ip.magic("run " + fname) # Modify the file - with open(fname, 'w', encoding='utf-8') as f: + with open(fname, "w", encoding="utf-8") as f: f.write(se_file_2) # The SyntaxError should point to the correct line diff --git a/IPython/extensions/storemagic.py b/IPython/extensions/storemagic.py index ff124ee38bd..d9d00f14b9a 100644 --- a/IPython/extensions/storemagic.py +++ b/IPython/extensions/storemagic.py @@ -176,12 +176,12 @@ def store(self, parameter_s=''): # default action - store the variable else: # %store foo >file.txt or >>file.txt - if len(args) > 1 and args[1].startswith('>'): - fnam = os.path.expanduser(args[1].lstrip('>').lstrip()) - if args[1].startswith('>>'): - fil = open(fnam, 'a', encoding='utf-8') + if len(args) > 1 and args[1].startswith(">"): + fnam = os.path.expanduser(args[1].lstrip(">").lstrip()) + if args[1].startswith(">>"): + fil = open(fnam, "a", encoding="utf-8") else: - fil = open(fnam, 'w', encoding='utf-8') + fil = open(fnam, "w", encoding="utf-8") with fil: obj = ip.ev(args[0]) print("Writing '%s' (%s) to file '%s'." % (args[0], diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 63bae802524..a0fe725aa68 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -119,13 +119,13 @@ def write_file(self, filename, content): time.sleep(1.05) # Write - with open(filename, "w", encoding='utf-8') as f: + with open(filename, "w", encoding="utf-8") as f: f.write(content) def new_module(self, code): code = textwrap.dedent(code) mod_name, mod_fn = self.get_module() - with open(mod_fn, "w", encoding='utf-8') as f: + with open(mod_fn, "w", encoding="utf-8") as f: f.write(code) return mod_name, mod_fn diff --git a/IPython/lib/demo.py b/IPython/lib/demo.py index 0c96de6e72d..8c9ae905d49 100644 --- a/IPython/lib/demo.py +++ b/IPython/lib/demo.py @@ -405,7 +405,7 @@ def edit(self,index=None): filename = self.shell.mktempfile(self.src_blocks[index]) self.shell.hooks.editor(filename, 1) - with open(Path(filename), "r", encoding='utf-8') as f: + with open(Path(filename), "r", encoding="utf-8") as f: new_block = f.read() # update the source and colored block self.src_blocks[index] = new_block diff --git a/IPython/lib/tests/test_deepreload.py b/IPython/lib/tests/test_deepreload.py index f86e1027f98..827249cbad0 100644 --- a/IPython/lib/tests/test_deepreload.py +++ b/IPython/lib/tests/test_deepreload.py @@ -19,9 +19,9 @@ def test_deepreload(): with TemporaryDirectory() as tmpdir: with prepended_to_syspath(tmpdir): tmpdirpath = Path(tmpdir) - with open(tmpdirpath / "A.py", "w", encoding='utf-8') as f: + with open(tmpdirpath / "A.py", "w", encoding="utf-8") as f: f.write("class Object:\n pass\nok = True\n") - with open(tmpdirpath / "B.py", "w", encoding='utf-8') as f: + with open(tmpdirpath / "B.py", "w", encoding="utf-8") as f: f.write("import A\nassert A.ok, 'we are fine'\n") import A import B diff --git a/IPython/testing/tests/test_tools.py b/IPython/testing/tests/test_tools.py index 0e66ca39a99..178863cf686 100644 --- a/IPython/testing/tests/test_tools.py +++ b/IPython/testing/tests/test_tools.py @@ -62,7 +62,7 @@ def test_temp_pyfile(): src = 'pass\n' fname = tt.temp_pyfile(src) assert os.path.isfile(fname) - with open(fname, encoding='utf-8') as fh2: + with open(fname, encoding="utf-8") as fh2: src2 = fh2.read() assert src2 == src diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py index fafb73fb8f7..2ff63a6d4a0 100644 --- a/IPython/testing/tools.py +++ b/IPython/testing/tools.py @@ -426,9 +426,8 @@ def mute_warn(): @contextmanager def make_tempfile(name): - """ Create an empty, named, temporary file for the duration of the context. - """ - open(name, 'w', encoding='utf-8').close() + """Create an empty, named, temporary file for the duration of the context.""" + open(name, "w", encoding="utf-8").close() try: yield finally: diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py index aba6e20fa79..0a9dec4e854 100644 --- a/IPython/tests/cve.py +++ b/IPython/tests/cve.py @@ -33,7 +33,9 @@ def test_cve_2022_21699(): with TemporaryWorkingDirectory() as t: dangerous_startup_dir.mkdir(parents=True) - (dangerous_startup_dir / "foo.py").write_text(f'print("{dangerous_expected}")', encoding='utf-8') + (dangerous_startup_dir / "foo.py").write_text( + f'print("{dangerous_expected}")', encoding="utf-8" + ) # 1 sec to make sure FS is flushed. # time.sleep(1) cmd = [sys.executable, "-m", "IPython"] diff --git a/IPython/utils/io.py b/IPython/utils/io.py index ef1be80988f..170bc625acb 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -20,7 +20,7 @@ from .capture import CapturedIO, capture_output # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr -devnull = open(os.devnull, 'w', encoding='utf-8') +devnull = open(os.devnull, "w", encoding="utf-8") atexit.register(devnull.close) @@ -52,7 +52,7 @@ def __init__(self, file_or_name, mode="w", channel='stdout'): if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'): self.file = file_or_name else: - encoding = None if 'b' in mode else 'utf-8' + encoding = None if "b" in mode else "utf-8" self.file = open(file_or_name, mode, encoding=encoding) self.channel = channel self.ostream = getattr(sys, channel) @@ -132,7 +132,7 @@ def temp_pyfile(src, ext='.py'): It is the caller's responsibility to close the open file and unlink it. """ fname = tempfile.mkstemp(ext)[1] - with open(Path(fname), "w", encoding='utf-8') as f: + with open(Path(fname), "w", encoding="utf-8") as f: f.write(src) f.flush() return fname diff --git a/IPython/utils/tempdir.py b/IPython/utils/tempdir.py index 0729a8417dc..c3918d05bb0 100644 --- a/IPython/utils/tempdir.py +++ b/IPython/utils/tempdir.py @@ -24,7 +24,7 @@ def __init__(self, filename, mode='w+b', bufsize=-1, **kwds): """ self._tmpdir = TemporaryDirectory(**kwds) path = Path(self._tmpdir.name) / filename - encoding = None if 'b' in mode else 'utf-8' + encoding = None if "b" in mode else "utf-8" self.file = open(path, mode, bufsize, encoding=encoding) def cleanup(self): diff --git a/IPython/utils/tests/test_module_paths.py b/IPython/utils/tests/test_module_paths.py index 2abf889514c..8438a1e737f 100644 --- a/IPython/utils/tests/test_module_paths.py +++ b/IPython/utils/tests/test_module_paths.py @@ -32,7 +32,7 @@ old_syspath = sys.path def make_empty_file(fname): - open(fname, 'w', encoding='utf-8').close() + open(fname, "w", encoding="utf-8").close() def setup_module(): diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index f92e6ae1f6c..13e322320e0 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -295,7 +295,7 @@ def test_not_writable_ipdir(self): ipdir = os.path.join(tmpdir, '.ipython') os.mkdir(ipdir, 0o555) try: - open(os.path.join(ipdir, "_foo_"), 'w', encoding='utf-8').close() + open(os.path.join(ipdir, "_foo_"), "w", encoding="utf-8").close() except IOError: pass else: @@ -352,7 +352,7 @@ def setUpClass(cls): with cls.in_tempdir(): # Create empty files for fname in cls.filenames: - open(os.path.join(td, fname), 'w', encoding='utf-8').close() + open(os.path.join(td, fname), "w", encoding="utf-8").close() @classmethod def tearDownClass(cls): @@ -426,9 +426,9 @@ def test_ensure_dir_exists(): d = os.path.join(td, '∂ir') path.ensure_dir_exists(d) # create it assert os.path.isdir(d) - path.ensure_dir_exists(d) # no-op - f = os.path.join(td, 'ƒile') - open(f, 'w', encoding='utf-8').close() # touch + path.ensure_dir_exists(d) # no-op + f = os.path.join(td, "ƒile") + open(f, "w", encoding="utf-8").close() # touch with pytest.raises(IOError): path.ensure_dir_exists(f) @@ -436,7 +436,7 @@ class TestLinkOrCopy(unittest.TestCase): def setUp(self): self.tempdir = TemporaryDirectory() self.src = self.dst("src") - with open(self.src, "w", encoding='utf-8') as f: + with open(self.src, "w", encoding="utf-8") as f: f.write("Hello, world!") def tearDown(self): @@ -456,8 +456,8 @@ def assert_inode_equal(self, a, b): ), "%r and %r do not reference the same indoes" % (a, b) def assert_content_equal(self, a, b): - with open(a, 'rb') as a_f: - with open(b, 'rb') as b_f: + with open(a, "rb") as a_f: + with open(b, "rb") as b_f: assert a_f.read() == b_f.read() @skip_win32 @@ -477,7 +477,7 @@ def test_link_into_dir(self): @skip_win32 def test_target_exists(self): dst = self.dst("target") - open(dst, "w", encoding='utf-8').close() + open(dst, "w", encoding="utf-8").close() path.link_or_copy(self.src, dst) self.assert_inode_equal(self.src, dst) diff --git a/docs/autogen_config.py b/docs/autogen_config.py index 56733f3b422..43c38dd2d43 100755 --- a/docs/autogen_config.py +++ b/docs/autogen_config.py @@ -102,7 +102,7 @@ def reverse_aliases(app): def write_doc(name, title, app, preamble=None): trait_aliases = reverse_aliases(app) filename = options / (name + ".rst") - with open(filename, "w", encoding='utf-8') as f: + with open(filename, "w", encoding="utf-8") as f: f.write(title + "\n") f.write(("=" * len(title)) + "\n") f.write("\n") @@ -117,7 +117,7 @@ def write_doc(name, title, app, preamble=None): if __name__ == '__main__': # Touch this file for the make target - Path(generated).write_text("", encoding='utf-8') + Path(generated).write_text("", encoding="utf-8") write_doc('terminal', 'Terminal IPython options', TerminalIPythonApp()) write_doc('kernel', 'IPython kernel options', IPKernelApp(), diff --git a/docs/autogen_magics.py b/docs/autogen_magics.py index bfeb6aa7733..6102d0950c6 100644 --- a/docs/autogen_magics.py +++ b/docs/autogen_magics.py @@ -63,4 +63,4 @@ def sortkey(s): return s[0].lower() src_path = Path(__file__).parent dest = src_path.joinpath("source", "interactive", "magics-generated.txt") -dest.write_text("\n".join(output), encoding='utf-8') +dest.write_text("\n".join(output), encoding="utf-8") diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index 7ee0f910768..db7fe8d4917 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -89,6 +89,8 @@ def sort_key(item): (single_filter, "single_filtered"), (multi_filter, "multi_filtered"), ]: - with (dest / "{}.csv".format(output_filename)).open("w", encoding='utf-8') as csv: + with (dest / "{}.csv".format(output_filename)).open( + "w", encoding="utf-8" + ) as csv: for (shortcut, flt), v in sorted(filters.items(), key=sort_key): csv.write(":kbd:`{}`\t{}\t{}\n".format(shortcut, flt, v)) diff --git a/docs/source/conf.py b/docs/source/conf.py index ba9aa1dbc88..29212af8bf7 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,11 +28,14 @@ for name in ("config", "api", "magics", "shortcuts"): fname = Path("autogen_{}.py".format(name)) fpath = (Path(__file__).parent).joinpath("..", fname) - with open(fpath, encoding='utf-8') as f: - exec(compile(f.read(), fname, 'exec'), { - '__file__': fpath, - '__name__': '__main__', - }) + with open(fpath, encoding="utf-8") as f: + exec( + compile(f.read(), fname, "exec"), + { + "__file__": fpath, + "__name__": "__main__", + }, + ) else: import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" @@ -45,7 +48,14 @@ # We load the ipython release info into a dict by explicit execution iprelease = {} -exec(compile(open('../../IPython/core/release.py', encoding='utf-8').read(), '../../IPython/core/release.py', 'exec'),iprelease) +exec( + compile( + open("../../IPython/core/release.py", encoding="utf-8").read(), + "../../IPython/core/release.py", + "exec", + ), + iprelease, +) # General configuration # --------------------- diff --git a/docs/sphinxext/apigen.py b/docs/sphinxext/apigen.py index 71a384b7d2a..e58493b17fd 100644 --- a/docs/sphinxext/apigen.py +++ b/docs/sphinxext/apigen.py @@ -390,9 +390,8 @@ def write_modules_api(self, modules,outdir): if not api_str: continue # write out to file - outfile = os.path.join(outdir, - m + self.rst_extension) - with open(outfile, 'wt', encoding='utf-8') as fileobj: + outfile = os.path.join(outdir, m + self.rst_extension) + with open(outfile, "wt", encoding="utf-8") as fileobj: fileobj.write(api_str) written_modules.append(m) self.written_modules = written_modules @@ -444,7 +443,7 @@ def write_index(self, outdir, path='gen.rst', relative_to=None): relpath = outdir.replace(relative_to + os.path.sep, '') else: relpath = outdir - with open(path,'wt', encoding='utf-8') as idx: + with open(path, "wt", encoding="utf-8") as idx: w = idx.write w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') w('.. autosummary::\n' diff --git a/examples/IPython Kernel/ipython-get-history.py b/examples/IPython Kernel/ipython-get-history.py index fe38081fbad..116b4b7c155 100755 --- a/examples/IPython Kernel/ipython-get-history.py +++ b/examples/IPython Kernel/ipython-get-history.py @@ -24,7 +24,7 @@ session_number = int(sys.argv[1]) if len(sys.argv) > 2: filepath = Path(sys.argv[2]) - dest = open(filepath, "w", encoding='utf-8') + dest = open(filepath, "w", encoding="utf-8") raw = not filepath.name.endswith(".py") else: dest = sys.stdout diff --git a/setupbase.py b/setupbase.py index 788ce2491fb..b57dcc1b2a5 100644 --- a/setupbase.py +++ b/setupbase.py @@ -36,7 +36,7 @@ def execfile(fname, globs, locs=None): locs = locs or globs - with open(fname, encoding='utf-8') as f: + with open(fname, encoding="utf-8") as f: exec(compile(f.read(), fname, "exec"), globs, locs) # A little utility we'll need below, since glob() does NOT allow you to do @@ -336,10 +336,13 @@ def _record_commit(self, base_dir): os.remove(out_pth) except (IOError, OSError): pass - with open(out_pth, 'w', encoding='utf-8') as out_file: - out_file.writelines([ - '# GENERATED BY setup.py\n', - 'commit = u"%s"\n' % repo_commit, - ]) + with open(out_pth, "w", encoding="utf-8") as out_file: + out_file.writelines( + [ + "# GENERATED BY setup.py\n", + 'commit = u"%s"\n' % repo_commit, + ] + ) + return MyBuildPy diff --git a/tools/fixup_whats_new_pr.py b/tools/fixup_whats_new_pr.py index 397c0bf38e3..9b74da8055c 100644 --- a/tools/fixup_whats_new_pr.py +++ b/tools/fixup_whats_new_pr.py @@ -22,14 +22,14 @@ def main(): print("Adding pseudo-title to:", filepath.name) title = filepath.name[:-4].split("/")[-1].replace("-", " ").capitalize() - data = filepath.read_text(encoding='utf-8') + data = filepath.read_text(encoding="utf-8") try: if data and data.splitlines()[1].startswith('='): continue except IndexError: pass - with filepath.open("w", encoding='utf-8') as f: + with filepath.open("w", encoding="utf-8") as f: f.write(title + "\n") f.write("=" * len(title) + "\n\n") f.write(data) diff --git a/tools/toollib.py b/tools/toollib.py index c77bbbc6853..f32e06a5fa5 100644 --- a/tools/toollib.py +++ b/tools/toollib.py @@ -45,4 +45,4 @@ def get_ipdir(): def execfile(fname, globs, locs=None): locs = locs or globs - exec(compile(open(fname, encoding='utf-8').read(), fname, "exec"), globs, locs) + exec(compile(open(fname, encoding="utf-8").read(), fname, "exec"), globs, locs) From 7a171250be20214526089e7638a6002e83a3fcf4 Mon Sep 17 00:00:00 2001 From: gousaiyang Date: Fri, 4 Feb 2022 22:53:36 -0800 Subject: [PATCH 0056/1752] IPython currently incompatible with pytest 7 --- .github/workflows/downstream.yml | 2 +- docs/requirements.txt | 2 +- setup.cfg | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 168b32e2678..5c5adc1e0ab 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -41,7 +41,7 @@ jobs: python -m pip install --upgrade -e file://$PWD#egg=ipython[test] # we must instal IPython after ipykernel to get the right versions. python -m pip install --upgrade --upgrade-strategy eager flaky ipyparallel - python -m pip install --upgrade pytest + python -m pip install --upgrade 'pytest<7' - name: pytest env: COLUMNS: 120 diff --git a/docs/requirements.txt b/docs/requirements.txt index 93e162f6660..587288c2a0f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,4 @@ sphinx-rtd-theme docrepr matplotlib stack_data -pytest +pytest<7 diff --git a/setup.cfg b/setup.cfg index 2da02e4e598..4fbffaa1a07 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ qtconsole = qtconsole terminal = test = - pytest + pytest<7 pytest-asyncio testpath test_extra = @@ -72,7 +72,7 @@ test_extra = nbformat numpy>=1.19 pandas - pytest + pytest<7 testpath trio all = From 65f7fdfefa2c9ff872bcbd63f827b9256ed28594 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 7 Feb 2022 11:26:44 +0100 Subject: [PATCH 0057/1752] pin pytest --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2da02e4e598..4fbffaa1a07 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ qtconsole = qtconsole terminal = test = - pytest + pytest<7 pytest-asyncio testpath test_extra = @@ -72,7 +72,7 @@ test_extra = nbformat numpy>=1.19 pandas - pytest + pytest<7 testpath trio all = From e306c9d3f707de42b47a1e7c4c8034d6862fba5f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 7 Feb 2022 11:21:10 +0100 Subject: [PATCH 0058/1752] Update old deprecation --- IPython/core/interactiveshell.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index ecb9c5f1652..902da1bd9a1 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2287,7 +2287,11 @@ def find_magic(self, magic_name, magic_kind='line'): return self.magics_manager.magics[magic_kind].get(magic_name) def magic(self, arg_s): - """DEPRECATED. Use run_line_magic() instead. + """ + DEPRECATED + + Deprecated since IPython 0.13 (warning added in + 8.1), use run_line_magic(magic_name, parameter_s). Call a magic function by name. @@ -2305,6 +2309,12 @@ def magic(self, arg_s): valid Python code you can type at the interpreter, including loops and compound statements. """ + warnings.warn( + "`magic(...)` is deprecated since IPython 0.13 (warning added in " + "8.1), use run_line_magic(magic_name, parameter_s).", + DeprecationWarning, + stacklevel=2, + ) # TODO: should we issue a loud deprecation warning here? magic_name, _, magic_arg_s = arg_s.partition(' ') magic_name = magic_name.lstrip(prefilter.ESC_MAGIC) From e65762abb111da59c215c1382b8a3a601c57eea6 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 8 Feb 2022 11:32:28 +0100 Subject: [PATCH 0059/1752] Make unicode backslach completion fuzzy and case insensitive Ok, the fuzzy part is super simple. - candidates matching prefix - If no matches: names that contain the string - If no matches :whether each part of the string split on space is contained in the name of the unicode character. That is to say, `\GREEK OMICRON` will search whether both GREEK and and OMICRON (case insensitive) are available, in the worst case scenario This allows things like `\omicron` to give you proper suggestions. `\nu` will give you latex nu, `\greek nu` with match as the prefix of `GREEK NUMERAL ...` `\Nu` will match all the `... NUMERAL...` in unicode, but `\Nu greek` will limit the searches enough Mitigate #13514 --- IPython/core/completer.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index ccb1f465f5b..9774fd5a4e7 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -2222,12 +2222,22 @@ def fwd_unicode_match(self, text:str) -> Tuple[str, Sequence[str]]: # initialized, and it takes a user-noticeable amount of time to # initialize it, so we don't want to initialize it unless we're # actually going to use it. - s = text[slashpos+1:] - candidates = [x for x in self.unicode_names if x.startswith(s)] + s = text[slashpos + 1 :] + sup = s.upper() + candidates = [x for x in self.unicode_names if x.startswith(sup)] if candidates: return s, candidates - else: - return '', () + candidates = [x for x in self.unicode_names if sup in x] + if candidates: + return s, candidates + splitsup = sup.split(" ") + candidates = [ + x for x in self.unicode_names if all(u in x for u in splitsup) + ] + if candidates: + return s, candidates + + return "", () # if text does not start with slash else: From e5bbea060db04b6a3d578e6a444278c981dc1db5 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 1 Feb 2022 15:14:34 +0100 Subject: [PATCH 0060/1752] Allow to configure lazy loadable magics. While we encourage load_ipython_ext to be lazy and not take too much resources, it might not be practical especially when the top level module takes a long time to import. Here we allow to define a mapping between magics names and extension name, and on attempt to execute a non-existing magics we'll look into the lazy mapping and try to load it. We also add a helper to let us interactively register a magic lazily, see the `register_lazy()` method of to magics_manager --- IPython/core/interactiveshell.py | 41 +++++++++++++++++--- IPython/core/magic.py | 46 +++++++++++++++++++++++ IPython/core/tests/test_magic.py | 44 +++++++++++++++++++++- IPython/core/tests/test_magic_terminal.py | 35 ++++++++++++++--- IPython/terminal/ipapp.py | 2 + docs/source/whatsnew/version7.rst | 18 +++++++++ 6 files changed, 175 insertions(+), 11 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 902da1bd9a1..3d9ec35b2ed 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -500,7 +500,6 @@ def profile(self): def __init__(self, ipython_dir=None, profile_dir=None, user_module=None, user_ns=None, custom_exceptions=((), None), **kwargs): - # This is where traits with a config_key argument are updated # from the values on config. super(InteractiveShell, self).__init__(**kwargs) @@ -1643,7 +1642,7 @@ def _inspect(self, meth, oname, namespaces=None, **kw): formatter, info, enable_html_pager=self.enable_html_pager, - **kw + **kw, ) else: pmethod(info.obj, oname) @@ -2173,7 +2172,34 @@ def register_magic_function(self, func, magic_kind='line', magic_name=None): func, magic_kind=magic_kind, magic_name=magic_name ) - def run_line_magic(self, magic_name, line, _stack_depth=1): + def _find_with_lazy_load(self, /, type_, magic_name: str): + """ + Try to find a magic potentially lazy-loading it. + + Parameters + ---------- + + type_: "line"|"cell" + the type of magics we are trying to find/lazy load. + magic_name: str + The name of the magic we are trying to find/lazy load + + + Note that this may have any side effects + """ + finder = {"line": self.find_line_magic, "cell": self.find_cell_magic}[type_] + fn = finder(magic_name) + if fn is not None: + return fn + lazy = self.magics_manager.lazy_magics.get(magic_name) + if lazy is None: + return None + + self.run_line_magic("load_ext", lazy) + res = finder(magic_name) + return res + + def run_line_magic(self, magic_name: str, line, _stack_depth=1): """Execute the given line magic. Parameters @@ -2186,7 +2212,12 @@ def run_line_magic(self, magic_name, line, _stack_depth=1): If run_line_magic() is called from magic() then _stack_depth=2. This is added to ensure backward compatibility for use of 'get_ipython().magic()' """ - fn = self.find_line_magic(magic_name) + fn = self._find_with_lazy_load("line", magic_name) + if fn is None: + lazy = self.magics_manager.lazy_magics.get(magic_name) + if lazy: + self.run_line_magic("load_ext", lazy) + fn = self.find_line_magic(magic_name) if fn is None: cm = self.find_cell_magic(magic_name) etpl = "Line magic function `%%%s` not found%s." @@ -2237,7 +2268,7 @@ def run_cell_magic(self, magic_name, line, cell): cell : str The body of the cell as a (possibly multiline) string. """ - fn = self.find_cell_magic(magic_name) + fn = self._find_with_lazy_load("cell", magic_name) if fn is None: lm = self.find_line_magic(magic_name) etpl = "Cell magic `%%{0}` not found{1}." diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 3dc3480b561..79983df09f2 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -302,6 +302,34 @@ class MagicsManager(Configurable): # holding the actual callable object as value. This is the dict used for # magic function dispatch magics = Dict() + lazy_magics = Dict( + help=""" + Mapping from magic names to modules to load. + + This can be used in IPython/IPykernel configuration to declare lazy magics + that will only be imported/registered on first use. + + For example:: + + c.MagicsManger.lazy_magics = { + "my_magic": "slow.to.import", + "my_other_magic": "also.slow", + } + + On first invocation of `%my_magic`, `%%my_magic`, `%%my_other_magic` or + `%%my_other_magic`, the corresponding module will be loaded as an ipython + extensions as if you had previously done `%load_ext ipython`. + + Magics names should be without percent(s) as magics can be both cell + and line magics. + + Lazy loading happen relatively late in execution process, and + complex extensions that manipulate Python/IPython internal state or global state + might not support lazy loading. + """ + ).tag( + config=True, + ) # A registry of the original objects that we've been given holding magics. registry = Dict() @@ -366,6 +394,24 @@ def lsmagic_docs(self, brief=False, missing=''): docs[m_type] = m_docs return docs + def register_lazy(self, name: str, fully_qualified_name: str): + """ + Lazily register a magic via an extension. + + + Parameters + ---------- + name : str + Name of the magic you wish to register. + fully_qualified_name : + Fully qualified name of the module/submodule that should be loaded + as an extensions when the magic is first called. + It is assumed that loading this extensions will register the given + magic. + """ + + self.lazy_magics[name] = fully_qualified_name + def register(self, *magic_objects): """Register one or more instances of Magics. diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 5294f82c51c..cbcb5c1411c 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -34,9 +34,11 @@ from IPython.utils.io import capture_output from IPython.utils.process import find_cmd from IPython.utils.tempdir import TemporaryDirectory, TemporaryWorkingDirectory +from IPython.utils.syspathcontext import prepended_to_syspath from .test_debugger import PdbTestInput +from tempfile import NamedTemporaryFile @magic.magics_class class DummyMagics(magic.Magics): pass @@ -1325,13 +1327,53 @@ def test_timeit_arguments(): _ip.magic("timeit -n1 -r1 a=('#')") +MINIMAL_LAZY_MAGIC = """ +from IPython.core.magic import ( + Magics, + magics_class, + line_magic, + cell_magic, +) + + +@magics_class +class LazyMagics(Magics): + @line_magic + def lazy_line(self, line): + print("Lazy Line") + + @cell_magic + def lazy_cell(self, line, cell): + print("Lazy Cell") + + +def load_ipython_extension(ipython): + ipython.register_magics(LazyMagics) +""" + + +def test_lazy_magics(): + with pytest.raises(UsageError): + ip.run_line_magic("lazy_line", "") + + startdir = os.getcwd() + + with TemporaryDirectory() as tmpdir: + with prepended_to_syspath(tmpdir): + ptempdir = Path(tmpdir) + tf = ptempdir / "lazy_magic_module.py" + tf.write_text(MINIMAL_LAZY_MAGIC) + ip.magics_manager.register_lazy("lazy_line", Path(tf.name).name[:-3]) + with tt.AssertPrints("Lazy Line"): + ip.run_line_magic("lazy_line", "") + + TEST_MODULE = """ print('Loaded my_tmp') if __name__ == "__main__": print('I just ran a script') """ - def test_run_module_from_import_hook(): "Test that a module can be loaded via an import hook" with TemporaryDirectory() as tmpdir: diff --git a/IPython/core/tests/test_magic_terminal.py b/IPython/core/tests/test_magic_terminal.py index 721fd5eda4d..f09014786e6 100644 --- a/IPython/core/tests/test_magic_terminal.py +++ b/IPython/core/tests/test_magic_terminal.py @@ -9,11 +9,35 @@ from unittest import TestCase from IPython.testing import tools as tt - #----------------------------------------------------------------------------- # Test functions begin #----------------------------------------------------------------------------- + +MINIMAL_LAZY_MAGIC = """ +from IPython.core.magic import ( + Magics, + magics_class, + line_magic, + cell_magic, +) + + +@magics_class +class LazyMagics(Magics): + @line_magic + def lazy_line(self, line): + print("Lazy Line") + + @cell_magic + def lazy_cell(self, line, cell): + print("Lazy Cell") + + +def load_ipython_extension(ipython): + ipython.register_magics(LazyMagics) +""" + def check_cpaste(code, should_fail=False): """Execute code via 'cpaste' and ensure it was executed, unless should_fail is set. @@ -31,7 +55,7 @@ def check_cpaste(code, should_fail=False): try: context = tt.AssertPrints if should_fail else tt.AssertNotPrints with context("Traceback (most recent call last)"): - ip.magic('cpaste') + ip.run_line_magic("cpaste", "") if not should_fail: assert ip.user_ns['code_ran'], "%r failed" % code @@ -68,13 +92,14 @@ def runf(): check_cpaste(code, should_fail=True) + class PasteTestCase(TestCase): """Multiple tests for clipboard pasting""" def paste(self, txt, flags='-q'): """Paste input text, by default in quiet mode""" - ip.hooks.clipboard_get = lambda : txt - ip.magic('paste '+flags) + ip.hooks.clipboard_get = lambda: txt + ip.run_line_magic("paste", flags) def setUp(self): # Inject fake clipboard hook but save original so we can restore it later @@ -114,7 +139,7 @@ def test_paste_py_multi_r(self): self.assertEqual(ip.user_ns.pop("x"), [1, 2, 3]) self.assertEqual(ip.user_ns.pop("y"), [1, 4, 9]) self.assertFalse("x" in ip.user_ns) - ip.magic("paste -r") + ip.run_line_magic("paste", "-r") self.assertEqual(ip.user_ns["x"], [1, 2, 3]) self.assertEqual(ip.user_ns["y"], [1, 4, 9]) diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index ed39b7dc5d0..e735a209255 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -25,6 +25,7 @@ from IPython.core.application import ( ProfileDir, BaseIPythonApplication, base_flags, base_aliases ) +from IPython.core.magic import MagicsManager from IPython.core.magics import ( ScriptMagics, LoggingMagics ) @@ -200,6 +201,7 @@ def _classes_default(self): self.__class__, # it will also affect subclasses (e.g. QtConsole) TerminalInteractiveShell, HistoryManager, + MagicsManager, ProfileDir, PlainTextFormatter, IPCompleter, diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 55b1b2f8439..6597fe555f8 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -3,6 +3,24 @@ ============ +.. _version 7.32: + +IPython 7.32 +============ + + +The ability to configure magics to be lazily loaded has been added to IPython. +See the ``ipython --help-all`` section on ``MagicsManager.lazy_magic``. +One can now use:: + + c.MagicsManger.lazy_magics = { + "my_magic": "slow.to.import", + "my_other_magic": "also.slow", + } + +And on first use of ``%my_magic``, or corresponding cell magic, or other line magic, +the corresponding ``load_ext`` will be called just before trying to invoke the magic. + .. _version 7.31: IPython 7.31 From a96dd9e4724f4c425a14257e0f592a1f79a1ff0e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 8 Feb 2022 12:10:55 +0100 Subject: [PATCH 0061/1752] MAINT: deprecate append_to_syspath --- IPython/utils/syspathcontext.py | 11 ++++++++++- IPython/utils/tests/test_deprecated.py | 7 +++++++ IPython/utils/version.py | 5 ++++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 IPython/utils/tests/test_deprecated.py diff --git a/IPython/utils/syspathcontext.py b/IPython/utils/syspathcontext.py index bd1c51500d6..7af1ab60af9 100644 --- a/IPython/utils/syspathcontext.py +++ b/IPython/utils/syspathcontext.py @@ -15,12 +15,21 @@ #----------------------------------------------------------------------------- import sys +import warnings class appended_to_syspath(object): - """A context for appending a directory to sys.path for a second.""" + """ + Deprecated since IPython 8.1, no replacements. + + A context for appending a directory to sys.path for a second.""" def __init__(self, dir): + warnings.warn( + "`appended_to_syspath` is deprecated since IPython 8.1, and has no replacements", + DeprecationWarning, + stacklevel=2, + ) self.dir = dir def __enter__(self): diff --git a/IPython/utils/tests/test_deprecated.py b/IPython/utils/tests/test_deprecated.py new file mode 100644 index 00000000000..f6f54ce52a9 --- /dev/null +++ b/IPython/utils/tests/test_deprecated.py @@ -0,0 +1,7 @@ +from IPython.utils.syspathcontext import appended_to_syspath +import pytest + + +def test_append_deprecated(): + with pytest.warns(DeprecationWarning): + appended_to_syspath(".") diff --git a/IPython/utils/version.py b/IPython/utils/version.py index 050155640d4..8c65c78e15e 100644 --- a/IPython/utils/version.py +++ b/IPython/utils/version.py @@ -14,7 +14,10 @@ from warnings import warn -warn("The `IPython.utils.version` module has been deprecated since IPython 8.0.") +warn( + "The `IPython.utils.version` module has been deprecated since IPython 8.0.", + DeprecationWarning, +) def check_version(v, check): From fcd299c5726988dfae1aa81f5e47348eb7300231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Leli=C3=A8vre?= Date: Sun, 6 Feb 2022 15:23:06 +0100 Subject: [PATCH 0062/1752] Sync `latex_symbols.jl` url in two places Follow-up to #11399 and #11400. Make sure the comment at the top of https://github.com/ipython/ipython/blob/master/IPython/core/latex_symbols.py points to the file actually used in - https://github.com/ipython/ipython/edit/master/tools/gen_latex_symbols.py --- tools/gen_latex_symbols.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/gen_latex_symbols.py b/tools/gen_latex_symbols.py index e1b441054ac..b623755136a 100644 --- a/tools/gen_latex_symbols.py +++ b/tools/gen_latex_symbols.py @@ -58,18 +58,18 @@ def test_ident(i): # Write the `latex_symbols.py` module in the cwd -s = """# encoding: utf-8 +s = f"""# encoding: utf-8 # DO NOT EDIT THIS FILE BY HAND. # To update this file, run the script /tools/gen_latex_symbols.py using Python 3 # This file is autogenerated from the file: -# https://raw.githubusercontent.com/JuliaLang/julia/master/base/latex_symbols.jl +# {url} # This original list is filtered to remove any unicode characters that are not valid # Python identifiers. -latex_symbols = {\n +latex_symbols = {{\n """ for line in valid_idents: s += ' "%s" : "%s",\n' % (line[0], line[1]) From 16451266fc75855a617f61a4e7bf6d29330965b1 Mon Sep 17 00:00:00 2001 From: ltrujello Date: Sun, 30 Jan 2022 11:09:36 -0800 Subject: [PATCH 0063/1752] Fixes #13472 by restoring user's terminal cursor --- IPython/core/interactiveshell.py | 4 ++++ IPython/terminal/shortcuts.py | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 3d9ec35b2ed..17aacfc3013 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3679,6 +3679,10 @@ def atexit_operations(self): pass del self.tempdirs + # Restore user's cursor + if hasattr(self, "editing_mode") and self.editing_mode == 'vi': + sys.stdout.write("\x1b[0 q") + sys.stdout.flush() def cleanup(self): self.restore_sys_module_state() diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index ea204f8ddd8..2cceb0786be 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -336,12 +336,7 @@ def set_input_mode(self, mode): shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6) cursor = "\x1b[{} q".format(shape) - if hasattr(sys.stdout, "_cli"): - write = sys.stdout._cli.output.write_raw - else: - write = sys.stdout.write - - write(cursor) + sys.stdout.write(cursor) sys.stdout.flush() self._input_mode = mode From 42e22f8e67a24dc274652afaab046c6d57654714 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 8 Feb 2022 12:25:55 +0100 Subject: [PATCH 0064/1752] MAINT: cleanup imports of tempdir. Many files were importing tempdir from IPython.utils, though all the changes are now upstream in Python. Import directly from tempdir. Use isort/darker on touched files to cleanup imports at the same time --- IPython/core/interactiveshell.py | 81 +++++++++++++++---------- IPython/core/tests/test_application.py | 2 +- IPython/core/tests/test_completerlib.py | 3 +- IPython/core/tests/test_extension.py | 3 +- IPython/core/tests/test_history.py | 8 ++- IPython/core/tests/test_logger.py | 3 +- IPython/core/tests/test_paths.py | 4 +- IPython/core/tests/test_profile.py | 7 +-- IPython/core/tests/test_run.py | 8 ++- IPython/core/tests/test_ultratb.py | 9 ++- IPython/lib/tests/test_deepreload.py | 9 +-- IPython/utils/tempdir.py | 3 +- IPython/utils/tests/test_path.py | 10 +-- 13 files changed, 85 insertions(+), 65 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 902da1bd9a1..7d5a7d340b9 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -20,34 +20,53 @@ import os import re import runpy +import subprocess import sys import tempfile import traceback import types -import subprocess import warnings +from ast import stmt from io import open as io_open - +from logging import error from pathlib import Path -from pickleshare import PickleShareDB +from typing import Callable +from typing import List as ListType +from typing import Optional, Tuple +from warnings import warn +from pickleshare import PickleShareDB +from tempfile import TemporaryDirectory +from traitlets import ( + Any, + Bool, + CaselessStrEnum, + Dict, + Enum, + Instance, + Integer, + List, + Type, + Unicode, + default, + observe, + validate, +) from traitlets.config.configurable import SingletonConfigurable from traitlets.utils.importstring import import_item -from IPython.core import oinspect -from IPython.core import magic -from IPython.core import page -from IPython.core import prefilter -from IPython.core import ultratb + +import IPython.core.hooks +from IPython.core import magic, oinspect, page, prefilter, ultratb from IPython.core.alias import Alias, AliasManager from IPython.core.autocall import ExitAutocall from IPython.core.builtin_trap import BuiltinTrap -from IPython.core.events import EventManager, available_events from IPython.core.compilerop import CachingCompiler, check_linecache_ipython from IPython.core.debugger import InterruptiblePdb from IPython.core.display_trap import DisplayTrap from IPython.core.displayhook import DisplayHook from IPython.core.displaypub import DisplayPublisher from IPython.core.error import InputRejected, UsageError +from IPython.core.events import EventManager, available_events from IPython.core.extensions import ExtensionManager from IPython.core.formatters import DisplayFormatter from IPython.core.history import HistoryManager @@ -59,31 +78,17 @@ from IPython.core.profiledir import ProfileDir from IPython.core.usage import default_banner from IPython.display import display +from IPython.paths import get_ipython_dir from IPython.testing.skipdoctest import skip_doctest -from IPython.utils import PyColorize -from IPython.utils import io -from IPython.utils import py3compat -from IPython.utils import openpy +from IPython.utils import PyColorize, io, openpy, py3compat from IPython.utils.decorators import undoc from IPython.utils.io import ask_yes_no from IPython.utils.ipstruct import Struct -from IPython.paths import get_ipython_dir -from IPython.utils.path import get_home_dir, get_py_filename, ensure_dir_exists -from IPython.utils.process import system, getoutput +from IPython.utils.path import ensure_dir_exists, get_home_dir, get_py_filename +from IPython.utils.process import getoutput, system from IPython.utils.strdispatch import StrDispatch from IPython.utils.syspathcontext import prepended_to_syspath -from IPython.utils.text import format_screen, LSString, SList, DollarFormatter -from IPython.utils.tempdir import TemporaryDirectory -from traitlets import ( - Integer, Bool, CaselessStrEnum, Enum, List, Dict, Unicode, Instance, Type, - observe, default, validate, Any -) -from warnings import warn -from logging import error -import IPython.core.hooks - -from typing import List as ListType, Tuple, Optional, Callable -from ast import stmt +from IPython.utils.text import DollarFormatter, LSString, SList, format_screen sphinxify: Optional[Callable] @@ -122,8 +127,13 @@ class ProvisionalWarning(DeprecationWarning): # we still need to run things using the asyncio eventloop, but there is no # async integration -from .async_helpers import _asyncio_runner, _pseudo_sync_runner -from .async_helpers import _curio_runner, _trio_runner, _should_be_async +from .async_helpers import ( + _asyncio_runner, + _curio_runner, + _pseudo_sync_runner, + _should_be_async, + _trio_runner, +) #----------------------------------------------------------------------------- # Globals @@ -2035,8 +2045,12 @@ def init_completer(self): (typically over the network by remote frontends). """ from IPython.core.completer import IPCompleter - from IPython.core.completerlib import (module_completer, - magic_run_completer, cd_completer, reset_completer) + from IPython.core.completerlib import ( + cd_completer, + magic_run_completer, + module_completer, + reset_completer, + ) self.Completer = IPCompleter(shell=self, namespace=self.user_ns, @@ -3344,8 +3358,9 @@ def enable_matplotlib(self, gui=None): make sense in all contexts, for example a terminal ipython can't display figures inline. """ - from IPython.core import pylabtools as pt from matplotlib_inline.backend_inline import configure_inline_support + + from IPython.core import pylabtools as pt gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select) if gui != 'inline': diff --git a/IPython/core/tests/test_application.py b/IPython/core/tests/test_application.py index 891908e98e7..07ad291659f 100644 --- a/IPython/core/tests/test_application.py +++ b/IPython/core/tests/test_application.py @@ -4,11 +4,11 @@ import os import tempfile +from tempfile import TemporaryDirectory from traitlets import Unicode from IPython.core.application import BaseIPythonApplication from IPython.testing import decorators as dec -from IPython.utils.tempdir import TemporaryDirectory @dec.onlyif_unicode_paths diff --git a/IPython/core/tests/test_completerlib.py b/IPython/core/tests/test_completerlib.py index b5508447895..827204ccfa6 100644 --- a/IPython/core/tests/test_completerlib.py +++ b/IPython/core/tests/test_completerlib.py @@ -14,8 +14,9 @@ import unittest from os.path import join +from tempfile import TemporaryDirectory + from IPython.core.completerlib import magic_run_completer, module_completion, try_import -from IPython.utils.tempdir import TemporaryDirectory from IPython.testing.decorators import onlyif_unicode_paths diff --git a/IPython/core/tests/test_extension.py b/IPython/core/tests/test_extension.py index 51db4b6258c..8297bc4d40c 100644 --- a/IPython/core/tests/test_extension.py +++ b/IPython/core/tests/test_extension.py @@ -1,8 +1,9 @@ import os.path +from tempfile import TemporaryDirectory + import IPython.testing.tools as tt from IPython.utils.syspathcontext import prepended_to_syspath -from IPython.utils.tempdir import TemporaryDirectory ext1_content = """ def load_ipython_extension(ip): diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index 6d6a1b1dd15..388ebc64acd 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -7,17 +7,19 @@ # stdlib import io -from pathlib import Path +import sqlite3 import sys import tempfile from datetime import datetime -import sqlite3 +from pathlib import Path +from tempfile import TemporaryDirectory # our own packages from traitlets.config.loader import Config -from IPython.utils.tempdir import TemporaryDirectory + from IPython.core.history import HistoryManager, extract_hist_ranges + def test_proper_default_encoding(): assert sys.getdefaultencoding() == "utf-8" diff --git a/IPython/core/tests/test_logger.py b/IPython/core/tests/test_logger.py index 71c4e6b60c0..10e462098be 100644 --- a/IPython/core/tests/test_logger.py +++ b/IPython/core/tests/test_logger.py @@ -2,9 +2,10 @@ """Test IPython.core.logger""" import os.path + import pytest +from tempfile import TemporaryDirectory -from IPython.utils.tempdir import TemporaryDirectory def test_logstart_inaccessible_file(): with pytest.raises(IOError): diff --git a/IPython/core/tests/test_paths.py b/IPython/core/tests/test_paths.py index d1366ee34ef..eb754b81529 100644 --- a/IPython/core/tests/test_paths.py +++ b/IPython/core/tests/test_paths.py @@ -6,11 +6,11 @@ import warnings from unittest.mock import patch -from testpath import modified_env, assert_isdir, assert_isfile +from tempfile import TemporaryDirectory +from testpath import assert_isdir, assert_isfile, modified_env from IPython import paths from IPython.testing.decorators import skip_win32 -from IPython.utils.tempdir import TemporaryDirectory TMP_TEST_DIR = os.path.realpath(tempfile.mkdtemp()) HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir") diff --git a/IPython/core/tests/test_profile.py b/IPython/core/tests/test_profile.py index 8dd58cc67f4..8734ec3513e 100644 --- a/IPython/core/tests/test_profile.py +++ b/IPython/core/tests/test_profile.py @@ -23,17 +23,16 @@ import shutil import sys import tempfile - from pathlib import Path from unittest import TestCase -from IPython.core.profileapp import list_profiles_in, list_bundled_profiles -from IPython.core.profiledir import ProfileDir +from tempfile import TemporaryDirectory +from IPython.core.profileapp import list_bundled_profiles, list_profiles_in +from IPython.core.profiledir import ProfileDir from IPython.testing import decorators as dec from IPython.testing import tools as tt from IPython.utils.process import getoutput -from IPython.utils.tempdir import TemporaryDirectory #----------------------------------------------------------------------------- # Globals diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py index 0f73a781e3e..c7841759170 100644 --- a/IPython/core/tests/test_run.py +++ b/IPython/core/tests/test_run.py @@ -19,21 +19,22 @@ import functools import os import platform -from os.path import join as pjoin import random import string import sys import textwrap import unittest +from os.path import join as pjoin from unittest.mock import patch import pytest +from tempfile import TemporaryDirectory +from IPython.core import debugger from IPython.testing import decorators as dec from IPython.testing import tools as tt from IPython.utils.io import capture_output -from IPython.utils.tempdir import TemporaryDirectory -from IPython.core import debugger + def doctest_refbug(): """Very nasty problem with references held by multiple runs of a script. @@ -411,6 +412,7 @@ def test_run_nb_error(self): """Test %run notebook.ipynb error""" pytest.importorskip("nbformat") from nbformat import v4, writes + # %run when a file name isn't provided pytest.raises(Exception, _ip.magic, "run") diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index 423998019b5..4b31ef0cd93 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -3,21 +3,20 @@ """ import io import logging +import os.path import platform import re import sys -import os.path -from textwrap import dedent import traceback import unittest +from textwrap import dedent -from IPython.core.ultratb import ColorTB, VerboseTB - +from tempfile import TemporaryDirectory +from IPython.core.ultratb import ColorTB, VerboseTB from IPython.testing import tools as tt from IPython.testing.decorators import onlyif_unicode_paths from IPython.utils.syspathcontext import prepended_to_syspath -from IPython.utils.tempdir import TemporaryDirectory file_1 = """1 2 diff --git a/IPython/lib/tests/test_deepreload.py b/IPython/lib/tests/test_deepreload.py index 9759a7f69f7..b8097905787 100644 --- a/IPython/lib/tests/test_deepreload.py +++ b/IPython/lib/tests/test_deepreload.py @@ -4,14 +4,15 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -import pytest import types - from pathlib import Path +import pytest +from tempfile import TemporaryDirectory + +from IPython.lib.deepreload import modules_reloading +from IPython.lib.deepreload import reload as dreload from IPython.utils.syspathcontext import prepended_to_syspath -from IPython.utils.tempdir import TemporaryDirectory -from IPython.lib.deepreload import reload as dreload, modules_reloading def test_deepreload(): diff --git a/IPython/utils/tempdir.py b/IPython/utils/tempdir.py index bbfffe9c923..b3e151de59e 100644 --- a/IPython/utils/tempdir.py +++ b/IPython/utils/tempdir.py @@ -10,8 +10,7 @@ class NamedFileInTemporaryDirectory(object): - - def __init__(self, filename, mode='w+b', bufsize=-1, **kwds): + def __init__(self, filename, mode="w+b", bufsize=-1, add_to_syspath=False, **kwds): """ Open a file named `filename` in a temporary directory. diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index b27e4355383..16fcd5f341f 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -10,24 +10,23 @@ import tempfile import unittest from contextlib import contextmanager -from unittest.mock import patch -from os.path import join, abspath from importlib import reload +from os.path import abspath, join +from unittest.mock import patch import pytest +from tempfile import TemporaryDirectory import IPython from IPython import paths from IPython.testing import decorators as dec from IPython.testing.decorators import ( + onlyif_unicode_paths, skip_if_not_win32, skip_win32, - onlyif_unicode_paths, ) from IPython.testing.tools import make_tempfile from IPython.utils import path -from IPython.utils.tempdir import TemporaryDirectory - # Platform-dependent imports try: @@ -41,6 +40,7 @@ import winreg as wreg except ImportError: import _winreg as wreg + #Add entries that needs to be stubbed by the testing code (wreg.OpenKey, wreg.QueryValueEx,) = (None, None) From 234b6375bf384c571eaacf984ed43503b98a8448 Mon Sep 17 00:00:00 2001 From: Nikita Kniazev Date: Wed, 9 Feb 2022 00:07:16 +0300 Subject: [PATCH 0065/1752] ipdoctest: Merge upstream changes to pytest doctest plugin --- IPython/testing/plugin/pytest_ipdoctest.py | 183 +++++++++++---------- 1 file changed, 93 insertions(+), 90 deletions(-) diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 603e33c0be4..230b159b36b 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -14,6 +14,7 @@ import types import warnings from contextlib import contextmanager +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -28,8 +29,6 @@ from typing import TYPE_CHECKING from typing import Union -import py.path - import pytest from _pytest import outcomes from _pytest._code.code import ExceptionInfo @@ -42,6 +41,7 @@ from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector from _pytest.outcomes import OutcomeException +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path from _pytest.python_api import approx from _pytest.warning_types import PytestWarning @@ -126,35 +126,38 @@ def pytest_unconfigure() -> None: def pytest_collect_file( - path: py.path.local, + file_path: Path, parent: Collector, ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: config = parent.config - if path.ext == ".py": - if config.option.ipdoctestmodules and not _is_setup_py(path): - mod: IPDoctestModule = IPDoctestModule.from_parent(parent, fspath=path) + if file_path.suffix == ".py": + if config.option.ipdoctestmodules and not any( + (_is_setup_py(file_path), _is_main_py(file_path)) + ): + mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path) return mod - elif _is_ipdoctest(config, path, parent): - txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, fspath=path) + elif _is_ipdoctest(config, file_path, parent): + txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path) return txt return None -def _is_setup_py(path: py.path.local) -> bool: - if path.basename != "setup.py": +def _is_setup_py(path: Path) -> bool: + if path.name != "setup.py": return False - contents = path.read_binary() + contents = path.read_bytes() return b"setuptools" in contents or b"distutils" in contents -def _is_ipdoctest(config: Config, path: py.path.local, parent) -> bool: - if path.ext in (".txt", ".rst") and parent.session.isinitpath(path): +def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool: + if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): return True globs = config.getoption("ipdoctestglob") or ["test*.txt"] - for glob in globs: - if path.check(fnmatch=glob): - return True - return False + return any(fnmatch_ex(glob, path) for glob in globs) + + +def _is_main_py(path: Path) -> bool: + return path.name == "__main__.py" class ReprFailDoctest(TerminalRepr): @@ -273,7 +276,7 @@ def from_parent( # type: ignore runner: "IPDocTestRunner", dtest: "doctest.DocTest", ): - # incompatible signature due to to imposed limits on sublcass + # incompatible signature due to imposed limits on subclass """The public named constructor.""" return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) @@ -372,61 +375,57 @@ def repr_failure( # type: ignore[override] elif isinstance(excinfo.value, MultipleDoctestFailures): failures = excinfo.value.failures - if failures is not None: - reprlocation_lines = [] - for failure in failures: - example = failure.example - test = failure.test - filename = test.filename - if test.lineno is None: - lineno = None - else: - lineno = test.lineno + example.lineno + 1 - message = type(failure).__name__ - # TODO: ReprFileLocation doesn't expect a None lineno. - reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] - checker = _get_checker() - report_choice = _get_report_choice( - self.config.getoption("ipdoctestreport") - ) - if lineno is not None: - assert failure.test.docstring is not None - lines = failure.test.docstring.splitlines(False) - # add line numbers to the left of the error message - assert test.lineno is not None - lines = [ - "%03d %s" % (i + test.lineno + 1, x) - for (i, x) in enumerate(lines) - ] - # trim docstring error lines to 10 - lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] - else: - lines = [ - "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" - ] - indent = ">>>" - for line in example.source.splitlines(): - lines.append(f"??? {indent} {line}") - indent = "..." - if isinstance(failure, doctest.DocTestFailure): - lines += checker.output_difference( - example, failure.got, report_choice - ).split("\n") - else: - inner_excinfo = ExceptionInfo(failure.exc_info) - lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] - lines += [ - x.strip("\n") - for x in traceback.format_exception(*failure.exc_info) - ] - reprlocation_lines.append((reprlocation, lines)) - return ReprFailDoctest(reprlocation_lines) - else: + if failures is None: return super().repr_failure(excinfo) - def reportinfo(self): + reprlocation_lines = [] + for failure in failures: + example = failure.example + test = failure.test + filename = test.filename + if test.lineno is None: + lineno = None + else: + lineno = test.lineno + example.lineno + 1 + message = type(failure).__name__ + # TODO: ReprFileLocation doesn't expect a None lineno. + reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] + checker = _get_checker() + report_choice = _get_report_choice(self.config.getoption("ipdoctestreport")) + if lineno is not None: + assert failure.test.docstring is not None + lines = failure.test.docstring.splitlines(False) + # add line numbers to the left of the error message + assert test.lineno is not None + lines = [ + "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines) + ] + # trim docstring error lines to 10 + lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] + else: + lines = [ + "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" + ] + indent = ">>>" + for line in example.source.splitlines(): + lines.append(f"??? {indent} {line}") + indent = "..." + if isinstance(failure, doctest.DocTestFailure): + lines += checker.output_difference( + example, failure.got, report_choice + ).split("\n") + else: + inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) + lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] + lines += [ + x.strip("\n") for x in traceback.format_exception(*failure.exc_info) + ] + reprlocation_lines.append((reprlocation, lines)) + return ReprFailDoctest(reprlocation_lines) + + def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: assert self.dtest is not None - return self.fspath, self.dtest.lineno, "[ipdoctest] %s" % self.name + return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name def _get_flag_lookup() -> Dict[str, int]: @@ -474,9 +473,9 @@ def collect(self) -> Iterable[IPDoctestItem]: # Inspired by doctest.testfile; ideally we would use it directly, # but it doesn't support passing a custom checker. encoding = self.config.getini("ipdoctest_encoding") - text = self.fspath.read_text(encoding) - filename = str(self.fspath) - name = self.fspath.basename + text = self.path.read_text(encoding) + filename = str(self.path) + name = self.path.name globs = {"__name__": "__main__"} optionflags = get_optionflags(self) @@ -559,15 +558,20 @@ class MockAwareDocTestFinder(DocTestFinder): def _find_lineno(self, obj, source_lines): """Doctest code does not take into account `@property`, this - is a hackish way to fix it. + is a hackish way to fix it. https://bugs.python.org/issue17446 - https://bugs.python.org/issue17446 + Wrapped Doctests will need to be unwrapped so the correct + line number is returned. This will be reported upstream. #8796 """ if isinstance(obj, property): obj = getattr(obj, "fget", obj) + + if hasattr(obj, "__wrapped__"): + # Get the main obj in case of it being wrapped + obj = inspect.unwrap(obj) + # Type ignored because this is a private function. - return DocTestFinder._find_lineno( # type: ignore - self, + return super()._find_lineno( # type:ignore[misc] obj, source_lines, ) @@ -580,20 +584,22 @@ def _find( with _patch_unwrap_mock_aware(): # Type ignored because this is a private function. - DocTestFinder._find( # type: ignore - self, tests, obj, name, module, source_lines, globs, seen + super()._find( # type:ignore[misc] + tests, obj, name, module, source_lines, globs, seen ) - if self.fspath.basename == "conftest.py": + if self.path.name == "conftest.py": module = self.config.pluginmanager._importconftest( - self.fspath, self.config.getoption("importmode") + self.path, + self.config.getoption("importmode"), + rootpath=self.config.rootpath, ) else: try: - module = import_path(self.fspath) + module = import_path(self.path, root=self.config.rootpath) except ImportError: if self.config.getvalue("ipdoctest_ignore_import_errors"): - pytest.skip("unable to import module %r" % self.fspath) + pytest.skip("unable to import module %r" % self.path) else: raise # Uses internal doctest module parsing mechanism. @@ -665,7 +671,7 @@ class LiteralsOutputChecker(IPDoctestOutputChecker): ) def check_output(self, want: str, got: str, optionflags: int) -> bool: - if IPDoctestOutputChecker.check_output(self, want, got, optionflags): + if super().check_output(want, got, optionflags): return True allow_unicode = optionflags & _get_allow_unicode_flag() @@ -689,7 +695,7 @@ def remove_prefixes(regex: Pattern[str], txt: str) -> str: if allow_number: got = self._remove_unwanted_precision(want, got) - return IPDoctestOutputChecker.check_output(self, want, got, optionflags) + return super().check_output(want, got, optionflags) def _remove_unwanted_precision(self, want: str, got: str) -> str: wants = list(self._number_re.finditer(want)) @@ -702,13 +708,10 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: exponent: Optional[str] = w.group("exponent1") if exponent is None: exponent = w.group("exponent2") - if fraction is None: - precision = 0 - else: - precision = len(fraction) + precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + if float(w.group()) == approx(float(g.group()), abs=10**-precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. From beb0b8251992ff8d2ee2c182666cb089781a382e Mon Sep 17 00:00:00 2001 From: Nikita Kniazev Date: Wed, 9 Feb 2022 01:24:16 +0300 Subject: [PATCH 0066/1752] ipdoctest: pytest<7 compatibility --- IPython/testing/plugin/pytest_ipdoctest.py | 83 ++++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 230b159b36b..809713d7c8e 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -142,6 +142,23 @@ def pytest_collect_file( return None +if int(pytest.__version__.split(".")[0]) < 7: + _collect_file = pytest_collect_file + + def pytest_collect_file( + path, + parent: Collector, + ) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]: + return _collect_file(Path(path), parent) + + _import_path = import_path + + def import_path(path, root): + import py.path + + return _import_path(py.path.local(path)) + + def _is_setup_py(path: Path) -> bool: if path.name != "setup.py": return False @@ -427,6 +444,12 @@ def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str assert self.dtest is not None return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name + if int(pytest.__version__.split(".")[0]) < 7: + + @property + def path(self) -> Path: + return Path(self.fspath) + def _get_flag_lookup() -> Dict[str, int]: import doctest @@ -494,6 +517,27 @@ def collect(self) -> Iterable[IPDoctestItem]: self, name=test.name, runner=runner, dtest=test ) + if int(pytest.__version__.split(".")[0]) < 7: + + @property + def path(self) -> Path: + return Path(self.fspath) + + @classmethod + def from_parent( + cls, + parent, + *, + fspath=None, + path: Optional[Path] = None, + **kw, + ): + if path is not None: + import py.path + + fspath = py.path.local(path) + return super().from_parent(parent=parent, fspath=fspath, **kw) + def _check_all_skipped(test: "doctest.DocTest") -> None: """Raise pytest.skip() if all examples in the given DocTest have the SKIP @@ -589,11 +633,17 @@ def _find( ) if self.path.name == "conftest.py": - module = self.config.pluginmanager._importconftest( - self.path, - self.config.getoption("importmode"), - rootpath=self.config.rootpath, - ) + if int(pytest.__version__.split(".")[0]) < 7: + module = self.config.pluginmanager._importconftest( + self.path, + self.config.getoption("importmode"), + ) + else: + module = self.config.pluginmanager._importconftest( + self.path, + self.config.getoption("importmode"), + rootpath=self.config.rootpath, + ) else: try: module = import_path(self.path, root=self.config.rootpath) @@ -618,6 +668,27 @@ def _find( self, name=test.name, runner=runner, dtest=test ) + if int(pytest.__version__.split(".")[0]) < 7: + + @property + def path(self) -> Path: + return Path(self.fspath) + + @classmethod + def from_parent( + cls, + parent, + *, + fspath=None, + path: Optional[Path] = None, + **kw, + ): + if path is not None: + import py.path + + fspath = py.path.local(path) + return super().from_parent(parent=parent, fspath=fspath, **kw) + def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest: """Used by IPDoctestTextfile and IPDoctestItem to setup fixture information.""" @@ -711,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10**-precision): + if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. From 31afadfb688639b1d0f72e9242144a3aadd4cde4 Mon Sep 17 00:00:00 2001 From: Nikita Kniazev Date: Wed, 9 Feb 2022 02:34:07 +0300 Subject: [PATCH 0067/1752] unpin pytest --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4fbffaa1a07..2da02e4e598 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ qtconsole = qtconsole terminal = test = - pytest<7 + pytest pytest-asyncio testpath test_extra = @@ -72,7 +72,7 @@ test_extra = nbformat numpy>=1.19 pandas - pytest<7 + pytest testpath trio all = From dc84061e9da02efcd601f9a94e149a47bbd54acb Mon Sep 17 00:00:00 2001 From: Artur Svistunov <18216480+madbird1304@users.noreply.github.com> Date: Wed, 9 Feb 2022 04:51:26 +0300 Subject: [PATCH 0068/1752] issue#13073: fix `%paste` magic under Wayland --- IPython/core/hooks.py | 4 ++-- IPython/lib/clipboard.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/IPython/core/hooks.py b/IPython/core/hooks.py index 09b08d942e0..0f1f5af2dc7 100644 --- a/IPython/core/hooks.py +++ b/IPython/core/hooks.py @@ -156,14 +156,14 @@ def clipboard_get(self): """ from ..lib.clipboard import ( osx_clipboard_get, tkinter_clipboard_get, - win32_clipboard_get + win32_clipboard_get, wayland_clipboard_get, ) if sys.platform == 'win32': chain = [win32_clipboard_get, tkinter_clipboard_get] elif sys.platform == 'darwin': chain = [osx_clipboard_get, tkinter_clipboard_get] else: - chain = [tkinter_clipboard_get] + chain = [wayland_clipboard_get, tkinter_clipboard_get] dispatcher = CommandChainDispatcher() for func in chain: dispatcher.add(func) diff --git a/IPython/lib/clipboard.py b/IPython/lib/clipboard.py index 95a6b0a0a34..5d6630d17a7 100644 --- a/IPython/lib/clipboard.py +++ b/IPython/lib/clipboard.py @@ -67,3 +67,26 @@ def tkinter_clipboard_get(): return text +def wayland_clipboard_get(): + """Get the clipboard's text under Wayland using wl-paste command. + + This requires Wayland and wl-clipboard installed and running. + """ + if os.environ.get("XDG_SESSION_TYPE") != "wayland": + raise TryNext("wayland is not detected") + + try: + with subprocess.Popen(["wl-paste"], stdout=subprocess.PIPE) as p: + raw, _ = p.communicate() + except FileNotFoundError as e: + raise ClipboardEmpty( + "Getting text from the clipboard under Wayland requires the wl-clipboard " + "extension: https://github.com/bugaevc/wl-clipboard" + ) from e + + try: + text = py3compat.decode(raw) + except UnicodeDecodeError as e: + raise ClipboardEmpty from e + + return text From b2f71a87af023177d38196dcf8c9c1a725b8cf59 Mon Sep 17 00:00:00 2001 From: luz paz Date: Wed, 9 Feb 2022 13:59:30 -0500 Subject: [PATCH 0069/1752] Fix various typos Found via `codespell -q 3` --- .github/workflows/downstream.yml | 2 +- IPython/core/completer.py | 4 ++-- IPython/extensions/tests/test_autoreload.py | 2 +- SECURITY.md | 2 +- appveyor.yml | 2 +- docs/source/sphinxext.rst | 2 +- tools/release_helper.sh | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 5c5adc1e0ab..309d03a2204 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -39,7 +39,7 @@ jobs: - name: Install and update Python dependencies run: | python -m pip install --upgrade -e file://$PWD#egg=ipython[test] - # we must instal IPython after ipykernel to get the right versions. + # we must install IPython after ipykernel to get the right versions. python -m pip install --upgrade --upgrade-strategy eager flaky ipyparallel python -m pip install --upgrade 'pytest<7' - name: pytest diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 9774fd5a4e7..89b55d2b475 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -1182,7 +1182,7 @@ def __init__( # This is a list of names of unicode characters that can be completed # into their corresponding unicode value. The list is large, so we - # laziliy initialize it on first use. Consuming code should access this + # lazily initialize it on first use. Consuming code should access this # attribute through the `@unicode_names` property. self._unicode_names = None @@ -2070,7 +2070,7 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, indexed. line_buffer : optional, str The current line the cursor is in, this is mostly due to legacy - reason that readline coudl only give a us the single current line. + reason that readline could only give a us the single current line. Prefer `full_text`. text : str The current "token" the cursor is in, mostly also for historical diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index a0fe725aa68..88637fbab9c 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -33,7 +33,7 @@ if platform.python_implementation() == "PyPy": pytest.skip( - "Current autoreload implementation is extremly slow on PyPy", + "Current autoreload implementation is extremely slow on PyPy", allow_module_level=True, ) diff --git a/SECURITY.md b/SECURITY.md index a4b9435a975..dc5db66e2a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,4 +3,4 @@ ## Reporting a Vulnerability All IPython and Jupyter security are handled via security@ipython.org. -You can find more informations on the Jupyter website. https://jupyter.org/security +You can find more information on the Jupyter website. https://jupyter.org/security diff --git a/appveyor.yml b/appveyor.yml index b1c0abe0b6c..7637841b2fd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ matrix: environment: global: APPVEYOR_BUILD_WORKER_IMAGE: 'Visual Studio 2022' - COLUMNS: 120 # Appveyor web viwer window width is 130 chars + COLUMNS: 120 # Appveyor web viewer window width is 130 chars matrix: - PYTHON: "C:\\Python38" diff --git a/docs/source/sphinxext.rst b/docs/source/sphinxext.rst index b2012fa62f6..093e04a90fc 100644 --- a/docs/source/sphinxext.rst +++ b/docs/source/sphinxext.rst @@ -204,7 +204,7 @@ suppress the seed line so it doesn't show up in the rendered output [0.22591016, 0.77731835], [0.0072729 , 0.34273127]]) -For more information on @supress and @doctest decorators, please refer to the end of this file in +For more information on @suppress and @doctest decorators, please refer to the end of this file in Pseudo-Decorators section. Another demonstration of multi-line input and output diff --git a/tools/release_helper.sh b/tools/release_helper.sh index a9208449f93..54114d18bb8 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -111,7 +111,7 @@ then sleep 1 echo $BLUE"Saving API to file $PREV_RELEASE"$NOR frappuccino IPython IPython.kernel IPython.lib IPython.qt IPython.lib.kernel IPython.html IPython.frontend IPython.external --save IPython-$PREV_RELEASE.json - echo $BLUE"comming back to $BRANCH"$NOR + echo $BLUE"coming back to $BRANCH"$NOR git checkout $BRANCH sleep 1 echo $BLUE"comparing ..."$NOR From 89f7392432db54236f7f82332700d3be0d2d88a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=84lgmyr?= Date: Sat, 12 Feb 2022 02:52:24 +0100 Subject: [PATCH 0070/1752] Add support for autoformatting using yapf --- IPython/terminal/interactiveshell.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 4a46f2702cd..4498888fcc7 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -116,6 +116,20 @@ def black_reformat_handler(text_before_cursor): return formatted_text +def yapf_reformat_handler(text_before_cursor): + from yapf.yapflib import file_resources + from yapf.yapflib import yapf_api + + style_config = file_resources.GetDefaultStyleForDir(os.getcwd()) + formatted_text, was_formatted = yapf_api.FormatCode(text_before_cursor, style_config=style_config) + if was_formatted: + if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"): + formatted_text = formatted_text[:-1] + return formatted_text + else: + return text_before_cursor + + class TerminalInteractiveShell(InteractiveShell): mime_renderers = Dict().tag(config=True) @@ -185,7 +199,7 @@ def debugger_cls(self): autoformatter = Unicode( "black", - help="Autoformatter to reformat Terminal code. Can be `'black'` or `None`", + help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`", allow_none=True ).tag(config=True) @@ -232,6 +246,8 @@ def _set_formatter(self, formatter): self.reformat_handler = lambda x:x elif formatter == 'black': self.reformat_handler = black_reformat_handler + elif formatter == 'yapf': + self.reformat_handler = yapf_reformat_handler else: raise ValueError From a036e1fb432b1951caa6730ea0909be5d53917ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=84lgmyr?= Date: Sat, 12 Feb 2022 03:00:20 +0100 Subject: [PATCH 0071/1752] Fix some formatting --- IPython/terminal/interactiveshell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 4498888fcc7..03e91dc6830 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -121,7 +121,9 @@ def yapf_reformat_handler(text_before_cursor): from yapf.yapflib import yapf_api style_config = file_resources.GetDefaultStyleForDir(os.getcwd()) - formatted_text, was_formatted = yapf_api.FormatCode(text_before_cursor, style_config=style_config) + formatted_text, was_formatted = yapf_api.FormatCode( + text_before_cursor, style_config=style_config + ) if was_formatted: if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"): formatted_text = formatted_text[:-1] From 5b6dbf75bcbf153cadf509962e95c50a3d9bd68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=84lgmyr?= Date: Sat, 12 Feb 2022 16:16:52 +0100 Subject: [PATCH 0072/1752] Fix darker complaint, even though inconsistent --- IPython/terminal/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 03e91dc6830..37773023db4 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -248,7 +248,7 @@ def _set_formatter(self, formatter): self.reformat_handler = lambda x:x elif formatter == 'black': self.reformat_handler = black_reformat_handler - elif formatter == 'yapf': + elif formatter == "yapf": self.reformat_handler = yapf_reformat_handler else: raise ValueError From 83684a4776fddbbbbcaa0a47fa3914262fc5cf2a Mon Sep 17 00:00:00 2001 From: "Samuel B. Johnson" Date: Thu, 17 Feb 2022 20:13:30 -0600 Subject: [PATCH 0073/1752] :bug: Remove `ls` from list of system commands to warn about --- IPython/core/interactiveshell.py | 9 ++------- IPython/core/tests/test_interactiveshell.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 61e15415000..a87db36d0b0 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2414,14 +2414,9 @@ def system_raw(self, cmd): cmd = self.var_expand(cmd, depth=1) # warn if there is an IPython magic alternative. main_cmd = cmd.split()[0] - has_magic_alternatives = ("pip", "conda", "cd", "ls") + has_magic_alternatives = ("pip", "conda", "cd") - # had to check if the command was an alias expanded because of `ls` - is_alias_expanded = self.alias_manager.is_alias(main_cmd) and ( - self.alias_manager.retrieve_alias(main_cmd).strip() == cmd.strip() - ) - - if main_cmd in has_magic_alternatives and not is_alias_expanded: + if main_cmd in has_magic_alternatives: warnings.warn( ( "You executed the system command !{0} which may not work " diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 09dbd967706..230a498cfcc 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -623,7 +623,7 @@ def test_control_c(self, *mocks): self.assertEqual(ip.user_ns["_exit_code"], -signal.SIGINT) def test_magic_warnings(self): - for magic_cmd in ("ls", "pip", "conda", "cd"): + for magic_cmd in ("pip", "conda", "cd"): with self.assertWarnsRegex(Warning, "You executed the system command"): ip.system_raw(magic_cmd) From d11e987f174a15f1640f8006c86f58d884c3faa4 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Thu, 17 Feb 2022 16:39:32 -0300 Subject: [PATCH 0074/1752] Set co_name for cells run line by line. Fixes https://github.com/ipython/ipykernel/issues/841 --- IPython/core/interactiveshell.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 61e15415000..a50816f181e 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -15,6 +15,7 @@ import ast import atexit import builtins as builtin_mod +import dis import functools import inspect import os @@ -3141,6 +3142,29 @@ def transform_ast(self, node): ast.fix_missing_locations(node) return node + def _update_code_co_name(self, code): + """Python 3.10 changed the behaviour so that whenever a code object + is assembled in the compile(ast) the co_firstlineno would be == 1. + + This makes pydevd/debugpy think that all cells invoked are the same + since it caches information based on (co_firstlineno, co_name, co_filename). + + Given that, this function changes the code 'co_name' to be unique + based on the first real lineno of the code (which also has a nice + side effect of customizing the name so that it's not always ). + + See: https://github.com/ipython/ipykernel/issues/841 + """ + if not hasattr(code, "replace"): + # It may not be available on older versions of Python (only + # available for 3.8 onwards). + return code + try: + first_real_line = next(dis.findlinestarts(code))[1] + except StopIteration: + return code + return code.replace(co_name="" % (first_real_line,)) + async def run_ast_nodes( self, nodelist: ListType[stmt], @@ -3239,6 +3263,7 @@ def compare(code): else 0x0 ): code = compiler(mod, cell_name, mode) + code = self._update_code_co_name(code) asy = compare(code) if await self.run_code(code, result, async_=asy): return True From 258a093e0e4fd27ca7ee24da9793609f811d2856 Mon Sep 17 00:00:00 2001 From: Nathan Buckner Date: Sun, 20 Feb 2022 08:50:13 -0800 Subject: [PATCH 0075/1752] Fix for symlink resolving --- IPython/core/interactiveshell.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 61e15415000..26198831992 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -772,10 +772,13 @@ def init_virtualenv(self): # stdlib venv may symlink sys.executable, so we can't use realpath. # but others can symlink *to* the venv Python, so we can't just use sys.executable. # So we just check every item in the symlink tree (generally <= 3) + current_dir = Path(os.curdir).absolute() paths = [p] while p.is_symlink(): - p = Path(os.readlink(p)) - paths.append(p.resolve()) + os.chdir(p.parent) + p = Path(os.readlink(p)).absolute() + paths.append(p) + os.chdir(current_dir) # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible if p_venv.parts[1] == "cygdrive": From 3ad6bdca0e5f186e1205b7436668731539173111 Mon Sep 17 00:00:00 2001 From: Nathan Buckner Date: Mon, 21 Feb 2022 05:19:24 -0800 Subject: [PATCH 0076/1752] Removed chdir and changed to joining the path if not absolute path. This better follows the readlink docs. --- IPython/core/interactiveshell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 26198831992..42364eb960a 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -772,13 +772,13 @@ def init_virtualenv(self): # stdlib venv may symlink sys.executable, so we can't use realpath. # but others can symlink *to* the venv Python, so we can't just use sys.executable. # So we just check every item in the symlink tree (generally <= 3) - current_dir = Path(os.curdir).absolute() paths = [p] while p.is_symlink(): - os.chdir(p.parent) - p = Path(os.readlink(p)).absolute() + new_path = p.readlink() + if not new_path.is_absolute(): + new_path = p.parent / new_path + p = new_path paths.append(p) - os.chdir(current_dir) # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible if p_venv.parts[1] == "cygdrive": From 946e545b181fea95e876b15c51fc60a5a89bf934 Mon Sep 17 00:00:00 2001 From: Lucy McPhail Date: Mon, 21 Feb 2022 20:26:24 +0000 Subject: [PATCH 0077/1752] Improve auto_match for quotes Only insert a pair of quotes if there are an even number of quotes preceding the cursor. This way, if the cursor is inside an unclosed string, typing the closing quote will not insert a pair. --- IPython/terminal/shortcuts.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index ea204f8ddd8..274f9b250bb 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -139,12 +139,24 @@ def _(event): event.current_buffer.insert_text("{}") event.current_buffer.cursor_left() - @kb.add('"', filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) + @kb.add( + '"', + filter=focused_insert + & auto_match + & preceding_text(r'^([^"]+|"[^"]*")*$') + & following_text(r"[,)}\]]|$"), + ) def _(event): event.current_buffer.insert_text('""') event.current_buffer.cursor_left() - @kb.add("'", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) + @kb.add( + "'", + filter=focused_insert + & auto_match + & preceding_text(r"^([^']+|'[^']*')*$") + & following_text(r"[,)}\]]|$"), + ) def _(event): event.current_buffer.insert_text("''") event.current_buffer.cursor_left() @@ -186,16 +198,6 @@ def _(event): event.current_buffer.insert_text("{}" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) - @kb.add('"', filter=focused_insert & auto_match & preceding_text(r".*(r|R)$")) - def _(event): - event.current_buffer.insert_text('""') - event.current_buffer.cursor_left() - - @kb.add("'", filter=focused_insert & auto_match & preceding_text(r".*(r|R)$")) - def _(event): - event.current_buffer.insert_text("''") - event.current_buffer.cursor_left() - # just move cursor @kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)")) @kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]")) From 1cf66263c96b85749216150d72bf644d6349ed2d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 10:59:58 +0100 Subject: [PATCH 0078/1752] doc parssing issue --- IPython/utils/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/utils/text.py b/IPython/utils/text.py index a1754d254de..ef75f9331d7 100644 --- a/IPython/utils/text.py +++ b/IPython/utils/text.py @@ -471,7 +471,7 @@ def strip_ansi(source): class EvalFormatter(Formatter): """A String Formatter that allows evaluation of simple expressions. - Note that this version interprets a : as specifying a format string (as per + Note that this version interprets a `:` as specifying a format string (as per standard string formatting), so if slicing is required, you must explicitly create a slice. From bce649cba874ba7965ac894564e67e446a7854ff Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 11:17:06 +0100 Subject: [PATCH 0079/1752] formatting --- IPython/core/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 17aacfc3013..9012f0f20a2 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3680,7 +3680,7 @@ def atexit_operations(self): del self.tempdirs # Restore user's cursor - if hasattr(self, "editing_mode") and self.editing_mode == 'vi': + if hasattr(self, "editing_mode") and self.editing_mode == "vi": sys.stdout.write("\x1b[0 q") sys.stdout.flush() From 422f8d9cf8c3858aab934853780a872610df50e4 Mon Sep 17 00:00:00 2001 From: Can Sarigol Date: Mon, 24 Jan 2022 08:44:18 +0100 Subject: [PATCH 0080/1752] Get history file from shell to debugger if it exists. --- IPython/terminal/debugger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index d76550d878f..8448d96370d 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -68,6 +68,8 @@ def gen_comp(self, text): self.debugger_history = FileHistory(os.path.expanduser(str(p))) else: self.debugger_history = InMemoryHistory() + else: + self.debugger_history = self.shell.debugger_history options = dict( message=(lambda: PygmentsTokens(get_prompt_tokens())), From a9b523c7047fe12c49373972c6b092ed5fc29e99 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Wed, 16 Feb 2022 16:03:03 -0500 Subject: [PATCH 0081/1752] match only pseudo-decorators --- IPython/sphinxext/ipython_directive.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py index ac0964032a5..093fd7a362c 100644 --- a/IPython/sphinxext/ipython_directive.py +++ b/IPython/sphinxext/ipython_directive.py @@ -220,6 +220,8 @@ # for tokenizing blocks COMMENT, INPUT, OUTPUT = range(3) +PSEUDO_DECORATORS = ["suppress", "verbatim", "savefig", "doctest"] + #----------------------------------------------------------------------------- # Functions and class declarations #----------------------------------------------------------------------------- @@ -263,11 +265,14 @@ def block_parser(part, rgxin, rgxout, fmtin, fmtout): block.append((COMMENT, line)) continue - if line_stripped.startswith('@'): - # Here is where we assume there is, at most, one decorator. - # Might need to rethink this. - decorator = line_stripped - continue + if any( + line_stripped.startswith('@' + pseudo_decorator) for pseudo_decorator in PSEUDO_DECORATORS + ): + if decorator: + raise RuntimeError("Applying multiple pseudo-decorators on one line is not supported") + else: + decorator = line_stripped + continue # does this look like an input line? matchin = rgxin.match(line) From 5fec429e39671a5c359e6fc17ab59ece4e139847 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 11:27:37 +0100 Subject: [PATCH 0082/1752] reformatting --- IPython/sphinxext/ipython_directive.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py index 093fd7a362c..18bdfcae993 100644 --- a/IPython/sphinxext/ipython_directive.py +++ b/IPython/sphinxext/ipython_directive.py @@ -266,10 +266,13 @@ def block_parser(part, rgxin, rgxout, fmtin, fmtout): continue if any( - line_stripped.startswith('@' + pseudo_decorator) for pseudo_decorator in PSEUDO_DECORATORS - ): + line_stripped.startswith("@" + pseudo_decorator) + for pseudo_decorator in PSEUDO_DECORATORS + ): if decorator: - raise RuntimeError("Applying multiple pseudo-decorators on one line is not supported") + raise RuntimeError( + "Applying multiple pseudo-decorators on one line is not supported" + ) else: decorator = line_stripped continue From f6e3393f88b8d188726220c72e775a7a26c3249b Mon Sep 17 00:00:00 2001 From: ygeyzel Date: Sat, 22 Jan 2022 17:24:56 +0200 Subject: [PATCH 0083/1752] Typing '%' restrict autocompletion to magics --- IPython/core/completer.py | 5 +++-- IPython/core/tests/test_completer.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 89b55d2b475..0579e684e81 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -2139,8 +2139,9 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, # different types of objects. The rlcomplete() method could then # simply collapse the dict into a list for readline, but we'd have # richer completion semantics in other environments. - completions:Iterable[Any] = [] - if self.use_jedi: + is_magic_prefix = len(text) > 0 and text[0] == "%" + completions: Iterable[Any] = [] + if self.use_jedi and not is_magic_prefix: if not full_text: full_text = line_buffer completions = self._jedi_matches( diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 5f791e84f83..746a1e68261 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -1262,3 +1262,14 @@ def meth_2(self, meth2_arg1, meth2_arg2): _, matches = ip.complete(None, "test.meth(") self.assertIn("meth_arg1=", matches) self.assertNotIn("meth2_arg1=", matches) + + def test_percent_symbol_restrict_to_magic_completions(self): + ip = get_ipython() + completer = ip.Completer + text = "%a" + + with provisionalcompleter(): + completer.use_jedi = True + completions = completer.completions(text, len(text)) + for c in completions: + self.assertEqual(c.text[0], "%") From 5ccd62a38c58306166898a417e115707e3e673fe Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 15 Jan 2022 10:09:31 +0000 Subject: [PATCH 0084/1752] Revert "enable formatting by default" This reverts PR #13397 / commit df3248f00a60859ed07f81e87531856b30891396, whilst leaving in place the internal improvements. --- IPython/terminal/interactiveshell.py | 2 +- docs/source/whatsnew/version8.rst | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 37773023db4..212692ab6d1 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -200,7 +200,7 @@ def debugger_cls(self): ).tag(config=True) autoformatter = Unicode( - "black", + None, help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`", allow_none=True ).tag(config=True) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 7b2318f3dcd..12c5d1f17c7 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -344,12 +344,11 @@ For more information please see the following unit test : ``extensions/tests/tes Auto formatting with black in the CLI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If ``black`` is installed in the same environment as IPython, terminal IPython -will now *by default* reformat the code in the CLI when possible. You can -disable this with ``--TerminalInteractiveShell.autoformatter=None``. - This feature was present in 7.x, but disabled by default. +In 8.0, input was automatically reformatted with Black when black was installed. +This feature has been reverted for the time being. +You can re-enable it by setting ``TerminalInteractiveShell.autoformatter`` to ``"black"`` History Range Glob feature ~~~~~~~~~~~~~~~~~~~~~~~~~~ From 96cbe589e4b201af115a443bcaa3bab6f9fcc697 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 11:39:18 +0100 Subject: [PATCH 0085/1752] Remove IPython/lib/security. This should be part of Jupyter notebook / jupyter lab, and has no reason to stay here. It might even be dangerous as it is not maintained here. --- IPython/lib/__init__.py | 10 --- IPython/lib/security.py | 114 ----------------------------- IPython/lib/tests/test_security.py | 27 ------- 3 files changed, 151 deletions(-) delete mode 100644 IPython/lib/security.py delete mode 100644 IPython/lib/tests/test_security.py diff --git a/IPython/lib/__init__.py b/IPython/lib/__init__.py index 8eb89012df1..94b8ade4ec9 100644 --- a/IPython/lib/__init__.py +++ b/IPython/lib/__init__.py @@ -9,13 +9,3 @@ # Distributed under the terms of the BSD License. The full license is in # the file COPYING, distributed as part of this software. #----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - -from IPython.lib.security import passwd - -#----------------------------------------------------------------------------- -# Code -#----------------------------------------------------------------------------- diff --git a/IPython/lib/security.py b/IPython/lib/security.py deleted file mode 100644 index 152561dabad..00000000000 --- a/IPython/lib/security.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Password generation for the IPython notebook. -""" -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- -# Stdlib -import getpass -import hashlib -import random - -# Our own -from IPython.core.error import UsageError -from IPython.utils.py3compat import encode - -#----------------------------------------------------------------------------- -# Globals -#----------------------------------------------------------------------------- - -# Length of the salt in nr of hex chars, which implies salt_len * 4 -# bits of randomness. -salt_len = 12 - -#----------------------------------------------------------------------------- -# Functions -#----------------------------------------------------------------------------- - -def passwd(passphrase=None, algorithm='sha1'): - """Generate hashed password and salt for use in notebook configuration. - - In the notebook configuration, set `c.NotebookApp.password` to - the generated string. - - Parameters - ---------- - passphrase : str - Password to hash. If unspecified, the user is asked to input - and verify a password. - algorithm : str - Hashing algorithm to use (e.g, 'sha1' or any argument supported - by :func:`hashlib.new`). - - Returns - ------- - hashed_passphrase : str - Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'. - - Examples - -------- - >>> passwd('mypassword') - 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12' # random - - """ - if passphrase is None: - for i in range(3): - p0 = getpass.getpass('Enter password: ') - p1 = getpass.getpass('Verify password: ') - if p0 == p1: - passphrase = p0 - break - else: - print('Passwords do not match.') - else: - raise UsageError('No matching passwords found. Giving up.') - - h = hashlib.new(algorithm) - salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len) - h.update(encode(passphrase, 'utf-8') + encode(salt, 'ascii')) - - return ':'.join((algorithm, salt, h.hexdigest())) - - -def passwd_check(hashed_passphrase, passphrase): - """Verify that a given passphrase matches its hashed version. - - Parameters - ---------- - hashed_passphrase : str - Hashed password, in the format returned by `passwd`. - passphrase : str - Passphrase to validate. - - Returns - ------- - valid : bool - True if the passphrase matches the hash. - - Examples - -------- - >>> from IPython.lib.security import passwd_check - >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', - ... 'mypassword') - True - - >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a', - ... 'anotherpassword') - False - """ - try: - algorithm, salt, pw_digest = hashed_passphrase.split(':', 2) - except (ValueError, TypeError): - return False - - try: - h = hashlib.new(algorithm) - except ValueError: - return False - - if len(pw_digest) == 0: - return False - - h.update(encode(passphrase, 'utf-8') + encode(salt, 'ascii')) - - return h.hexdigest() == pw_digest diff --git a/IPython/lib/tests/test_security.py b/IPython/lib/tests/test_security.py deleted file mode 100644 index 27c32ab7328..00000000000 --- a/IPython/lib/tests/test_security.py +++ /dev/null @@ -1,27 +0,0 @@ -# coding: utf-8 -from IPython.lib import passwd -from IPython.lib.security import passwd_check, salt_len - -def test_passwd_structure(): - p = passwd("passphrase") - algorithm, salt, hashed = p.split(":") - assert algorithm == "sha1" - assert len(salt) == salt_len - assert len(hashed) == 40 - -def test_roundtrip(): - p = passwd("passphrase") - assert passwd_check(p, "passphrase") is True - - -def test_bad(): - p = passwd('passphrase') - assert passwd_check(p, p) is False - assert passwd_check(p, "a:b:c:d") is False - assert passwd_check(p, "a:b") is False - - -def test_passwd_check_unicode(): - # GH issue #4524 - phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' - assert passwd_check(phash, u"łe¶ŧ←↓→") From 44d64dc36e727cde56d7b1986ee52228ddde1af9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 11:56:58 +0100 Subject: [PATCH 0086/1752] Update some deprecated ip.magic() to run_line-magic in tests --- IPython/core/tests/test_alias.py | 8 ++-- IPython/core/tests/test_handlers.py | 64 ++++++++++++++++------------- IPython/core/tests/test_history.py | 12 +++--- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/IPython/core/tests/test_alias.py b/IPython/core/tests/test_alias.py index a84b0095334..32d2e2f711e 100644 --- a/IPython/core/tests/test_alias.py +++ b/IPython/core/tests/test_alias.py @@ -43,11 +43,11 @@ def test_alias_args_error(): def test_alias_args_commented(): """Check that alias correctly ignores 'commented out' args""" - _ip.magic('alias commetarg echo this is %%s a commented out arg') - + _ip.run_line_magic("alias", "commentarg echo this is %%s a commented out arg") + with capture_output() as cap: - _ip.run_cell('commetarg') - + _ip.run_cell("commentarg") + # strip() is for pytest compat; testing via iptest patch IPython shell # in testing.globalipapp and replace the system call which messed up the # \r\n diff --git a/IPython/core/tests/test_handlers.py b/IPython/core/tests/test_handlers.py index e151e70ee91..604dadee1ab 100644 --- a/IPython/core/tests/test_handlers.py +++ b/IPython/core/tests/test_handlers.py @@ -56,36 +56,42 @@ def test_handlers(): ip.user_ns['autocallable'] = autocallable # auto - ip.magic('autocall 0') + ip.run_line_magic("autocall", "0") # Only explicit escapes or instances of IPyAutocallable should get # expanded - run([ - ('len "abc"', 'len "abc"'), - ('autocallable', 'autocallable()'), - # Don't add extra brackets (gh-1117) - ('autocallable()', 'autocallable()'), - ]) - ip.magic('autocall 1') - run([ - ('len "abc"', 'len("abc")'), - ('len "abc";', 'len("abc");'), # ; is special -- moves out of parens - # Autocall is turned off if first arg is [] and the object - # is both callable and indexable. Like so: - ('len [1,2]', 'len([1,2])'), # len doesn't support __getitem__... - ('call_idx [1]', 'call_idx [1]'), # call_idx *does*.. - ('call_idx 1', 'call_idx(1)'), - ('len', 'len'), # only at 2 does it auto-call on single args - ]) - ip.magic('autocall 2') - run([ - ('len "abc"', 'len("abc")'), - ('len "abc";', 'len("abc");'), - ('len [1,2]', 'len([1,2])'), - ('call_idx [1]', 'call_idx [1]'), - ('call_idx 1', 'call_idx(1)'), - # This is what's different: - ('len', 'len()'), # only at 2 does it auto-call on single args - ]) - ip.magic('autocall 1') + run( + [ + ('len "abc"', 'len "abc"'), + ("autocallable", "autocallable()"), + # Don't add extra brackets (gh-1117) + ("autocallable()", "autocallable()"), + ] + ) + ip.run_line_magic("autocall", "1") + run( + [ + ('len "abc"', 'len("abc")'), + ('len "abc";', 'len("abc");'), # ; is special -- moves out of parens + # Autocall is turned off if first arg is [] and the object + # is both callable and indexable. Like so: + ("len [1,2]", "len([1,2])"), # len doesn't support __getitem__... + ("call_idx [1]", "call_idx [1]"), # call_idx *does*.. + ("call_idx 1", "call_idx(1)"), + ("len", "len"), # only at 2 does it auto-call on single args + ] + ) + ip.run_line_magic("autocall", "2") + run( + [ + ('len "abc"', 'len("abc")'), + ('len "abc";', 'len("abc");'), + ("len [1,2]", "len([1,2])"), + ("call_idx [1]", "call_idx [1]"), + ("call_idx 1", "call_idx(1)"), + # This is what's different: + ("len", "len()"), # only at 2 does it auto-call on single args + ] + ) + ip.run_line_magic("autocall", "1") assert failures == [] diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index 388ebc64acd..73d50c87d34 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -52,13 +52,13 @@ def test_history(): # Check whether specifying a range beyond the end of the current # session results in an error (gh-804) - ip.magic('%hist 2-500') + ip.run_line_magic("hist", "2-500") # Check that we can write non-ascii characters to a file - ip.magic("%%hist -f %s" % (tmp_path / "test1")) - ip.magic("%%hist -pf %s" % (tmp_path / "test2")) - ip.magic("%%hist -nf %s" % (tmp_path / "test3")) - ip.magic("%%save %s 1-10" % (tmp_path / "test4")) + ip.run_line_magic("hist", "-f %s" % (tmp_path / "test1")) + ip.run_line_magic("hist", "-pf %s" % (tmp_path / "test2")) + ip.run_line_magic("hist", "-nf %s" % (tmp_path / "test3")) + ip.run_line_magic("save", "%s 1-10" % (tmp_path / "test4")) # New session ip.history_manager.reset() @@ -124,7 +124,7 @@ def test_history(): # Cross testing: check that magic %save can get previous session. testfilename = (tmp_path / "test.py").resolve() - ip.magic("save " + str(testfilename) + " ~1/1-3") + ip.run_line_magic("save", str(testfilename) + " ~1/1-3") with io.open(testfilename, encoding="utf-8") as testfile: assert testfile.read() == "# coding: utf-8\n" + "\n".join(hist) + "\n" From 88897b13403f10e426cd4fb5a5d5d3e205cb8b92 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 12:39:35 +0100 Subject: [PATCH 0087/1752] Try to avoid network connection during tests. Mitigate #13468 --- IPython/core/display.py | 4 ++-- IPython/core/magics/osm.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index d36a176c3bf..933295ad6ce 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -884,7 +884,7 @@ def __init__( a URL, or a filename from which to load image data. The result is always embedding image data for inline images. - >>> Image('http://www.google.fr/images/srpr/logo3w.png') + >>> Image('https://www.google.fr/images/srpr/logo3w.png') # doctest: +SKIP >>> Image('/path/to/image.jpg') @@ -897,7 +897,7 @@ def __init__( it only generates ```` tag with a link to the source. This will not work in the qtconsole or offline. - >>> Image(url='http://www.google.fr/images/srpr/logo3w.png') + >>> Image(url='https://www.google.fr/images/srpr/logo3w.png') """ diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index 11fec812ef6..42ed876ed15 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -127,7 +127,7 @@ def alias(self, parameter_s=''): Aliases expand Python variables just like system calls using ! or !! do: all expressions prefixed with '$' get expanded. For details of the semantic rules, see PEP-215: - http://www.python.org/peps/pep-0215.html. This is the library used by + https://www.python.org/dev/peps/pep-0215/. This is the library used by IPython for variable expansion. If you want to access a true shell variable, an extra $ is necessary to prevent its expansion by IPython:: From ccfe7e7389055919817f4acb60f58c3e926a8519 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 14:31:28 +0100 Subject: [PATCH 0088/1752] Whats new 7.32 --- docs/source/whatsnew/version7.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 6597fe555f8..331db654b58 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -9,6 +9,10 @@ IPython 7.32 ============ + +Autoload magic lazily +--------------------- + The ability to configure magics to be lazily loaded has been added to IPython. See the ``ipython --help-all`` section on ``MagicsManager.lazy_magic``. One can now use:: @@ -21,6 +25,21 @@ One can now use:: And on first use of ``%my_magic``, or corresponding cell magic, or other line magic, the corresponding ``load_ext`` will be called just before trying to invoke the magic. +Misc +---- + + - Update sphinxify for Docrepr 0.2.0 :ghpull:`13503`. + - Set co_name for cells run line by line (to fix debugging with Python 3.10) + :ghpull:`13535` + + +Many thanks to all the contributors to this release. You can find all individual +contributions to this milestone `on github +`__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + .. _version 7.31: IPython 7.31 From 218264979f050dcdacd8964e16de79154f466cb6 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 14:24:36 +0100 Subject: [PATCH 0089/1752] wn 8 --- docs/source/whatsnew/version8.rst | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 12c5d1f17c7..02544df0ebe 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -3,6 +3,76 @@ ============ +.. _version 8.1: + +IPython 8.1.0 +------------- + +IPython 8.1 is the first minor release after 8.0 and fixes a number of bugs and +Update a few behavior that were problematic with the 8.0 as with many new major +release. + +Note that beyond the changes listed here, IPython 8.1.0 also contains all the +features listed in :ref:`version 7.32`. + + - Misc and multiple fixes around quotation auto-closing. It is now disabled by + default. Run with ``TerminalInteractiveShell.auto_match=True`` to re-enabled + - Require pygments>=2.4.0 :ghpull:`13459`, this was implicit in the code, but + is now explicit in ``setup.cfg``/``setup.py`` + - Docs improvement of ``core.magic_arguments`` examples. :ghpull:`13433` + - Multi-line edit executes too early with await. :ghpull:`13424` + + - ``black`` is back as an optional dependency, and autoformatting disabled by + default until some fixes are implemented (black improperly reformat magics). + :ghpull:`13471` Additionally the ability to use ``yapf`` as a code + reformatter has been added :ghpull:`13528` . You can use + ``TerminalInteractiveShell.autoformatter="black"``, + ``TerminalInteractiveShell.autoformatter="yapf"`` to re-enable auto formating + with black, or switch to yapf. + + - Fix and issue where ``display`` was not defined. + + - Auto suggestions are now configurable. Currently only + ``AutoSuggestFromHistory`` (default) and ``None``. new provider contribution + welcomed. :ghpull:`13475` + + - multiple packaging/testing improvement to simplify downstream packaging + (xfail with reasons, try to not access network...). + + - Update deprecation. ``InteractiveShell.magic`` internal method has been + deprecated for many years but did not emit a warning until now. + + - internal ``appended_to_syspath`` context manager has been deprecated. + + - fix an issue with symlinks in virtualenv :ghpull:`13537` + + - Fix an issue with vim mode, where cursor would not be reset on exit :ghpull:`13472` + + - ipython directive now remove only known pseudo-decorators :ghpull:`13532` + + - ``IPython/lib/security`` which used to be used for jupyter notebook has been + removed. + + - Fix an issue where ``async with`` would execute on new lines. :ghpull:`13436` + + +We want to remind users that IPython is part of the Jupyter organisations, and +thus governed by a Code of Conduct. Some of the behavior we have seen on GitHub is not acceptable. +Abuse and non-respectful comments on discussion will not be tolerated. + +Many thanks to all the contributors to this release, many of the above fixed issue and +new features where done by first time contributors, showing there is still +plenty of easy contribution possible in IPython +. You can find all individual contributions +to this milestone `on github `__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. In particular the Lazy autoloading of +magics that you will find described in the 7.32 release notes. + + +.. _version 8.0.1: + IPython 8.0.1 (CVE-2022-21699) ------------------------------ @@ -45,6 +115,7 @@ Thus starting with this version: Further details can be read on the `GitHub Advisory `__ +.. _version 8.0: IPython 8.0 ----------- From e5d78c598bcd615b1007c41508a2486a97598a97 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 15:12:07 +0100 Subject: [PATCH 0090/1752] Increase test coverage --- IPython/core/tests/test_interactiveshell.py | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 230a498cfcc..10cce1fd646 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -380,7 +380,8 @@ def test_ofind_property_with_error(self): class A(object): @property def foo(self): - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover + a = A() found = ip._ofind('a.foo', [('locals', locals())]) @@ -392,7 +393,7 @@ def test_ofind_multiple_attribute_lookups(self): class A(object): @property def foo(self): - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover a = A() a.a = A() @@ -585,9 +586,9 @@ def test_exit_code_signal(self): self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGALRM) @onlyif_cmds_exist("csh") - def test_exit_code_signal_csh(self): - SHELL = os.environ.get('SHELL', None) - os.environ['SHELL'] = find_cmd("csh") + def test_exit_code_signal_csh(self): # pragma: no cover + SHELL = os.environ.get("SHELL", None) + os.environ["SHELL"] = find_cmd("csh") try: self.test_exit_code_signal() finally: @@ -615,7 +616,7 @@ def test_1(self): def test_control_c(self, *mocks): try: self.system("sleep 1 # wont happen") - except KeyboardInterrupt: + except KeyboardInterrupt: # pragma: no cove self.fail( "system call should intercept " "keyboard interrupt from subprocess.call" @@ -679,16 +680,20 @@ def setUp(self): def tearDown(self): ip.ast_transformers.remove(self.negator) - + + def test_non_int_const(self): + with tt.AssertPrints("hello"): + ip.run_cell('print("hello")') + def test_run_cell(self): - with tt.AssertPrints('-34'): - ip.run_cell('print (12 + 22)') - + with tt.AssertPrints("-34"): + ip.run_cell("print(12 + 22)") + # A named reference to a number shouldn't be transformed. - ip.user_ns['n'] = 55 - with tt.AssertNotPrints('-55'): - ip.run_cell('print (n)') - + ip.user_ns["n"] = 55 + with tt.AssertNotPrints("-55"): + ip.run_cell("print(n)") + def test_timeit(self): called = set() def f(x): @@ -796,7 +801,11 @@ def test_run_cell(self): # This shouldn't throw an error ip.run_cell("o = 2.0") self.assertEqual(ip.user_ns['o'], 2.0) - + + def test_run_cell_non_int(self): + ip.run_cell("n = 'a'") + assert self.calls == [] + def test_timeit(self): called = set() def f(x): @@ -815,14 +824,9 @@ def f(x): class ErrorTransformer(ast.NodeTransformer): """Throws an error when it sees a number.""" - # for Python 3.7 and earlier - def visit_Num(self, node): - raise ValueError("test") - - # for Python 3.8+ def visit_Constant(self, node): if isinstance(node.value, int): - return self.visit_Num(node) + raise ValueError("test") return node @@ -845,10 +849,6 @@ class StringRejector(ast.NodeTransformer): not be executed by throwing an InputRejected. """ - #for python 3.7 and earlier - def visit_Str(self, node): - raise InputRejected("test") - # 3.8 only def visit_Constant(self, node): if isinstance(node.value, str): From bd470fe8cea36d6b1c65820e3e9fdb26d26ad422 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 16:08:21 +0100 Subject: [PATCH 0091/1752] release 8.1.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index c1c90b13bf2..d67ef21018c 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 5d746502e38426a6b322bc0dd58bef723b02a40e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 25 Feb 2022 16:15:33 +0100 Subject: [PATCH 0092/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index d67ef21018c..1164867ced7 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 1 +_version_minor = 2 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From d73d05141e673b30fe1c081826d50355086a93da Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Feb 2022 07:28:10 -0800 Subject: [PATCH 0093/1752] Revert "Fix for symlink resolving" --- IPython/core/interactiveshell.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 85b63755087..6b30a22e3f0 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -785,11 +785,8 @@ def init_virtualenv(self): # So we just check every item in the symlink tree (generally <= 3) paths = [p] while p.is_symlink(): - new_path = p.readlink() - if not new_path.is_absolute(): - new_path = p.parent / new_path - p = new_path - paths.append(p) + p = Path(os.readlink(p)) + paths.append(p.resolve()) # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible if p_venv.parts[1] == "cygdrive": From ef1c5e7380631215e572619771e4edb69d64ba6c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Feb 2022 16:37:30 +0100 Subject: [PATCH 0094/1752] wn 8.1.1 --- docs/source/whatsnew/version8.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 02544df0ebe..8bc34f7b498 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -3,6 +3,13 @@ ============ +.. _version 8.1.1: + +IPython 8.1.1 +------------- + +Revert :ghpull:`13537` (fix an issue with symlinks in virtualenv) that raises an error in Python 3.8. + .. _version 8.1: IPython 8.1.0 @@ -44,7 +51,7 @@ features listed in :ref:`version 7.32`. - internal ``appended_to_syspath`` context manager has been deprecated. - - fix an issue with symlinks in virtualenv :ghpull:`13537` + - fix an issue with symlinks in virtualenv :ghpull:`13537` (Reverted in 8.1.1) - Fix an issue with vim mode, where cursor would not be reset on exit :ghpull:`13472` From 506ca3c48b0c0177dc780144f7e68dbdc6d81794 Mon Sep 17 00:00:00 2001 From: Nathan Buckner Date: Sat, 26 Feb 2022 08:23:48 -0800 Subject: [PATCH 0095/1752] Fix for the path look that's compatible with python 3.8 --- IPython/core/interactiveshell.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 6b30a22e3f0..dd200e9bf0a 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -758,6 +758,33 @@ def init_displayhook(self): # the appropriate time. self.display_trap = DisplayTrap(hook=self.displayhook) + @staticmethod + def get_python_link_paths(p: Path): + """Gets python paths including symlinks + + Examples + -------- + In [1]: from IPython.core.interactiveshell import InteractiveShell + + In [2]: import sys, pathlib + + In [3]: paths = InteractiveShell.get_python_link_paths(pathlib.Path(sys.executable)) + + In [4]: len(paths) == len(set(paths)) + Out[4]: True + + In [5]: bool(paths) + Out[5]: True + """ + paths = [p] + while p.is_symlink(): + new_path = Path(os.readlink(p)) + if not new_path.is_absolute(): + new_path = p.parent / new_path + p = new_path + paths.append(p) + return paths + def init_virtualenv(self): """Add the current virtualenv to sys.path so the user can import modules from it. This isn't perfect: it doesn't use the Python interpreter with which the @@ -783,10 +810,7 @@ def init_virtualenv(self): # stdlib venv may symlink sys.executable, so we can't use realpath. # but others can symlink *to* the venv Python, so we can't just use sys.executable. # So we just check every item in the symlink tree (generally <= 3) - paths = [p] - while p.is_symlink(): - p = Path(os.readlink(p)) - paths.append(p.resolve()) + paths = self.get_python_link_paths(p) # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible if p_venv.parts[1] == "cygdrive": From 3e572a4edd6cf36331fa626f4c7d65ed70dbf38a Mon Sep 17 00:00:00 2001 From: Nathan Buckner Date: Sun, 27 Feb 2022 16:39:20 -0800 Subject: [PATCH 0096/1752] Name of function didn't make sense because it doesn't call sys.executable and takes in any path --- IPython/core/interactiveshell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index dd200e9bf0a..93c234fcf91 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -759,8 +759,8 @@ def init_displayhook(self): self.display_trap = DisplayTrap(hook=self.displayhook) @staticmethod - def get_python_link_paths(p: Path): - """Gets python paths including symlinks + def get_path_links(p: Path): + """Gets path links including all symlinks Examples -------- @@ -768,7 +768,7 @@ def get_python_link_paths(p: Path): In [2]: import sys, pathlib - In [3]: paths = InteractiveShell.get_python_link_paths(pathlib.Path(sys.executable)) + In [3]: paths = InteractiveShell.get_path_links(pathlib.Path(sys.executable)) In [4]: len(paths) == len(set(paths)) Out[4]: True @@ -810,7 +810,7 @@ def init_virtualenv(self): # stdlib venv may symlink sys.executable, so we can't use realpath. # but others can symlink *to* the venv Python, so we can't just use sys.executable. # So we just check every item in the symlink tree (generally <= 3) - paths = self.get_python_link_paths(p) + paths = self.get_path_links(p) # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible if p_venv.parts[1] == "cygdrive": From 5f648692d8da1737f330b70e8eb97cec1320e9f9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 3 Mar 2022 11:38:54 +0100 Subject: [PATCH 0097/1752] update whats new one moer time --- docs/source/whatsnew/version8.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 8bc34f7b498..783011fa74c 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -8,7 +8,10 @@ IPython 8.1.1 ------------- -Revert :ghpull:`13537` (fix an issue with symlinks in virtualenv) that raises an error in Python 3.8. +Fix an issue with virtualenv and Python 3.8 introduced in 8.1 + +Revert :ghpull:`13537` (fix an issue with symlinks in virtualenv) that raises an +error in Python 3.8, and fixed in a different way in :ghpull:`13559`. .. _version 8.1: From e9b2ec3bc5372d1ece0e28916cc51ed6bf9625f0 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 3 Mar 2022 11:46:27 +0100 Subject: [PATCH 0098/1752] release 8.1.1 --- IPython/core/release.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 1164867ced7..e4ecf38f0fa 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 2 -_version_patch = 0 +_version_minor = 1 +_version_patch = 1 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 385aad4abd987bda8821849cea85952fe09bbe57 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 3 Mar 2022 11:50:07 +0100 Subject: [PATCH 0099/1752] back to dev --- IPython/core/release.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index e4ecf38f0fa..1164867ced7 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 1 -_version_patch = 1 +_version_minor = 2 +_version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 82e6fdcae2c60870da4f03fc654a6a950acc777d Mon Sep 17 00:00:00 2001 From: Yuval Date: Thu, 3 Mar 2022 21:22:31 +0200 Subject: [PATCH 0100/1752] Adding and "End" key binding for applying an auto suggestion --- IPython/terminal/shortcuts.py | 11 +++++++++-- .../whatsnew/pr/end-shortcut-accept-suggestion.rst | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index b53e5b49a7c..95e956c3f87 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -267,8 +267,7 @@ def ebivim(): focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode # Needed for to accept autosuggestions in vi insert mode - @kb.add("c-e", filter=focused_insert_vi & ebivim) - def _(event): + def _apply_autosuggest(event): b = event.current_buffer suggestion = b.suggestion if suggestion: @@ -276,6 +275,14 @@ def _(event): else: nc.end_of_line(event) + @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & ebivim) + def _(event): + _apply_autosuggest(event) + + @kb.add("c-e", filter=focused_insert_vi & ebivim) + def _(event): + _apply_autosuggest(event) + @kb.add("c-f", filter=focused_insert_vi) def _(event): b = event.current_buffer diff --git a/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst b/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst new file mode 100644 index 00000000000..4fc4c991e37 --- /dev/null +++ b/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst @@ -0,0 +1,5 @@ +Added shortcut for accepting auto suggestion +============================================ + +Added End key shortcut for accepting auto-suggestion + From 3d15172f2ecf8368bf12ac68efc2961b63647489 Mon Sep 17 00:00:00 2001 From: Yuval Date: Thu, 3 Mar 2022 21:48:42 +0200 Subject: [PATCH 0101/1752] Format according to black/darker --- IPython/terminal/shortcuts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 95e956c3f87..ba0cf123be3 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -278,7 +278,7 @@ def _apply_autosuggest(event): @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & ebivim) def _(event): _apply_autosuggest(event) - + @kb.add("c-e", filter=focused_insert_vi & ebivim) def _(event): _apply_autosuggest(event) From fcb3ee519fc37c9fc2562cf9e1e2b77be6523998 Mon Sep 17 00:00:00 2001 From: john Date: Sat, 5 Mar 2022 23:40:46 -0500 Subject: [PATCH 0102/1752] fix minor typo 'if by any chance IPython does not the right thing' -> ...IPython does not do the right thing...' --- docs/source/interactive/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/interactive/tutorial.rst b/docs/source/interactive/tutorial.rst index 511e7463875..947d6ad4905 100644 --- a/docs/source/interactive/tutorial.rst +++ b/docs/source/interactive/tutorial.rst @@ -59,7 +59,7 @@ while some other do not. We'll come to this later. Depending on the exact command you are typing you might realize that sometimes :kbd:`Enter` will add a new line, and sometimes it will execute the current statement. IPython tries to guess what you are doing, so most of the time you -should not have to care. Though if by any chance IPython does not the right +should not have to care. Though if by any chance IPython does not do the right thing you can force execution of the current code block by pressing in sequence :kbd:`Esc` and :kbd:`Enter`. You can also force the insertion of a new line at the position of the cursor by using :kbd:`Ctrl-o`. From 3da97ccfdd6072238a5d8a538adfdf3641573fe6 Mon Sep 17 00:00:00 2001 From: Yuval Date: Sun, 6 Mar 2022 13:52:42 +0200 Subject: [PATCH 0103/1752] Adapted binding to when ~ebvim + elaborated on how the binding works --- IPython/terminal/shortcuts.py | 2 +- docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index ba0cf123be3..44007d1a225 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -275,7 +275,7 @@ def _apply_autosuggest(event): else: nc.end_of_line(event) - @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & ebivim) + @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) def _(event): _apply_autosuggest(event) diff --git a/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst b/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst index 4fc4c991e37..c04998e8f3f 100644 --- a/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst +++ b/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst @@ -2,4 +2,6 @@ Added shortcut for accepting auto suggestion ============================================ Added End key shortcut for accepting auto-suggestion +This binding works in Vi mode too, provided +TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode is set to be True. From c365d5102d577f75ded01a3d01009e94047cb258 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 10 Mar 2022 08:44:59 +0100 Subject: [PATCH 0104/1752] Typo Manger -> Manager --- IPython/core/magic.py | 2 +- docs/source/whatsnew/version7.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 79983df09f2..cedba619378 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -311,7 +311,7 @@ class MagicsManager(Configurable): For example:: - c.MagicsManger.lazy_magics = { + c.MagicsManager.lazy_magics = { "my_magic": "slow.to.import", "my_other_magic": "also.slow", } diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 331db654b58..4dea95d6c78 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -17,7 +17,7 @@ The ability to configure magics to be lazily loaded has been added to IPython. See the ``ipython --help-all`` section on ``MagicsManager.lazy_magic``. One can now use:: - c.MagicsManger.lazy_magics = { + c.MagicsManager.lazy_magics = { "my_magic": "slow.to.import", "my_other_magic": "also.slow", } From 1c763ce9c8c6b66f3e369005e491314ddf7b026c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 10 Mar 2022 08:55:09 +0100 Subject: [PATCH 0105/1752] Links cleanup, in particular peps have moved --- IPython/core/magics/osm.py | 2 +- docs/source/links.txt | 6 +----- docs/source/whatsnew/version6.rst | 2 +- examples/IPython Kernel/Importing Notebooks.ipynb | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index 42ed876ed15..41957a28509 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -127,7 +127,7 @@ def alias(self, parameter_s=''): Aliases expand Python variables just like system calls using ! or !! do: all expressions prefixed with '$' get expanded. For details of the semantic rules, see PEP-215: - https://www.python.org/dev/peps/pep-0215/. This is the library used by + https://peps.python.org/pep-0215/. This is the library used by IPython for variable expansion. If you want to access a true shell variable, an extra $ is necessary to prevent its expansion by IPython:: diff --git a/docs/source/links.txt b/docs/source/links.txt index 9379d1f9357..642389cbf09 100644 --- a/docs/source/links.txt +++ b/docs/source/links.txt @@ -20,9 +20,6 @@ .. _ipython: https://ipython.org .. _`ipython manual`: https://ipython.org/documentation.html .. _ipython_github: http://github.com/ipython/ipython/ -.. _ipython_github_repo: http://github.com/ipython/ipython/ -.. _ipython_downloads: https://ipython.org/download.html -.. _ipython_pypi: http://pypi.python.org/pypi/ipython .. _nbviewer: http://nbviewer.ipython.org .. _ZeroMQ: http://zeromq.org @@ -35,8 +32,7 @@ .. _reST: http://docutils.sourceforge.net/rst.html .. _docutils: http://docutils.sourceforge.net .. _lyx: http://www.lyx.org -.. _pep8: http://www.python.org/dev/peps/pep-0008 -.. _numpy_coding_guide: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +.. _pep8: https://peps.python.org/pep-0008/ .. Licenses .. _GPL: http://www.gnu.org/licenses/gpl.html diff --git a/docs/source/whatsnew/version6.rst b/docs/source/whatsnew/version6.rst index 461132ecfce..cb3b42dd2f4 100644 --- a/docs/source/whatsnew/version6.rst +++ b/docs/source/whatsnew/version6.rst @@ -277,7 +277,7 @@ automatically upgrade to the latest version compatible with your system. The ability to use only Python 3 on the code base of IPython brings a number of advantages. Most of the newly written code make use of `optional function type -annotation `_ leading to clearer code +annotation `_ leading to clearer code and better documentation. The total size of the repository has also decreased by about 1500 lines (for the diff --git a/examples/IPython Kernel/Importing Notebooks.ipynb b/examples/IPython Kernel/Importing Notebooks.ipynb index 8a0aac77c7f..9d091aabc44 100644 --- a/examples/IPython Kernel/Importing Notebooks.ipynb +++ b/examples/IPython Kernel/Importing Notebooks.ipynb @@ -15,7 +15,7 @@ "This is made difficult by the fact that Notebooks are not plain Python files,\n", "and thus cannot be imported by the regular Python machinery.\n", "\n", - "Fortunately, Python provides some fairly sophisticated [hooks](http://www.python.org/dev/peps/pep-0302/) into the import machinery,\n", + "Fortunately, Python provides some fairly sophisticated [hooks](https://peps.python.org/pep-0302/) into the import machinery,\n", "so we can actually make IPython notebooks importable without much difficulty,\n", "and only using public APIs." ] From 46c63a5ba82fbc030506b23237450d021ef5caa8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 12 Mar 2022 11:04:27 +0100 Subject: [PATCH 0106/1752] Restore support for etb=None Closes #13584, #13538 Should close #13586 and #12467 --- IPython/core/ultratb.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 1f08fa51ed2..96d118f6a23 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -763,7 +763,7 @@ def format_exception_as_a_whole( self, etype: type, evalue: BaseException, - etb: TracebackType, + etb: Optional[TracebackType], number_of_lines_of_context, tb_offset: Optional[int], ): @@ -772,7 +772,6 @@ def format_exception_as_a_whole( This may be called multiple times by Python 3 exception chaining (PEP 3134). """ - assert etb is not None # some locals orig_etype = etype try: @@ -783,7 +782,9 @@ def format_exception_as_a_whole( tb_offset = self.tb_offset if tb_offset is None else tb_offset assert isinstance(tb_offset, int) head = self.prepare_header(etype, self.long_header) - records = self.get_records(etb, number_of_lines_of_context, tb_offset) + records = ( + self.get_records(etb, number_of_lines_of_context, tb_offset) if etb else [] + ) frames = [] skipped = 0 @@ -822,6 +823,7 @@ def format_exception_as_a_whole( def get_records( self, etb: TracebackType, number_of_lines_of_context: int, tb_offset: int ): + assert etb is not None context = number_of_lines_of_context - 1 after = context // 2 before = context - after @@ -836,19 +838,17 @@ def get_records( after=after, pygments_formatter=formatter, ) - assert etb is not None return list(stack_data.FrameInfo.stack_data(etb, options=options))[tb_offset:] def structured_traceback( self, etype: type, evalue: Optional[BaseException], - etb: TracebackType, + etb: Optional[TracebackType], tb_offset: Optional[int] = None, number_of_lines_of_context: int = 5, ): """Return a nice text document describing the traceback.""" - assert etb is not None formatted_exception = self.format_exception_as_a_whole(etype, evalue, etb, number_of_lines_of_context, tb_offset) From 75b3d1cc6d5e1e629705d8a7233a374f1e4235e7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 14 Mar 2022 10:51:57 +0100 Subject: [PATCH 0107/1752] Get history from sql. Fixes #13585 By getting history from sql we can get the transformed history. This also skip storing history if `%paste` is used and `%paste` itself will insert the pasted value in history which is more conveninent. --- IPython/core/interactiveshell.py | 5 ++- IPython/terminal/interactiveshell.py | 53 +++++++++++++++++++++------- IPython/terminal/magics.py | 2 +- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 93c234fcf91..8b8abe13d42 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3042,9 +3042,8 @@ def error_before_exec(value): cell = raw_cell # Store raw and processed history - if store_history: - self.history_manager.store_inputs(self.execution_count, - cell, raw_cell) + if store_history and raw_cell.strip(" %") != "paste": + self.history_manager.store_inputs(self.execution_count, cell, raw_cell) if not silent: self.logger.log(cell, raw_cell) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 212692ab6d1..06724bea870 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -3,12 +3,10 @@ import asyncio import os import sys -import warnings from warnings import warn from IPython.core.async_helpers import get_asyncio_loop from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC -from IPython.utils import io from IPython.utils.py3compat import input from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title from IPython.utils.process import abbrev_cwd @@ -32,7 +30,7 @@ from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import (HasFocus, Condition, IsDone) from prompt_toolkit.formatted_text import PygmentsTokens -from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.history import History from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor from prompt_toolkit.output import ColorDepth from prompt_toolkit.patch_stdout import patch_stdout @@ -132,6 +130,43 @@ def yapf_reformat_handler(text_before_cursor): return text_before_cursor +class PtkHistoryAdapter(History): + """ + Prompt toolkit has it's own way of handling history, Where it assumes it can + Push/pull from history. + + """ + + def __init__(self, shell): + super().__init__() + self.shell = shell + self._refresh() + + def append_string(self, string): + # we rely on sql for that. + self._loaded = False + self._refresh() + + def _refresh(self): + if not self._loaded: + self._loaded_strings = list(self.load_history_strings()) + + def load_history_strings(self): + last_cell = "" + res = [] + for __, ___, cell in self.shell.history_manager.get_tail( + self.shell.history_load_length, include_latest=True + ): + # Ignore blank lines and consecutive duplicates + cell = cell.rstrip() + if cell and (cell != last_cell): + res.append(cell) + last_cell = cell + yield from res[::-1] + + def store_string(self, string: str) -> None: + pass + class TerminalInteractiveShell(InteractiveShell): mime_renderers = Dict().tag(config=True) @@ -397,16 +432,9 @@ def prompt(): # Set up keyboard shortcuts key_bindings = create_ipython_shortcuts(self) + # Pre-populate history from IPython's history database - history = InMemoryHistory() - last_cell = u"" - for __, ___, cell in self.history_manager.get_tail(self.history_load_length, - include_latest=True): - # Ignore blank lines and consecutive duplicates - cell = cell.rstrip() - if cell and (cell != last_cell): - history.append_string(cell) - last_cell = cell + history = PtkHistoryAdapter(self) self._style = self._make_style_from_name_or_cls(self.highlighting_style) self.style = DynamicStyle(lambda: self._style) @@ -586,7 +614,6 @@ def prompt_for_code(self): def enable_win_unicode_console(self): # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows # console by default, so WUC shouldn't be needed. - from warnings import warn warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future", DeprecationWarning, stacklevel=2) diff --git a/IPython/terminal/magics.py b/IPython/terminal/magics.py index 498a05027e5..206ff20a0f8 100644 --- a/IPython/terminal/magics.py +++ b/IPython/terminal/magics.py @@ -53,7 +53,7 @@ def store_or_execute(self, block, name): self.shell.user_ns['pasted_block'] = b self.shell.using_paste_magics = True try: - self.shell.run_cell(b) + self.shell.run_cell(b, store_history=True) finally: self.shell.using_paste_magics = False From 64c9adbccf45c19818168e8252f77af569128aef Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 14 Mar 2022 11:05:07 +0100 Subject: [PATCH 0108/1752] pin pytest --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2da02e4e598..ae2fd9278fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ qtconsole = qtconsole terminal = test = - pytest + pytest<7.1 pytest-asyncio testpath test_extra = From 196f7ebed836085bd393b8b724bd99c0cc493649 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 14 Mar 2022 11:15:20 +0100 Subject: [PATCH 0109/1752] fix setup.cfg --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index ae2fd9278fb..2027b531d02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,13 +67,12 @@ test = pytest-asyncio testpath test_extra = + %(test)s curio matplotlib!=3.2.0 nbformat numpy>=1.19 pandas - pytest - testpath trio all = %(black)s From 6c515657207df9b9f41f421b558bbc4349583387 Mon Sep 17 00:00:00 2001 From: Maor Kleinberger Date: Fri, 25 Mar 2022 17:16:49 +0300 Subject: [PATCH 0110/1752] Fix ctrl-\ behavior This commit fixes two issues with ctrl-\ when using IPython on linux: - Previously, pressing ctrl-\ would make IPython exit without resetting the terminal configuration - IPython users could not override the behavior of ctrl-\ using `signal.signal(signal.SIGQUIT, ...)` as they would in other terminal apps --- IPython/terminal/shortcuts.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 44007d1a225..3a0017c101e 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -10,6 +10,7 @@ import signal import sys import re +import os from typing import Callable @@ -56,7 +57,7 @@ def reformat_and_execute(event): & insert_mode ))(reformat_and_execute) - kb.add('c-\\')(force_exit) + kb.add("c-\\")(quit) kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) )(previous_history_or_previous_completion) @@ -458,11 +459,16 @@ def reset_search_buffer(event): def suspend_to_bg(event): event.app.suspend_to_background() -def force_exit(event): +def quit(event): """ - Force exit (with a non-zero return value) + On platforms that support SIGQUIT, send SIGQUIT to the current process. + On other platforms, just exit the process with a message. """ - sys.exit("Quit") + sigquit = getattr(signal, "SIGQUIT", None) + if sigquit is not None: + os.kill(0, signal.SIGQUIT) + else: + sys.exit("Quit") def indent_buffer(event): event.current_buffer.insert_text(' ' * 4) From ad57192258523877ade1ea4afcbb073735247271 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Mar 2022 10:19:53 +0100 Subject: [PATCH 0111/1752] Attempt at debugging #13598 --- IPython/core/interactiveshell.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 8b8abe13d42..790a75e7d4e 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1976,10 +1976,19 @@ def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None, # Exception classes can customise their traceback - we # use this in IPython.parallel for exceptions occurring # in the engines. This should return a list of strings. - stb = value._render_traceback_() + if hasattr(value, "_render_traceback_"): + stb = value._render_traceback_() + else: + stb = self.InteractiveTB.structured_traceback( + etype, value, tb, tb_offset=tb_offset + ) + except Exception: - stb = self.InteractiveTB.structured_traceback(etype, - value, tb, tb_offset=tb_offset) + print( + "Unexpected exception formatting exception. Falling back to standard exception" + ) + traceback.print_exc() + return None self._showtraceback(etype, value, stb) if self.call_pdb: From 43fd87866d39b7a25c22fa3e4aeac616e083213d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 27 Mar 2022 09:10:45 +0200 Subject: [PATCH 0112/1752] Move debugger_cls one class higher --- IPython/core/ultratb.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 146ee568e94..2c9e8e016d4 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -207,7 +207,16 @@ class TBTools(colorable.Colorable): # Number of frames to skip when reporting tracebacks tb_offset = 0 - def __init__(self, color_scheme='NoColor', call_pdb=False, ostream=None, parent=None, config=None): + def __init__( + self, + color_scheme="NoColor", + call_pdb=False, + ostream=None, + parent=None, + config=None, + *, + debugger_cls=None, + ): # Whether to call the interactive pdb debugger after printing # tracebacks or not super(TBTools, self).__init__(parent=parent, config=config) @@ -227,9 +236,10 @@ def __init__(self, color_scheme='NoColor', call_pdb=False, ostream=None, parent= self.set_colors(color_scheme) self.old_scheme = color_scheme # save initial value for toggles + self.debugger_cls = debugger_cls or debugger.Pdb if call_pdb: - self.pdb = debugger.Pdb() + self.pdb = debugger_cls() else: self.pdb = None @@ -628,8 +638,15 @@ def __init__( tb_offset=1 allows use of this handler in interpreters which will have their own code at the top of the traceback (VerboseTB will first remove that frame before printing the traceback info).""" - TBTools.__init__(self, color_scheme=color_scheme, call_pdb=call_pdb, - ostream=ostream, parent=parent, config=config) + TBTools.__init__( + self, + color_scheme=color_scheme, + call_pdb=call_pdb, + ostream=ostream, + parent=parent, + config=config, + debugger_cls=debugger_cls, + ) self.tb_offset = tb_offset self.long_header = long_header self.include_vars = include_vars @@ -642,7 +659,6 @@ def __init__( check_cache = linecache.checkcache self.check_cache = check_cache - self.debugger_cls = debugger_cls or debugger.Pdb self.skip_hidden = True def format_record(self, frame_info): @@ -907,7 +923,7 @@ def debugger(self, force: bool = False): fix that by hand after invoking the exception handler.""" if force or self.call_pdb: - if self.debugger_cls: + if self.pdb is None: self.pdb = self.debugger_cls() # the system displayhook may have changed, restore the original # for pdb From d90a85ca4dd8ec1feebffef208b8cf3f8ef22b21 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 27 Mar 2022 09:16:28 +0200 Subject: [PATCH 0113/1752] remove empty __init_ --- IPython/core/ultratb.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 2c9e8e016d4..a44d5c2d2f1 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -360,9 +360,6 @@ class ListTB(TBTools): Because they are meant to be called without a full traceback (only a list), instances of this class can't call the interactive pdb debugger.""" - def __init__(self, color_scheme='NoColor', call_pdb=False, ostream=None, parent=None, config=None): - TBTools.__init__(self, color_scheme=color_scheme, call_pdb=call_pdb, - ostream=ostream, parent=parent,config=config) def __call__(self, etype, value, elist): self.ostream.flush() From 9901f306d318442cff61f87378159e0a433f570c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 27 Mar 2022 11:54:20 +0200 Subject: [PATCH 0114/1752] update whatsnew 8.2 --- docs/source/whatsnew/version8.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 783011fa74c..c2398d5861b 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -3,6 +3,29 @@ ============ +.. _version 8.2.0: + +IPython 8.2.0 +------------- + +IPython 8.2 mostly bring bugfixes to IPython. + + - Auto-suggestion can now be elected with the ``end`` key. :ghpull:`13566` + - Some traceback issues with ``assert etb is not None`` have been fixed. :ghpull:`13588` + - History is now pulled from the sqitel database and not from in-memory. + In particular when using the ``%paste`` magic, the content of the pasted text will + be part of the history and not the verbatim text ``%paste`` anymore. :ghpull:`13592` + - Fix ``Ctrl-\\`` exit cleanup :ghpull:`13603` + - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498` + + +I am still trying to fix and investigate :ghissue:`13598`, which seem to be +random, and would appreciate help if you find reproducible minimal case. I've +tried to make various changes to the codebase to mitigate it, but a proper fix +will be difficult without understating the cause. + + + .. _version 8.1.1: IPython 8.1.1 From 6bd2c03d0b2fc6e4e598620debd74493eaee93af Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 27 Mar 2022 12:06:10 +0200 Subject: [PATCH 0115/1752] second whats new update --- docs/source/whatsnew/version8.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index c2398d5861b..6630fe6e602 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -22,9 +22,16 @@ IPython 8.2 mostly bring bugfixes to IPython. I am still trying to fix and investigate :ghissue:`13598`, which seem to be random, and would appreciate help if you find reproducible minimal case. I've tried to make various changes to the codebase to mitigate it, but a proper fix -will be difficult without understating the cause. +will be difficult without understanding the cause. +All the issues on pull-requests for this release can be found in the `8.2 +milestone. `__ . And some +documentation only PR can be found as part of the `7.33 milestone +`__ (currently not released). + +Thanks to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. .. _version 8.1.1: From b5cd02544acd3e3e3a32872700954025a6b6916b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 27 Mar 2022 12:08:49 +0200 Subject: [PATCH 0116/1752] release 8.2.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 1164867ced7..0483150bf46 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 669396ae9e948f9d673527051a0a453bcfd1db94 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 27 Mar 2022 12:09:33 +0200 Subject: [PATCH 0117/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 0483150bf46..ae19fa66f18 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 2 +_version_minor = 3 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 42309de567484cde5225f03d77e15e9edefbf4b6 Mon Sep 17 00:00:00 2001 From: Blazej Michalik Date: Sun, 27 Mar 2022 19:09:13 +0200 Subject: [PATCH 0118/1752] Remove unused 'chain' import from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 159f8f2cbc9..5614d3ef8c8 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ import os import sys -from itertools import chain # **Python version check** # From bc12e6dcb215cb6cea47f57f7a58ecf3a89e3b56 Mon Sep 17 00:00:00 2001 From: Blazej Michalik Date: Mon, 4 Apr 2022 21:22:12 +0200 Subject: [PATCH 0119/1752] Fix empty autosuggestions bugging out the end key The `nc.end_of_line(event)` line was never triggered. During writing this, the `Suggestion` class from prompt-toolkit has no `__bool__` implemented, which meant its objects always evaluate to `True`, even if `b.suggestion.text == ''`. The above resulted in the end key being unusable after an autosuggestion has been accepted by the user. --- IPython/terminal/shortcuts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 3a0017c101e..615397abc5f 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -271,7 +271,7 @@ def ebivim(): def _apply_autosuggest(event): b = event.current_buffer suggestion = b.suggestion - if suggestion: + if suggestion is not None and suggestion.text: b.insert_text(suggestion.text) else: nc.end_of_line(event) From 199b25bc5ba2a9c1e575a82abc9323b6a87173ee Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 22 Mar 2022 16:53:44 +0100 Subject: [PATCH 0120/1752] Allow to get cellid from ipykernel Other side of https://github.com/ipython/ipykernel/pull/886 half of https://github.com/ipython/ipython/issues/13579 --- IPython/core/completer.py | 2 +- IPython/core/interactiveshell.py | 52 ++++++++++++++++++++++++-------- IPython/lib/display.py | 4 +-- IPython/terminal/ipapp.py | 2 +- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 0579e684e81..5a00f2c83a0 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -41,7 +41,7 @@ Only valid Python identifiers will complete. Combining characters (like arrow or dots) are also available, unlike latex they need to be put after the their -counterpart that is to say, `F\\\\vec` is correct, not `\\\\vecF`. +counterpart that is to say, ``F\\\\vec`` is correct, not ``\\\\vecF``. Some browsers are known to display combining characters incorrectly. diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 790a75e7d4e..ea9f6310ba5 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -199,19 +199,29 @@ class ExecutionInfo(object): store_history = False silent = False shell_futures = True + cell_id = None - def __init__(self, raw_cell, store_history, silent, shell_futures): + def __init__(self, raw_cell, store_history, silent, shell_futures, cell_id): self.raw_cell = raw_cell self.store_history = store_history self.silent = silent self.shell_futures = shell_futures + self.cell_id = cell_id def __repr__(self): name = self.__class__.__qualname__ - raw_cell = ((self.raw_cell[:50] + '..') - if len(self.raw_cell) > 50 else self.raw_cell) - return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s>' %\ - (name, id(self), raw_cell, self.store_history, self.silent, self.shell_futures) + raw_cell = ( + (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell + ) + return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % ( + name, + id(self), + raw_cell, + self.store_history, + self.silent, + self.shell_futures, + self.cell_id, + ) class ExecutionResult(object): @@ -2834,7 +2844,14 @@ def safe_run_module(self, mod_name, where): self.showtraceback() warn('Unknown failure executing module: <%s>' % mod_name) - def run_cell(self, raw_cell, store_history=False, silent=False, shell_futures=True): + def run_cell( + self, + raw_cell, + store_history=False, + silent=False, + shell_futures=True, + cell_id=None, + ): """Run a complete IPython cell. Parameters @@ -2861,14 +2878,22 @@ def run_cell(self, raw_cell, store_history=False, silent=False, shell_futures=Tr result = None try: result = self._run_cell( - raw_cell, store_history, silent, shell_futures) + raw_cell, store_history, silent, shell_futures, cell_id + ) finally: self.events.trigger('post_execute') if not silent: self.events.trigger('post_run_cell', result) return result - def _run_cell(self, raw_cell:str, store_history:bool, silent:bool, shell_futures:bool) -> ExecutionResult: + def _run_cell( + self, + raw_cell: str, + store_history: bool, + silent: bool, + shell_futures: bool, + cell_id: str, + ) -> ExecutionResult: """Internal method to run a complete IPython cell.""" # we need to avoid calling self.transform_cell multiple time on the same thing @@ -2888,6 +2913,7 @@ def _run_cell(self, raw_cell:str, store_history:bool, silent:bool, shell_futures shell_futures=shell_futures, transformed_cell=transformed_cell, preprocessing_exc_tuple=preprocessing_exc_tuple, + cell_id=cell_id, ) # run_cell_async is async, but may not actually need an eventloop. @@ -2908,7 +2934,9 @@ def _run_cell(self, raw_cell:str, store_history:bool, silent:bool, shell_futures try: return runner(coro) except BaseException as e: - info = ExecutionInfo(raw_cell, store_history, silent, shell_futures) + info = ExecutionInfo( + raw_cell, store_history, silent, shell_futures, cell_id + ) result = ExecutionResult(info) result.error_in_exec = e self.showtraceback(running_compiled_code=True) @@ -2964,7 +2992,8 @@ async def run_cell_async( shell_futures=True, *, transformed_cell: Optional[str] = None, - preprocessing_exc_tuple: Optional[Any] = None + preprocessing_exc_tuple: Optional[Any] = None, + cell_id=None, ) -> ExecutionResult: """Run a complete IPython cell asynchronously. @@ -2995,8 +3024,7 @@ async def run_cell_async( .. versionadded:: 7.0 """ - info = ExecutionInfo( - raw_cell, store_history, silent, shell_futures) + info = ExecutionInfo(raw_cell, store_history, silent, shell_futures, cell_id) result = ExecutionResult(info) if (not raw_cell) or raw_cell.isspace(): diff --git a/IPython/lib/display.py b/IPython/lib/display.py index 52060da37f3..4481683c4a0 100644 --- a/IPython/lib/display.py +++ b/IPython/lib/display.py @@ -92,8 +92,8 @@ class Audio(DisplayObject): From a File: - >>> Audio('/path/to/sound.wav') # doctest: +SKIP - >>> Audio(filename='/path/to/sound.ogg') # doctest: +SKIP + >>> Audio('IPython/lib/tests/test.wav') # doctest: +SKIP + >>> Audio(filename='IPython/lib/tests/test.wav') # doctest: +SKIP From Bytes: diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index e735a209255..a87eb2f4434 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # encoding: utf-8 """ -The :class:`~IPython.core.application.Application` object for the command +The :class:`~traitlets.config.application.Application` object for the command line :command:`ipython` program. """ From f3957c0fa08df8aa87f0a21435557d11f6903624 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 5 Apr 2022 13:59:12 +0200 Subject: [PATCH 0121/1752] docs --- docs/source/config/callbacks.rst | 32 ++++++++++++++++++++++--------- docs/source/whatsnew/version8.rst | 8 ++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/source/config/callbacks.rst b/docs/source/config/callbacks.rst index 6c75c731b0e..60c7aba4a35 100644 --- a/docs/source/config/callbacks.rst +++ b/docs/source/config/callbacks.rst @@ -17,22 +17,29 @@ For example:: def __init__(self, ip): self.shell = ip self.last_x = None - + def pre_execute(self): self.last_x = self.shell.user_ns.get('x', None) - + def pre_run_cell(self, info): - print('Cell code: "%s"' % info.raw_cell) - + print('info.raw_cell =', info.raw_cell) + print('info.store_history =', info.store_history) + print('info.silent =', info.silent) + print('info.shell_futures =', info.shell_futures) + print('info.cell_id =', info.cell_id) + print(dir(info)) + def post_execute(self): if self.shell.user_ns.get('x', None) != self.last_x: print("x changed!") - + def post_run_cell(self, result): - print('Cell code: "%s"' % result.info.raw_cell) - if result.error_before_exec: - print('Error before execution: %s' % result.error_before_exec) - + print('result.execution_count = ', result.execution_count) + print('result.error_before_exec = ', result.error_before_exec) + print('result.error_in_exec = ', result.error_in_exec) + print('result.info = ', result.info) + print('result.result = ', result.result) + def load_ipython_extension(ip): vw = VarWatcher(ip) ip.events.register('pre_execute', vw.pre_execute) @@ -40,6 +47,13 @@ For example:: ip.events.register('post_execute', vw.post_execute) ip.events.register('post_run_cell', vw.post_run_cell) +.. versionadded:: 8.3 + + Since IPython 8.3 and ipykernel 6.12.1, the ``info`` objects in the callback + now have a the ``cell_id`` that will be set to the value sent by the + frontened, when those send it. + + Events ====== diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 6630fe6e602..b557eb1935b 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -3,6 +3,14 @@ ============ +.. _version 8.3.0: + +IPython 8.3.0 +------------- + + - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on + the info object when frontend provide it. + .. _version 8.2.0: IPython 8.2.0 From 32c393b6babeb82dab97aa4f2ac73edc358b4ebf Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 5 Apr 2022 14:06:44 +0200 Subject: [PATCH 0122/1752] Remove set-next input when triggering help. This was a long standing feature from when the main way to edit code was readline. Now that we have proper history and editing frontend, including prompt toolkit this is not necessary, especially since it creates issue like in JupyterLab. Should close #13602 --- IPython/core/inputtransformer.py | 20 +- IPython/core/inputtransformer2.py | 16 +- IPython/core/tests/test_inputtransformer.py | 189 +++++++++---------- IPython/core/tests/test_inputtransformer2.py | 167 +++++++++------- docs/source/whatsnew/version8.rst | 5 + 5 files changed, 207 insertions(+), 190 deletions(-) diff --git a/IPython/core/inputtransformer.py b/IPython/core/inputtransformer.py index f668f466aae..77f69f388f8 100644 --- a/IPython/core/inputtransformer.py +++ b/IPython/core/inputtransformer.py @@ -193,7 +193,7 @@ def assemble_logical_lines(): line = ''.join(parts) # Utilities -def _make_help_call(target, esc, lspace, next_input=None): +def _make_help_call(target, esc, lspace): """Prepares a pinfo(2)/psearch call from a target name and the escape (i.e. ? or ??)""" method = 'pinfo2' if esc == '??' \ @@ -203,12 +203,13 @@ def _make_help_call(target, esc, lspace, next_input=None): #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args) t_magic_name, _, t_magic_arg_s = arg.partition(' ') t_magic_name = t_magic_name.lstrip(ESC_MAGIC) - if next_input is None: - return '%sget_ipython().run_line_magic(%r, %r)' % (lspace, t_magic_name, t_magic_arg_s) - else: - return '%sget_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \ - (lspace, next_input, t_magic_name, t_magic_arg_s) - + return "%sget_ipython().run_line_magic(%r, %r)" % ( + lspace, + t_magic_name, + t_magic_arg_s, + ) + + # These define the transformations for the different escape characters. def _tr_system(line_info): "Translate lines escaped with: !" @@ -349,10 +350,7 @@ def help_end(line): esc = m.group(3) lspace = _initial_space_re.match(line).group(0) - # If we're mid-command, put it back on the next prompt for the user. - next_input = line.rstrip('?') if line.strip() != m.group(0) else None - - return _make_help_call(target, esc, lspace, next_input) + return _make_help_call(target, esc, lspace) @CoroutineInputTransformer.wrap diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py index 3a560073b22..a8f676f4952 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -325,7 +325,7 @@ def transform(self, lines: List[str]): ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'} ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately -def _make_help_call(target, esc, next_input=None): +def _make_help_call(target, esc): """Prepares a pinfo(2)/psearch call from a target name and the escape (i.e. ? or ??)""" method = 'pinfo2' if esc == '??' \ @@ -335,11 +335,8 @@ def _make_help_call(target, esc, next_input=None): #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args) t_magic_name, _, t_magic_arg_s = arg.partition(' ') t_magic_name = t_magic_name.lstrip(ESC_MAGIC) - if next_input is None: - return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s) - else: - return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \ - (next_input, t_magic_name, t_magic_arg_s) + return "get_ipython().run_line_magic(%r, %r)" % (t_magic_name, t_magic_arg_s) + def _tr_help(content): """Translate lines escaped with: ? @@ -480,13 +477,8 @@ def transform(self, lines): target = m.group(1) esc = m.group(3) - # If we're mid-command, put it back on the next prompt for the user. - next_input = None - if (not lines_before) and (not lines_after) \ - and content.strip() != m.group(0): - next_input = content.rstrip('?\n') - call = _make_help_call(target, esc, next_input=next_input) + call = _make_help_call(target, esc) new_line = indent + call + '\n' return lines_before + [new_line] + lines_after diff --git a/IPython/core/tests/test_inputtransformer.py b/IPython/core/tests/test_inputtransformer.py index 4de97b87cb8..bfc936d3176 100644 --- a/IPython/core/tests/test_inputtransformer.py +++ b/IPython/core/tests/test_inputtransformer.py @@ -59,108 +59,93 @@ def transform_checker(tests, transformer, **kwargs): ('x=1', 'x=1'), # normal input is unmodified (' ',' '), # blank lines are kept intact ("a, b = %foo", "a, b = get_ipython().run_line_magic('foo', '')"), - ], - - classic_prompt = - [('>>> x=1', 'x=1'), - ('x=1', 'x=1'), # normal input is unmodified - (' ', ' '), # blank lines are kept intact - ], - - ipy_prompt = - [('In [1]: x=1', 'x=1'), - ('x=1', 'x=1'), # normal input is unmodified - (' ',' '), # blank lines are kept intact - ], - - # Tests for the escape transformer to leave normal code alone - escaped_noesc = - [ (' ', ' '), - ('x=1', 'x=1'), - ], - - # System calls - escaped_shell = - [ ('!ls', "get_ipython().system('ls')"), - # Double-escape shell, this means to capture the output of the - # subprocess and return it - ('!!ls', "get_ipython().getoutput('ls')"), - ], - - # Help/object info - escaped_help = - [ ('?', 'get_ipython().show_usage()'), - ('?x1', "get_ipython().run_line_magic('pinfo', 'x1')"), - ('??x2', "get_ipython().run_line_magic('pinfo2', 'x2')"), - ('?a.*s', "get_ipython().run_line_magic('psearch', 'a.*s')"), - ('?%hist1', "get_ipython().run_line_magic('pinfo', '%hist1')"), - ('?%%hist2', "get_ipython().run_line_magic('pinfo', '%%hist2')"), - ('?abc = qwe', "get_ipython().run_line_magic('pinfo', 'abc')"), - ], - - end_help = - [ ('x3?', "get_ipython().run_line_magic('pinfo', 'x3')"), - ('x4??', "get_ipython().run_line_magic('pinfo2', 'x4')"), - ('%hist1?', "get_ipython().run_line_magic('pinfo', '%hist1')"), - ('%hist2??', "get_ipython().run_line_magic('pinfo2', '%hist2')"), - ('%%hist3?', "get_ipython().run_line_magic('pinfo', '%%hist3')"), - ('%%hist4??', "get_ipython().run_line_magic('pinfo2', '%%hist4')"), - ('π.foo?', "get_ipython().run_line_magic('pinfo', 'π.foo')"), - ('f*?', "get_ipython().run_line_magic('psearch', 'f*')"), - ('ax.*aspe*?', "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"), - ('a = abc?', "get_ipython().set_next_input('a = abc');" - "get_ipython().run_line_magic('pinfo', 'abc')"), - ('a = abc.qe??', "get_ipython().set_next_input('a = abc.qe');" - "get_ipython().run_line_magic('pinfo2', 'abc.qe')"), - ('a = *.items?', "get_ipython().set_next_input('a = *.items');" - "get_ipython().run_line_magic('psearch', '*.items')"), - ('plot(a?', "get_ipython().set_next_input('plot(a');" - "get_ipython().run_line_magic('pinfo', 'a')"), - ('a*2 #comment?', 'a*2 #comment?'), - ], - - # Explicit magic calls - escaped_magic = - [ ('%cd', "get_ipython().run_line_magic('cd', '')"), - ('%cd /home', "get_ipython().run_line_magic('cd', '/home')"), - # Backslashes need to be escaped. - ('%cd C:\\User', "get_ipython().run_line_magic('cd', 'C:\\\\User')"), - (' %magic', " get_ipython().run_line_magic('magic', '')"), - ], - - # Quoting with separate arguments - escaped_quote = - [ (',f', 'f("")'), - (',f x', 'f("x")'), - (' ,f y', ' f("y")'), - (',f a b', 'f("a", "b")'), - ], - - # Quoting with single argument - escaped_quote2 = - [ (';f', 'f("")'), - (';f x', 'f("x")'), - (' ;f y', ' f("y")'), - (';f a b', 'f("a b")'), - ], - - # Simply apply parens - escaped_paren = - [ ('/f', 'f()'), - ('/f x', 'f(x)'), - (' /f y', ' f(y)'), - ('/f a b', 'f(a, b)'), - ], - - # Check that we transform prompts before other transforms - mixed = - [ ('In [1]: %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"), - ('>>> %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"), - ('In [2]: !ls', "get_ipython().system('ls')"), - ('In [3]: abs?', "get_ipython().run_line_magic('pinfo', 'abs')"), - ('In [4]: b = %who', "b = get_ipython().run_line_magic('who', '')"), - ], - ) + ], + classic_prompt=[ + (">>> x=1", "x=1"), + ("x=1", "x=1"), # normal input is unmodified + (" ", " "), # blank lines are kept intact + ], + ipy_prompt=[ + ("In [1]: x=1", "x=1"), + ("x=1", "x=1"), # normal input is unmodified + (" ", " "), # blank lines are kept intact + ], + # Tests for the escape transformer to leave normal code alone + escaped_noesc=[ + (" ", " "), + ("x=1", "x=1"), + ], + # System calls + escaped_shell=[ + ("!ls", "get_ipython().system('ls')"), + # Double-escape shell, this means to capture the output of the + # subprocess and return it + ("!!ls", "get_ipython().getoutput('ls')"), + ], + # Help/object info + escaped_help=[ + ("?", "get_ipython().show_usage()"), + ("?x1", "get_ipython().run_line_magic('pinfo', 'x1')"), + ("??x2", "get_ipython().run_line_magic('pinfo2', 'x2')"), + ("?a.*s", "get_ipython().run_line_magic('psearch', 'a.*s')"), + ("?%hist1", "get_ipython().run_line_magic('pinfo', '%hist1')"), + ("?%%hist2", "get_ipython().run_line_magic('pinfo', '%%hist2')"), + ("?abc = qwe", "get_ipython().run_line_magic('pinfo', 'abc')"), + ], + end_help=[ + ("x3?", "get_ipython().run_line_magic('pinfo', 'x3')"), + ("x4??", "get_ipython().run_line_magic('pinfo2', 'x4')"), + ("%hist1?", "get_ipython().run_line_magic('pinfo', '%hist1')"), + ("%hist2??", "get_ipython().run_line_magic('pinfo2', '%hist2')"), + ("%%hist3?", "get_ipython().run_line_magic('pinfo', '%%hist3')"), + ("%%hist4??", "get_ipython().run_line_magic('pinfo2', '%%hist4')"), + ("π.foo?", "get_ipython().run_line_magic('pinfo', 'π.foo')"), + ("f*?", "get_ipython().run_line_magic('psearch', 'f*')"), + ("ax.*aspe*?", "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"), + ("a = abc?", "get_ipython().run_line_magic('pinfo', 'abc')"), + ("a = abc.qe??", "get_ipython().run_line_magic('pinfo2', 'abc.qe')"), + ("a = *.items?", "get_ipython().run_line_magic('psearch', '*.items')"), + ("plot(a?", "get_ipython().run_line_magic('pinfo', 'a')"), + ("a*2 #comment?", "a*2 #comment?"), + ], + # Explicit magic calls + escaped_magic=[ + ("%cd", "get_ipython().run_line_magic('cd', '')"), + ("%cd /home", "get_ipython().run_line_magic('cd', '/home')"), + # Backslashes need to be escaped. + ("%cd C:\\User", "get_ipython().run_line_magic('cd', 'C:\\\\User')"), + (" %magic", " get_ipython().run_line_magic('magic', '')"), + ], + # Quoting with separate arguments + escaped_quote=[ + (",f", 'f("")'), + (",f x", 'f("x")'), + (" ,f y", ' f("y")'), + (",f a b", 'f("a", "b")'), + ], + # Quoting with single argument + escaped_quote2=[ + (";f", 'f("")'), + (";f x", 'f("x")'), + (" ;f y", ' f("y")'), + (";f a b", 'f("a b")'), + ], + # Simply apply parens + escaped_paren=[ + ("/f", "f()"), + ("/f x", "f(x)"), + (" /f y", " f(y)"), + ("/f a b", "f(a, b)"), + ], + # Check that we transform prompts before other transforms + mixed=[ + ("In [1]: %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), + (">>> %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), + ("In [2]: !ls", "get_ipython().system('ls')"), + ("In [3]: abs?", "get_ipython().run_line_magic('pinfo', 'abs')"), + ("In [4]: b = %who", "b = get_ipython().run_line_magic('who', '')"), + ], +) # multiline syntax examples. Each of these should be a list of lists, with # each entry itself having pairs of raw/transformed input. The union (with diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py index abc63031d3a..0613dc02c93 100644 --- a/IPython/core/tests/test_inputtransformer2.py +++ b/IPython/core/tests/test_inputtransformer2.py @@ -14,45 +14,65 @@ from IPython.core import inputtransformer2 as ipt2 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line -MULTILINE_MAGIC = ("""\ +MULTILINE_MAGIC = ( + """\ a = f() %foo \\ bar g() -""".splitlines(keepends=True), (2, 0), """\ +""".splitlines( + keepends=True + ), + (2, 0), + """\ a = f() get_ipython().run_line_magic('foo', ' bar') g() -""".splitlines(keepends=True)) +""".splitlines( + keepends=True + ), +) -INDENTED_MAGIC = ("""\ +INDENTED_MAGIC = ( + """\ for a in range(5): %ls -""".splitlines(keepends=True), (2, 4), """\ +""".splitlines( + keepends=True + ), + (2, 4), + """\ for a in range(5): get_ipython().run_line_magic('ls', '') -""".splitlines(keepends=True)) +""".splitlines( + keepends=True + ), +) -CRLF_MAGIC = ([ - "a = f()\n", - "%ls\r\n", - "g()\n" -], (2, 0), [ - "a = f()\n", - "get_ipython().run_line_magic('ls', '')\n", - "g()\n" -]) - -MULTILINE_MAGIC_ASSIGN = ("""\ +CRLF_MAGIC = ( + ["a = f()\n", "%ls\r\n", "g()\n"], + (2, 0), + ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"], +) + +MULTILINE_MAGIC_ASSIGN = ( + """\ a = f() b = %foo \\ bar g() -""".splitlines(keepends=True), (2, 4), """\ +""".splitlines( + keepends=True + ), + (2, 4), + """\ a = f() b = get_ipython().run_line_magic('foo', ' bar') g() -""".splitlines(keepends=True)) +""".splitlines( + keepends=True + ), +) MULTILINE_SYSTEM_ASSIGN = ("""\ a = f() @@ -72,68 +92,70 @@ def test(): for i in range(1): print(i) res =! ls -""".splitlines(keepends=True), (4, 7), '''\ +""".splitlines( + keepends=True + ), + (4, 7), + """\ def test(): for i in range(1): print(i) res =get_ipython().getoutput(\' ls\') -'''.splitlines(keepends=True)) +""".splitlines( + keepends=True + ), +) ###### -AUTOCALL_QUOTE = ( - [",f 1 2 3\n"], (1, 0), - ['f("1", "2", "3")\n'] -) +AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n']) -AUTOCALL_QUOTE2 = ( - [";f 1 2 3\n"], (1, 0), - ['f("1 2 3")\n'] -) +AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n']) -AUTOCALL_PAREN = ( - ["/f 1 2 3\n"], (1, 0), - ['f(1, 2, 3)\n'] -) +AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"]) -SIMPLE_HELP = ( - ["foo?\n"], (1, 0), - ["get_ipython().run_line_magic('pinfo', 'foo')\n"] -) +SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"]) DETAILED_HELP = ( - ["foo??\n"], (1, 0), - ["get_ipython().run_line_magic('pinfo2', 'foo')\n"] + ["foo??\n"], + (1, 0), + ["get_ipython().run_line_magic('pinfo2', 'foo')\n"], ) -MAGIC_HELP = ( - ["%foo?\n"], (1, 0), - ["get_ipython().run_line_magic('pinfo', '%foo')\n"] -) +MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"]) HELP_IN_EXPR = ( - ["a = b + c?\n"], (1, 0), - ["get_ipython().set_next_input('a = b + c');" - "get_ipython().run_line_magic('pinfo', 'c')\n"] + ["a = b + c?\n"], + (1, 0), + ["get_ipython().run_line_magic('pinfo', 'c')\n"], ) -HELP_CONTINUED_LINE = ("""\ +HELP_CONTINUED_LINE = ( + """\ a = \\ zip? -""".splitlines(keepends=True), (1, 0), -[r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"] +""".splitlines( + keepends=True + ), + (1, 0), + [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"], ) -HELP_MULTILINE = ("""\ +HELP_MULTILINE = ( + """\ (a, b) = zip? -""".splitlines(keepends=True), (1, 0), -[r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"] +""".splitlines( + keepends=True + ), + (1, 0), + [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"], ) HELP_UNICODE = ( - ["π.foo?\n"], (1, 0), - ["get_ipython().run_line_magic('pinfo', 'π.foo')\n"] + ["π.foo?\n"], + (1, 0), + ["get_ipython().run_line_magic('pinfo', 'π.foo')\n"], ) @@ -149,6 +171,7 @@ def test_check_make_token_by_line_never_ends_empty(): Check that not sequence of single or double characters ends up leading to en empty list of tokens """ from string import printable + for c in printable: assert make_tokens_by_line(c)[-1] != [] for k in printable: @@ -156,7 +179,7 @@ def test_check_make_token_by_line_never_ends_empty(): def check_find(transformer, case, match=True): - sample, expected_start, _ = case + sample, expected_start, _ = case tbl = make_tokens_by_line(sample) res = transformer.find(tbl) if match: @@ -166,25 +189,30 @@ def check_find(transformer, case, match=True): else: assert res is None + def check_transform(transformer_cls, case): lines, start, expected = case transformer = transformer_cls(start) assert transformer.transform(lines) == expected + def test_continued_line(): lines = MULTILINE_MAGIC_ASSIGN[0] assert ipt2.find_end_of_continued_line(lines, 1) == 2 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar" + def test_find_assign_magic(): check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN) check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False) check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False) + def test_transform_assign_magic(): check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN) + def test_find_assign_system(): check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN) check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT) @@ -192,30 +220,36 @@ def test_find_assign_system(): check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None)) check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False) + def test_transform_assign_system(): check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN) check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT) + def test_find_magic_escape(): check_find(ipt2.EscapedCommand, MULTILINE_MAGIC) check_find(ipt2.EscapedCommand, INDENTED_MAGIC) check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False) + def test_transform_magic_escape(): check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC) check_transform(ipt2.EscapedCommand, INDENTED_MAGIC) check_transform(ipt2.EscapedCommand, CRLF_MAGIC) + def test_find_autocalls(): for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]: print("Testing %r" % case[0]) check_find(ipt2.EscapedCommand, case) + def test_transform_autocall(): for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]: print("Testing %r" % case[0]) check_transform(ipt2.EscapedCommand, case) + def test_find_help(): for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]: check_find(ipt2.HelpEnd, case) @@ -233,6 +267,7 @@ def test_find_help(): # Nor in a string check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False) + def test_transform_help(): tf = ipt2.HelpEnd((1, 0), (1, 9)) assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2] @@ -246,10 +281,12 @@ def test_transform_help(): tf = ipt2.HelpEnd((1, 0), (1, 0)) assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2] + def test_find_assign_op_dedent(): """ be careful that empty token like dedent are not counted as parens """ + class Tk: def __init__(self, s): self.string = s @@ -302,21 +339,23 @@ def test_check_complete_param(code, expected, number): def test_check_complete(): cc = ipt2.TransformerManager().check_complete - example = dedent(""" + example = dedent( + """ if True: - a=1""" ) + a=1""" + ) assert cc(example) == ("incomplete", 4) assert cc(example + "\n") == ("complete", None) assert cc(example + "\n ") == ("complete", None) # no need to loop on all the letters/numbers. - short = '12abAB'+string.printable[62:] + short = "12abAB" + string.printable[62:] for c in short: # test does not raise: cc(c) for k in short: - cc(c+k) + cc(c + k) assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2) @@ -371,10 +410,9 @@ def test_null_cleanup_transformer(): assert manager.transform_cell("") == "" - - def test_side_effects_I(): count = 0 + def counter(lines): nonlocal count count += 1 @@ -384,14 +422,13 @@ def counter(lines): manager = ipt2.TransformerManager() manager.cleanup_transforms.insert(0, counter) - assert manager.check_complete("a=1\n") == ('complete', None) + assert manager.check_complete("a=1\n") == ("complete", None) assert count == 0 - - def test_side_effects_II(): count = 0 + def counter(lines): nonlocal count count += 1 @@ -401,5 +438,5 @@ def counter(lines): manager = ipt2.TransformerManager() manager.line_transforms.insert(0, counter) - assert manager.check_complete("b=1\n") == ('complete', None) + assert manager.check_complete("b=1\n") == ("complete", None) assert count == 0 diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index b557eb1935b..24fed4b2f71 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -8,6 +8,11 @@ IPython 8.3.0 ------------- + - :ghpull:`13625`, using ``?``, ``??``, ``*?`` will not call + ``set_next_input`` as most frontend allow proper multiline editing and it was + causing issues for many users of multi-cell frontends. + + - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on the info object when frontend provide it. From 0fab8f53b821019f60a00a70516a646fcd72e0ce Mon Sep 17 00:00:00 2001 From: Blazej Michalik Date: Tue, 5 Apr 2022 18:27:26 +0200 Subject: [PATCH 0123/1752] Add a changenote for #13624 --- docs/source/whatsnew/version8.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 24fed4b2f71..6ad4da1855a 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -16,6 +16,9 @@ IPython 8.3.0 - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on the info object when frontend provide it. + - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an + autosuggestion. + .. _version 8.2.0: IPython 8.2.0 From 73e71b17354720f0462f3327872210b6380251bf Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 7 Apr 2022 14:17:38 +0200 Subject: [PATCH 0124/1752] DOC: misc fixes --- IPython/core/completer.py | 2 +- IPython/core/oinspect.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 5a00f2c83a0..cdd28f65687 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -50,7 +50,7 @@ It is sometime challenging to know how to type a character, if you are using IPython, or any compatible frontend you can prepend backslash to the character -and press `` to expand it to its latex form. +and press ```` to expand it to its latex form. .. code:: diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 0096b9c9441..1a5c0ae070c 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -518,12 +518,12 @@ def _mime_format(self, text:str, formatter=None) -> dict: """Return a mime bundle representation of the input text. - if `formatter` is None, the returned mime bundle has - a `text/plain` field, with the input text. - a `text/html` field with a `
` tag containing the input text.
+           a ``text/plain`` field, with the input text.
+           a ``text/html`` field with a ``
`` tag containing the input text.
 
-        - if `formatter` is not None, it must be a callable transforming the
-          input text into a mime bundle. Default values for `text/plain` and
-          `text/html` representations are the ones described above.
+        - if ``formatter`` is not None, it must be a callable transforming the
+          input text into a mime bundle. Default values for ``text/plain`` and
+          ``text/html`` representations are the ones described above.
 
         Note:
 

From cf0cbaf5cfbd8464e5b0ea69da8d141671008e82 Mon Sep 17 00:00:00 2001
From: Tim Metzler <33530562+tmetzl@users.noreply.github.com>
Date: Tue, 26 Apr 2022 01:31:26 +0200
Subject: [PATCH 0125/1752] Fix typo

---
 docs/source/config/shell_mimerenderer.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/source/config/shell_mimerenderer.rst b/docs/source/config/shell_mimerenderer.rst
index 0fb2ffd9862..75872ac6a35 100644
--- a/docs/source/config/shell_mimerenderer.rst
+++ b/docs/source/config/shell_mimerenderer.rst
@@ -10,7 +10,7 @@ thought to render a number of mimetypes in the shell. This can be used to either
 display inline images if your terminal emulator supports it; or open some
 display results with external file viewers.
 
-Registering new mimetype handlers can so far only be done my extensions and
+Registering new mimetype handlers can so far only be done by extensions and
 requires 4 steps:
 
    - Define a callable that takes 2 parameters:``data`` and ``metadata``; return

From 9f3065cb1e06f0d84deba523cb258eb407a6aca6 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 11:12:50 +0200
Subject: [PATCH 0126/1752] misc doc fixes

---
 docs/source/config/integrating.rst         | 2 +-
 docs/source/index.rst                      | 6 +++---
 docs/source/whatsnew/github-stats-0.11.rst | 4 ++--
 docs/source/whatsnew/github-stats-2.0.rst  | 4 ++--
 docs/source/whatsnew/version0.9.rst        | 4 ++--
 docs/source/whatsnew/version8.rst          | 9 +++------
 6 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/docs/source/config/integrating.rst b/docs/source/config/integrating.rst
index a0459685236..07429ef1792 100644
--- a/docs/source/config/integrating.rst
+++ b/docs/source/config/integrating.rst
@@ -135,7 +135,7 @@ Metadata
 ^^^^^^^^
 
 We often want to provide frontends with guidance on how to display the data. To
-support this, ``_repr_*_()`` methods (except `_repr_pretty_``?) can also return a ``(data, metadata)``
+support this, ``_repr_*_()`` methods (except ``_repr_pretty_``?) can also return a ``(data, metadata)``
 tuple where ``metadata`` is a dictionary containing arbitrary key-value pairs for
 the frontend to interpret. An example use case is ``_repr_jpeg_()``, which can
 be set to return a jpeg image and a ``{'height': 400, 'width': 600}`` dictionary
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 9cec0577bed..ebef06fe900 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -17,9 +17,9 @@ interactively.  Its main components are:
 * A powerful interactive Python shell.
 
 
-.. image:: ./_images/ipython-6-screenshot.png
-    :alt: Screenshot of IPython 6.0
-    :align: center
+    .. image:: ./_images/ipython-6-screenshot.png
+        :alt: Screenshot of IPython 6.0
+        :align: center
 
 
 * A `Jupyter `_ kernel to work with Python code in Jupyter
diff --git a/docs/source/whatsnew/github-stats-0.11.rst b/docs/source/whatsnew/github-stats-0.11.rst
index 2f647ce7414..8fd4680c95b 100644
--- a/docs/source/whatsnew/github-stats-0.11.rst
+++ b/docs/source/whatsnew/github-stats-0.11.rst
@@ -51,9 +51,9 @@ Pull requests (226):
 * `542 `_: issue 440
 * `533 `_: Remove unused configobj and validate libraries from externals.
 * `538 `_: fix various tests on Windows
-* `540 `_: support `-pylab` flag with deprecation warning
+* `540 `_: support ``-pylab`` flag with deprecation warning
 * `537 `_: Docs update
-* `536 `_: `setup.py install` depends on setuptools on Windows
+* `536 `_: ``setup.py install`` depends on setuptools on Windows
 * `480 `_: Get help mid-command
 * `462 `_: Str and Bytes traitlets
 * `534 `_: Handle unicode properly in IPython.zmq.iostream
diff --git a/docs/source/whatsnew/github-stats-2.0.rst b/docs/source/whatsnew/github-stats-2.0.rst
index 5033994e1e1..98e3d5e3963 100644
--- a/docs/source/whatsnew/github-stats-2.0.rst
+++ b/docs/source/whatsnew/github-stats-2.0.rst
@@ -916,7 +916,7 @@ Pull Requests (687):
 * :ghpull:`4444`: Css cleaning
 * :ghpull:`4523`: Use username and password for MongoDB on ShiningPanda
 * :ghpull:`4510`: Update whatsnew from PR files
-* :ghpull:`4441`: add `setup.py jsversion`
+* :ghpull:`4441`: add ``setup.py jsversion``
 * :ghpull:`4518`: Fix for race condition in url file decoding.
 * :ghpull:`4497`: don't automatically unpack datetime objects in the message spec
 * :ghpull:`4506`: wait for empty queues as well as load-balanced tasks
@@ -1050,7 +1050,7 @@ Pull Requests (687):
 * :ghpull:`4214`: engine ID metadata should be unicode, not bytes
 * :ghpull:`4232`: no highlight if no language specified
 * :ghpull:`4218`: Fix display of SyntaxError when .py file is modified
-* :ghpull:`4207`: add `setup.py css` command
+* :ghpull:`4207`: add ``setup.py css`` command
 * :ghpull:`4224`: clear previous callbacks on execute
 * :ghpull:`4180`: Iptest refactoring
 * :ghpull:`4105`: JS output area misaligned
diff --git a/docs/source/whatsnew/version0.9.rst b/docs/source/whatsnew/version0.9.rst
index 81ebba1cac8..8d5f73c8667 100644
--- a/docs/source/whatsnew/version0.9.rst
+++ b/docs/source/whatsnew/version0.9.rst
@@ -81,7 +81,7 @@ New features
   :mod:`IPython.kernel`, :mod:`IPython.kernel.core`, :mod:`traitlets.config`,
   :mod:`IPython.tools` and :mod:`IPython.testing`.
 
-* As part of merging in the `ipython1-dev` stuff, the `setup.py` script and
+* As part of merging in the `ipython1-dev` stuff, the ``setup.py`` script and
   friends have been completely refactored.  Now we are checking for
   dependencies using the approach that matplotlib uses.
 
@@ -161,7 +161,7 @@ Backwards incompatible changes
   `'basic'` to `'b'`.
 
 * IPython has a larger set of dependencies if you want all of its capabilities.
-  See the `setup.py` script for details.
+  See the ``setup.py`` script for details.
 
 * The constructors for :class:`IPython.kernel.client.MultiEngineClient` and 
   :class:`IPython.kernel.client.TaskClient` no longer take the (ip,port) tuple.
diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst
index 6ad4da1855a..b88dfb4151a 100644
--- a/docs/source/whatsnew/version8.rst
+++ b/docs/source/whatsnew/version8.rst
@@ -16,9 +16,6 @@ IPython 8.3.0
  - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on
    the info object when frontend provide it.
 
- - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an
-   autosuggestion.
-
 .. _version 8.2.0:
 
 IPython 8.2.0
@@ -519,8 +516,8 @@ Automatic Vi prompt stripping
 
 When pasting code into IPython, it will strip the leading prompt characters if
 there are any. For example, you can paste the following code into the console -
-it will still work, even though each line is prefixed with prompts (`In`,
-`Out`)::
+it will still work, even though each line is prefixed with prompts (``In``,
+``Out``)::
 
     In [1]: 2 * 2 == 4
     Out[1]: True
@@ -615,7 +612,7 @@ who did a fantastic job at updating our code base, migrating to pytest, pushing
 our coverage, and fixing a large number of bugs. I highly recommend contacting
 them if you need help with C++ and Python projects.
 
-You can find all relevant issues and PRs with the SDG 2021 tag ``__
+You can find all relevant issues and PRs with `the SDG 2021 tag `__
 
 Removing support for older Python versions
 ------------------------------------------

From e25b9ab98fca64c136a50e2fde256f7cad9d0777 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 11:16:21 +0200
Subject: [PATCH 0127/1752] fix pytest

---
 pytest.ini | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pytest.ini b/pytest.ini
index e0cfec07bdf..81511e9ce51 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -45,3 +45,4 @@ addopts = --durations=10
     --ignore=IPython/utils/version.py
 doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS
 ipdoctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS
+asyncio_mode = strict

From c55dbf99334886b9ef7edd0fe8d5598e68ea6edc Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 11:18:00 +0200
Subject: [PATCH 0128/1752] typo and reformat

---
 IPython/__init__.py                          |  5 +++--
 IPython/core/autocall.py                     |  2 +-
 IPython/core/completer.py                    |  2 +-
 IPython/core/history.py                      |  2 +-
 IPython/core/interactiveshell.py             | 19 +++++++++++--------
 IPython/core/magics/execution.py             |  4 ++--
 IPython/core/magics/script.py                |  4 ++--
 IPython/core/tests/test_inputtransformer2.py |  3 ++-
 IPython/core/tests/test_interactiveshell.py  |  2 +-
 IPython/core/tests/test_iplib.py             |  3 ++-
 IPython/core/tests/test_magic.py             |  4 +++-
 IPython/core/tests/test_magic_terminal.py    | 12 ++++++++----
 IPython/core/tests/test_profile.py           |  2 +-
 IPython/external/qt_loaders.py               |  4 +++-
 IPython/lib/display.py                       | 16 +++++++---------
 IPython/lib/tests/test_latextools.py         |  2 +-
 IPython/lib/tests/test_pretty.py             |  2 +-
 IPython/testing/plugin/dtexample.py          |  4 ++--
 IPython/testing/plugin/pytest_ipdoctest.py   |  2 +-
 IPython/utils/text.py                        |  8 ++++----
 20 files changed, 57 insertions(+), 45 deletions(-)

diff --git a/IPython/__init__.py b/IPython/__init__.py
index e12da90d375..7ebb80b3621 100644
--- a/IPython/__init__.py
+++ b/IPython/__init__.py
@@ -28,7 +28,7 @@
 # Don't forget to also update setup.py when this changes!
 if sys.version_info < (3, 8):
     raise ImportError(
-"""
+        """
 IPython 8+ supports Python 3.8 and above, following NEP 29.
 When using Python 2.7, please install IPython 5.x LTS Long Term Support version.
 Python 3.3 and 3.4 were supported up to IPython 6.x.
@@ -40,7 +40,8 @@
 
     https://github.com/ipython/ipython/blob/master/README.rst
 
-""")
+"""
+    )
 
 #-----------------------------------------------------------------------------
 # Setup the top level names
diff --git a/IPython/core/autocall.py b/IPython/core/autocall.py
index 5f7720bb46c..54beec3f58d 100644
--- a/IPython/core/autocall.py
+++ b/IPython/core/autocall.py
@@ -40,7 +40,7 @@ def __init__(self, ip=None):
         self._ip = ip
     
     def set_ip(self, ip):
-        """ Will be used to set _ip point to current ipython instance b/f call
+        """Will be used to set _ip point to current ipython instance b/f call
 
         Override this method if you don't want this to happen.
 
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
index cdd28f65687..59d3e9930fc 100644
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -589,7 +589,7 @@ class Completer(Configurable):
 
         This will enable completion on elements of lists, results of function calls, etc.,
         but can be unsafe because the code is actually evaluated on TAB.
-        """
+        """,
     ).tag(config=True)
 
     use_jedi = Bool(default_value=JEDI_INSTALLED,
diff --git a/IPython/core/history.py b/IPython/core/history.py
index 41a04330b9b..579f6031dae 100644
--- a/IPython/core/history.py
+++ b/IPython/core/history.py
@@ -166,7 +166,7 @@ class HistoryAccessor(HistoryAccessorBase):
         in which case there will be no stored history, no SQLite connection,
         and no background saving thread.  This may be necessary in some
         threaded environments where IPython is embedded.
-        """
+        """,
     ).tag(config=True)
 
     connection_options = Dict(
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index ea9f6310ba5..2bb27a6e2b7 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -213,14 +213,17 @@ def __repr__(self):
         raw_cell = (
             (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell
         )
-        return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % (
-            name,
-            id(self),
-            raw_cell,
-            self.store_history,
-            self.silent,
-            self.shell_futures,
-            self.cell_id,
+        return (
+            '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>'
+            % (
+                name,
+                id(self),
+                raw_cell,
+                self.store_history,
+                self.silent,
+                self.shell_futures,
+                self.cell_id,
+            )
         )
 
 
diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py
index 371da5b1f98..da7f780b9cb 100644
--- a/IPython/core/magics/execution.py
+++ b/IPython/core/magics/execution.py
@@ -511,7 +511,7 @@ def run(self, parameter_s='', runner=None,
         """Run the named file inside IPython as a program.
 
         Usage::
-        
+
           %run [-n -i -e -G]
                [( -t [-N] | -d [-b] | -p [profile options] )]
                ( -m mod | filename ) [args]
@@ -552,7 +552,7 @@ def run(self, parameter_s='', runner=None,
         *two* back slashes (e.g. ``\\\\*``) to suppress expansions.
         To completely disable these expansions, you can use -G flag.
 
-        On Windows systems, the use of single quotes `'` when specifying 
+        On Windows systems, the use of single quotes `'` when specifying
         a file is not supported. Use double quotes `"`.
 
         Options:
diff --git a/IPython/core/magics/script.py b/IPython/core/magics/script.py
index a0028c25eea..9fd2fc6c0dd 100644
--- a/IPython/core/magics/script.py
+++ b/IPython/core/magics/script.py
@@ -58,8 +58,8 @@ def script_args(f):
             '--no-raise-error', action="store_false", dest='raise_error',
             help="""Whether you should raise an error message in addition to
             a stream on stderr if you get a nonzero exit code.
-            """
-        )
+            """,
+        ),
     ]
     for arg in args:
         f = arg(f)
diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py
index 0613dc02c93..cddb32f7138 100644
--- a/IPython/core/tests/test_inputtransformer2.py
+++ b/IPython/core/tests/test_inputtransformer2.py
@@ -87,7 +87,8 @@
 
 #####
 
-MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
+MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = (
+    """\
 def test():
   for i in range(1):
     print(i)
diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py
index 10cce1fd646..10827b5fa0f 100644
--- a/IPython/core/tests/test_interactiveshell.py
+++ b/IPython/core/tests/test_interactiveshell.py
@@ -547,7 +547,7 @@ def setUp(self):
         self.TESTDIR = join(self.BASETESTDIR, u"åäö")
         os.mkdir(self.TESTDIR)
         with open(
-            join(self.TESTDIR, u"åäötestscript.py"), "w", encoding="utf-8"
+            join(self.TESTDIR, "åäötestscript.py"), "w", encoding="utf-8"
         ) as sfile:
             sfile.write("pass\n")
         self.oldpath = os.getcwd()
diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py
index 94ce5185865..ec7007e0f2c 100644
--- a/IPython/core/tests/test_iplib.py
+++ b/IPython/core/tests/test_iplib.py
@@ -236,7 +236,8 @@ def test_run_cell():
     if 4:
         print "bar"
     
-    """)
+    """
+    )
     # Simply verifies that this kind of input is run
     ip.run_cell(complex)
     
diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py
index b46a9e8388c..850e22792b9 100644
--- a/IPython/core/tests/test_magic.py
+++ b/IPython/core/tests/test_magic.py
@@ -448,7 +448,9 @@ def test_multiline_time():
     ip = get_ipython()
     ip.user_ns.pop('run', None)
 
-    ip.run_cell(dedent("""\
+    ip.run_cell(
+        dedent(
+            """\
         %%time
         a = "ho"
         b = "hey"
diff --git a/IPython/core/tests/test_magic_terminal.py b/IPython/core/tests/test_magic_terminal.py
index f09014786e6..5dfa0f0ed65 100644
--- a/IPython/core/tests/test_magic_terminal.py
+++ b/IPython/core/tests/test_magic_terminal.py
@@ -122,7 +122,8 @@ def test_paste_pyprompt(self):
         ip.user_ns.pop("x")
 
     def test_paste_py_multi(self):
-        self.paste("""
+        self.paste(
+            """
         >>> x = [1,2,3]
         >>> y = []
         >>> for i in x:
@@ -145,7 +146,8 @@ def test_paste_py_multi_r(self):
 
     def test_paste_email(self):
         "Test pasting of email-quoted contents"
-        self.paste("""\
+        self.paste(
+            """\
         >> def foo(x):
         >>     return x + 1
         >> xx = foo(1.1)"""
@@ -154,7 +156,8 @@ def test_paste_email(self):
 
     def test_paste_email2(self):
         "Email again; some programs add a space also at each quoting level"
-        self.paste("""\
+        self.paste(
+            """\
         > > def foo(x):
         > >     return x + 1
         > > yy = foo(2.1)     """
@@ -163,7 +166,8 @@ def test_paste_email2(self):
 
     def test_paste_email_py(self):
         "Email quoting of interactive input"
-        self.paste("""\
+        self.paste(
+            """\
         >> >>> def f(x):
         >> ...   return x+1
         >> ... 
diff --git a/IPython/core/tests/test_profile.py b/IPython/core/tests/test_profile.py
index a0de2f2d593..876c7fda856 100644
--- a/IPython/core/tests/test_profile.py
+++ b/IPython/core/tests/test_profile.py
@@ -108,7 +108,7 @@ def test_list_profiles_in():
     for name in ("profile_foo", "profile_hello", "not_a_profile"):
         Path(td / name).mkdir(parents=True)
     if dec.unicode_paths:
-        Path(td / u"profile_ünicode").mkdir(parents=True)
+        Path(td / "profile_ünicode").mkdir(parents=True)
 
     with open(td / "profile_file", "w", encoding="utf-8") as f:
         f.write("I am not a profile directory")
diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py
index 975855ca6bc..39ea298460b 100644
--- a/IPython/external/qt_loaders.py
+++ b/IPython/external/qt_loaders.py
@@ -66,7 +66,9 @@ def find_spec(self, fullname, path, target=None):
                 """
     Importing %s disabled by IPython, which has
     already imported an Incompatible QT Binding: %s
-    """ % (fullname, loaded_api()))
+    """
+                % (fullname, loaded_api())
+            )
 
 
 ID = ImportDenier()
diff --git a/IPython/lib/display.py b/IPython/lib/display.py
index 4481683c4a0..5ff2983dbf4 100644
--- a/IPython/lib/display.py
+++ b/IPython/lib/display.py
@@ -103,9 +103,9 @@ class Audio(DisplayObject):
     See Also
     --------
     ipywidgets.Audio
-    
-         AUdio widget with more more flexibility and options.
-    
+
+         Audio widget with more more flexibility and options.
+
     """
     _read_flags = 'rb'
 
@@ -510,12 +510,10 @@ def __init__(self,
 
         self.recursive = recursive
 
-    def _get_display_formatter(self,
-                               dirname_output_format,
-                               fname_output_format,
-                               fp_format,
-                               fp_cleaner=None):
-        """ generate built-in formatter function
+    def _get_display_formatter(
+        self, dirname_output_format, fname_output_format, fp_format, fp_cleaner=None
+    ):
+        """generate built-in formatter function
 
         this is used to define both the notebook and terminal built-in
          formatters as they only differ by some wrapper text for each entry
diff --git a/IPython/lib/tests/test_latextools.py b/IPython/lib/tests/test_latextools.py
index ead73abff93..d035752b4fe 100644
--- a/IPython/lib/tests/test_latextools.py
+++ b/IPython/lib/tests/test_latextools.py
@@ -31,7 +31,7 @@ def no_op(*args, **kwargs):
 
 
 @onlyif_cmds_exist("latex", "dvipng")
-@pytest.mark.parametrize("s, wrap", [(u"$$x^2$$", False), (u"x^2", True)])
+@pytest.mark.parametrize("s, wrap", [("$$x^2$$", False), ("x^2", True)])
 def test_latex_to_png_dvipng_runs(s, wrap):
     """
     Test that latex_to_png_dvipng just runs without error.
diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py
index ca16924e8f7..86085166071 100644
--- a/IPython/lib/tests/test_pretty.py
+++ b/IPython/lib/tests/test_pretty.py
@@ -273,7 +273,7 @@ def __repr__(self):
     p = pretty.pretty(c)
     assert p == u
     p = pretty.pretty([c])
-    assert p == u"[%s]" % u
+    assert p == "[%s]" % u
 
 
 def test_basic_class():
diff --git a/IPython/testing/plugin/dtexample.py b/IPython/testing/plugin/dtexample.py
index 119e0a0536c..aff616fa0d9 100644
--- a/IPython/testing/plugin/dtexample.py
+++ b/IPython/testing/plugin/dtexample.py
@@ -38,7 +38,7 @@ def ipfunc():
        ....:     print(i, end=' ')
        ....:     print(i+1, end=' ')
        ....:
-    0 1 1 2 2 3 
+    0 1 1 2 2 3
 
 
     It's OK to use '_' for the last result, but do NOT try to use IPython's
@@ -50,7 +50,7 @@ def ipfunc():
 
     In [8]: print(repr(_))
     'hi'
-    
+
     In [7]: 3+4
     Out[7]: 7
 
diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py
index 809713d7c8e..4ba2f1adf8e 100644
--- a/IPython/testing/plugin/pytest_ipdoctest.py
+++ b/IPython/testing/plugin/pytest_ipdoctest.py
@@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str:
                 precision = 0 if fraction is None else len(fraction)
                 if exponent is not None:
                     precision -= int(exponent)
-                if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
+                if float(w.group()) == approx(float(g.group()), abs=10**-precision):
                     # They're close enough. Replace the text we actually
                     # got with the text we want, so that it will match when we
                     # check the string literally.
diff --git a/IPython/utils/text.py b/IPython/utils/text.py
index ef75f9331d7..74bccddf68b 100644
--- a/IPython/utils/text.py
+++ b/IPython/utils/text.py
@@ -470,11 +470,11 @@ def strip_ansi(source):
 
 class EvalFormatter(Formatter):
     """A String Formatter that allows evaluation of simple expressions.
-    
+
     Note that this version interprets a `:`  as specifying a format string (as per
     standard string formatting), so if slicing is required, you must explicitly
     create a slice.
-    
+
     This is to be used in templating cases, such as the parallel batch
     script templates, where simple arithmetic on arguments is useful.
 
@@ -690,8 +690,8 @@ def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) :
         return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
 
 
-def columnize(items, row_first=False, separator='  ', displaywidth=80, spread=False):
-    """ Transform a list of strings into a single string with columns.
+def columnize(items, row_first=False, separator="  ", displaywidth=80, spread=False):
+    """Transform a list of strings into a single string with columns.
 
     Parameters
     ----------

From 96aefe4cd3768893f22480afe710ae392dd3dad3 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 11:36:37 +0200
Subject: [PATCH 0129/1752] reformat 2

---
 IPython/core/interactiveshell.py           | 19 ++++++++-----------
 IPython/testing/plugin/dtexample.py        |  2 +-
 IPython/testing/plugin/pytest_ipdoctest.py |  2 +-
 3 files changed, 10 insertions(+), 13 deletions(-)

diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index 2bb27a6e2b7..ea9f6310ba5 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -213,17 +213,14 @@ def __repr__(self):
         raw_cell = (
             (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell
         )
-        return (
-            '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>'
-            % (
-                name,
-                id(self),
-                raw_cell,
-                self.store_history,
-                self.silent,
-                self.shell_futures,
-                self.cell_id,
-            )
+        return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % (
+            name,
+            id(self),
+            raw_cell,
+            self.store_history,
+            self.silent,
+            self.shell_futures,
+            self.cell_id,
         )
 
 
diff --git a/IPython/testing/plugin/dtexample.py b/IPython/testing/plugin/dtexample.py
index aff616fa0d9..68f7016e34d 100644
--- a/IPython/testing/plugin/dtexample.py
+++ b/IPython/testing/plugin/dtexample.py
@@ -60,7 +60,7 @@ def ipfunc():
     In [9]: ipfunc()
     Out[9]: 'ipfunc'
     """
-    return 'ipfunc'
+    return "ipfunc"
 
 
 def ipos():
diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py
index 4ba2f1adf8e..809713d7c8e 100644
--- a/IPython/testing/plugin/pytest_ipdoctest.py
+++ b/IPython/testing/plugin/pytest_ipdoctest.py
@@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str:
                 precision = 0 if fraction is None else len(fraction)
                 if exponent is not None:
                     precision -= int(exponent)
-                if float(w.group()) == approx(float(g.group()), abs=10**-precision):
+                if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
                     # They're close enough. Replace the text we actually
                     # got with the text we want, so that it will match when we
                     # check the string literally.

From d979804ce3789d77eec4e01004ae1e40714ed645 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 11:44:39 +0200
Subject: [PATCH 0130/1752] reformat setup.py

---
 setup.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 5614d3ef8c8..fbaa5f63cba 100644
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,9 @@
 
 Python {py} detected.
 {pip}
-""".format(py=sys.version_info, pip=pip_message )
+""".format(
+        py=sys.version_info, pip=pip_message
+    )
 
     print(error, file=sys.stderr)
     sys.exit(1)

From 22b4af1911f0bb234eaa9689ad20fd5883e8203c Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 03:09:06 -0700
Subject: [PATCH 0131/1752] Update test.yml

---
 .github/workflows/test.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 601828aac9c..1bbddbfbf98 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,6 +17,7 @@ jobs:
   test:
     runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest]
         python-version: ["3.8", "3.9", "3.10"]

From dc5bcc1c50892a5128fcf128af28887226144927 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 11:11:31 +0200
Subject: [PATCH 0132/1752] This fixed the mixing of multiple history seen in
 #13631

It forces get_tail to put the current session last in the returned
results.
---
 IPython/core/history.py | 37 +++++++++++++++++++++++++++++++------
 1 file changed, 31 insertions(+), 6 deletions(-)

diff --git a/IPython/core/history.py b/IPython/core/history.py
index 579f6031dae..9b0b2cbd048 100644
--- a/IPython/core/history.py
+++ b/IPython/core/history.py
@@ -296,8 +296,8 @@ def _run_sql(self, sql, params, raw=True, output=False, latest=False):
             toget = "history.%s, output_history.output" % toget
         if latest:
             toget += ", MAX(session * 128 * 1024 + line)"
-        cur = self.db.execute("SELECT session, line, %s FROM %s " %\
-                                (toget, sqlfrom) + sql, params)
+        this_querry = "SELECT session, line, %s FROM %s " % (toget, sqlfrom) + sql
+        cur = self.db.execute(this_querry, params)
         if latest:
             cur = (row[:-1] for row in cur)
         if output:    # Regroup into 3-tuples, and parse JSON
@@ -344,6 +344,11 @@ def get_last_session_id(self):
     def get_tail(self, n=10, raw=True, output=False, include_latest=False):
         """Get the last n lines from the history database.
 
+        Most recent entry last.
+
+        Completion will be reordered so that that the last ones are when
+        possible from current session.
+
         Parameters
         ----------
         n : int
@@ -362,11 +367,31 @@ def get_tail(self, n=10, raw=True, output=False, include_latest=False):
         self.writeout_cache()
         if not include_latest:
             n += 1
-        cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
-                                (n,), raw=raw, output=output)
+        # cursor/line/entry
+        this_cur = list(
+            self._run_sql(
+                "WHERE session == ? ORDER BY line DESC LIMIT ?  ",
+                (self.session_number, n),
+                raw=raw,
+                output=output,
+            )
+        )
+        other_cur = list(
+            self._run_sql(
+                "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?",
+                (self.session_number, n),
+                raw=raw,
+                output=output,
+            )
+        )
+
+        everything = this_cur + other_cur
+
+        everything = everything[:n]
+
         if not include_latest:
-            return reversed(list(cur)[1:])
-        return reversed(list(cur))
+            return list(everything)[:0:-1]
+        return list(everything)[::-1]
 
     @catch_corrupt_db
     def search(self, pattern="*", raw=True, search_raw=True,

From af8de90a2537fbf4bb49a1c005b854d32248fd36 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 13:58:31 +0200
Subject: [PATCH 0133/1752] whats new 7.33

---
 docs/source/whatsnew/version7.rst | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst
index 4dea95d6c78..f6ecd257273 100644
--- a/docs/source/whatsnew/version7.rst
+++ b/docs/source/whatsnew/version7.rst
@@ -2,6 +2,17 @@
  7.x Series
 ============
 
+.. _version 7.33:
+
+IPython 7.33
+============
+
+ - Allow IPython hooks to receive current cell ids when frontend support it. See
+   :ghpull:`13600`
+
+ - ``?`` does not trigger the insertion of a new cell anymore as most frontend
+   allow proper multiline edition. :ghpull:`13625`
+
 
 .. _version 7.32:
 

From 1150470ad809bf095a5ac78f35748e42c08f5c9d Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 14:13:52 +0200
Subject: [PATCH 0134/1752] whats new 8.3

---
 docs/source/whatsnew/version8.rst | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst
index b88dfb4151a..f9636e4877b 100644
--- a/docs/source/whatsnew/version8.rst
+++ b/docs/source/whatsnew/version8.rst
@@ -10,11 +10,16 @@ IPython 8.3.0
 
  - :ghpull:`13625`, using ``?``, ``??``, ``*?`` will not call
    ``set_next_input`` as most frontend allow proper multiline editing and it was
-   causing issues for many users of multi-cell frontends.
+   causing issues for many users of multi-cell frontends. This has been backported to 7.33
 
 
  - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on
-   the info object when frontend provide it.
+   the info object when frontend provide it. This has been backported to 7.33
+
+ - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an
+   auto-suggestion.
+
+ - :ghpull:`13657` fix issue where history from different sessions would be mixed.
 
 .. _version 8.2.0:
 

From 55e81b920d19c6f7ca4588f1b85f2c3b11b209d3 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 14:30:15 +0200
Subject: [PATCH 0135/1752] release 8.3.0

---
 IPython/core/release.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/IPython/core/release.py b/IPython/core/release.py
index ae19fa66f18..4e8c82cdb7d 100644
--- a/IPython/core/release.py
+++ b/IPython/core/release.py
@@ -20,7 +20,7 @@
 _version_patch = 0
 _version_extra = ".dev"
 # _version_extra = "rc1"
-# _version_extra = ""  # Uncomment this for full releases
+_version_extra = ""  # Uncomment this for full releases
 
 # Construct full version string from these.
 _ver = [_version_major, _version_minor, _version_patch]

From d1746145d828b4eb01f1e4d44d3f33d75206b1aa Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Fri, 29 Apr 2022 14:30:44 +0200
Subject: [PATCH 0136/1752] back to dev

---
 IPython/core/release.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/IPython/core/release.py b/IPython/core/release.py
index 4e8c82cdb7d..9d236997c27 100644
--- a/IPython/core/release.py
+++ b/IPython/core/release.py
@@ -16,11 +16,11 @@
 # release.  'dev' as a _version_extra string means this is a development
 # version
 _version_major = 8
-_version_minor = 3
+_version_minor = 4
 _version_patch = 0
 _version_extra = ".dev"
 # _version_extra = "rc1"
-_version_extra = ""  # Uncomment this for full releases
+# _version_extra = ""  # Uncomment this for full releases
 
 # Construct full version string from these.
 _ver = [_version_major, _version_minor, _version_patch]

From fc235985508d0431192405b46c6fbfb209069048 Mon Sep 17 00:00:00 2001
From: telamonian 
Date: Mon, 2 May 2022 19:40:28 -0400
Subject: [PATCH 0137/1752] fix uncaught `BdbQuit` exceptions on ipdb `exit`

- `BdbQuit` is now handled in the top-most scope of `InteractiveShell.run_code`. This ensures that `BdbQuit` is correctly handled but can still do its job of breaking out of all user code/loops/further breakpoint requests. Hopefully will work better than previous attempts, which put the `BdqQuit` handling in `Pdb.set_trace`

- fixes:
  - jupyterlab/jupyterlab#12501

- refs:
  - ipython/ipython#876
  - ipython/ipython#1273
  - ipython/ipython#4474
  - ipython/ipython#5306
  - ipython/ipython#9731
  - ipython/ipython#9942
  - ipython/ipython#9950
  - ipython/ipython#10006
  - ipython/ipython#12378
---
 .gitignore                       | 10 +++++++++-
 IPython/core/debugger.py         |  1 -
 IPython/core/interactiveshell.py |  6 ++++++
 3 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index 5b45fd4d6b8..f4736530e10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,5 +28,13 @@ __pycache__
 .pytest_cache
 .python-version
 venv*/
-.idea/
 .mypy_cache/
+
+# jetbrains ide stuff
+*.iml
+.idea/
+
+# vscode ide stuff
+*.code-workspace
+.history
+.vscode
diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py
index 8e3dd9678cd..ba12e3eac39 100644
--- a/IPython/core/debugger.py
+++ b/IPython/core/debugger.py
@@ -101,7 +101,6 @@
 #
 #*****************************************************************************
 
-import bdb
 import inspect
 import linecache
 import sys
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index ea9f6310ba5..371a3dad77e 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -14,6 +14,7 @@
 import abc
 import ast
 import atexit
+import bdb
 import builtins as builtin_mod
 import dis
 import functools
@@ -3403,6 +3404,11 @@ async def run_code(self, code_obj, result=None, *, async_=False):
                 result.error_in_exec = e
             self.showtraceback(exception_only=True)
             warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)
+        except bdb.BdbQuit:
+            etype, value, tb = sys.exc_info()
+            if result is not None:
+                result.error_in_exec = value
+            # the BdbQuit stops here
         except self.custom_exceptions:
             etype, value, tb = sys.exc_info()
             if result is not None:

From d15e4f6a089b785a380b991b5ebc177f0060aac1 Mon Sep 17 00:00:00 2001
From: Artur Svistunov <18216480+madbird1304@users.noreply.github.com>
Date: Fri, 6 May 2022 02:45:57 +0300
Subject: [PATCH 0138/1752] issue#13073: Made a couple of fixes after testing
 on Wayland (Gnome)

---
 IPython/lib/clipboard.py | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/IPython/lib/clipboard.py b/IPython/lib/clipboard.py
index 5d6630d17a7..1d691a7ea63 100644
--- a/IPython/lib/clipboard.py
+++ b/IPython/lib/clipboard.py
@@ -1,14 +1,16 @@
 """ Utilities for accessing the platform's clipboard.
 """
-
+import os
 import subprocess
 
 from IPython.core.error import TryNext
 import IPython.utils.py3compat as py3compat
 
+
 class ClipboardEmpty(ValueError):
     pass
 
+
 def win32_clipboard_get():
     """ Get the current clipboard's text on Windows.
 
@@ -32,6 +34,7 @@ def win32_clipboard_get():
         win32clipboard.CloseClipboard()
     return text
 
+
 def osx_clipboard_get() -> str:
     """ Get the clipboard's text on OS X.
     """
@@ -43,6 +46,7 @@ def osx_clipboard_get() -> str:
     text = py3compat.decode(bytes_)
     return text
 
+
 def tkinter_clipboard_get():
     """ Get the clipboard's text using Tkinter.
 
@@ -77,13 +81,18 @@ def wayland_clipboard_get():
 
     try:
         with subprocess.Popen(["wl-paste"], stdout=subprocess.PIPE) as p:
-            raw, _ = p.communicate()
+            raw, err = p.communicate()
+            if p.wait():
+                raise TryNext(err)
     except FileNotFoundError as e:
-        raise ClipboardEmpty(
+        raise TryNext(
             "Getting text from the clipboard under Wayland requires the wl-clipboard "
             "extension: https://github.com/bugaevc/wl-clipboard"
         ) from e
 
+    if not raw:
+        raise ClipboardEmpty
+
     try:
         text = py3compat.decode(raw)
     except UnicodeDecodeError as e:

From 7d4445743ee27266003ef407a9090bd21170a848 Mon Sep 17 00:00:00 2001
From: Artur Svistunov <18216480+madbird1304@users.noreply.github.com>
Date: Fri, 6 May 2022 03:16:19 +0300
Subject: [PATCH 0139/1752] issue#13073: Fix formatting

---
 IPython/core/hooks.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/IPython/core/hooks.py b/IPython/core/hooks.py
index 0f1f5af2dc7..f73c5657638 100644
--- a/IPython/core/hooks.py
+++ b/IPython/core/hooks.py
@@ -155,8 +155,10 @@ def clipboard_get(self):
     """ Get text from the clipboard.
     """
     from ..lib.clipboard import (
-        osx_clipboard_get, tkinter_clipboard_get,
-        win32_clipboard_get, wayland_clipboard_get,
+        osx_clipboard_get,
+        tkinter_clipboard_get,
+        win32_clipboard_get,
+        wayland_clipboard_get,
     )
     if sys.platform == 'win32':
         chain = [win32_clipboard_get, tkinter_clipboard_get]

From c0a6176d65787bcd41e9b66ea0b8d5838f313af1 Mon Sep 17 00:00:00 2001
From: Blazej Michalik 
Date: Tue, 24 May 2022 03:10:20 +0200
Subject: [PATCH 0140/1752] Limit supported prompt-toolkit versions to
 3.0.2-3.1.0

Fixes #13645. Stems from #13592.
---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 2027b531d02..0e371836877 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -37,7 +37,7 @@ install_requires =
     matplotlib-inline
     pexpect>4.3; sys_platform != "win32"
     pickleshare
-    prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1
+    prompt_toolkit>3.0.1,<3.1.0
     pygments>=2.4.0
     setuptools>=18.5
     stack_data

From 2a80ede91b251d1aac2b7821441126a7bd87394e Mon Sep 17 00:00:00 2001
From: Thomas Grainger 
Date: Wed, 25 May 2022 20:31:47 +0100
Subject: [PATCH 0141/1752] fix get_event_loop typo

---
 docs/source/interactive/autoawait.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/source/interactive/autoawait.rst b/docs/source/interactive/autoawait.rst
index e4ed965a13e..3b62fdae05a 100644
--- a/docs/source/interactive/autoawait.rst
+++ b/docs/source/interactive/autoawait.rst
@@ -177,7 +177,7 @@ On top of the above there are significant modification to the AST of
 significant overhead to this kind of code.
 
 By default the generated coroutine function will be consumed by Asyncio's
-``loop_runner = asyncio.get_evenloop().run_until_complete()`` method if
+``loop_runner = asyncio.get_event_loop().run_until_complete()`` method if
 ``async`` mode is deemed necessary, otherwise the coroutine will just be
 exhausted in a simple runner. It is possible, though, to change the default
 runner.

From 2e08c72d61d2b03e6edd9a13a8976bf1d4fa482c Mon Sep 17 00:00:00 2001
From: Gene Louis Kim 
Date: Sat, 28 May 2022 02:31:00 -0400
Subject: [PATCH 0142/1752] Correct the debugger_cls() call to
 self.debugger_cls() call in TBTools.__init__() so that it doesn't crash when
 the optionally argument, debugger_cls, is unspecified.

---
 IPython/core/ultratb.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py
index 85697919408..5649a78d4bd 100644
--- a/IPython/core/ultratb.py
+++ b/IPython/core/ultratb.py
@@ -239,7 +239,7 @@ def __init__(
         self.debugger_cls = debugger_cls or debugger.Pdb
 
         if call_pdb:
-            self.pdb = debugger_cls()
+            self.pdb = self.debugger_cls()
         else:
             self.pdb = None
 

From daca7fa3beab97fac7e61458b40511546f160e31 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Sat, 28 May 2022 13:54:53 +0200
Subject: [PATCH 0143/1752] wn 734

---
 docs/source/whatsnew/version7.rst | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst
index f6ecd257273..16987a77048 100644
--- a/docs/source/whatsnew/version7.rst
+++ b/docs/source/whatsnew/version7.rst
@@ -2,6 +2,15 @@
  7.x Series
 ============
 
+.. _version 7.34:
+
+IPython 7.34
+============
+
+This version contains a single fix:  fix uncaught BdbQuit exceptions on ipdb
+exit :ghpull:`13668`
+
+
 .. _version 7.33:
 
 IPython 7.33

From b64600e6e235263961acdecfc0f5e6cb84d4909b Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Sat, 28 May 2022 13:59:04 +0200
Subject: [PATCH 0144/1752] What's new 8.4

---
 docs/source/whatsnew/version8.rst | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst
index f9636e4877b..c4b7b155212 100644
--- a/docs/source/whatsnew/version8.rst
+++ b/docs/source/whatsnew/version8.rst
@@ -2,6 +2,17 @@
  8.x Series
 ============
 
+.. _version 8.4.0:
+
+IPython 8.4.0
+-------------
+
+As for 7.34, this version contains a single fix:  fix uncaught BdbQuit exceptions on ipdb
+exit :ghpull:`13668`, and a single typo fix in documentation: :ghpull:`13682`
+
+Thanks to the `D. E. Shaw group `__ for sponsoring
+work on IPython and related libraries.
+
 
 .. _version 8.3.0:
 

From 4396dd6207f5bb1c310ea897065c0c6e17d5f5b1 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Sat, 28 May 2022 14:32:56 +0200
Subject: [PATCH 0145/1752] release 8.4.0

---
 IPython/core/release.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/IPython/core/release.py b/IPython/core/release.py
index 9d236997c27..cda5c32d444 100644
--- a/IPython/core/release.py
+++ b/IPython/core/release.py
@@ -20,7 +20,7 @@
 _version_patch = 0
 _version_extra = ".dev"
 # _version_extra = "rc1"
-# _version_extra = ""  # Uncomment this for full releases
+_version_extra = ""  # Uncomment this for full releases
 
 # Construct full version string from these.
 _ver = [_version_major, _version_minor, _version_patch]

From af37d7b2206fa3c3520b304011b4294fa3019da2 Mon Sep 17 00:00:00 2001
From: Matthias Bussonnier 
Date: Sat, 28 May 2022 14:33:21 +0200
Subject: [PATCH 0146/1752] back to dev

---
 IPython/core/release.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/IPython/core/release.py b/IPython/core/release.py
index cda5c32d444..1d6483ce50e 100644
--- a/IPython/core/release.py
+++ b/IPython/core/release.py
@@ -16,11 +16,11 @@
 # release.  'dev' as a _version_extra string means this is a development
 # version
 _version_major = 8
-_version_minor = 4
+_version_minor = 5
 _version_patch = 0
 _version_extra = ".dev"
 # _version_extra = "rc1"
-_version_extra = ""  # Uncomment this for full releases
+# _version_extra = ""  # Uncomment this for full releases
 
 # Construct full version string from these.
 _ver = [_version_major, _version_minor, _version_patch]

From d09a817d1deeea97ea43778780da7ee6ce47cbe9 Mon Sep 17 00:00:00 2001
From: TenzinRabgy 
Date: Thu, 2 Jun 2022 12:05:04 -0400
Subject: [PATCH 0147/1752] Update python2 code example to python3, print '' ->
 print('')

---
 IPython/lib/demo.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/IPython/lib/demo.py b/IPython/lib/demo.py
index 8c9ae905d49..ebffd54abde 100644
--- a/IPython/lib/demo.py
+++ b/IPython/lib/demo.py
@@ -136,7 +136,7 @@
     #################### EXAMPLE DEMO  ###############################
     '''A simple interactive demo to illustrate the use of IPython's Demo class.'''
 
-    print 'Hello, welcome to an interactive IPython demo.'
+    print('Hello, welcome to an interactive IPython demo.')
 
     # The mark below defines a block boundary, which is a point where IPython will
     # stop execution and return to the interactive prompt. The dashes are actually
@@ -152,21 +152,21 @@
     # the mark below makes this block as silent
     #  silent
 
-    print 'This is a silent block, which gets executed but not printed.'
+    print('This is a silent block, which gets executed but not printed.')
 
     #  stop
     #  auto
-    print 'This is an automatic block.'
-    print 'It is executed without asking for confirmation, but printed.'
-    z = x+y
+    print('This is an automatic block.')
+    print('It is executed without asking for confirmation, but printed.')
+    z = x + y
 
-    print 'z=',x
+    print('z =', x)
 
     #  stop
     # This is just another normal block.
-    print 'z is now:', z
+    print('z is now:', z)
 
-    print 'bye!'
+    print('bye!')
     ################### END EXAMPLE DEMO  ############################
 """
 

From 971e5ce2f61bc688e70b91280cff0ab0a3e098c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= 
Date: Fri, 10 Jun 2022 14:19:38 +0200
Subject: [PATCH 0148/1752] Modernize setuptools usage in pyproject.toml

1. Remove the redundant `wheel` dependency.  The setuptools build
   backend has been adding it automatically since day one, and it was
   explicitly specified in the docs as a mistake.  See:
   https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a

2. Replace the legacy backend with the regular backend.  The legacy
   backend was only intended to be used implicitly when `pyproject.toml`
   does not specify only, and was not supposed to be specified
   explicitly there.  See:
   https://github.com/pypa/setuptools/issues/1689

3. Prepend the current directory to `sys.path` as required for
   `setup.py` to reliably import `setupbase`.  The non-legacy backend
   no longer does this for us.
---
 pyproject.toml | 4 ++--
 setup.py       | 2 ++
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index c68b8c200f3..48ea37fb0f7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,3 @@
 [build-system]
-requires = ["setuptools >= 51.0.0", "wheel"]
-build-backend = "setuptools.build_meta:__legacy__"
+requires = ["setuptools >= 51.0.0"]
+build-backend = "setuptools.build_meta"
diff --git a/setup.py b/setup.py
index fbaa5f63cba..3c1dff9cc1c 100644
--- a/setup.py
+++ b/setup.py
@@ -64,6 +64,8 @@
 from setuptools import setup
 
 # Our own imports
+sys.path.insert(0, ".")
+
 from setupbase import target_update
 
 from setupbase import (

From ccd53253d1bdbf0ac1be7ad21be52e7a59a54364 Mon Sep 17 00:00:00 2001
From: Tom Nicholas <35968931+TomNicholas@users.noreply.github.com>
Date: Thu, 16 Jun 2022 14:57:09 -0400
Subject: [PATCH 0149/1752] Don't strip decorators before feeding into
 block_parser (#13612)

make sure only pseudo-decorators are stripped before feeding into block_parser
---
 IPython/sphinxext/ipython_directive.py               | 7 +++++--
 docs/source/whatsnew/pr/stripping-decorators-bug.rst | 4 ++++
 2 files changed, 9 insertions(+), 2 deletions(-)
 create mode 100644 docs/source/whatsnew/pr/stripping-decorators-bug.rst

diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py
index 18bdfcae993..b1f3931a475 100644
--- a/IPython/sphinxext/ipython_directive.py
+++ b/IPython/sphinxext/ipython_directive.py
@@ -821,8 +821,11 @@ def process_pure_python(self, content):
                 output.append(line)
                 continue
 
-            # handle decorators
-            if line_stripped.startswith('@'):
+            # handle pseudo-decorators, whilst ensuring real python decorators are treated as input
+            if any(
+                line_stripped.startswith("@" + pseudo_decorator)
+                for pseudo_decorator in PSEUDO_DECORATORS
+            ):
                 output.extend([line])
                 if 'savefig' in line:
                     savefig = True # and need to clear figure
diff --git a/docs/source/whatsnew/pr/stripping-decorators-bug.rst b/docs/source/whatsnew/pr/stripping-decorators-bug.rst
new file mode 100644
index 00000000000..2ea03d7f9f7
--- /dev/null
+++ b/docs/source/whatsnew/pr/stripping-decorators-bug.rst
@@ -0,0 +1,4 @@
+Stripping decorators bug
+========================
+
+Fixed bug which meant that ipython code blocks in restructured text documents executed with the ipython-sphinx extension skipped any lines of code containing python decorators.

From a72418e2dcdfc3c91f70d724d16d2691a41c9c24 Mon Sep 17 00:00:00 2001
From: JD Smith <93749+jdtsmith@users.noreply.github.com>
Date: Thu, 16 Jun 2022 15:15:36 -0400
Subject: [PATCH 0150/1752] Restore lineno's for Input mapped files (#13560)

* Implement lineno's for Input mapped files
* Adopt In [123], line 123 format
* Revert "Set co_name for cells run line by line. Fixes https://github.com/ipython/ipykernel/issues/841"
  (This reverts commit d11e987f174a15f1640f8006c86f58d884c3faa4.)
* Omit mention of ", in " for input tracebacks
* Input cell -> Cell
* Remove  from traceback doctests
* Use f-string for `in ...' format
* Simplify _format_list logic, converting to f-strings
---
 IPython/core/interactiveshell.py              | 25 ----------
 IPython/core/tests/test_iplib.py              | 12 ++---
 IPython/core/ultratb.py                       | 50 ++++++++-----------
 .../whatsnew/pr/restore-line-numbers.rst      | 50 +++++++++++++++++++
 4 files changed, 78 insertions(+), 59 deletions(-)
 create mode 100644 docs/source/whatsnew/pr/restore-line-numbers.rst

diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index 371a3dad77e..7587a1d6bb8 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -16,7 +16,6 @@
 import atexit
 import bdb
 import builtins as builtin_mod
-import dis
 import functools
 import inspect
 import os
@@ -3212,29 +3211,6 @@ def transform_ast(self, node):
             ast.fix_missing_locations(node)
         return node
 
-    def _update_code_co_name(self, code):
-        """Python 3.10 changed the behaviour so that whenever a code object
-        is assembled in the compile(ast) the co_firstlineno would be == 1.
-
-        This makes pydevd/debugpy think that all cells invoked are the same
-        since it caches information based on (co_firstlineno, co_name, co_filename).
-
-        Given that, this function changes the code 'co_name' to be unique
-        based on the first real lineno of the code (which also has a nice
-        side effect of customizing the name so that it's not always ).
-
-        See: https://github.com/ipython/ipykernel/issues/841
-        """
-        if not hasattr(code, "replace"):
-            # It may not be available on older versions of Python (only
-            # available for 3.8 onwards).
-            return code
-        try:
-            first_real_line = next(dis.findlinestarts(code))[1]
-        except StopIteration:
-            return code
-        return code.replace(co_name="" % (first_real_line,))
-
     async def run_ast_nodes(
         self,
         nodelist: ListType[stmt],
@@ -3333,7 +3309,6 @@ def compare(code):
                     else 0x0
                 ):
                     code = compiler(mod, cell_name, mode)
-                    code = self._update_code_co_name(code)
                     asy = compare(code)
                 if await self.run_code(code, result, async_=asy):
                     return True
diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py
index ec7007e0f2c..14474bb0da1 100644
--- a/IPython/core/tests/test_iplib.py
+++ b/IPython/core/tests/test_iplib.py
@@ -45,7 +45,7 @@ def doctest_tb_plain():
 
     In [19]: run simpleerr.py
     Traceback (most recent call last):
-      File ...:... in 
+      File ...:...
         bar(mode)
       File ...:... in bar
         div0()
@@ -64,7 +64,7 @@ def doctest_tb_context():
     ---------------------------------------------------------------------------
     ZeroDivisionError                         Traceback (most recent call last)
     
-    ... in 
+    ...
          30     except IndexError:
          31         mode = 'div'
     ---> 33     bar(mode)
@@ -93,7 +93,7 @@ def doctest_tb_verbose():
     ---------------------------------------------------------------------------
     ZeroDivisionError                         Traceback (most recent call last)
     
-    ... in 
+    ...
          30     except IndexError:
          31         mode = 'div'
     ---> 33     bar(mode)
@@ -134,7 +134,7 @@ def doctest_tb_sysexit():
     Traceback (most recent call last):
       File ...:... in execfile
         exec(compiler(f.read(), fname, "exec"), glob, loc)
-      File ...:... in 
+      File ...:...
         bar(mode)
       File ...:... in bar
         sysexit(stat, mode)
@@ -152,7 +152,7 @@ def doctest_tb_sysexit():
          ... with open(fname, "rb") as f:
          ...     compiler = compiler or compile
     ---> ...     exec(compiler(f.read(), fname, "exec"), glob, loc)
-    ...
+    ...
          30     except IndexError:
          31         mode = 'div'
     ---> 33     bar(mode)
@@ -189,7 +189,7 @@ def doctest_tb_sysexit_verbose():
     ---------------------------------------------------------------------------
     SystemExit                                Traceback (most recent call last)
     
-    ... in 
+    ...
          30     except IndexError:
          31         mode = 'div'
     ---> 33     bar(mode)
diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py
index 5649a78d4bd..125ee9a78a4 100644
--- a/IPython/core/ultratb.py
+++ b/IPython/core/ultratb.py
@@ -187,7 +187,10 @@ def _format_filename(file, ColorFilename, ColorNormal, *, lineno=None):
 
     if ipinst is not None and file in ipinst.compile._filename_map:
         file = "[%s]" % ipinst.compile._filename_map[file]
-        tpl_link = f"Input {ColorFilename}In {{file}}{ColorNormal}"
+        if lineno is None:
+            tpl_link = f"Cell {ColorFilename}In {{file}}{ColorNormal}"
+        else:
+            tpl_link = f"Cell {ColorFilename}In {{file}}, line {{lineno}}{ColorNormal}"
     else:
         file = util_path.compress_user(
             py3compat.cast_unicode(file, util_path.fs_encoding)
@@ -463,34 +466,25 @@ def _format_list(self, extracted_list):
 
         Colors = self.Colors
         list = []
-        for filename, lineno, name, line in extracted_list[:-1]:
-            item = "  %s in %s%s%s\n" % (
-                _format_filename(
-                    filename, Colors.filename, Colors.Normal, lineno=lineno
-                ),
-                Colors.name,
-                name,
-                Colors.Normal,
+        for ind, (filename, lineno, name, line) in enumerate(extracted_list):
+            normalCol, nameCol, fileCol, lineCol = (
+                # Emphasize the last entry
+                (Colors.normalEm, Colors.nameEm, Colors.filenameEm, Colors.line)
+                if ind == len(extracted_list) - 1
+                else (Colors.Normal, Colors.name, Colors.filename, "")
             )
+
+            fns = _format_filename(filename, fileCol, normalCol, lineno=lineno)
+            item = f"{normalCol}  {fns}"
+
+            if name != "":
+                item += f" in {nameCol}{name}{normalCol}\n"
+            else:
+                item += "\n"
             if line:
-                item += '    %s\n' % line.strip()
+                item += f"{lineCol}    {line.strip()}{normalCol}\n"
             list.append(item)
-        # Emphasize the last entry
-        filename, lineno, name, line = extracted_list[-1]
-        item = "%s  %s in %s%s%s%s\n" % (
-            Colors.normalEm,
-            _format_filename(
-                filename, Colors.filenameEm, Colors.normalEm, lineno=lineno
-            ),
-            Colors.nameEm,
-            name,
-            Colors.normalEm,
-            Colors.Normal,
-        )
-        if line:
-            item += '%s    %s%s\n' % (Colors.line, line.strip(),
-                                      Colors.Normal)
-        list.append(item)
+
         return list
 
     def _format_exception_only(self, etype, value):
@@ -687,7 +681,7 @@ def format_record(self, frame_info):
 
         func = frame_info.executing.code_qualname()
         if func == "":
-            call = tpl_call.format(file=func, scope="")
+            call = ""
         else:
             # Decide whether to include variable details or not
             var_repr = eqrepr if self.include_vars else nullrepr
@@ -731,7 +725,7 @@ def format_record(self, frame_info):
         if lvals_list:
             lvals = '%s%s' % (indent, em_normal.join(lvals_list))
 
-        result = "%s, %s\n" % (link, call)
+        result = f'{link}{", " if call else ""}{call}\n'
 
         result += ''.join(_format_traceback_lines(frame_info.lines, Colors, self.has_colors, lvals))
         return result
diff --git a/docs/source/whatsnew/pr/restore-line-numbers.rst b/docs/source/whatsnew/pr/restore-line-numbers.rst
new file mode 100644
index 00000000000..fb0729247a3
--- /dev/null
+++ b/docs/source/whatsnew/pr/restore-line-numbers.rst
@@ -0,0 +1,50 @@
+Restore line numbers for Input
+==================================
+
+Line number information in tracebacks from input are restored.
+Line numbers from input were removed during the transition to v8 enhanced traceback reporting.
+
+So, instead of::
+
+    ---------------------------------------------------------------------------
+    ZeroDivisionError                         Traceback (most recent call last)
+    Input In [3], in ()
+    ----> 1 myfunc(2)
+
+    Input In [2], in myfunc(z)
+          1 def myfunc(z):
+    ----> 2     foo.boo(z-1)
+
+    File ~/code/python/ipython/foo.py:3, in boo(x)
+          2 def boo(x):
+    ----> 3     return 1/(1-x)
+
+    ZeroDivisionError: division by zero
+
+The error traceback now looks like::
+
+      ---------------------------------------------------------------------------
+      ZeroDivisionError                         Traceback (most recent call last)
+      Cell In [3], line 1
+      ----> 1 myfunc(2)
+
+      Cell In [2], line 2, in myfunc(z)
+            1 def myfunc(z):
+      ----> 2     foo.boo(z-1)
+
+      File ~/code/python/ipython/foo.py:3, in boo(x)
+            2 def boo(x):
+      ----> 3     return 1/(1-x)
+
+      ZeroDivisionError: division by zero
+
+or, with xmode=Plain::
+
+    Traceback (most recent call last):
+      Cell In [12], line 1
+        myfunc(2)
+      Cell In [6], line 2 in myfunc
+        foo.boo(z-1)
+      File ~/code/python/ipython/foo.py:3 in boo
+        return 1/(1-x)
+    ZeroDivisionError: division by zero

From 71d665c47dfea1bf5aea9985caded825fba73abe Mon Sep 17 00:00:00 2001
From: Maxim Cournoyer 
Date: Sun, 17 Apr 2022 00:16:27 -0400
Subject: [PATCH 0151/1752] ipython_directive: Adjust doc examples for
 reproducibility.

Before this change, building the documentation twice in a row in an
environment configured for reproducible bulids would result in
discrepancies such as:

   diff -ru /gnu/store/...-python-ipython-documentation-8.2.0/share/doc/python-ipython-documentation-8.2.0/html/sphinxext.html /gnu/store/...-python-ipython-documentation-8.2.0-check/share/doc/python-ipython-documentation-8.2.0/html/sphinxext.html
   --- /gnu/store/...-python-ipython-documentation-8.2.0/share/doc/python-ipython-documentation-8.2.0/html/sphinxext.html       1969-12-31 19:00:01.000000000 -0500
   +++ /gnu/store/...-python-ipython-documentation-8.2.0-check/share/doc/python-ipython-documentation-8.2.0/html/sphinxext.html 1969-12-31 19:00:01.000000000 -0500
   @@ -682,7 +682,7 @@
    In [2]: import datetime
       ...: datetime.datetime.now()
       ...: 
   -Out[2]: datetime.datetime(2022, 4, 17, 3, 21, 14, 978155)
   +Out[2]: datetime.datetime(2022, 4, 17, 3, 37, 37, 115081)
    

It supports IPython construct that plain @@ -690,7 +690,7 @@

In [3]: import time

    In [4]: %timeit time.sleep(0.05)
   -50.2 ms +- 104 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
   +50.1 ms +- 8.86 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
    

This will also support top-level async when using IPython 7.0+

* IPython/sphinxext/ipython_directive.py: Use a fixed date string in the datetime example, and replace the %timeit example by %pdoc, whole output is static. --- IPython/sphinxext/ipython_directive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py index b1f3931a475..9e3c7b2276a 100644 --- a/IPython/sphinxext/ipython_directive.py +++ b/IPython/sphinxext/ipython_directive.py @@ -19,7 +19,7 @@ In [1]: 1+1 In [1]: import datetime - ...: datetime.datetime.now() + ...: datetime.date.fromisoformat('2022-02-22') It supports IPython construct that plain Python does not understand (like magics): @@ -28,7 +28,7 @@ In [0]: import time - In [0]: %timeit time.sleep(0.05) + In [0]: %pdoc time.sleep This will also support top-level async when using IPython 7.0+ From f20e3b80393a1a5909a050cb7bb9cbce9e044827 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Sun, 17 Apr 2022 00:21:46 -0400 Subject: [PATCH 0152/1752] utils: coloransi: Escape Unicode U0001 and U0002 non-printable characters. Fixes #13637. These Unicode characters, would cause problems when processed by LaTeX to generate the info or PDF documentation targets. * IPython/utils/coloransi.py (InputTermColors): Escape the backslashes in \001 and \002 so that they are shown as literals '\001' and '\002' strings in the Sphinx-generated documentation rather than as non-printable Unicode characters. --- IPython/utils/coloransi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/utils/coloransi.py b/IPython/utils/coloransi.py index e3314218025..9300b010856 100644 --- a/IPython/utils/coloransi.py +++ b/IPython/utils/coloransi.py @@ -74,8 +74,8 @@ class TermColors: class InputTermColors: """Color escape sequences for input prompts. - This class is similar to TermColors, but the escapes are wrapped in \001 - and \002 so that readline can properly know the length of each line and + This class is similar to TermColors, but the escapes are wrapped in \\001 + and \\002 so that readline can properly know the length of each line and can wrap lines accordingly. Use this class for any colored text which needs to be used in input prompts, such as in calls to raw_input(). From b3dca841a72a00f8642e921259130fc0c78e4170 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 22 Jun 2022 17:08:01 +0200 Subject: [PATCH 0153/1752] Misc documentation fixes. Should in particular close Quansight/deshaw#227 --- IPython/core/magics/basic.py | 2 +- docs/source/interactive/shell.rst | 2 +- docs/source/whatsnew/version7.rst | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index c1f69451100..ea407c61811 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -368,7 +368,7 @@ def xmode(self, parameter_s=''): If called without arguments, acts as a toggle. - When in verbose mode the value --show (and --hide) + When in verbose mode the value `--show` (and `--hide`) will respectively show (or hide) frames with ``__tracebackhide__ = True`` value set. """ diff --git a/docs/source/interactive/shell.rst b/docs/source/interactive/shell.rst index 0ea125cb9ef..6362b21ea61 100644 --- a/docs/source/interactive/shell.rst +++ b/docs/source/interactive/shell.rst @@ -1,10 +1,10 @@ -.. _ipython_as_shell: .. note:: This page has been kept for historical reason. You most likely want to use `Xonsh `__ instead of this. +.. _ipython_as_shell: ========================= IPython as a system shell diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 16987a77048..9c510462408 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -241,6 +241,7 @@ such that it allows autoplay. the HTML allowing it. It also could get blocked by some browser extensions. Try it out! + :: In [1]: from IPython.display import YouTubeVideo @@ -1368,7 +1369,6 @@ Miscellaneous IPython 7.3.0 ============= -.. _whatsnew720: IPython 7.3.0 bring several bug fixes and small improvements that you will described bellow. @@ -1393,6 +1393,8 @@ Misc bug fixes and improvements: - Re-initialize posix aliases after a ``%reset`` :ghpull:`11528` - Allow the IPython command line to run ``*.ipynb`` files :ghpull:`11529` +.. _whatsnew720: + IPython 7.2.0 ============= @@ -1423,8 +1425,7 @@ and we're now proud to have code contributed by Chris in IPython. OSMagics.cd_force_quiet configuration option -------------------------------------------- -You can set this option to force the %cd magic to behave as if ``-q`` was passed: -:: +You can set this option to force the %cd magic to behave as if ``-q`` was passed:: In [1]: cd / / From aff5624c5da935350b802e6b066a154a439d91b6 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 23 Jun 2022 11:27:35 +0200 Subject: [PATCH 0154/1752] extend buffer len --- IPython/core/tests/test_debugger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index d2d5bd09991..0c479abd30d 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -379,6 +379,7 @@ def _decorator_skip_setup(): child.expect("\n") child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE + child.str_last_chars = 500 dedented_blocks = [dedent(b).strip() for b in skip_decorators_blocks] in_prompt_number = 1 From 079c54f4a370d9dddda81a21a41a602afeaf79d6 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 23 Jun 2022 11:41:19 +0200 Subject: [PATCH 0155/1752] no CPR --- IPython/core/tests/test_debugger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 0c479abd30d..fc81b71bc55 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -369,6 +369,7 @@ def _decorator_skip_setup(): env = os.environ.copy() env["IPY_TEST_SIMPLE_PROMPT"] = "1" + env["PROMPT_TOOLKIT_NO_CPR"] = "1" child = pexpect.spawn( sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env From 99e4bc42310710f4c126a9bf1047ebc70d0baa3a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 23 Jun 2022 11:52:53 +0200 Subject: [PATCH 0156/1752] try to fix tests --- IPython/core/tests/test_debugger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index fc81b71bc55..171f03d664a 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -452,11 +452,13 @@ def test_decorator_skip_with_breakpoint(): env = os.environ.copy() env["IPY_TEST_SIMPLE_PROMPT"] = "1" + env["PROMPT_TOOLKIT_NO_CPR"] = "1" child = pexpect.spawn( sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env ) child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE + child.str_last_chars = 500 child.expect("IPython") child.expect("\n") From 9891cc714271c35d72f34f3625cdea11f070cf36 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 23 Jun 2022 12:00:31 +0200 Subject: [PATCH 0157/1752] try to avoid using ptk in subprocess tests --- IPython/terminal/debugger.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index 8448d96370d..1859da20410 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -20,6 +20,11 @@ PTK3 = ptk_version.startswith('3.') +# we want to avoid ptk as much as possible when using subprocesses +# as it uses cursor positioning requests, deletes color .... +_use_simple_prompt = "IPY_TEST_SIMPLE_PROMPT" in os.environ + + class TerminalPdb(Pdb): """Standalone IPython debugger.""" @@ -87,8 +92,9 @@ def gen_comp(self, text): if not PTK3: options['inputhook'] = self.shell.inputhook options.update(pt_session_options) - self.pt_loop = asyncio.new_event_loop() - self.pt_app = PromptSession(**options) + if not _use_simple_prompt: + self.pt_loop = asyncio.new_event_loop() + self.pt_app = PromptSession(**options) def cmdloop(self, intro=None): """Repeatedly issue a prompt, accept input, parse an initial prefix @@ -121,10 +127,15 @@ def cmdloop(self, intro=None): self._ptcomp.ipy_completer.global_namespace = self.curframe.f_globals # Run the prompt in a different thread. - try: - line = self.thread_executor.submit(self.pt_app.prompt).result() - except EOFError: - line = "EOF" + if not _use_simple_prompt: + try: + line = self.thread_executor.submit( + self.pt_app.prompt + ).result() + except EOFError: + line = "EOF" + else: + line = input("ipdb> ") line = self.precmd(line) stop = self.onecmd(line) From 55c0cedfa94e71a49cd4e60df803098f8d93daef Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 23 Jun 2022 12:22:53 +0200 Subject: [PATCH 0158/1752] debug --- IPython/core/tests/test_debugger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 171f03d664a..d5ccfba4ee3 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -400,12 +400,15 @@ def test_decorator_skip(): child = _decorator_skip_setup() + child.expect_exact("ipython-input-8") child.expect_exact("3 bar(3, 4)") child.expect("ipdb>") child.expect("ipdb>") child.sendline("step") child.expect_exact("step") + child.expect_exact("--Call--") + child.expect_exact("ipython-input-6") child.expect_exact("1 @pdb_skipped_decorator") From 8cce7903f85887f137ca0ac9567b14b666dbd2e3 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 23 Jun 2022 12:53:50 +0200 Subject: [PATCH 0159/1752] always skip --- IPython/core/tests/test_debugger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index d5ccfba4ee3..5a12c3bb2ad 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -394,6 +394,7 @@ def _decorator_skip_setup(): return child +@pytest.mark.skip(reason="recently fail for unknown reason on CI") @skip_win32 def test_decorator_skip(): """test that decorator frames can be skipped.""" @@ -418,6 +419,7 @@ def test_decorator_skip(): child.close() +@pytest.mark.skip(reason="recently fail for unknown reason on CI") @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="issues on PyPy") @skip_win32 def test_decorator_skip_disabled(): From 66aeb3fc55c8ac04242e566172af5de5cc6fe71e Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 18 Jul 2022 17:03:32 +0200 Subject: [PATCH 0160/1752] Add setting to disable venv warning (#13706) * Add setting to disable venv warning * Fix style * Add what's new entry * Improve help text * Use double quotes Co-authored-by: Blazej Michalik <6691643+MrMino@users.noreply.github.com> * Add missing comma Co-authored-by: Blazej Michalik <6691643+MrMino@users.noreply.github.com> --- IPython/core/interactiveshell.py | 15 ++++++++++----- .../pr/silence-running-in-venv-warning.rst | 8 ++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 docs/source/whatsnew/pr/silence-running-in-venv-warning.rst diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 7587a1d6bb8..06912640673 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -479,6 +479,11 @@ def input_splitter(self): """ ).tag(config=True) + warn_venv = Bool( + True, + help="Warn if running in a virtual environment with no IPython installed (so IPython from the global environment is used).", + ).tag(config=True) + # TODO: this part of prompt management should be moved to the frontends. # Use custom TraitTypes that convert '0'->'' and '\\n'->'\n' separate_in = SeparateUnicode('\n').tag(config=True) @@ -847,11 +852,11 @@ def init_virtualenv(self): p_ver = re_m.groups() virtual_env = str(virtual_env_path).format(*p_ver) - - warn( - "Attempting to work in a virtualenv. If you encounter problems, " - "please install IPython inside the virtualenv." - ) + if self.warn_venv: + warn( + "Attempting to work in a virtualenv. If you encounter problems, " + "please install IPython inside the virtualenv." + ) import site sys.path.insert(0, virtual_env) site.addsitedir(virtual_env) diff --git a/docs/source/whatsnew/pr/silence-running-in-venv-warning.rst b/docs/source/whatsnew/pr/silence-running-in-venv-warning.rst new file mode 100644 index 00000000000..ed245bd157a --- /dev/null +++ b/docs/source/whatsnew/pr/silence-running-in-venv-warning.rst @@ -0,0 +1,8 @@ +New setting to silence warning if working inside a virtual environment +====================================================================== + +Previously, when starting IPython in a virtual environment without IPython installed (so IPython from the global environment is used), the following warning was printed: + + Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. + +This warning can be permanently silenced by setting ``c.InteractiveShell.warn_venv`` to ``False`` (the default is ``True``). From d858213d4088237e1481038865bc52ccdd074053 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Fri, 29 Jul 2022 08:48:59 +0200 Subject: [PATCH 0161/1752] xxlimited_35 module now has the same name in repr in Py 3.11 See https://github.com/python/cpython/commit/a87c9b538fbfc42883417c4d5e69f1a5922690e3 --- IPython/lib/tests/test_pretty.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py index 86085166071..b93ab97eb9e 100644 --- a/IPython/lib/tests/test_pretty.py +++ b/IPython/lib/tests/test_pretty.py @@ -141,9 +141,12 @@ def test_pprint_heap_allocated_type(): Test that pprint works for heap allocated types. """ module_name = "xxlimited" if sys.version_info < (3, 10) else "xxlimited_35" + expected_output = ( + "xxlimited.Null" if sys.version_info < (3, 11) else "xxlimited_35.Null" + ) xxlimited = pytest.importorskip(module_name) output = pretty.pretty(xxlimited.Null) - assert output == "xxlimited.Null" + assert output == expected_output def test_pprint_nomod(): From b7dfcf479203b2423df15b4b0a435dfe3d02efcf Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 24 May 2022 21:29:59 +0200 Subject: [PATCH 0162/1752] prevent popup windows during execution of latex_to_png_dvipng --- IPython/lib/latextools.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/IPython/lib/latextools.py b/IPython/lib/latextools.py index 27aeef5b0eb..e832a9ea863 100644 --- a/IPython/lib/latextools.py +++ b/IPython/lib/latextools.py @@ -144,6 +144,13 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): find_cmd('dvipng') except FindCmdError: return None + + startupinfo = None + if os.name == 'nt': + # prevent popup-windows + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + try: workdir = Path(tempfile.mkdtemp()) tmpfile = workdir.joinpath("tmp.tex") @@ -156,7 +163,7 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): with open(os.devnull, 'wb') as devnull: subprocess.check_call( ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile], - cwd=workdir, stdout=devnull, stderr=devnull) + cwd=workdir, stdout=devnull, stderr=devnull, startupinfo=startupinfo) resolution = round(150*scale) subprocess.check_call( @@ -179,6 +186,7 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): cwd=workdir, stdout=devnull, stderr=devnull, + startupinfo=startupinfo ) with outfile.open("rb") as f: From 6dbc49a85fb433079faf96572da0db464b9fd7e1 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 24 May 2022 21:35:42 +0200 Subject: [PATCH 0163/1752] darker --- IPython/lib/latextools.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/IPython/lib/latextools.py b/IPython/lib/latextools.py index e832a9ea863..45eaa29805f 100644 --- a/IPython/lib/latextools.py +++ b/IPython/lib/latextools.py @@ -146,7 +146,7 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): return None startupinfo = None - if os.name == 'nt': + if os.name == "nt": # prevent popup-windows startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW @@ -163,7 +163,11 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): with open(os.devnull, 'wb') as devnull: subprocess.check_call( ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile], - cwd=workdir, stdout=devnull, stderr=devnull, startupinfo=startupinfo) + cwd=workdir, + stdout=devnull, + stderr=devnull, + startupinfo=startupinfo, + ) resolution = round(150*scale) subprocess.check_call( @@ -186,7 +190,7 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): cwd=workdir, stdout=devnull, stderr=devnull, - startupinfo=startupinfo + startupinfo=startupinfo, ) with outfile.open("rb") as f: From c60a87a0deae6afd4a4ed7f932aa26c23b716477 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 27 May 2022 19:48:23 +0200 Subject: [PATCH 0164/1752] add whatsnew entry --- docs/source/whatsnew/pr/latex-generation-no-popup-window.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/source/whatsnew/pr/latex-generation-no-popup-window.rst diff --git a/docs/source/whatsnew/pr/latex-generation-no-popup-window.rst b/docs/source/whatsnew/pr/latex-generation-no-popup-window.rst new file mode 100644 index 00000000000..4cf73afdf22 --- /dev/null +++ b/docs/source/whatsnew/pr/latex-generation-no-popup-window.rst @@ -0,0 +1,5 @@ +No popup in window for latex generation +======================================= + +When generating latex (e.g. via `_latex_repr_`) no popup window is shows under Windows. + From df3b160586ee348b6bf5cd7d5a8c55032b246cfe Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 24 May 2022 22:30:56 +0200 Subject: [PATCH 0165/1752] use relative filename in latex and dvips commands --- IPython/lib/latextools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IPython/lib/latextools.py b/IPython/lib/latextools.py index 27aeef5b0eb..a6f44b85c60 100644 --- a/IPython/lib/latextools.py +++ b/IPython/lib/latextools.py @@ -146,11 +146,11 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): return None try: workdir = Path(tempfile.mkdtemp()) - tmpfile = workdir.joinpath("tmp.tex") - dvifile = workdir.joinpath("tmp.dvi") - outfile = workdir.joinpath("tmp.png") + tmpfile = "tmp.tex" + dvifile = "tmp.dvi" + outfile = "tmp.png" - with tmpfile.open("w", encoding="utf8") as f: + with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f: f.writelines(genelatex(s, wrap)) with open(os.devnull, 'wb') as devnull: @@ -181,7 +181,7 @@ def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0): stderr=devnull, ) - with outfile.open("rb") as f: + with workdir.joinpath(outfile).open("rb") as f: return f.read() except subprocess.CalledProcessError: return None From 14f8a2ab5ffccdb77c52b445769ffc0445f06b31 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 27 May 2022 19:52:58 +0200 Subject: [PATCH 0166/1752] add whatsnew entry --- docs/source/whatsnew/pr/latex_rendering_relative_files.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/source/whatsnew/pr/latex_rendering_relative_files.rst diff --git a/docs/source/whatsnew/pr/latex_rendering_relative_files.rst b/docs/source/whatsnew/pr/latex_rendering_relative_files.rst new file mode 100644 index 00000000000..5201e0a6493 --- /dev/null +++ b/docs/source/whatsnew/pr/latex_rendering_relative_files.rst @@ -0,0 +1,6 @@ +Relative filenames in Latex rendering +===================================== + +The input and output file arguments to `latex` and `dvipis` are files relative to the current working directory. +This solves a problem where the current working directory contains characters that are not handled properly by `latex` and `dvips`. + From ba4b5b33cef669b356ba106531f1f9754e40e3e8 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 13 Jun 2022 13:43:28 +0200 Subject: [PATCH 0167/1752] update whatsnew --- docs/source/whatsnew/pr/latex_rendering_relative_files.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/pr/latex_rendering_relative_files.rst b/docs/source/whatsnew/pr/latex_rendering_relative_files.rst index 5201e0a6493..befd4afa35a 100644 --- a/docs/source/whatsnew/pr/latex_rendering_relative_files.rst +++ b/docs/source/whatsnew/pr/latex_rendering_relative_files.rst @@ -1,6 +1,7 @@ Relative filenames in Latex rendering ===================================== -The input and output file arguments to `latex` and `dvipis` are files relative to the current working directory. +The `latex_to_png_dvipng` command internally generates input and output file arguments to `latex` and `dvipis`. These arguments are now generated as relative files to the current working directory instead of absolute file paths. This solves a problem where the current working directory contains characters that are not handled properly by `latex` and `dvips`. +There are no changes to the user API. From 7a493863e55e6f055576e3a3a7c73379843141d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lum=C3=ADr=20=27Frenzy=27=20Balhar?= Date: Fri, 5 Aug 2022 21:53:55 +0200 Subject: [PATCH 0168/1752] Update IPython/lib/tests/test_pretty.py Co-authored-by: Ben Greiner --- IPython/lib/tests/test_pretty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py index b93ab97eb9e..2d320bffd2f 100644 --- a/IPython/lib/tests/test_pretty.py +++ b/IPython/lib/tests/test_pretty.py @@ -142,7 +142,7 @@ def test_pprint_heap_allocated_type(): """ module_name = "xxlimited" if sys.version_info < (3, 10) else "xxlimited_35" expected_output = ( - "xxlimited.Null" if sys.version_info < (3, 11) else "xxlimited_35.Null" + "xxlimited.Null" if sys.version_info < (3, 10, 6) else "xxlimited_35.Null" ) xxlimited = pytest.importorskip(module_name) output = pretty.pretty(xxlimited.Null) From ebea766ceb57ec0080300b246be0699f41957e26 Mon Sep 17 00:00:00 2001 From: Aleksey Bogdanov Date: Sat, 6 Aug 2022 23:21:35 +0300 Subject: [PATCH 0169/1752] Fix HistoryAccessor.get_tail bug (#13666) Current implementation of get_tail in HistoryAccessor assumes context present only in subclass , so it's moved there and the old implementation is restored. --- IPython/core/history.py | 88 +++++++++++++++++++----------- IPython/core/tests/test_history.py | 67 ++++++++++++++++++++++- 2 files changed, 123 insertions(+), 32 deletions(-) diff --git a/IPython/core/history.py b/IPython/core/history.py index 9b0b2cbd048..f247f7169b7 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -202,7 +202,6 @@ def __init__(self, profile="default", hist_file="", **traits): config : :class:`~traitlets.config.loader.Config` Config object. hist_file can also be set through this. """ - # We need a pointer back to the shell for various tasks. super(HistoryAccessor, self).__init__(**traits) # defer setting hist_file from kwarg until after init, # otherwise the default kwarg value would clobber any value @@ -344,11 +343,6 @@ def get_last_session_id(self): def get_tail(self, n=10, raw=True, output=False, include_latest=False): """Get the last n lines from the history database. - Most recent entry last. - - Completion will be reordered so that that the last ones are when - possible from current session. - Parameters ---------- n : int @@ -367,31 +361,11 @@ def get_tail(self, n=10, raw=True, output=False, include_latest=False): self.writeout_cache() if not include_latest: n += 1 - # cursor/line/entry - this_cur = list( - self._run_sql( - "WHERE session == ? ORDER BY line DESC LIMIT ? ", - (self.session_number, n), - raw=raw, - output=output, - ) - ) - other_cur = list( - self._run_sql( - "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?", - (self.session_number, n), - raw=raw, - output=output, - ) - ) - - everything = this_cur + other_cur - - everything = everything[:n] - + cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?", + (n,), raw=raw, output=output) if not include_latest: - return list(everything)[:0:-1] - return list(everything)[::-1] + return reversed(list(cur)[1:]) + return reversed(list(cur)) @catch_corrupt_db def search(self, pattern="*", raw=True, search_raw=True, @@ -560,7 +534,6 @@ def _dir_hist_default(self): def __init__(self, shell=None, config=None, **traits): """Create a new history manager associated with a shell instance. """ - # We need a pointer back to the shell for various tasks. super(HistoryManager, self).__init__(shell=shell, config=config, **traits) self.save_flag = threading.Event() @@ -656,6 +629,59 @@ def get_session_info(self, session=0): return super(HistoryManager, self).get_session_info(session=session) + @catch_corrupt_db + def get_tail(self, n=10, raw=True, output=False, include_latest=False): + """Get the last n lines from the history database. + + Most recent entry last. + + Completion will be reordered so that that the last ones are when + possible from current session. + + Parameters + ---------- + n : int + The number of lines to get + raw, output : bool + See :meth:`get_range` + include_latest : bool + If False (default), n+1 lines are fetched, and the latest one + is discarded. This is intended to be used where the function + is called by a user command, which it should not return. + + Returns + ------- + Tuples as :meth:`get_range` + """ + self.writeout_cache() + if not include_latest: + n += 1 + # cursor/line/entry + this_cur = list( + self._run_sql( + "WHERE session == ? ORDER BY line DESC LIMIT ? ", + (self.session_number, n), + raw=raw, + output=output, + ) + ) + other_cur = list( + self._run_sql( + "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?", + (self.session_number, n), + raw=raw, + output=output, + ) + ) + + everything = this_cur + other_cur + + everything = everything[:n] + + if not include_latest: + return list(everything)[:0:-1] + return list(everything)[::-1] + def _get_range_session(self, start=1, stop=None, raw=True, output=False): """Get input and output history from the current session. Called by get_range, and takes similar parameters.""" diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index 73d50c87d34..b98ef7683a3 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -17,7 +17,7 @@ # our own packages from traitlets.config.loader import Config -from IPython.core.history import HistoryManager, extract_hist_ranges +from IPython.core.history import HistoryAccessor, HistoryManager, extract_hist_ranges def test_proper_default_encoding(): @@ -227,3 +227,68 @@ def test_histmanager_disabled(): # hist_file should not be created assert hist_file.exists() is False + +def test_get_tail_session_awareness(): + """Test .get_tail() is: + - session specific in HistoryManager + - session agnostic in HistoryAccessor + same for .get_last_session_id() + """ + ip = get_ipython() + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + hist_file = tmp_path / "history.sqlite" + get_source = lambda x: x[2] + + # hm1 creates a new session and adds history entries, + # ha catches up + hm1 = HistoryManager(shell=ip, hist_file=hist_file) + hm1_last_sid = hm1.get_last_session_id + ha = HistoryAccessor(hist_file=hist_file) + ha_last_sid = ha.get_last_session_id + + hist1 = ["a=1", "b=1", "c=1"] + for i, h in enumerate(hist1 + [""], start=1): + hm1.store_inputs(i, h) + assert list(map(get_source, hm1.get_tail())) == hist1 + assert list(map(get_source, ha.get_tail())) == hist1 + sid1 = hm1_last_sid() + assert sid1 is not None + assert ha_last_sid() == sid1 + + # hm2 creates a new session and adds entries, + # ha catches up + hm2 = HistoryManager(shell=ip, hist_file=hist_file) + hm2_last_sid = hm2.get_last_session_id + + hist2 = ["a=2", "b=2", "c=2"] + for i, h in enumerate(hist2 + [""], start=1): + hm2.store_inputs(i, h) + tail = hm2.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + tail = ha.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + sid2 = hm2_last_sid() + assert sid2 is not None + assert ha_last_sid() == sid2 + assert sid2 != sid1 + + # but hm1 still maintains its point of reference + # and adding more entries to it doesn't change others + # immediate perspective + assert hm1_last_sid() == sid1 + tail = hm1.get_tail(n=3) + assert list(map(get_source, tail)) == hist1 + + hist3 = ["a=3", "b=3", "c=3"] + for i, h in enumerate(hist3 + [""], start=5): + hm1.store_inputs(i, h) + tail = hm1.get_tail(n=7) + assert list(map(get_source, tail)) == hist1 + [""] + hist3 + tail = hm2.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + tail = ha.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + assert hm1_last_sid() == sid1 + assert hm2_last_sid() == sid2 + assert ha_last_sid() == sid2 From 7a11a58da2a65f1cc7b177f5161a7f17050e26b7 Mon Sep 17 00:00:00 2001 From: Aleksey Bogdanov Date: Sun, 7 Aug 2022 10:35:08 +0300 Subject: [PATCH 0170/1752] Fix formatting issues --- IPython/core/history.py | 5 +++-- IPython/core/tests/test_history.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/IPython/core/history.py b/IPython/core/history.py index f247f7169b7..1a89060e92e 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -361,8 +361,9 @@ def get_tail(self, n=10, raw=True, output=False, include_latest=False): self.writeout_cache() if not include_latest: n += 1 - cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?", - (n,), raw=raw, output=output) + cur = self._run_sql( + "ORDER BY session DESC, line DESC LIMIT ?", (n,), raw=raw, output=output + ) if not include_latest: return reversed(list(cur)[1:]) return reversed(list(cur)) diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index b98ef7683a3..46c0ea89e8a 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -228,11 +228,12 @@ def test_histmanager_disabled(): # hist_file should not be created assert hist_file.exists() is False + def test_get_tail_session_awareness(): """Test .get_tail() is: - - session specific in HistoryManager - - session agnostic in HistoryAccessor - same for .get_last_session_id() + - session specific in HistoryManager + - session agnostic in HistoryAccessor + same for .get_last_session_id() """ ip = get_ipython() with TemporaryDirectory() as tmpdir: From 30528a19e0e0dbee58e584577d632af1c559dbcd Mon Sep 17 00:00:00 2001 From: Aleksey Bogdanov Date: Sun, 7 Aug 2022 13:15:15 +0300 Subject: [PATCH 0171/1752] wrap db before host dir is gone in new test --- IPython/core/tests/test_history.py | 116 ++++++++++++++++------------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index 46c0ea89e8a..be152975633 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -240,56 +240,66 @@ def test_get_tail_session_awareness(): tmp_path = Path(tmpdir) hist_file = tmp_path / "history.sqlite" get_source = lambda x: x[2] - - # hm1 creates a new session and adds history entries, - # ha catches up - hm1 = HistoryManager(shell=ip, hist_file=hist_file) - hm1_last_sid = hm1.get_last_session_id - ha = HistoryAccessor(hist_file=hist_file) - ha_last_sid = ha.get_last_session_id - - hist1 = ["a=1", "b=1", "c=1"] - for i, h in enumerate(hist1 + [""], start=1): - hm1.store_inputs(i, h) - assert list(map(get_source, hm1.get_tail())) == hist1 - assert list(map(get_source, ha.get_tail())) == hist1 - sid1 = hm1_last_sid() - assert sid1 is not None - assert ha_last_sid() == sid1 - - # hm2 creates a new session and adds entries, - # ha catches up - hm2 = HistoryManager(shell=ip, hist_file=hist_file) - hm2_last_sid = hm2.get_last_session_id - - hist2 = ["a=2", "b=2", "c=2"] - for i, h in enumerate(hist2 + [""], start=1): - hm2.store_inputs(i, h) - tail = hm2.get_tail(n=3) - assert list(map(get_source, tail)) == hist2 - tail = ha.get_tail(n=3) - assert list(map(get_source, tail)) == hist2 - sid2 = hm2_last_sid() - assert sid2 is not None - assert ha_last_sid() == sid2 - assert sid2 != sid1 - - # but hm1 still maintains its point of reference - # and adding more entries to it doesn't change others - # immediate perspective - assert hm1_last_sid() == sid1 - tail = hm1.get_tail(n=3) - assert list(map(get_source, tail)) == hist1 - - hist3 = ["a=3", "b=3", "c=3"] - for i, h in enumerate(hist3 + [""], start=5): - hm1.store_inputs(i, h) - tail = hm1.get_tail(n=7) - assert list(map(get_source, tail)) == hist1 + [""] + hist3 - tail = hm2.get_tail(n=3) - assert list(map(get_source, tail)) == hist2 - tail = ha.get_tail(n=3) - assert list(map(get_source, tail)) == hist2 - assert hm1_last_sid() == sid1 - assert hm2_last_sid() == sid2 - assert ha_last_sid() == sid2 + hm1 = None + hm2 = None + try: + # hm1 creates a new session and adds history entries, + # ha catches up + hm1 = HistoryManager(shell=ip, hist_file=hist_file) + hm1_last_sid = hm1.get_last_session_id + ha = HistoryAccessor(hist_file=hist_file) + ha_last_sid = ha.get_last_session_id + + hist1 = ["a=1", "b=1", "c=1"] + for i, h in enumerate(hist1 + [""], start=1): + hm1.store_inputs(i, h) + assert list(map(get_source, hm1.get_tail())) == hist1 + assert list(map(get_source, ha.get_tail())) == hist1 + sid1 = hm1_last_sid() + assert sid1 is not None + assert ha_last_sid() == sid1 + + # hm2 creates a new session and adds entries, + # ha catches up + hm2 = HistoryManager(shell=ip, hist_file=hist_file) + hm2_last_sid = hm2.get_last_session_id + + hist2 = ["a=2", "b=2", "c=2"] + for i, h in enumerate(hist2 + [""], start=1): + hm2.store_inputs(i, h) + tail = hm2.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + tail = ha.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + sid2 = hm2_last_sid() + assert sid2 is not None + assert ha_last_sid() == sid2 + assert sid2 != sid1 + + # but hm1 still maintains its point of reference + # and adding more entries to it doesn't change others + # immediate perspective + assert hm1_last_sid() == sid1 + tail = hm1.get_tail(n=3) + assert list(map(get_source, tail)) == hist1 + + hist3 = ["a=3", "b=3", "c=3"] + for i, h in enumerate(hist3 + [""], start=5): + hm1.store_inputs(i, h) + tail = hm1.get_tail(n=7) + assert list(map(get_source, tail)) == hist1 + [""] + hist3 + tail = hm2.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + tail = ha.get_tail(n=3) + assert list(map(get_source, tail)) == hist2 + assert hm1_last_sid() == sid1 + assert hm2_last_sid() == sid2 + assert ha_last_sid() == sid2 + finally: + if hm1: + hm1.save_thread.stop() + if hm2: + hm2.save_thread.stop() + hm = hm1 or hm2 + if hm: + hm.db.close() From 59071056fd74bf0ff6819d666bd5749b6c0dddf1 Mon Sep 17 00:00:00 2001 From: Aleksey Bogdanov Date: Sun, 7 Aug 2022 15:53:11 +0300 Subject: [PATCH 0172/1752] wrap db before host dir is gone in new test (v2) --- IPython/core/tests/test_history.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index be152975633..a9ebafddd4c 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -242,6 +242,7 @@ def test_get_tail_session_awareness(): get_source = lambda x: x[2] hm1 = None hm2 = None + ha = None try: # hm1 creates a new session and adds history entries, # ha catches up @@ -298,8 +299,9 @@ def test_get_tail_session_awareness(): finally: if hm1: hm1.save_thread.stop() + hm1.db.close() if hm2: hm2.save_thread.stop() - hm = hm1 or hm2 - if hm: - hm.db.close() + hm2.db.close() + if ha: + ha.db.close() From 9ed1939c703f9ce56bbfd375b6847428a3a44388 Mon Sep 17 00:00:00 2001 From: Jarrod Millman Date: Sat, 13 Aug 2022 13:20:57 -0700 Subject: [PATCH 0173/1752] Rename master to main --- .github/workflows/mypy.yml | 4 ++-- .github/workflows/python-package.yml | 4 ++-- .github/workflows/test.yml | 1 - CONTRIBUTING.md | 4 ++-- IPython/__init__.py | 2 +- README.rst | 4 ++-- docs/source/coredev/index.rst | 6 +++--- examples/IPython Kernel/Terminal Usage.ipynb | 2 +- setup.py | 2 +- tools/github_stats.py | 4 ++-- tools/release_helper.sh | 2 +- 11 files changed, 17 insertions(+), 18 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 03b58c64c46..8b1a4c37d89 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -2,9 +2,9 @@ name: Run MyPy on: push: - branches: [ master, 7.x] + branches: [ main, 7.x] pull_request: - branches: [ master, 7.x] + branches: [ main, 7.x] jobs: build: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 663607f0246..37d021890f0 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ master, 7.x ] + branches: [ main, 7.x ] pull_request: - branches: [ master, 7.x ] + branches: [ main, 7.x ] jobs: formatting: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bbddbfbf98..f57425a03aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - master - '*.x' pull_request: # Run weekly on Monday at 1:23 UTC diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11321a4ca4c..5826baf599c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ When opening a new Issue, please take the following steps: 1. Search GitHub and/or Google for your issue to avoid duplicate reports. Keyword searches for your error messages are most helpful. -2. If possible, try updating to master and reproducing your issue, +2. If possible, try updating to main and reproducing your issue, because we may have already fixed it. 3. Try to include a minimal reproducible test case. 4. Include relevant system information. Start with the output of: @@ -53,7 +53,7 @@ Some guidelines on contributing to IPython: Review and discussion can begin well before the work is complete, and the more discussion the better. The worst case is that the PR is closed. -* Pull Requests should generally be made against master +* Pull Requests should generally be made against main * Pull Requests should be tested, if feasible: - bugfixes should include regression tests. - new behavior should at least get minimal exercise. diff --git a/IPython/__init__.py b/IPython/__init__.py index 7ebb80b3621..b5410f63da9 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -38,7 +38,7 @@ See IPython `README.rst` file for more information: - https://github.com/ipython/ipython/blob/master/README.rst + https://github.com/ipython/ipython/blob/main/README.rst """ ) diff --git a/README.rst b/README.rst index ec160311665..5107eae9b5d 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -.. image:: https://codecov.io/github/ipython/ipython/coverage.svg?branch=master - :target: https://codecov.io/github/ipython/ipython?branch=master +.. image:: https://codecov.io/github/ipython/ipython/coverage.svg?branch=main + :target: https://codecov.io/github/ipython/ipython?branch=main .. image:: https://img.shields.io/pypi/v/IPython.svg :target: https://pypi.python.org/pypi/ipython diff --git a/docs/source/coredev/index.rst b/docs/source/coredev/index.rst index ee1eadb9b1e..3ba94755eda 100644 --- a/docs/source/coredev/index.rst +++ b/docs/source/coredev/index.rst @@ -14,12 +14,12 @@ For instructions on how to make a developer install see :ref:`devinstall`. Backporting Pull requests ========================= -All pull requests should usually be made against ``master``, if a Pull Request +All pull requests should usually be made against ``main``, if a Pull Request need to be backported to an earlier release; then it should be tagged with the correct ``milestone``. If you tag a pull request with a milestone **before** merging the pull request, -and the base ref is ``master``, then our backport bot should automatically create +and the base ref is ``main``, then our backport bot should automatically create a corresponding pull-request that backport on the correct branch. If you have write access to the IPython repository you can also just mention the @@ -78,7 +78,7 @@ for the release you are actually making:: PREV_RELEASE=4.2.1 MILESTONE=5.0 VERSION=5.0.0 - BRANCH=master + BRANCH=main For `reproducibility of builds `_, we recommend setting ``SOURCE_DATE_EPOCH`` prior to running the build; record the used value diff --git a/examples/IPython Kernel/Terminal Usage.ipynb b/examples/IPython Kernel/Terminal Usage.ipynb index e6bd4c0f116..935bdd4eb24 100644 --- a/examples/IPython Kernel/Terminal Usage.ipynb +++ b/examples/IPython Kernel/Terminal Usage.ipynb @@ -196,7 +196,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The `%gui` magic can be similarly used to control Wx, Tk, glut and pyglet applications, [as can be seen in our examples](https://github.com/ipython/ipython/tree/master/examples/lib)." + "The `%gui` magic can be similarly used to control Wx, Tk, glut and pyglet applications, [as can be seen in our examples](https://github.com/ipython/ipython/tree/main/examples/lib)." ] }, { diff --git a/setup.py b/setup.py index 3c1dff9cc1c..bfdf5fb88bf 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ See IPython `README.rst` file for more information: - https://github.com/ipython/ipython/blob/master/README.rst + https://github.com/ipython/ipython/blob/main/README.rst Python {py} detected. {pip} diff --git a/tools/github_stats.py b/tools/github_stats.py index f1a44fa69a3..6a2f8f17fd5 100644 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -79,8 +79,8 @@ def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", p filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ] if pulls: filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] - # filter out PRs not against master (backports) - filtered = [ i for i in filtered if i['base']['ref'] == 'master' ] + # filter out PRs not against main (backports) + filtered = [ i for i in filtered if i['base']['ref'] == 'main' ] else: filtered = [ i for i in filtered if not is_pull_request(i) ] diff --git a/tools/release_helper.sh b/tools/release_helper.sh index 54114d18bb8..697ed859e84 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -31,7 +31,7 @@ MILESTONE=${input:-$MILESTONE} echo -n "VERSION (X.y.z) [$VERSION]:" read input VERSION=${input:-$VERSION} -echo -n "BRANCH (master|X.y) [$BRANCH]:" +echo -n "BRANCH (main|X.y) [$BRANCH]:" read input BRANCH=${input:-$BRANCH} From 517a92f878588484116edd6b88dfc738dcfe3cfb Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 15 Aug 2022 15:39:26 +0200 Subject: [PATCH 0174/1752] Apply autosuggestion only at EOL. As they are displayed only at EOL. Fixes #13724 --- IPython/terminal/shortcuts.py | 25 +++++++++++++-------- IPython/tests/test_shortcuts.py | 40 +++++++++++++++++++++++++++++++++ tools/retar.py | 2 ++ 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 IPython/tests/test_shortcuts.py diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 615397abc5f..6c1ba0418bc 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -32,6 +32,22 @@ def cursor_in_leading_ws(): return (not before) or before.isspace() +# Needed for to accept autosuggestions in vi insert mode +def _apply_autosuggest(event): + """ + Apply autosuggestion if at end of line. + """ + b = event.current_buffer + d = b.document + after_cursor = d.text[d.cursor_position :] + lines = after_cursor.split("\n") + end_of_current_line = lines[0].strip() + suggestion = b.suggestion + if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): + b.insert_text(suggestion.text) + else: + nc.end_of_line(event) + def create_ipython_shortcuts(shell): """Set up the prompt_toolkit keyboard shortcuts for IPython""" @@ -267,15 +283,6 @@ def ebivim(): focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode - # Needed for to accept autosuggestions in vi insert mode - def _apply_autosuggest(event): - b = event.current_buffer - suggestion = b.suggestion - if suggestion is not None and suggestion.text: - b.insert_text(suggestion.text) - else: - nc.end_of_line(event) - @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) def _(event): _apply_autosuggest(event) diff --git a/IPython/tests/test_shortcuts.py b/IPython/tests/test_shortcuts.py new file mode 100644 index 00000000000..42edb92ba58 --- /dev/null +++ b/IPython/tests/test_shortcuts.py @@ -0,0 +1,40 @@ +import pytest +from IPython.terminal.shortcuts import _apply_autosuggest + +from unittest.mock import Mock + + +def make_event(text, cursor, suggestion): + event = Mock() + event.current_buffer = Mock() + event.current_buffer.suggestion = Mock() + event.current_buffer.cursor_position = cursor + event.current_buffer.suggestion.text = suggestion + event.current_buffer.document = Mock() + event.current_buffer.document.get_end_of_line_position = Mock(return_value=0) + event.current_buffer.document.text = text + event.current_buffer.document.cursor_position = cursor + return event + + +@pytest.mark.parametrize( + "text, cursor, suggestion, called", + [ + ("123456", 6, "123456789", True), + ("123456", 3, "123456789", False), + ("123456 \n789", 6, "123456789", True), + ], +) +def test_autosuggest_at_EOL(text, cursor, suggestion, called): + """ + test that autosuggest is only applied at end of line. + """ + + event = make_event(text, cursor, suggestion) + event.current_buffer.insert_text = Mock() + _apply_autosuggest(event) + if called: + event.current_buffer.insert_text.assert_called() + else: + event.current_buffer.insert_text.assert_not_called() + # event.current_buffer.document.get_end_of_line_position.assert_called() diff --git a/tools/retar.py b/tools/retar.py index ccf1a1328a1..f8da2908fbe 100644 --- a/tools/retar.py +++ b/tools/retar.py @@ -4,6 +4,8 @@ usage: $ export SOURCE_DATE_EPOCH=$(date +%s) + # or + $ export SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) ... $ python retar.py From becb905f043c2b8a85b6ef099e82220988eae3fd Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Mon, 15 Aug 2022 16:30:35 -0700 Subject: [PATCH 0175/1752] Let %autoreload work on modules with frozen dataclasses See Issue #12411 and Issue #12185. The problem is that a frozen `dataclasses.dataclass` overrides the `__setattr__()` method, so updating its `__class__` member requires going through the base `object` class. This seems to fix the problem. --- IPython/extensions/autoreload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 816d2f35ea2..a0a8c27f134 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -300,7 +300,7 @@ class definition and update their __class__ to point to the new class for ref in refs: if type(ref) is old: - ref.__class__ = new + object.__setattr__(ref, "__class__", new) def update_class(old, new): From b22400df04b0125f5127f142f391e26bb30de679 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 17 Aug 2022 10:34:07 +0200 Subject: [PATCH 0176/1752] fix linter --- tools/github_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/github_stats.py b/tools/github_stats.py index 6a2f8f17fd5..af00a7b6ed9 100644 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -80,7 +80,7 @@ def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", p if pulls: filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] # filter out PRs not against main (backports) - filtered = [ i for i in filtered if i['base']['ref'] == 'main' ] + filtered = [i for i in filtered if i["base"]["ref"] == "main"] else: filtered = [ i for i in filtered if not is_pull_request(i) ] From 32a84f8444899fd06c021bccbc142bf79b2ee941 Mon Sep 17 00:00:00 2001 From: Richard Fung Date: Thu, 11 Aug 2022 12:33:17 -0700 Subject: [PATCH 0177/1752] keep autocomplete results stable in multi-thread access --- IPython/core/completer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 59d3e9930fc..9cc81a49512 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -677,15 +677,15 @@ def global_matches(self, text): n = len(text) for lst in [keyword.kwlist, builtin_mod.__dict__.keys(), - self.namespace.keys(), - self.global_namespace.keys()]: + list(self.namespace.keys()), + list(self.global_namespace.keys())]: for word in lst: if word[:n] == text and word != "__builtins__": match_append(word) snake_case_re = re.compile(r"[^_]+(_[^_]+)+?\Z") - for lst in [self.namespace.keys(), - self.global_namespace.keys()]: + for lst in [list(self.namespace.keys()), + list(self.global_namespace.keys())]: shortened = {"_".join([sub[0] for sub in word.split('_')]) : word for word in lst if snake_case_re.match(word)} for word in shortened.keys(): From 399e97f1617927f836044b5c24eb16089b88992f Mon Sep 17 00:00:00 2001 From: Richard Fung Date: Fri, 19 Aug 2022 13:21:53 -0700 Subject: [PATCH 0178/1752] apply lint --- IPython/core/completer.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 9cc81a49512..cffd0866f42 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -675,19 +675,23 @@ def global_matches(self, text): matches = [] match_append = matches.append n = len(text) - for lst in [keyword.kwlist, - builtin_mod.__dict__.keys(), - list(self.namespace.keys()), - list(self.global_namespace.keys())]: + for lst in [ + keyword.kwlist, + builtin_mod.__dict__.keys(), + list(self.namespace.keys()), + list(self.global_namespace.keys()), + ]: for word in lst: if word[:n] == text and word != "__builtins__": match_append(word) snake_case_re = re.compile(r"[^_]+(_[^_]+)+?\Z") - for lst in [list(self.namespace.keys()), - list(self.global_namespace.keys())]: - shortened = {"_".join([sub[0] for sub in word.split('_')]) : word - for word in lst if snake_case_re.match(word)} + for lst in [list(self.namespace.keys()), list(self.global_namespace.keys())]: + shortened = { + "_".join([sub[0] for sub in word.split("_")]): word + for word in lst + if snake_case_re.match(word) + } for word in shortened.keys(): if word[:n] == text and word != "__builtins__": match_append(shortened[word]) From eaade5b8bf1f3c3bc1124a53baf15d4fe64d5745 Mon Sep 17 00:00:00 2001 From: Richard Fung Date: Fri, 19 Aug 2022 14:01:34 -0700 Subject: [PATCH 0179/1752] unrelated lint change --- tools/github_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/github_stats.py b/tools/github_stats.py index 6a2f8f17fd5..af00a7b6ed9 100644 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -80,7 +80,7 @@ def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", p if pulls: filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] # filter out PRs not against main (backports) - filtered = [ i for i in filtered if i['base']['ref'] == 'main' ] + filtered = [i for i in filtered if i["base"]["ref"] == "main"] else: filtered = [ i for i in filtered if not is_pull_request(i) ] From 9af3aa0d91df4d55c09c8e12d836239a24e57bc8 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 28 Aug 2022 12:56:50 +0200 Subject: [PATCH 0180/1752] Upgrade GitHub Actions --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f57425a03aa..53ccb6f78ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,9 +49,9 @@ jobs: deps: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: pip From c66c03c2d0e90f6f2a27800699e6bec2529a9300 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 28 Aug 2022 12:58:35 +0200 Subject: [PATCH 0181/1752] Upgrade GitHub Actions --- .github/workflows/docs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 96ed2172e68..e4be71c0ddb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.x - name: Install Graphviz run: | sudo apt-get update From c0b5bb3af2426c0d5701e9ea641f6a45e2a7c953 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 28 Aug 2022 13:00:29 +0200 Subject: [PATCH 0182/1752] Upgrade GitHub Actions --- .github/workflows/downstream.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 309d03a2204..ae2dbe5efe9 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -21,9 +21,9 @@ jobs: python-version: "3.9" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Update Python installer From eee497f00e42efaea94c1117bd82b4d3de775efa Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 28 Aug 2022 13:01:39 +0200 Subject: [PATCH 0183/1752] Upgrade GitHub Actions --- .github/workflows/mypy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 8b1a4c37d89..2725c92923c 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -15,9 +15,9 @@ jobs: python-version: [3.8] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From de158eaa6e6f0fb05621052f259682278bd1791e Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 28 Aug 2022 13:02:53 +0200 Subject: [PATCH 0184/1752] Upgrade GitHub Actions --- .github/workflows/python-package.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 37d021890f0..fc28ac8e4c5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,18 +14,14 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 - strategy: - matrix: - python-version: [3.8] - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: 3.x - name: Install dependencies run: | python -m pip install --upgrade pip From 091ec63736ca5f2ce8d7835208a3644ed7c58f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=B6ppe?= Date: Fri, 22 Jul 2022 10:38:09 -0700 Subject: [PATCH 0185/1752] setup.cfg: Remove setuptools from install_requires It is not a runtime dependency. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0e371836877..274e338d0ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,6 @@ install_requires = pickleshare prompt_toolkit>3.0.1,<3.1.0 pygments>=2.4.0 - setuptools>=18.5 stack_data traitlets>=5 From 87ae1d99027a60e871bd4bb19fa069bf64c08634 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 20 Jul 2022 23:14:43 +0430 Subject: [PATCH 0186/1752] Fix `overrite` typo --- IPython/core/tests/test_displayhook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/tests/test_displayhook.py b/IPython/core/tests/test_displayhook.py index 6ad89793442..22899f3dd02 100644 --- a/IPython/core/tests/test_displayhook.py +++ b/IPython/core/tests/test_displayhook.py @@ -28,7 +28,7 @@ def test_output_quiet(): with AssertNotPrints('2'): ip.run_cell('1+1;\n#commented_out_function()', store_history=True) -def test_underscore_no_overrite_user(): +def test_underscore_no_overwrite_user(): ip.run_cell('_ = 42', store_history=True) ip.run_cell('1+1', store_history=True) @@ -41,7 +41,7 @@ def test_underscore_no_overrite_user(): ip.run_cell('_', store_history=True) -def test_underscore_no_overrite_builtins(): +def test_underscore_no_overwrite_builtins(): ip.run_cell("import gettext ; gettext.install('foo')", store_history=True) ip.run_cell('3+3', store_history=True) From ac69cfdfd83ab56c430f307b137fd64616e126b7 Mon Sep 17 00:00:00 2001 From: Lucy McPhail Date: Fri, 25 Feb 2022 11:30:56 +0000 Subject: [PATCH 0187/1752] Improve auto_match for triple quotes and escaped quotes --- IPython/terminal/shortcuts.py | 38 +++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 6c1ba0418bc..a68be9a6fca 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -140,6 +140,18 @@ def _following_text(): _following_text_cache[pattern] = condition return condition + @Condition + def not_inside_unclosed_string(): + app = get_app() + s = app.current_buffer.document.text_before_cursor + # remove escaped quotes + s = s.replace('\\"', "").replace("\\'", "") + # remove triple-quoted string literals + s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s) + # remove single-quoted string literals + s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s) + return not ('"' in s or "'" in s) + # auto match @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) def _(event): @@ -160,7 +172,7 @@ def _(event): '"', filter=focused_insert & auto_match - & preceding_text(r'^([^"]+|"[^"]*")*$') + & not_inside_unclosed_string & following_text(r"[,)}\]]|$"), ) def _(event): @@ -171,13 +183,35 @@ def _(event): "'", filter=focused_insert & auto_match - & preceding_text(r"^([^']+|'[^']*')*$") + & not_inside_unclosed_string & following_text(r"[,)}\]]|$"), ) def _(event): event.current_buffer.insert_text("''") event.current_buffer.cursor_left() + @kb.add( + '"', + filter=focused_insert + & auto_match + & not_inside_unclosed_string + & preceding_text(r'^.*""$'), + ) + def _(event): + event.current_buffer.insert_text('""""') + event.current_buffer.cursor_left(3) + + @kb.add( + "'", + filter=focused_insert + & auto_match + & not_inside_unclosed_string + & preceding_text(r"^.*''$"), + ) + def _(event): + event.current_buffer.insert_text("''''") + event.current_buffer.cursor_left(3) + # raw string @kb.add( "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") From b8d9894be683a3f9547c840a9e2627402b3a5d8e Mon Sep 17 00:00:00 2001 From: David Dorfman Date: Mon, 16 May 2022 04:31:19 +0300 Subject: [PATCH 0188/1752] terminal: Fix IndexError when attempting to elide paths with empty components --- IPython/terminal/ptutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/ptutils.py b/IPython/terminal/ptutils.py index c390d4972a7..1357abab71f 100644 --- a/IPython/terminal/ptutils.py +++ b/IPython/terminal/ptutils.py @@ -47,10 +47,10 @@ def _elide_point(string:str, *, min_elide=30)->str: if file_parts[-1] == '': file_parts.pop() - if len(object_parts) > 3: + if len(object_parts) > 3 and object_parts[1] and object_parts[-2]: return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][0], object_parts[-2][-1], object_parts[-1]) - elif len(file_parts) > 3: + elif len(file_parts) > 3 and file_parts[1] and file_parts[-2]: return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][0], file_parts[-2][-1], file_parts[-1]) return string From fa47bf46a5a716685e109629128a9d1a4679ea17 Mon Sep 17 00:00:00 2001 From: David Dorfman Date: Sun, 12 Jun 2022 13:45:01 +0300 Subject: [PATCH 0189/1752] Use slicing instead of condition --- IPython/terminal/ptutils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/terminal/ptutils.py b/IPython/terminal/ptutils.py index 1357abab71f..e9995a4a049 100644 --- a/IPython/terminal/ptutils.py +++ b/IPython/terminal/ptutils.py @@ -47,11 +47,11 @@ def _elide_point(string:str, *, min_elide=30)->str: if file_parts[-1] == '': file_parts.pop() - if len(object_parts) > 3 and object_parts[1] and object_parts[-2]: - return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][0], object_parts[-2][-1], object_parts[-1]) + if len(object_parts) > 3: + return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][:1], object_parts[-2][-1:], object_parts[-1]) - elif len(file_parts) > 3 and file_parts[1] and file_parts[-2]: - return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][0], file_parts[-2][-1], file_parts[-1]) + elif len(file_parts) > 3: + return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1]) return string From 26b8d437dbd63df3093f3b44bfefd7431724c16b Mon Sep 17 00:00:00 2001 From: David Dorfman Date: Sun, 12 Jun 2022 13:50:21 +0300 Subject: [PATCH 0190/1752] Reformat with darker --- IPython/terminal/ptutils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/ptutils.py b/IPython/terminal/ptutils.py index e9995a4a049..39bc2e15af9 100644 --- a/IPython/terminal/ptutils.py +++ b/IPython/terminal/ptutils.py @@ -48,10 +48,17 @@ def _elide_point(string:str, *, min_elide=30)->str: file_parts.pop() if len(object_parts) > 3: - return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][:1], object_parts[-2][-1:], object_parts[-1]) + return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format( + object_parts[0], + object_parts[1][:1], + object_parts[-2][-1:], + object_parts[-1], + ) elif len(file_parts) > 3: - return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1]) + return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format( + file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1] + ) return string From 81795d984256a3e3ba5ab5388408f10b801a4f2d Mon Sep 17 00:00:00 2001 From: David Dorfman Date: Mon, 13 Jun 2022 13:07:57 +0300 Subject: [PATCH 0191/1752] Add WN entry --- .../pr/fix-tab-completion-consecutive-separators.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst diff --git a/docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst b/docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst new file mode 100644 index 00000000000..3b1eed6c2ec --- /dev/null +++ b/docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst @@ -0,0 +1,5 @@ +Fixed tab completion for inputs with consecutive separators +=========================================================== + +Fixed error raised when attempting to tab-complete an input string with +consecutive periods or forward slashes (such as "file:///var/log/..."). From 89dc552787e6e3d66c39ad3a262275a51eac4e0f Mon Sep 17 00:00:00 2001 From: satoru Date: Sat, 16 Jul 2022 14:54:00 +0800 Subject: [PATCH 0192/1752] Fix #13700, consistently use the original name in error message --- IPython/utils/path.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/IPython/utils/path.py b/IPython/utils/path.py index 3db33e4c43e..51013c05f4b 100644 --- a/IPython/utils/path.py +++ b/IPython/utils/path.py @@ -83,12 +83,13 @@ def get_py_filename(name): """ name = os.path.expanduser(name) - if not os.path.isfile(name) and not name.endswith('.py'): - name += '.py' if os.path.isfile(name): return name - else: - raise IOError('File `%r` not found.' % name) + if not name.endswith('.py'): + py_name = name + '.py' + if os.path.isfile(py_name): + return py_name + raise IOError('File `%r` not found.' % name) def filefind(filename: str, path_dirs=None) -> str: From e53acc3604f3756e24171c3b4303bcba9bc82d4d Mon Sep 17 00:00:00 2001 From: satoru Date: Sat, 16 Jul 2022 15:21:15 +0800 Subject: [PATCH 0193/1752] Run darker as instructed --- IPython/core/interactiveshell.py | 19 +++++++++++-------- IPython/testing/plugin/pytest_ipdoctest.py | 2 +- IPython/utils/path.py | 6 +++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 06912640673..f7bacf443b2 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -213,14 +213,17 @@ def __repr__(self): raw_cell = ( (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell ) - return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % ( - name, - id(self), - raw_cell, - self.store_history, - self.silent, - self.shell_futures, - self.cell_id, + return ( + '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' + % ( + name, + id(self), + raw_cell, + self.store_history, + self.silent, + self.shell_futures, + self.cell_id, + ) ) diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 809713d7c8e..4ba2f1adf8e 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + if float(w.group()) == approx(float(g.group()), abs=10**-precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. diff --git a/IPython/utils/path.py b/IPython/utils/path.py index 51013c05f4b..a3a417d29e6 100644 --- a/IPython/utils/path.py +++ b/IPython/utils/path.py @@ -85,11 +85,11 @@ def get_py_filename(name): name = os.path.expanduser(name) if os.path.isfile(name): return name - if not name.endswith('.py'): - py_name = name + '.py' + if not name.endswith(".py"): + py_name = name + ".py" if os.path.isfile(py_name): return py_name - raise IOError('File `%r` not found.' % name) + raise IOError("File `%r` not found." % name) def filefind(filename: str, path_dirs=None) -> str: From 46fd9a39bacbe4f5352b1df29699f5c17b79352e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 30 Aug 2022 11:45:47 +0200 Subject: [PATCH 0194/1752] formatter --- IPython/core/interactiveshell.py | 19 ++++++++----------- IPython/testing/plugin/pytest_ipdoctest.py | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index f7bacf443b2..06912640673 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -213,17 +213,14 @@ def __repr__(self): raw_cell = ( (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell ) - return ( - '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' - % ( - name, - id(self), - raw_cell, - self.store_history, - self.silent, - self.shell_futures, - self.cell_id, - ) + return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % ( + name, + id(self), + raw_cell, + self.store_history, + self.silent, + self.shell_futures, + self.cell_id, ) diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 4ba2f1adf8e..809713d7c8e 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10**-precision): + if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. From f5d4e0ac4c72fd6051acdd084f8091114b634444 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 30 Aug 2022 11:47:24 +0200 Subject: [PATCH 0195/1752] MAINT: run black on files that ends up in a single line change. If running black on a file change one line of less, let just make it black Compliant --- IPython/core/tests/print_argv.py | 1 + IPython/core/tests/test_autocall.py | 1 + IPython/core/tests/test_splitinput.py | 1 + IPython/lib/tests/test_clipboard.py | 1 + IPython/utils/contexts.py | 1 + IPython/utils/eventful.py | 1 - IPython/utils/log.py | 1 - IPython/utils/tempdir.py | 1 + 8 files changed, 6 insertions(+), 2 deletions(-) diff --git a/IPython/core/tests/print_argv.py b/IPython/core/tests/print_argv.py index 0e92bddcb06..4ec9e2799ed 100644 --- a/IPython/core/tests/print_argv.py +++ b/IPython/core/tests/print_argv.py @@ -1,2 +1,3 @@ import sys + print(sys.argv[1:]) diff --git a/IPython/core/tests/test_autocall.py b/IPython/core/tests/test_autocall.py index ded9f78858a..925a1ccae37 100644 --- a/IPython/core/tests/test_autocall.py +++ b/IPython/core/tests/test_autocall.py @@ -8,6 +8,7 @@ from IPython.core.splitinput import LineInfo from IPython.core.prefilter import AutocallChecker + def doctest_autocall(): """ In [1]: def f1(a,b,c): diff --git a/IPython/core/tests/test_splitinput.py b/IPython/core/tests/test_splitinput.py index 8969da250ae..1462e7fa033 100644 --- a/IPython/core/tests/test_splitinput.py +++ b/IPython/core/tests/test_splitinput.py @@ -32,6 +32,7 @@ def test_split_user_input(): return tt.check_pairs(split_user_input, tests) + def test_LineInfo(): """Simple test for LineInfo construction and str()""" linfo = LineInfo(" %cd /home") diff --git a/IPython/lib/tests/test_clipboard.py b/IPython/lib/tests/test_clipboard.py index 802f753a339..6597c946b57 100644 --- a/IPython/lib/tests/test_clipboard.py +++ b/IPython/lib/tests/test_clipboard.py @@ -2,6 +2,7 @@ from IPython.lib.clipboard import ClipboardEmpty from IPython.testing.decorators import skip_if_no_x11 + @skip_if_no_x11 def test_clipboard_get(): # Smoketest for clipboard access - we can't easily guarantee that the diff --git a/IPython/utils/contexts.py b/IPython/utils/contexts.py index 7f95d4419dc..73c3f2e5b36 100644 --- a/IPython/utils/contexts.py +++ b/IPython/utils/contexts.py @@ -7,6 +7,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. + class preserve_keys(object): """Preserve a set of keys in a dictionary. diff --git a/IPython/utils/eventful.py b/IPython/utils/eventful.py index 661851ed37c..837c6e03442 100644 --- a/IPython/utils/eventful.py +++ b/IPython/utils/eventful.py @@ -1,4 +1,3 @@ - from warnings import warn warn("IPython.utils.eventful has moved to traitlets.eventful", stacklevel=2) diff --git a/IPython/utils/log.py b/IPython/utils/log.py index bb262eda936..f9dea91ce90 100644 --- a/IPython/utils/log.py +++ b/IPython/utils/log.py @@ -1,4 +1,3 @@ - from warnings import warn warn("IPython.utils.log has moved to traitlets.log", stacklevel=2) diff --git a/IPython/utils/tempdir.py b/IPython/utils/tempdir.py index 5afc5d64250..a233c73e382 100644 --- a/IPython/utils/tempdir.py +++ b/IPython/utils/tempdir.py @@ -48,6 +48,7 @@ class TemporaryWorkingDirectory(TemporaryDirectory): with TemporaryWorkingDirectory() as tmpdir: ... """ + def __enter__(self): self.old_wd = Path.cwd() _os.chdir(self.name) From 1dcbbc47bf458fa100769e50240de2d2bfaf388f Mon Sep 17 00:00:00 2001 From: Jacob Hall Date: Mon, 20 Jun 2022 21:31:03 -0400 Subject: [PATCH 0196/1752] terminal interface: catch exception when standard stream is closed if a standard stream is closed, e.g. `import sys; sys.stdin.close()`, a ValueError is raised when the stream is read. this commit adds a try .. except statement to catch that exception in IPython/terminal/interactiveshell.py --- IPython/terminal/interactiveshell.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 06724bea870..b7739c885a4 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -91,7 +91,12 @@ def get_default_editor(): # - no isatty method for _name in ('stdin', 'stdout', 'stderr'): _stream = getattr(sys, _name) - if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty(): + try: + if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty(): + _is_tty = False + break + except ValueError: + # stream is closed _is_tty = False break else: From 6e3d5b4f5290b03b42fc64c94f902281d1c739d9 Mon Sep 17 00:00:00 2001 From: Aman Date: Tue, 3 May 2022 21:23:43 -0400 Subject: [PATCH 0197/1752] Replaced getting cwd from os.getcwd() to pathlib.Path.cwd() in cd(). --- IPython/core/magics/osm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index 41957a28509..f3fa246567b 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -8,6 +8,7 @@ import io import os +import pathlib import re import sys from pprint import pformat @@ -409,7 +410,7 @@ def cd(self, parameter_s=''): except OSError: print(sys.exc_info()[1]) else: - cwd = os.getcwd() + cwd = pathlib.Path.cwd() dhist = self.shell.user_ns['_dh'] if oldcwd != cwd: dhist.append(cwd) @@ -419,7 +420,7 @@ def cd(self, parameter_s=''): os.chdir(self.shell.home_dir) if hasattr(self.shell, 'term_title') and self.shell.term_title: set_term_title(self.shell.term_title_format.format(cwd="~")) - cwd = os.getcwd() + cwd = pathlib.Path.cwd() dhist = self.shell.user_ns['_dh'] if oldcwd != cwd: From 0a8b189aa2ef5e146dcd6aa461c7ba67e979c430 Mon Sep 17 00:00:00 2001 From: William Andrea Date: Sat, 23 Apr 2022 13:02:35 -0400 Subject: [PATCH 0198/1752] Show "maxlen" in deque repr "collections.deque" has a "maxlen" attribute that should be shown in its repr if defined, just like its default repr does. This was brought up on Stack Overflow: https://stackoverflow.com/questions/71981214/python-deque-maxlen-does-not-show --- IPython/lib/pretty.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/lib/pretty.py b/IPython/lib/pretty.py index 72f143522df..f7feff9c3d3 100644 --- a/IPython/lib/pretty.py +++ b/IPython/lib/pretty.py @@ -908,6 +908,8 @@ def _deque_pprint(obj, p, cycle): cls_ctor = CallExpression.factory(obj.__class__.__name__) if cycle: p.pretty(cls_ctor(RawText("..."))) + elif obj.maxlen is not None: + p.pretty(cls_ctor(list(obj), maxlen=obj.maxlen)) else: p.pretty(cls_ctor(list(obj))) From 85a4bd798f2def36f662f064f5f64b6ef0762801 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 30 Aug 2022 16:38:25 +0200 Subject: [PATCH 0199/1752] update whatsnew --- docs/source/whatsnew/development.rst | 1 + .../pr/end-shortcut-accept-suggestion.rst | 7 -- ...-tab-completion-consecutive-separators.rst | 5 - .../pr/latex-generation-no-popup-window.rst | 5 - .../pr/latex_rendering_relative_files.rst | 7 -- .../whatsnew/pr/restore-line-numbers.rst | 50 -------- .../pr/silence-running-in-venv-warning.rst | 8 -- .../whatsnew/pr/stripping-decorators-bug.rst | 4 - docs/source/whatsnew/version8.rst | 116 ++++++++++++++++++ 9 files changed, 117 insertions(+), 86 deletions(-) delete mode 100644 docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst delete mode 100644 docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst delete mode 100644 docs/source/whatsnew/pr/latex-generation-no-popup-window.rst delete mode 100644 docs/source/whatsnew/pr/latex_rendering_relative_files.rst delete mode 100644 docs/source/whatsnew/pr/restore-line-numbers.rst delete mode 100644 docs/source/whatsnew/pr/silence-running-in-venv-warning.rst delete mode 100644 docs/source/whatsnew/pr/stripping-decorators-bug.rst diff --git a/docs/source/whatsnew/development.rst b/docs/source/whatsnew/development.rst index 502b0e0bbab..9969680d1a0 100644 --- a/docs/source/whatsnew/development.rst +++ b/docs/source/whatsnew/development.rst @@ -25,6 +25,7 @@ Need to be updated: + .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT. Backwards incompatible changes diff --git a/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst b/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst deleted file mode 100644 index c04998e8f3f..00000000000 --- a/docs/source/whatsnew/pr/end-shortcut-accept-suggestion.rst +++ /dev/null @@ -1,7 +0,0 @@ -Added shortcut for accepting auto suggestion -============================================ - -Added End key shortcut for accepting auto-suggestion -This binding works in Vi mode too, provided -TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode is set to be True. - diff --git a/docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst b/docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst deleted file mode 100644 index 3b1eed6c2ec..00000000000 --- a/docs/source/whatsnew/pr/fix-tab-completion-consecutive-separators.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fixed tab completion for inputs with consecutive separators -=========================================================== - -Fixed error raised when attempting to tab-complete an input string with -consecutive periods or forward slashes (such as "file:///var/log/..."). diff --git a/docs/source/whatsnew/pr/latex-generation-no-popup-window.rst b/docs/source/whatsnew/pr/latex-generation-no-popup-window.rst deleted file mode 100644 index 4cf73afdf22..00000000000 --- a/docs/source/whatsnew/pr/latex-generation-no-popup-window.rst +++ /dev/null @@ -1,5 +0,0 @@ -No popup in window for latex generation -======================================= - -When generating latex (e.g. via `_latex_repr_`) no popup window is shows under Windows. - diff --git a/docs/source/whatsnew/pr/latex_rendering_relative_files.rst b/docs/source/whatsnew/pr/latex_rendering_relative_files.rst deleted file mode 100644 index befd4afa35a..00000000000 --- a/docs/source/whatsnew/pr/latex_rendering_relative_files.rst +++ /dev/null @@ -1,7 +0,0 @@ -Relative filenames in Latex rendering -===================================== - -The `latex_to_png_dvipng` command internally generates input and output file arguments to `latex` and `dvipis`. These arguments are now generated as relative files to the current working directory instead of absolute file paths. -This solves a problem where the current working directory contains characters that are not handled properly by `latex` and `dvips`. -There are no changes to the user API. - diff --git a/docs/source/whatsnew/pr/restore-line-numbers.rst b/docs/source/whatsnew/pr/restore-line-numbers.rst deleted file mode 100644 index fb0729247a3..00000000000 --- a/docs/source/whatsnew/pr/restore-line-numbers.rst +++ /dev/null @@ -1,50 +0,0 @@ -Restore line numbers for Input -================================== - -Line number information in tracebacks from input are restored. -Line numbers from input were removed during the transition to v8 enhanced traceback reporting. - -So, instead of:: - - --------------------------------------------------------------------------- - ZeroDivisionError Traceback (most recent call last) - Input In [3], in () - ----> 1 myfunc(2) - - Input In [2], in myfunc(z) - 1 def myfunc(z): - ----> 2 foo.boo(z-1) - - File ~/code/python/ipython/foo.py:3, in boo(x) - 2 def boo(x): - ----> 3 return 1/(1-x) - - ZeroDivisionError: division by zero - -The error traceback now looks like:: - - --------------------------------------------------------------------------- - ZeroDivisionError Traceback (most recent call last) - Cell In [3], line 1 - ----> 1 myfunc(2) - - Cell In [2], line 2, in myfunc(z) - 1 def myfunc(z): - ----> 2 foo.boo(z-1) - - File ~/code/python/ipython/foo.py:3, in boo(x) - 2 def boo(x): - ----> 3 return 1/(1-x) - - ZeroDivisionError: division by zero - -or, with xmode=Plain:: - - Traceback (most recent call last): - Cell In [12], line 1 - myfunc(2) - Cell In [6], line 2 in myfunc - foo.boo(z-1) - File ~/code/python/ipython/foo.py:3 in boo - return 1/(1-x) - ZeroDivisionError: division by zero diff --git a/docs/source/whatsnew/pr/silence-running-in-venv-warning.rst b/docs/source/whatsnew/pr/silence-running-in-venv-warning.rst deleted file mode 100644 index ed245bd157a..00000000000 --- a/docs/source/whatsnew/pr/silence-running-in-venv-warning.rst +++ /dev/null @@ -1,8 +0,0 @@ -New setting to silence warning if working inside a virtual environment -====================================================================== - -Previously, when starting IPython in a virtual environment without IPython installed (so IPython from the global environment is used), the following warning was printed: - - Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. - -This warning can be permanently silenced by setting ``c.InteractiveShell.warn_venv`` to ``False`` (the default is ``True``). diff --git a/docs/source/whatsnew/pr/stripping-decorators-bug.rst b/docs/source/whatsnew/pr/stripping-decorators-bug.rst deleted file mode 100644 index 2ea03d7f9f7..00000000000 --- a/docs/source/whatsnew/pr/stripping-decorators-bug.rst +++ /dev/null @@ -1,4 +0,0 @@ -Stripping decorators bug -======================== - -Fixed bug which meant that ipython code blocks in restructured text documents executed with the ipython-sphinx extension skipped any lines of code containing python decorators. diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index c4b7b155212..59e71654faf 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,122 @@ 8.x Series ============ +.. _version 8.5.0: + +IPython 8.5.0 +------------- + +First release since a couple of month due to various reasons and timing preventing +me for sticking to the usual monthly release the last Friday of each month. This +is of non negligible size as it has more than two dozen PRs with various fixes +an bug fixes. + +Many thanks to everybody who contributed PRs for your patience in review and +merges. + +Here is a non exhaustive list of changes that have been implemented for IPython +8.5.0. As usual you can find the full list of issues and PRs tagged with `the +8.5 milestone +`__. + + - Added shortcut for accepting auto suggestion. The End key shortcut for + accepting auto-suggestion This binding works in Vi mode too, provided + ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be + ``True`` :ghpull:`13566`. + + - No popup in window for latex generation w hen generating latex (e.g. via + `_latex_repr_`) no popup window is shows under Windows. :ghpull:`13679` + + - Fixed error raised when attempting to tab-complete an input string with + consecutive periods or forward slashes (such as "file:///var/log/..."). + :ghpull:`13675` + + - Relative filenames in Latex rendering : + The `latex_to_png_dvipng` command internally generates input and output file + arguments to `latex` and `dvipis`. These arguments are now generated as + relative files to the current working directory instead of absolute file + paths. This solves a problem where the current working directory contains + characters that are not handled properly by `latex` and `dvips`. There are + no changes to the user API. :ghpull:`13680` + + - Stripping decorators bug: Fixed bug which meant that ipython code blocks in + restructured text documents executed with the ipython-sphinx extension + skipped any lines of code containing python decorators. :ghpull:`13612` + + - Allow some modules with frozen dataclasses to be reloaded. :ghpull:`13732` + - Fix paste magic on wayland. :ghpull:`13671` + - show maxlen in deque's repr. :ghpull:`13648` + +Restore line numbers for Input +------------------------------ + +Line number information in tracebacks from input are restored. +Line numbers from input were removed during the transition to v8 enhanced traceback reporting. + +So, instead of:: + + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + Input In [3], in () + ----> 1 myfunc(2) + + Input In [2], in myfunc(z) + 1 def myfunc(z): + ----> 2 foo.boo(z-1) + + File ~/code/python/ipython/foo.py:3, in boo(x) + 2 def boo(x): + ----> 3 return 1/(1-x) + + ZeroDivisionError: division by zero + +The error traceback now looks like:: + + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + Cell In [3], line 1 + ----> 1 myfunc(2) + + Cell In [2], line 2, in myfunc(z) + 1 def myfunc(z): + ----> 2 foo.boo(z-1) + + File ~/code/python/ipython/foo.py:3, in boo(x) + 2 def boo(x): + ----> 3 return 1/(1-x) + + ZeroDivisionError: division by zero + +or, with xmode=Plain:: + + Traceback (most recent call last): + Cell In [12], line 1 + myfunc(2) + Cell In [6], line 2 in myfunc + foo.boo(z-1) + File ~/code/python/ipython/foo.py:3 in boo + return 1/(1-x) + ZeroDivisionError: division by zero + +:ghpull:`13560` + +New setting to silence warning if working inside a virtual environment +---------------------------------------------------------------------- + +Previously, when starting IPython in a virtual environment without IPython installed (so IPython from the global environment is used), the following warning was printed: + + Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. + +This warning can be permanently silenced by setting ``c.InteractiveShell.warn_venv`` to ``False`` (the default is ``True``). + +:ghpull:`13706` + +------- + +Thanks to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + + .. _version 8.4.0: IPython 8.4.0 From c179c2a596e57d64940aa58160832a047600e474 Mon Sep 17 00:00:00 2001 From: satoru Date: Sat, 16 Jul 2022 16:06:14 +0800 Subject: [PATCH 0200/1752] Fix #13654, improve performance of auto match for quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As pointed out in #13654, auto matching of quotes may take a long time if the prefix is long. To be more precise, the longer the text before the first quote, the slower it is. This is all caused by the regex pattern used: `r'^([^"]+|"[^"]*")*$'`, which I suspect is O(2^N) slow. ```python In [1]: text = "function_with_long_nameeee('arg" In [2]: import re In [3]: pattern = re.compile(r"^([^']+|'[^']*')*$") In [4]: %timeit pattern.match(text) 10.3 s ± 67.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) In [5]: %timeit pattern.match("1'") 312 ns ± 0.775 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) In [6]: %timeit pattern.match("12'") 462 ns ± 1.95 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) In [7]: %timeit pattern.match("123'") 766 ns ± 6.32 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) In [8]: %timeit pattern.match("1234'") 1.59 µs ± 20.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) ``` But the pattern we want here can actually be detected with a Python implemention in O(N) time. --- IPython/terminal/shortcuts.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index a68be9a6fca..4b51b284ed6 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -106,20 +106,37 @@ def reformat_and_execute(event): def auto_match(): return shell.auto_match + def all_quotes_paired(quote, buf): + paired = True + i = 0 + while i < len(buf): + c = buf[i] + if c == quote: + paired = not paired + elif c == '\\': + i += 1 + i += 1 + return paired + focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER) _preceding_text_cache = {} _following_text_cache = {} def preceding_text(pattern): - try: + if pattern in _preceding_text_cache: return _preceding_text_cache[pattern] - except KeyError: - pass - m = re.compile(pattern) - def _preceding_text(): - app = get_app() - return bool(m.match(app.current_buffer.document.current_line_before_cursor)) + if callable(pattern): + def _preceding_text(): + app = get_app() + before_cursor = app.current_buffer.document.current_line_before_cursor + return bool(pattern(before_cursor)) + else: + m = re.compile(pattern) + + def _preceding_text(): + app = get_app() + return bool(m.match(app.current_buffer.document.current_line_before_cursor)) condition = Condition(_preceding_text) _preceding_text_cache[pattern] = condition @@ -173,6 +190,7 @@ def _(event): filter=focused_insert & auto_match & not_inside_unclosed_string + & preceding_text(lambda line: all_quotes_paired('"', line)) & following_text(r"[,)}\]]|$"), ) def _(event): @@ -184,6 +202,7 @@ def _(event): filter=focused_insert & auto_match & not_inside_unclosed_string + & preceding_text(lambda line: all_quotes_paired("'", line)) & following_text(r"[,)}\]]|$"), ) def _(event): From 0355b0b8a1e6ad2ae0d41081ce531bdbce0b34d6 Mon Sep 17 00:00:00 2001 From: satoru Date: Sat, 16 Jul 2022 16:19:04 +0800 Subject: [PATCH 0201/1752] Fix format --- IPython/terminal/shortcuts.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 4b51b284ed6..7d6de8b3b04 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -113,7 +113,7 @@ def all_quotes_paired(quote, buf): c = buf[i] if c == quote: paired = not paired - elif c == '\\': + elif c == "\\": i += 1 i += 1 return paired @@ -127,16 +127,19 @@ def preceding_text(pattern): return _preceding_text_cache[pattern] if callable(pattern): + def _preceding_text(): app = get_app() before_cursor = app.current_buffer.document.current_line_before_cursor return bool(pattern(before_cursor)) + else: m = re.compile(pattern) def _preceding_text(): app = get_app() - return bool(m.match(app.current_buffer.document.current_line_before_cursor)) + before_cursor = app.current_buffer.document.current_line_before_cursor + return bool(m.match(before_cursor)) condition = Condition(_preceding_text) _preceding_text_cache[pattern] = condition From aefe51c6071e045e00ed87fa5ef995eca17105f5 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 1 Sep 2022 12:02:43 +0200 Subject: [PATCH 0202/1752] try ruff to find unused imports --- IPython/__init__.py | 1 - IPython/core/application.py | 1 - IPython/core/crashhandler.py | 1 - IPython/core/debugger.py | 1 - IPython/core/excolors.py | 1 - IPython/core/formatters.py | 1 - IPython/core/inputtransformer2.py | 1 - IPython/core/magics/basic.py | 1 - IPython/core/oinspect.py | 1 - IPython/core/shellapp.py | 1 - IPython/core/tests/test_compilerop.py | 3 --- IPython/core/tests/test_debugger.py | 1 - IPython/core/tests/test_formatters.py | 1 - IPython/core/tests/test_history.py | 1 - IPython/core/tests/test_inputtransformer2_line.py | 1 - IPython/core/tests/test_iplib.py | 2 -- IPython/core/tests/test_magic.py | 1 - IPython/core/tests/test_paths.py | 1 - IPython/core/tests/test_pylabtools.py | 3 --- IPython/core/tests/test_ultratb.py | 1 - IPython/lib/latextools.py | 2 +- IPython/paths.py | 1 - IPython/testing/decorators.py | 1 - IPython/testing/globalipapp.py | 1 - IPython/testing/plugin/ipdoctest.py | 1 - IPython/tests/cve.py | 1 - IPython/utils/io.py | 1 - IPython/utils/module_paths.py | 1 - IPython/utils/path.py | 2 -- IPython/utils/tests/test_io.py | 1 - IPython/utils/tests/test_module_paths.py | 2 -- IPython/utils/tests/test_text.py | 1 - 32 files changed, 1 insertion(+), 39 deletions(-) diff --git a/IPython/__init__.py b/IPython/__init__.py index b5410f63da9..03b3116a98a 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -18,7 +18,6 @@ # Imports #----------------------------------------------------------------------------- -import os import sys #----------------------------------------------------------------------------- diff --git a/IPython/core/application.py b/IPython/core/application.py index 0cdea5c69b8..26c061661ac 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -14,7 +14,6 @@ import atexit from copy import deepcopy -import glob import logging import os import shutil diff --git a/IPython/core/crashhandler.py b/IPython/core/crashhandler.py index 4af39361e80..f60a75bbc5b 100644 --- a/IPython/core/crashhandler.py +++ b/IPython/core/crashhandler.py @@ -19,7 +19,6 @@ # Imports #----------------------------------------------------------------------------- -import os import sys import traceback from pprint import pformat diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index ba12e3eac39..73b0328743b 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -104,7 +104,6 @@ import inspect import linecache import sys -import warnings import re import os diff --git a/IPython/core/excolors.py b/IPython/core/excolors.py index c47ce922c4e..85eef81f0e0 100644 --- a/IPython/core/excolors.py +++ b/IPython/core/excolors.py @@ -4,7 +4,6 @@ """ import os -import warnings #***************************************************************************** # Copyright (C) 2005-2006 Fernando Perez diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index 4e0b9e455ae..e7aa6f3abab 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -11,7 +11,6 @@ # Distributed under the terms of the Modified BSD License. import abc -import json import sys import traceback import warnings diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py index a8f676f4952..e4e385a95ff 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -11,7 +11,6 @@ # Distributed under the terms of the Modified BSD License. import ast -import sys from codeop import CommandCompiler, Compile import re import tokenize diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index ea407c61811..af69b02676d 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -1,7 +1,6 @@ """Implementation of basic magic functions.""" -import argparse from logging import error import io import os diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 1a5c0ae070c..f1c454b2604 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -31,7 +31,6 @@ from IPython.testing.skipdoctest import skip_doctest from IPython.utils import PyColorize from IPython.utils import openpy -from IPython.utils import py3compat from IPython.utils.dir2 import safe_hasattr from IPython.utils.path import compress_user from IPython.utils.text import indent diff --git a/IPython/core/shellapp.py b/IPython/core/shellapp.py index f737bcb56b7..180f8b1b01f 100644 --- a/IPython/core/shellapp.py +++ b/IPython/core/shellapp.py @@ -19,7 +19,6 @@ from IPython.core import pylabtools from IPython.utils.contexts import preserve_keys from IPython.utils.path import filefind -import traitlets from traitlets import ( Unicode, Instance, List, Bool, CaselessStrEnum, observe, DottedObjectName, diff --git a/IPython/core/tests/test_compilerop.py b/IPython/core/tests/test_compilerop.py index b939bb60676..8a5c9dc085b 100644 --- a/IPython/core/tests/test_compilerop.py +++ b/IPython/core/tests/test_compilerop.py @@ -17,9 +17,6 @@ import linecache import sys -# Third-party imports -import pytest - # Our own imports from IPython.core import compilerop diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 5a12c3bb2ad..5f7f98d3c29 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -4,7 +4,6 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -import bdb import builtins import os import sys diff --git a/IPython/core/tests/test_formatters.py b/IPython/core/tests/test_formatters.py index 26d35837ac8..c642befaccf 100644 --- a/IPython/core/tests/test_formatters.py +++ b/IPython/core/tests/test_formatters.py @@ -1,6 +1,5 @@ """Tests for the Formatters.""" -import warnings from math import pi try: diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index a9ebafddd4c..fa64fe04df1 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -7,7 +7,6 @@ # stdlib import io -import sqlite3 import sys import tempfile from datetime import datetime diff --git a/IPython/core/tests/test_inputtransformer2_line.py b/IPython/core/tests/test_inputtransformer2_line.py index 30558fd7e25..ec7a8736412 100644 --- a/IPython/core/tests/test_inputtransformer2_line.py +++ b/IPython/core/tests/test_inputtransformer2_line.py @@ -3,7 +3,6 @@ Line-based transformers are the simpler ones; token-based transformers are more complex. See test_inputtransformer2 for tests for token-based transformers. """ -import pytest from IPython.core import inputtransformer2 as ipt2 diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py index 14474bb0da1..f12601823d1 100644 --- a/IPython/core/tests/test_iplib.py +++ b/IPython/core/tests/test_iplib.py @@ -4,8 +4,6 @@ # Module imports #----------------------------------------------------------------------------- -# third party -import pytest # our own packages diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 850e22792b9..1c793ca2915 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Tests for various magic functions.""" -import asyncio import gc import io import os diff --git a/IPython/core/tests/test_paths.py b/IPython/core/tests/test_paths.py index eb754b81529..86367b61ecb 100644 --- a/IPython/core/tests/test_paths.py +++ b/IPython/core/tests/test_paths.py @@ -1,7 +1,6 @@ import errno import os import shutil -import sys import tempfile import warnings from unittest.mock import patch diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index 78886373ced..59bf3bccd49 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -155,9 +155,6 @@ def test_import_pylab(): assert ns["np"] == np -from traitlets.config import Config - - class TestPylabSwitch(object): class Shell(InteractiveShell): def init_history(self): diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index 1f49603ee88..349d2ac9e46 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -2,7 +2,6 @@ """Tests for IPython.core.ultratb """ import io -import logging import os.path import platform import re diff --git a/IPython/lib/latextools.py b/IPython/lib/latextools.py index b90a5d78606..693060ba55c 100644 --- a/IPython/lib/latextools.py +++ b/IPython/lib/latextools.py @@ -12,7 +12,7 @@ from base64 import encodebytes import textwrap -from pathlib import Path, PurePath +from pathlib import Path from IPython.utils.process import find_cmd, FindCmdError from traitlets.config import get_config diff --git a/IPython/paths.py b/IPython/paths.py index 4fd253cf1e2..cc6408ca434 100644 --- a/IPython/paths.py +++ b/IPython/paths.py @@ -1,7 +1,6 @@ """Find files and directories which IPython uses. """ import os.path -import shutil import tempfile from warnings import warn diff --git a/IPython/testing/decorators.py b/IPython/testing/decorators.py index 644a513a8c3..af42f349d5a 100644 --- a/IPython/testing/decorators.py +++ b/IPython/testing/decorators.py @@ -36,7 +36,6 @@ import sys import tempfile import unittest -import warnings from importlib import import_module from decorator import decorator diff --git a/IPython/testing/globalipapp.py b/IPython/testing/globalipapp.py index 698e3d845aa..3a699e07d61 100644 --- a/IPython/testing/globalipapp.py +++ b/IPython/testing/globalipapp.py @@ -12,7 +12,6 @@ import builtins as builtin_mod import sys import types -import warnings from pathlib import Path diff --git a/IPython/testing/plugin/ipdoctest.py b/IPython/testing/plugin/ipdoctest.py index 52cd8fd3b8a..e7edf9837f1 100644 --- a/IPython/testing/plugin/ipdoctest.py +++ b/IPython/testing/plugin/ipdoctest.py @@ -21,7 +21,6 @@ # From the standard library import doctest import logging -import os import re from testpath import modified_env diff --git a/IPython/tests/cve.py b/IPython/tests/cve.py index 0a9dec4e854..fd1b807604a 100644 --- a/IPython/tests/cve.py +++ b/IPython/tests/cve.py @@ -9,7 +9,6 @@ import os import string import subprocess -import time def test_cve_2022_21699(): diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 170bc625acb..2eac5e66cba 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -12,7 +12,6 @@ import os import sys import tempfile -import warnings from pathlib import Path from warnings import warn diff --git a/IPython/utils/module_paths.py b/IPython/utils/module_paths.py index f9f7cacc332..6f8cb1004a6 100644 --- a/IPython/utils/module_paths.py +++ b/IPython/utils/module_paths.py @@ -17,7 +17,6 @@ # Stdlib imports import importlib -import os import sys # Third-party imports diff --git a/IPython/utils/path.py b/IPython/utils/path.py index a3a417d29e6..ccb70dccd43 100644 --- a/IPython/utils/path.py +++ b/IPython/utils/path.py @@ -12,10 +12,8 @@ import shutil import random import glob -from warnings import warn from IPython.utils.process import system -from IPython.utils.decorators import undoc #----------------------------------------------------------------------------- # Code diff --git a/IPython/utils/tests/test_io.py b/IPython/utils/tests/test_io.py index 3b4c03eae06..75d895e03c3 100644 --- a/IPython/utils/tests/test_io.py +++ b/IPython/utils/tests/test_io.py @@ -8,7 +8,6 @@ import sys from io import StringIO -from subprocess import Popen, PIPE import unittest from IPython.utils.io import Tee, capture_output diff --git a/IPython/utils/tests/test_module_paths.py b/IPython/utils/tests/test_module_paths.py index 8438a1e737f..8dc52fd3234 100644 --- a/IPython/utils/tests/test_module_paths.py +++ b/IPython/utils/tests/test_module_paths.py @@ -18,8 +18,6 @@ from pathlib import Path -from IPython.testing.tools import make_tempfile - import IPython.utils.module_paths as mp TEST_FILE_PATH = Path(__file__).resolve().parent diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index c036f5327c9..cc04cc93efa 100644 --- a/IPython/utils/tests/test_text.py +++ b/IPython/utils/tests/test_text.py @@ -15,7 +15,6 @@ import os import math import random -import sys from pathlib import Path From 1db65d02e89f31c28c221197a2ed04f3ade3b195 Mon Sep 17 00:00:00 2001 From: Nir Schulman Date: Sat, 3 Sep 2022 09:11:58 +0300 Subject: [PATCH 0203/1752] Added additional entrypoint script. Added a third entrypoint to use python's minor version as well. This can help when testing out differences of python versions. One could easily open "ipython3.10" and test it's differences with "ipython3.8". --- setupbase.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setupbase.py b/setupbase.py index b57dcc1b2a5..aaed8d1abaf 100644 --- a/setupbase.py +++ b/setupbase.py @@ -211,14 +211,16 @@ def find_entry_points(): use, our own build_scripts_entrypt class below parses these and builds command line scripts. - Each of our entry points gets both a plain name, e.g. ipython, and one - suffixed with the Python major version number, e.g. ipython3. + Each of our entry points gets a plain name, e.g. ipython, a name + suffixed with the Python major version number, e.g. ipython3, and + a name suffixed with the Python major.minor version number, eg. ipython3.8. """ ep = [ 'ipython%s = IPython:start_ipython', ] - suffix = str(sys.version_info[0]) - return [e % '' for e in ep] + [e % suffix for e in ep] + major_suffix = str(sys.version_info[0]) + minor_suffix = ".".join([str(sys.version_info[0]), str(sys.version_info[1])]) + return [e % '' for e in ep] + [e % major_suffix for e in ep] + [e % minor_suffix for e in ep] class install_lib_symlink(Command): user_options = [ From d0bc765bde7c0471806b89231ce980209cf66c71 Mon Sep 17 00:00:00 2001 From: Nir Schulman Date: Sat, 3 Sep 2022 09:40:33 +0300 Subject: [PATCH 0204/1752] Update setupbase.py --- setupbase.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setupbase.py b/setupbase.py index aaed8d1abaf..5db235cd7de 100644 --- a/setupbase.py +++ b/setupbase.py @@ -220,7 +220,11 @@ def find_entry_points(): ] major_suffix = str(sys.version_info[0]) minor_suffix = ".".join([str(sys.version_info[0]), str(sys.version_info[1])]) - return [e % '' for e in ep] + [e % major_suffix for e in ep] + [e % minor_suffix for e in ep] + return ( + [e % "" for e in ep] + + [e % major_suffix for e in ep] + + [e % minor_suffix for e in ep] + ) class install_lib_symlink(Command): user_options = [ From 77a4bce71bfcd1c2caf805a1743058c7d02af40d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 3 Sep 2022 12:00:40 +0200 Subject: [PATCH 0205/1752] Remove deprecated install_ext and ~/.ipython/extensions As pointed out in #13722 loading extensions from this place have been deprecated for about 5 years, so time for removal. I understand this will likely make it too hard for some user who can't seem to be able to write 8 lines of configuration: $ cat mod.py pyproject.toml """ipython ext""" __version__ = "0.0.1" [build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "mod" dynamic = ["version", "description"] Those can be reminded that they can also modify their $PYTHONPATH, so that py files can be imported from their preferred folder. Closes #13722 --- IPython/core/extensions.py | 18 +----------------- docs/source/config/extensions/index.rst | 8 ++------ 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/IPython/core/extensions.py b/IPython/core/extensions.py index ce419e1bd40..21fba40eaf4 100644 --- a/IPython/core/extensions.py +++ b/IPython/core/extensions.py @@ -88,13 +88,7 @@ def _load_extension(self, module_str: str): with self.shell.builtin_trap: if module_str not in sys.modules: - with prepended_to_syspath(self.ipython_extension_dir): - mod = import_module(module_str) - if mod.__file__.startswith(self.ipython_extension_dir): - print(("Loading extensions from {dir} is deprecated. " - "We recommend managing extensions like any " - "other Python packages, in site-packages.").format( - dir=compress_user(self.ipython_extension_dir))) + mod = import_module(module_str) mod = sys.modules[module_str] if self._call_load_ipython_extension(mod): self.loaded.add(module_str) @@ -155,13 +149,3 @@ def _call_unload_ipython_extension(self, mod): if hasattr(mod, 'unload_ipython_extension'): mod.unload_ipython_extension(self.shell) return True - - @undoc - def install_extension(self, url, filename=None): - """ - Deprecated. - """ - # Ensure the extension directory exists - raise DeprecationWarning( - '`install_extension` and the `install_ext` magic have been deprecated since IPython 4.0' - 'Use pip or other package managers to manage ipython extensions.') diff --git a/docs/source/config/extensions/index.rst b/docs/source/config/extensions/index.rst index e3c9cab4252..4b0a2222a8b 100644 --- a/docs/source/config/extensions/index.rst +++ b/docs/source/config/extensions/index.rst @@ -6,8 +6,7 @@ IPython extensions A level above configuration are IPython extensions, Python modules which modify the behaviour of the shell. They are referred to by an importable module name, -and can be placed anywhere you'd normally import from, or in -``.ipython/extensions/``. +and can be placed anywhere you'd normally import from. Getting extensions ================== @@ -71,10 +70,7 @@ Useful :class:`InteractiveShell` methods include :meth:`~IPython.core.interactiv :ref:`defining_magics` You can put your extension modules anywhere you want, as long as they can be -imported by Python's standard import mechanism. However, to make it easy to -write extensions, you can also put your extensions in :file:`extensions/` -within the :ref:`IPython directory `. This directory is -added to :data:`sys.path` automatically. +imported by Python's standard import mechanism. When your extension is ready for general use, please add it to the `extensions index `_. We also From d22310b932d989bce3ef82890f03a5d6ecb68775 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 5 Sep 2022 05:21:34 +0100 Subject: [PATCH 0206/1752] Refactor `IPCompleter` Matcher API --- IPython/core/completer.py | 767 ++++++++++++++++++++++----- IPython/core/tests/test_completer.py | 40 +- 2 files changed, 653 insertions(+), 154 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 59d3e9930fc..4d51ee45825 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -100,6 +100,30 @@ Be sure to update :any:`jedi` to the latest stable version or to try the current development version to get better completions. + +Matchers +======== + +All completions routines are implemented using unified ``matchers`` API. +The matchers API is provisional and subject to change without notice. + +The built-in matchers include: + +- ``IPCompleter.dict_key_matcher``: dictionary key completions, +- ``IPCompleter.magic_matcher``: completions for magics, +- ``IPCompleter.unicode_name_matcher``, ``IPCompleter.fwd_unicode_matcher`` and ``IPCompleter.latex_matcher``: see `Forward latex/unicode completion`_, +- ``back_unicode_name_matcher`` and ``back_latex_name_matcher``: see `Backward latex completion`_, +- ``IPCompleter.file_matcher``: paths to files and directories, +- ``IPCompleter.python_func_kw_matcher`` - function keywords, +- ``IPCompleter.python_matches`` - globals and attributes (v1 API), +- ``IPCompleter.jedi_matcher`` - static analysis with Jedi, +- ``IPCompleter.custom_completer_matcher`` - pluggable completer with a default implementation in ``core.InteractiveShell`` + which uses uses IPython hooks system (`complete_command`) with string dispatch (including regular expressions). + Differently to other matchers, ``custom_completer_matcher`` will not suppress Jedi results to match + behaviour in earlier IPython versions. + +Adding custom matchers is possible by appending to `IPCompleter.custom_matchers` list, +but please be aware that this API is subject to change. """ @@ -124,9 +148,26 @@ import uuid import warnings from contextlib import contextmanager +from functools import lru_cache, partial from importlib import import_module from types import SimpleNamespace -from typing import Iterable, Iterator, List, Tuple, Union, Any, Sequence, Dict, NamedTuple, Pattern, Optional +from typing import ( + Iterable, + Iterator, + List, + Tuple, + Union, + Any, + Sequence, + Dict, + NamedTuple, + Pattern, + Optional, + Callable, + TYPE_CHECKING, + Set, +) +from typing_extensions import TypedDict, NotRequired from IPython.core.error import TryNext from IPython.core.inputtransformer2 import ESC_MAGIC @@ -137,7 +178,17 @@ from IPython.utils.dir2 import dir2, get_real_method from IPython.utils.path import ensure_dir_exists from IPython.utils.process import arg_split -from traitlets import Bool, Enum, Int, List as ListTrait, Unicode, default, observe +from traitlets import ( + Bool, + Enum, + Int, + List as ListTrait, + Unicode, + Dict as DictTrait, + Union as UnionTrait, + default, + observe, +) from traitlets.config.configurable import Configurable import __main__ @@ -145,6 +196,7 @@ # skip module docstests __skip_doctest__ = True + try: import jedi jedi.settings.case_insensitive_completion = False @@ -153,7 +205,16 @@ JEDI_INSTALLED = True except ImportError: JEDI_INSTALLED = False -#----------------------------------------------------------------------------- + +if TYPE_CHECKING: + from typing import cast +else: + + def cast(obj, _type): + return obj + + +# ----------------------------------------------------------------------------- # Globals #----------------------------------------------------------------------------- @@ -177,6 +238,8 @@ # may have trouble processing. MATCHES_LIMIT = 500 +# Completion type reported when no type can be inferred. +_UNKNOWN_TYPE = "" class ProvisionalCompleterWarning(FutureWarning): """ @@ -355,6 +418,9 @@ def __repr__(self): return '' +_JediCompletionLike = Union[jedi.api.Completion, _FakeJediCompletion] + + class Completion: """ Completion object used and return by IPython completers. @@ -417,6 +483,131 @@ def __hash__(self): return hash((self.start, self.end, self.text)) +class SimpleCompletion: + # TODO: decide whether we should keep the ``SimpleCompletion`` separate from ``Completion`` + # there are two advantages of keeping them separate: + # - compatibility with old readline `Completer.complete` interface (less important) + # - ease of use for third parties (just return matched text and don't worry about coordinates) + # the disadvantage is that we need to loop over the completions again to transform them into + # `Completion` objects (but it was done like that before the refactor into `SimpleCompletion` too). + __slots__ = ["text", "type"] + + def __init__(self, text: str, *, type: str = None): + self.text = text + self.type = type + + def __repr__(self): + return f"" + + +class _MatcherResultBase(TypedDict): + + #: suffix of the provided ``CompletionContext.token``, if not given defaults to full token. + matched_fragment: NotRequired[str] + + #: whether to suppress results from other matchers; default is False. + suppress_others: NotRequired[bool] + + #: are completions already ordered and should be left as-is? default is False. + ordered: NotRequired[bool] + + # TODO: should we used a relevance score for ordering? + #: value between 0 (likely not relevant) and 100 (likely relevant); default is 50. + # relevance: NotRequired[float] + + +class SimpleMatcherResult(_MatcherResultBase): + """Result of new-style completion matcher.""" + + #: list of candidate completions + completions: Sequence[SimpleCompletion] + + +class _JediMatcherResult(_MatcherResultBase): + """Matching result returned by Jedi (will be processed differently)""" + + #: list of candidate completions + completions: Iterable[_JediCompletionLike] + + +class CompletionContext(NamedTuple): + # rationale: many legacy matchers relied on completer state (`self.text_until_cursor`) + # which was not explicitly visible as an argument of the matcher, making any refactor + # prone to errors; by explicitly passing `cursor_position` we can decouple the matchers + # from the completer, and make substituting them in sub-classes easier. + + #: Relevant fragment of code directly preceding the cursor. + #: The extraction of token is implemented via splitter heuristic + #: (following readline behaviour for legacy reasons), which is user configurable + #: (by switching the greedy mode). + token: str + + full_text: str + + #: Cursor position in the line (the same for ``full_text`` and `text``). + cursor_position: int + + #: Cursor line in ``full_text``. + cursor_line: int + + @property + @lru_cache(maxsize=None) # TODO change to @cache after dropping Python 3.7 + def text_until_cursor(self) -> str: + return self.line_with_cursor[: self.cursor_position] + + @property + @lru_cache(maxsize=None) # TODO change to @cache after dropping Python 3.7 + def line_with_cursor(self) -> str: + return self.full_text.split("\n")[self.cursor_line] + + +MatcherResult = Union[SimpleMatcherResult, _JediMatcherResult] + +MatcherAPIv1 = Callable[[str], List[str]] +MatcherAPIv2 = Callable[[CompletionContext], MatcherResult] +Matcher = Union[MatcherAPIv1, MatcherAPIv2] + + +def completion_matcher( + *, priority: float = None, identifier: str = None, api_version=1 +): + """Adds attributes describing the matcher. + + Parameters + ---------- + priority : Optional[float] + The priority of the matcher, determines the order of execution of matchers. + Higher priority means that the matcher will be executed first. Defaults to 50. + identifier : Optional[str] + identifier of the matcher allowing users to modify the behaviour via traitlets, + and also used to for debugging (will be passed as ``origin`` with the completions). + Defaults to matcher function ``__qualname__``. + api_version: Optional[int] + version of the Matcher API used by this matcher. + Currently supported values are 1 and 2. + Defaults to 1. + """ + + def wrapper(func: Matcher): + func.matcher_priority = priority + func.matcher_identifier = identifier or func.__qualname__ + func.matcher_api_version = api_version + return func + + return wrapper + + +def _get_matcher_id(matcher: Matcher): + return getattr(matcher, "matcher_identifier", matcher.__qualname__) + + +def _get_matcher_api_version(matcher): + return getattr(matcher, "matcher_api_version", 1) + + +context_matcher = partial(completion_matcher, api_version=2) + + _IC = Iterable[Completion] @@ -920,7 +1111,16 @@ def _safe_isinstance(obj, module, class_name): return (module in sys.modules and isinstance(obj, getattr(import_module(module), class_name))) -def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]: + +@context_matcher() +def back_unicode_name_matcher(context): + fragment, matches = back_unicode_name_matches(context.token) + return _convert_matcher_v1_result_to_v2( + matches, type="unicode", fragment=fragment, suppress_if_matches=True + ) + + +def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]: """Match Unicode characters back to Unicode name This does ``☃`` -> ``\\snowman`` @@ -959,7 +1159,16 @@ def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]: pass return '', () -def back_latex_name_matches(text:str) -> Tuple[str, Sequence[str]] : + +@context_matcher() +def back_latex_name_matcher(context): + fragment, matches = back_latex_name_matches(context.token) + return _convert_matcher_v1_result_to_v2( + matches, type="latex", fragment=fragment, suppress_if_matches=True + ) + + +def back_latex_name_matches(text: str) -> Tuple[str, Sequence[str]]: """Match latex characters back to unicode name This does ``\\ℵ`` -> ``\\aleph`` @@ -1038,11 +1247,25 @@ def _make_signature(completion)-> str: for p in signature.defined_names()) if f]) -class _CompleteResult(NamedTuple): - matched_text : str - matches: Sequence[str] - matches_origin: Sequence[str] - jedi_matches: Any +_CompleteResult = Dict[str, MatcherResult] + + +def _convert_matcher_v1_result_to_v2( + matches: Sequence[str], + type: str, + fragment: str = None, + suppress_if_matches: bool = False, +) -> SimpleMatcherResult: + """Utility to help with transition""" + result = { + "completions": [SimpleCompletion(text=match, type=type) for match in matches], + "suppress_others": (True if matches else False) + if suppress_if_matches + else False, + } + if fragment is not None: + result["matched_fragment"] = fragment + return result class IPCompleter(Completer): @@ -1058,17 +1281,58 @@ def _greedy_changed(self, change): else: self.splitter.delims = DELIMS - dict_keys_only = Bool(False, - help="""Whether to show dict key matches only""") + dict_keys_only = Bool( + False, + help=""" + Whether to show dict key matches only. + + (disables all matchers except for `IPCompleter.dict_key_matcher`). + """, + ) + + suppress_competing_matchers = UnionTrait( + [Bool(), DictTrait(Bool(None, allow_none=True))], + help=""" + Whether to suppress completions from other `Matchers`_. + + When set to ``None`` (default) the matchers will attempt to auto-detect + whether suppression of other matchers is desirable. For example, at + the beginning of a line followed by `%` we expect a magic completion + to be the only applicable option, and after ``my_dict['`` we usually + expect a completion with an existing dictionary key. + + If you want to disable this heuristic and see completions from all matchers, + set ``IPCompleter.suppress_competing_matchers = False``. + To disable the heuristic for specific matchers provide a dictionary mapping: + ``IPCompleter.suppress_competing_matchers = {'IPCompleter.dict_key_matcher': False}``. + + Set ``IPCompleter.suppress_competing_matchers = True`` to limit + completions to the set of matchers with the highest priority; + this is equivalent to ``IPCompleter.merge_completions`` and + can be beneficial for performance, but will sometimes omit relevant + candidates from matchers further down the priority list. + """, + ).tag(config=True) - merge_completions = Bool(True, + merge_completions = Bool( + True, help="""Whether to merge completion results into a single list If False, only the completion results from the first non-empty completer will be returned. - """ + + As of version 8.5.0, setting the value to ``False`` is an alias for: + ``IPCompleter.suppress_competing_matchers = True.``. + """, + ).tag(config=True) + + disable_matchers = ListTrait( + Unicode(), help="""List of matchers to disable.""" ).tag(config=True) - omit__names = Enum((0,1,2), default_value=2, + + omit__names = Enum( + (0, 1, 2), + default_value=2, help="""Instruct the completer to omit private method names Specifically, when completing on ``object.``. @@ -1144,7 +1408,7 @@ def __init__( namespace=namespace, global_namespace=global_namespace, config=config, - **kwargs + **kwargs, ) # List where completion matches will be stored @@ -1173,8 +1437,8 @@ def __init__( #= re.compile(r'[\s|\[]*(\w+)(?:\s*=?\s*.*)') self.magic_arg_matchers = [ - self.magic_config_matches, - self.magic_color_matches, + self.magic_config_matcher, + self.magic_color_matcher, ] # This is set externally by InteractiveShell @@ -1186,27 +1450,53 @@ def __init__( # attribute through the `@unicode_names` property. self._unicode_names = None + self._backslash_combining_matchers = [ + self.latex_name_matcher, + self.unicode_name_matcher, + back_latex_name_matcher, + back_unicode_name_matcher, + self.fwd_unicode_matcher, + ] + + if not self.backslash_combining_completions: + for matcher in self._backslash_combining_matchers: + self.disable_matchers.append(matcher.matcher_identifier) + + if not self.merge_completions: + self.suppress_competing_matchers = True + + if self.dict_keys_only: + self.disable_matchers.append(self.dict_key_matcher.matcher_identifier) + @property - def matchers(self) -> List[Any]: + def matchers(self) -> List[Matcher]: """All active matcher routines for completion""" if self.dict_keys_only: - return [self.dict_key_matches] + return [self.dict_key_matcher] if self.use_jedi: return [ *self.custom_matchers, - self.dict_key_matches, - self.file_matches, - self.magic_matches, + *self._backslash_combining_matchers, + *self.magic_arg_matchers, + self.custom_completer_matcher, + self.magic_matcher, + self._jedi_matcher, + self.dict_key_matcher, + self.file_matcher, ] else: return [ *self.custom_matchers, - self.dict_key_matches, + *self._backslash_combining_matchers, + *self.magic_arg_matchers, + self.custom_completer_matcher, + self.dict_key_matcher, + # TODO: convert python_matches to v2 API + self.magic_matcher, self.python_matches, - self.file_matches, - self.magic_matches, - self.python_func_kw_matches, + self.file_matcher, + self.python_func_kw_matcher, ] def all_completions(self, text:str) -> List[str]: @@ -1227,7 +1517,14 @@ def _clean_glob_win32(self, text:str): return [f.replace("\\","/") for f in self.glob("%s*" % text)] - def file_matches(self, text:str)->List[str]: + @context_matcher() + def file_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + matches = self.file_matches(context.token) + # TODO: add a heuristic for suppressing (e.g. if it has OS-specific delimiter, + # starts with `/home/`, `C:\`, etc) + return _convert_matcher_v1_result_to_v2(matches, type="path") + + def file_matches(self, text: str) -> List[str]: """Match filenames, expanding ~USER type strings. Most of the seemingly convoluted logic in this completer is an @@ -1309,7 +1606,16 @@ def file_matches(self, text:str)->List[str]: # Mark directories in input list by appending '/' to their names. return [x+'/' if os.path.isdir(x) else x for x in matches] - def magic_matches(self, text:str): + @context_matcher() + def magic_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + text = context.token + matches = self.magic_matches(text) + result = _convert_matcher_v1_result_to_v2(matches, type="magic") + is_magic_prefix = len(text) > 0 and text[0] == "%" + result["suppress_others"] = is_magic_prefix and bool(result["completions"]) + return result + + def magic_matches(self, text: str): """Match magics""" # Get all shell magics now rather than statically, so magics loaded at # runtime show up too. @@ -1351,8 +1657,14 @@ def matches(magic): return comp - def magic_config_matches(self, text:str) -> List[str]: - """ Match class names and attributes for %config magic """ + @context_matcher() + def magic_config_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + # NOTE: uses `line_buffer` equivalent for compatibility + matches = self.magic_config_matches(context.line_with_cursor) + return _convert_matcher_v1_result_to_v2(matches, type="param") + + def magic_config_matches(self, text: str) -> List[str]: + """Match class names and attributes for %config magic""" texts = text.strip().split() if len(texts) > 0 and (texts[0] == 'config' or texts[0] == '%config'): @@ -1386,8 +1698,14 @@ def magic_config_matches(self, text:str) -> List[str]: if attr.startswith(texts[1]) ] return [] - def magic_color_matches(self, text:str) -> List[str] : - """ Match color schemes for %colors magic""" + @context_matcher() + def magic_color_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + # NOTE: uses `line_buffer` equivalent for compatibility + matches = self.magic_color_matches(context.line_with_cursor) + return _convert_matcher_v1_result_to_v2(matches, type="param") + + def magic_color_matches(self, text: str) -> List[str]: + """Match color schemes for %colors magic""" texts = text.split() if text.endswith(' '): # .split() strips off the trailing whitespace. Add '' back @@ -1400,9 +1718,24 @@ def magic_color_matches(self, text:str) -> List[str] : if color.startswith(prefix) ] return [] - def _jedi_matches(self, cursor_column:int, cursor_line:int, text:str) -> Iterable[Any]: + @context_matcher(identifier="IPCompleter.jedi_matcher") + def _jedi_matcher(self, context: CompletionContext) -> _JediMatcherResult: + matches = self._jedi_matches( + cursor_column=context.cursor_position, + cursor_line=context.cursor_line, + text=context.full_text, + ) + return { + "completions": matches, + # statis analysis should not suppress other matchers + "suppress_others": False, + } + + def _jedi_matches( + self, cursor_column: int, cursor_line: int, text: str + ) -> Iterable[_JediCompletionLike]: """ - Return a list of :any:`jedi.api.Completions` object from a ``text`` and + Return a list of :any:`jedi.api.Completion`s object from a ``text`` and cursor position. Parameters @@ -1554,6 +1887,11 @@ def _default_arguments(self, obj): return list(set(ret)) + @context_matcher() + def python_func_kw_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + matches = self.python_func_kw_matches(context.token) + return _convert_matcher_v1_result_to_v2(matches, type="param") + def python_func_kw_matches(self, text): """Match named parameters (kwargs) of the last open function""" @@ -1650,9 +1988,18 @@ def _get_keys(obj: Any) -> List[Any]: return obj.dtype.names or [] return [] - def dict_key_matches(self, text:str) -> List[str]: - "Match string keys in a dictionary, after e.g. 'foo[' " + @context_matcher() + def dict_key_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + matches = self.dict_key_matches(context.token) + return _convert_matcher_v1_result_to_v2( + matches, type="dict key", suppress_if_matches=True + ) + + def dict_key_matches(self, text: str) -> List[str]: + """Match string keys in a dictionary, after e.g. ``foo[``. + DEPRECATED: Deprecated since 8.5. Use ``dict_key_matcher`` instead. + """ if self.__dict_key_regexps is not None: regexps = self.__dict_key_regexps @@ -1754,8 +2101,15 @@ def dict_key_matches(self, text:str) -> List[str]: return [leading + k + suf for k in matches] + @context_matcher() + def unicode_name_matcher(self, context): + fragment, matches = self.unicode_name_matches(context.token) + return _convert_matcher_v1_result_to_v2( + matches, type="unicode", fragment=fragment, suppress_if_matches=True + ) + @staticmethod - def unicode_name_matches(text:str) -> Tuple[str, List[str]] : + def unicode_name_matches(text: str) -> Tuple[str, List[str]]: """Match Latex-like syntax for unicode characters base on the name of the character. @@ -1776,8 +2130,14 @@ def unicode_name_matches(text:str) -> Tuple[str, List[str]] : pass return '', [] + @context_matcher() + def latex_name_matcher(self, context): + fragment, matches = self.latex_matches(context.token) + return _convert_matcher_v1_result_to_v2( + matches, type="latex", fragment=fragment, suppress_if_matches=True + ) - def latex_matches(self, text:str) -> Tuple[str, Sequence[str]]: + def latex_matches(self, text: str) -> Tuple[str, Sequence[str]]: """Match Latex syntax for unicode characters. This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α`` @@ -1797,6 +2157,15 @@ def latex_matches(self, text:str) -> Tuple[str, Sequence[str]]: return s, matches return '', () + @context_matcher() + def custom_completer_matcher(self, context): + matches = self.dispatch_custom_completer(context.token) or [] + result = _convert_matcher_v1_result_to_v2( + matches, type="", suppress_if_matches=True + ) + result["ordered"] = True + return result + def dispatch_custom_completer(self, text): if not self.custom_completers: return @@ -1951,12 +2320,25 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com """ deadline = time.monotonic() + _timeout - before = full_text[:offset] cursor_line, cursor_column = position_to_cursor(full_text, offset) - matched_text, matches, matches_origin, jedi_matches = self._complete( - full_text=full_text, cursor_line=cursor_line, cursor_pos=cursor_column) + jedi_matcher_id = _get_matcher_id(self._jedi_matcher) + + results = self._complete( + full_text=full_text, cursor_line=cursor_line, cursor_pos=cursor_column + ) + non_jedi_results: Dict[str, SimpleMatcherResult] = { + identifier: result + for identifier, result in results.items() + if identifier != jedi_matcher_id + } + + jedi_matches = ( + cast(results[jedi_matcher_id], _JediMatcherResult)["completions"] + if jedi_matcher_id in results + else () + ) iter_jm = iter(jedi_matches) if _timeout: @@ -1984,27 +2366,55 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com for jm in iter_jm: delta = len(jm.name_with_symbols) - len(jm.complete) - yield Completion(start=offset - delta, - end=offset, - text=jm.name_with_symbols, - type='', # don't compute type for speed - _origin='jedi', - signature='') - - - start_offset = before.rfind(matched_text) + yield Completion( + start=offset - delta, + end=offset, + text=jm.name_with_symbols, + type=_UNKNOWN_TYPE, # don't compute type for speed + _origin="jedi", + signature="", + ) # TODO: # Suppress this, right now just for debug. - if jedi_matches and matches and self.debug: - yield Completion(start=start_offset, end=offset, text='--jedi/ipython--', - _origin='debug', type='none', signature='') + if jedi_matches and non_jedi_results and self.debug: + some_start_offset = before.rfind( + next(iter(non_jedi_results.values()))["matched_fragment"] + ) + yield Completion( + start=some_start_offset, + end=offset, + text="--jedi/ipython--", + _origin="debug", + type="none", + signature="", + ) - # I'm unsure if this is always true, so let's assert and see if it - # crash - assert before.endswith(matched_text) - for m, t in zip(matches, matches_origin): - yield Completion(start=start_offset, end=offset, text=m, _origin=t, signature='', type='') + ordered = [] + sortable = [] + + for origin, result in non_jedi_results.items(): + matched_text = result["matched_fragment"] + start_offset = before.rfind(matched_text) + is_ordered = result.get("ordered", False) + container = ordered if is_ordered else sortable + + # I'm unsure if this is always true, so let's assert and see if it + # crash + assert before.endswith(matched_text) + + for simple_completion in result["completions"]: + completion = Completion( + start=start_offset, + end=offset, + text=simple_completion.text, + _origin=origin, + signature="", + type=simple_completion.type or _UNKNOWN_TYPE, + ) + container.append(completion) + + yield from self._deduplicate(ordered + self._sort(sortable)) def complete(self, text=None, line_buffer=None, cursor_pos=None) -> Tuple[str, Sequence[str]]: @@ -2046,7 +2456,54 @@ def complete(self, text=None, line_buffer=None, cursor_pos=None) -> Tuple[str, S PendingDeprecationWarning) # potential todo, FOLD the 3rd throw away argument of _complete # into the first 2 one. - return self._complete(line_buffer=line_buffer, cursor_pos=cursor_pos, text=text, cursor_line=0)[:2] + # TODO: Q: does the above refer to jedi completions (i.e. 0-indexed?) + # TODO: should we deprecate now, or does it stay? + + results = self._complete( + line_buffer=line_buffer, cursor_pos=cursor_pos, text=text, cursor_line=0 + ) + + jedi_matcher_id = _get_matcher_id(self._jedi_matcher) + + return self._arrange_and_extract( + results, + # TODO: can we confirm that excluding Jedi here was a deliberate choice in previous version? + skip_matchers={jedi_matcher_id}, + # this API does not support different start/end positions (fragments of token). + abort_if_offset_changes=True, + ) + + def _arrange_and_extract( + self, + results: Dict[str, MatcherResult], + skip_matchers: Set[str], + abort_if_offset_changes: bool, + ): + + sortable = [] + ordered = [] + most_recent_fragment = None + for identifier, result in results.items(): + if identifier in skip_matchers: + continue + if not most_recent_fragment: + most_recent_fragment = result["matched_fragment"] + if ( + abort_if_offset_changes + and result["matched_fragment"] != most_recent_fragment + ): + break + if result.get("ordered", False): + ordered.extend(result["completions"]) + else: + sortable.extend(result["completions"]) + + if not most_recent_fragment: + most_recent_fragment = "" # to satisfy typechecker (and just in case) + + return most_recent_fragment, [ + m.text for m in self._deduplicate(ordered + self._sort(sortable)) + ] def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, full_text=None) -> _CompleteResult: @@ -2081,14 +2538,10 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, Returns ------- - A tuple of N elements which are (likely): - matched_text: ? the text that the complete matched - matches: list of completions ? - matches_origin: ? list same length as matches, and where each completion came from - jedi_matches: list of Jedi matches, have it's own structure. + An ordered dictionary where keys are identifiers of completion + matchers and values are ``MatcherResult``s. """ - # if the cursor position isn't given, the only sane assumption we can # make is that it's at the end of the line (the common case) if cursor_pos is None: @@ -2100,93 +2553,131 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, # if text is either None or an empty string, rely on the line buffer if (not line_buffer) and full_text: line_buffer = full_text.split('\n')[cursor_line] - if not text: # issue #11508: check line_buffer before calling split_line - text = self.splitter.split_line(line_buffer, cursor_pos) if line_buffer else '' - - if self.backslash_combining_completions: - # allow deactivation of these on windows. - base_text = text if not line_buffer else line_buffer[:cursor_pos] - - for meth in (self.latex_matches, - self.unicode_name_matches, - back_latex_name_matches, - back_unicode_name_matches, - self.fwd_unicode_match): - name_text, name_matches = meth(base_text) - if name_text: - return _CompleteResult(name_text, name_matches[:MATCHES_LIMIT], \ - [meth.__qualname__]*min(len(name_matches), MATCHES_LIMIT), ()) - + if not text: # issue #11508: check line_buffer before calling split_line + text = ( + self.splitter.split_line(line_buffer, cursor_pos) if line_buffer else "" + ) # If no line buffer is given, assume the input text is all there was if line_buffer is None: line_buffer = text + # deprecated - do not use `line_buffer` in new code. self.line_buffer = line_buffer self.text_until_cursor = self.line_buffer[:cursor_pos] - # Do magic arg matches - for matcher in self.magic_arg_matchers: - matches = list(matcher(line_buffer))[:MATCHES_LIMIT] - if matches: - origins = [matcher.__qualname__] * len(matches) - return _CompleteResult(text, matches, origins, ()) + if not full_text: + full_text = line_buffer + + context = CompletionContext( + full_text=full_text, + cursor_position=cursor_pos, + cursor_line=cursor_line, + token=text, + ) # Start with a clean slate of completions - matches = [] + results = {} - # FIXME: we should extend our api to return a dict with completions for - # different types of objects. The rlcomplete() method could then - # simply collapse the dict into a list for readline, but we'd have - # richer completion semantics in other environments. - is_magic_prefix = len(text) > 0 and text[0] == "%" - completions: Iterable[Any] = [] - if self.use_jedi and not is_magic_prefix: - if not full_text: - full_text = line_buffer - completions = self._jedi_matches( - cursor_pos, cursor_line, full_text) - - if self.merge_completions: - matches = [] - for matcher in self.matchers: - try: - matches.extend([(m, matcher.__qualname__) - for m in matcher(text)]) - except: - # Show the ugly traceback if the matcher causes an - # exception, but do NOT crash the kernel! - sys.excepthook(*sys.exc_info()) - else: - for matcher in self.matchers: - matches = [(m, matcher.__qualname__) - for m in matcher(text)] - if matches: - break - - seen = set() - filtered_matches = set() - for m in matches: - t, c = m - if t not in seen: - filtered_matches.add(m) - seen.add(t) + custom_completer_matcher_id = _get_matcher_id(self.custom_completer_matcher) + jedi_matcher_id = _get_matcher_id(self._jedi_matcher) - _filtered_matches = sorted(filtered_matches, key=lambda x: completions_sorting_key(x[0])) + for matcher in self.matchers: + api_version = _get_matcher_api_version(matcher) + matcher_id = _get_matcher_id(matcher) - custom_res = [(m, 'custom') for m in self.dispatch_custom_completer(text) or []] - - _filtered_matches = custom_res or _filtered_matches - - _filtered_matches = _filtered_matches[:MATCHES_LIMIT] - _matches = [m[0] for m in _filtered_matches] - origins = [m[1] for m in _filtered_matches] + if matcher_id in results: + warnings.warn(f"Duplicate matcher ID: {matcher_id}.") - self.matches = _matches + try: + if api_version == 1: + result = _convert_matcher_v1_result_to_v2( + matcher(text), type=_UNKNOWN_TYPE + ) + elif api_version == 2: + # TODO: MATCHES_LIMIT was used inconsistently in previous version + # (applied individually to latex/unicode and magic arguments matcher, + # but not Jedi, paths, magics, etc). Jedi did not have a limit here at + # all, but others had a total limit (retained in `_deduplicate_and_sort`). + # 1) Was that deliberate or an omission? + # 2) Should we include the limit in the API v2 signature to allow + # more expensive matchers to return early? + result = cast(matcher, MatcherAPIv2)(context) + else: + raise ValueError(f"Unsupported API version {api_version}") + except: + # Show the ugly traceback if the matcher causes an + # exception, but do NOT crash the kernel! + sys.excepthook(*sys.exc_info()) + continue - return _CompleteResult(text, _matches, origins, completions) - - def fwd_unicode_match(self, text:str) -> Tuple[str, Sequence[str]]: + # set default value for matched fragment if suffix was not selected. + result["matched_fragment"] = result.get("matched_fragment", context.token) + + suppression_recommended = result.get("suppress_others", False) + + should_suppress = ( + self.suppress_competing_matchers is True + or suppression_recommended + or ( + isinstance(self.suppress_competing_matchers, dict) + and self.suppress_competing_matchers[matcher_id] + ) + ) and len(result["completions"]) + + if should_suppress: + new_results = {matcher_id: result} + if ( + matcher_id == custom_completer_matcher_id + and jedi_matcher_id in results + ): + # custom completer does not suppress Jedi (this may change in future versions). + new_results[jedi_matcher_id] = results[jedi_matcher_id] + results = new_results + break + + results[matcher_id] = result + + _, matches = self._arrange_and_extract( + results, + # TODO Jedi completions non included in legacy stateful API; was this deliberate or omission? + # if it was omission, we can remove the filtering step, otherwise remove this comment. + skip_matchers={jedi_matcher_id}, + abort_if_offset_changes=False, + ) + + # populate legacy stateful API + self.matches = matches + + return results + + @staticmethod + def _deduplicate( + matches: Sequence[SimpleCompletion], + ) -> Iterable[SimpleCompletion]: + filtered_matches = {} + for match in matches: + text = match.text + if ( + text not in filtered_matches + or filtered_matches[text].type == _UNKNOWN_TYPE + ): + filtered_matches[text] = match + + return filtered_matches.values() + + @staticmethod + def _sort(matches: Sequence[SimpleCompletion]): + return sorted(matches, key=lambda x: completions_sorting_key(x.text)) + + @context_matcher() + def fwd_unicode_matcher(self, context): + fragment, matches = self.latex_matches(context.token) + return _convert_matcher_v1_result_to_v2( + matches, type="unicode", fragment=fragment, suppress_if_matches=True + ) + + def fwd_unicode_match(self, text: str) -> Tuple[str, Sequence[str]]: """ Forward match a string starting with a backslash with a list of potential Unicode completions. diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 746a1e68261..2643816cceb 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -298,7 +298,7 @@ def test_back_unicode_completion(self): ip = get_ipython() name, matches = ip.complete("\\Ⅴ") - self.assertEqual(matches, ("\\ROMAN NUMERAL FIVE",)) + self.assertEqual(matches, ["\\ROMAN NUMERAL FIVE"]) def test_forward_unicode_completion(self): ip = get_ipython() @@ -379,6 +379,12 @@ def test_local_file_completions(self): def test_quoted_file_completions(self): ip = get_ipython() + + def _(text): + return ip.Completer._complete( + cursor_line=0, cursor_pos=len(text), full_text=text + )["IPCompleter.file_matcher"]["completions"] + with TemporaryWorkingDirectory(): name = "foo'bar" open(name, "w", encoding="utf-8").close() @@ -387,25 +393,16 @@ def test_quoted_file_completions(self): escaped = name if sys.platform == "win32" else "foo\\'bar" # Single quote matches embedded single quote - text = "open('foo" - c = ip.Completer._complete( - cursor_line=0, cursor_pos=len(text), full_text=text - )[1] - self.assertEqual(c, [escaped]) + c = _("open('foo")[0] + self.assertEqual(c.text, escaped) # Double quote requires no escape - text = 'open("foo' - c = ip.Completer._complete( - cursor_line=0, cursor_pos=len(text), full_text=text - )[1] - self.assertEqual(c, [name]) + c = _('open("foo')[0] + self.assertEqual(c.text, name) # No quote requires an escape - text = "%ls foo" - c = ip.Completer._complete( - cursor_line=0, cursor_pos=len(text), full_text=text - )[1] - self.assertEqual(c, [escaped]) + c = _("%ls foo")[0] + self.assertEqual(c.text, escaped) def test_all_completions_dups(self): """ @@ -475,6 +472,17 @@ def test_completion_have_signature(self): "encoding" in c.signature ), "Signature of function was not found by completer" + def test_completions_have_type(self): + """ + Lets make sure matchers provide completion type. + """ + ip = get_ipython() + with provisionalcompleter(): + ip.Completer.use_jedi = False + completions = ip.Completer.completions("%tim", 3) + c = next(completions) # should be `%time` or similar + assert c.type == "magic", "Type of magic was not assigned by completer" + @pytest.mark.xfail(reason="Known failure on jedi<=0.18.0") def test_deduplicate_completions(self): """ From 728bad98401fa6531483879f97a166f07e9d6570 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 5 Sep 2022 07:59:33 +0100 Subject: [PATCH 0207/1752] Shim TypedDict and NotRequired at runtime until the lowest supported Python version catches up. --- IPython/core/completer.py | 4 +++- setup.cfg | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 4d51ee45825..1e04800e8d3 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -167,7 +167,6 @@ TYPE_CHECKING, Set, ) -from typing_extensions import TypedDict, NotRequired from IPython.core.error import TryNext from IPython.core.inputtransformer2 import ESC_MAGIC @@ -208,11 +207,14 @@ if TYPE_CHECKING: from typing import cast + from typing_extensions import TypedDict, NotRequired else: def cast(obj, _type): return obj + TypedDict = Dict + NotRequired = Tuple # ----------------------------------------------------------------------------- # Globals diff --git a/setup.cfg b/setup.cfg index 0e371836877..8d5136d73a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -74,6 +74,7 @@ test_extra = numpy>=1.19 pandas trio + typing_extensions all = %(black)s %(doc)s From 93c8b4d5380d861bf77c590660b93e495bef893b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 5 Sep 2022 08:25:48 +0100 Subject: [PATCH 0208/1752] Update ipdoctest test --- IPython/core/completer.py | 2 ++ IPython/core/magics/config.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 1e04800e8d3..d8fecbbf298 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -101,6 +101,8 @@ Be sure to update :any:`jedi` to the latest stable version or to try the current development version to get better completions. +.. _Matchers: + Matchers ======== diff --git a/IPython/core/magics/config.py b/IPython/core/magics/config.py index c1387b601b8..efa992bbb79 100644 --- a/IPython/core/magics/config.py +++ b/IPython/core/magics/config.py @@ -80,6 +80,9 @@ def config(self, s): Enable debug for the Completer. Mostly print extra information for experimental jedi integration. Current: False + IPCompleter.disable_matchers=... + List of matchers to disable. + Current: [] IPCompleter.greedy= Activate greedy completion PENDING DEPRECATION. this is now mostly taken care of with Jedi. @@ -102,6 +105,8 @@ def config(self, s): Whether to merge completion results into a single list If False, only the completion results from the first non-empty completer will be returned. + As of version 8.5.0, setting the value to ``False`` is an alias for: + ``IPCompleter.suppress_competing_matchers = True.``. Current: True IPCompleter.omit__names= Instruct the completer to omit private method names @@ -117,6 +122,24 @@ def config(self, s): IPCompleter.profiler_output_dir= Template for path at which to output profile data for completions. Current: '.completion_profiles' + IPCompleter.suppress_competing_matchers= + Whether to suppress completions from other `Matchers`_. + When set to ``None`` (default) the matchers will attempt to auto-detect + whether suppression of other matchers is desirable. For example, at the + beginning of a line followed by `%` we expect a magic completion to be the + only applicable option, and after ``my_dict['`` we usually expect a + completion with an existing dictionary key. + If you want to disable this heuristic and see completions from all matchers, + set ``IPCompleter.suppress_competing_matchers = False``. To disable the + heuristic for specific matchers provide a dictionary mapping: + ``IPCompleter.suppress_competing_matchers = {'IPCompleter.dict_key_matcher': + False}``. + Set ``IPCompleter.suppress_competing_matchers = True`` to limit completions + to the set of matchers with the highest priority; this is equivalent to + ``IPCompleter.merge_completions`` and can be beneficial for + performance, but will sometimes omit relevant candidates from matchers + further down the priority list. + Current: False IPCompleter.use_jedi= Experimental: Use Jedi to generate autocompletions. Default to True if jedi is installed. From 311a3a0fd25d09cc4d314f900e31ee6ac2cfbba4 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 6 Sep 2022 11:02:43 +0200 Subject: [PATCH 0209/1752] release 8.5.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 1d6483ce50e..18aaa7f9765 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 84af48129c748341ce0b73cb42d310fc7966a51a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 6 Sep 2022 11:03:20 +0200 Subject: [PATCH 0210/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index 18aaa7f9765..a7e48a17b83 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 5 +_version_minor = 6 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 95de1fe40d1baa7e13c9635c335ca0b87cf5dad7 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 6 Sep 2022 21:19:39 +0100 Subject: [PATCH 0211/1752] Move documentation requirements to setup.cfg --- docs/requirements.txt | 10 +--------- setup.cfg | 10 +++++++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 587288c2a0f..add92ee1e12 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1 @@ --e .[test] -ipykernel -setuptools>=18.5 -sphinx -sphinx-rtd-theme -docrepr -matplotlib -stack_data -pytest<7 +-e .[doc] diff --git a/setup.cfg b/setup.cfg index 274e338d0ce..6004f3a4d3d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,15 @@ install_requires = black = black doc = - Sphinx>=1.3 + ipykernel + setuptools>=18.5 + sphinx>=1.3 + sphinx-rtd-theme + docrepr + matplotlib + stack_data + pytest<7 + %(test)s kernel = ipykernel nbconvert = From 4ab4de3bbf08805262e4b67957faf2e2c588e382 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 6 Sep 2022 21:27:27 +0100 Subject: [PATCH 0212/1752] Update README to use optional dependency over requirements.txt --- docs/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.rst b/docs/README.rst index ebdb17107e0..4bfc5b18ccc 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -26,7 +26,7 @@ the following tools are needed to build the documentation: In a conda environment, or a Python 3 ``venv``, you should be able to run:: cd ipython - pip install -U -r docs/requirements.txt + pip install .[doc] -U Build Commands From cce852948681451002291370b6fc6b64bae0d908 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 7 Sep 2022 03:05:37 +0100 Subject: [PATCH 0213/1752] Implement `priority`, `do_not_suppress`, add tests and docs. --- IPython/core/completer.py | 247 ++++++++++++++++++--------- IPython/core/magics/config.py | 10 +- IPython/core/tests/test_completer.py | 107 ++++++++++++ 3 files changed, 280 insertions(+), 84 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index d8fecbbf298..f15fabb7d7e 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -101,12 +101,10 @@ Be sure to update :any:`jedi` to the latest stable version or to try the current development version to get better completions. -.. _Matchers: - Matchers ======== -All completions routines are implemented using unified ``matchers`` API. +All completions routines are implemented using unified *Matchers* API. The matchers API is provisional and subject to change without notice. The built-in matchers include: @@ -119,13 +117,26 @@ - ``IPCompleter.python_func_kw_matcher`` - function keywords, - ``IPCompleter.python_matches`` - globals and attributes (v1 API), - ``IPCompleter.jedi_matcher`` - static analysis with Jedi, -- ``IPCompleter.custom_completer_matcher`` - pluggable completer with a default implementation in ``core.InteractiveShell`` - which uses uses IPython hooks system (`complete_command`) with string dispatch (including regular expressions). - Differently to other matchers, ``custom_completer_matcher`` will not suppress Jedi results to match - behaviour in earlier IPython versions. +- ``IPCompleter.custom_completer_matcher`` - pluggable completer with a default implementation in any:`core.InteractiveShell` + which uses uses IPython hooks system (`complete_command`) with string dispatch (including regular expressions). + Differently to other matchers, ``custom_completer_matcher`` will not suppress Jedi results to match + behaviour in earlier IPython versions. + +Custom matchers can be added by appending to ``IPCompleter.custom_matchers`` list. + +Suppression of competing matchers +--------------------------------- + +By default results from all matchers are combined, in the order determined by +their priority. Matchers can request to suppress results from subsequent +matchers by setting ``suppress`` to ``True`` in the ``MatcherResult``. -Adding custom matchers is possible by appending to `IPCompleter.custom_matchers` list, -but please be aware that this API is subject to change. +When multiple matchers simultaneously request surpression, the results from of +the matcher with higher priority will be returned. + +Sometimes it is desirable to suppress most but not all other matchers; +this can be achieved by adding a list of identifiers of matchers which +should not be suppressed to ``MatcherResult`` under ``do_not_suppress`` key. """ @@ -231,7 +242,7 @@ def cast(obj, _type): _UNICODE_RANGES = [(32, 0x3134b), (0xe0001, 0xe01f0)] # Public API -__all__ = ['Completer','IPCompleter'] +__all__ = ["Completer", "IPCompleter"] if sys.platform == 'win32': PROTECTABLES = ' ' @@ -427,7 +438,7 @@ def __repr__(self): class Completion: """ - Completion object used and return by IPython completers. + Completion object used and returned by IPython completers. .. warning:: @@ -488,12 +499,19 @@ def __hash__(self): class SimpleCompletion: - # TODO: decide whether we should keep the ``SimpleCompletion`` separate from ``Completion`` - # there are two advantages of keeping them separate: - # - compatibility with old readline `Completer.complete` interface (less important) - # - ease of use for third parties (just return matched text and don't worry about coordinates) - # the disadvantage is that we need to loop over the completions again to transform them into - # `Completion` objects (but it was done like that before the refactor into `SimpleCompletion` too). + """Completion item to be included in the dictionary returned by new-style Matcher (API v2). + + .. warning:: + + Provisional + + This class is used to describe the currently supported attributes of + simple completion items, and any additional implementation details + should not be relied on. Additional attributes may be included in + future versions, and meaning of text disambiguated from the current + dual meaning of "text to insert" and "text to used as a label". + """ + __slots__ = ["text", "type"] def __init__(self, text: str, *, type: str = None): @@ -504,30 +522,31 @@ def __repr__(self): return f"" -class _MatcherResultBase(TypedDict): +class MatcherResultBase(TypedDict): + """Definition of dictionary to be returned by new-style Matcher (API v2).""" #: suffix of the provided ``CompletionContext.token``, if not given defaults to full token. matched_fragment: NotRequired[str] - #: whether to suppress results from other matchers; default is False. - suppress_others: NotRequired[bool] + #: whether to suppress results from all other matchers (True), some + #: matchers (set of identifiers) or none (False); default is False. + suppress: NotRequired[Union[bool, Set[str]]] + + #: identifiers of matchers which should NOT be suppressed + do_not_suppress: NotRequired[Set[str]] #: are completions already ordered and should be left as-is? default is False. ordered: NotRequired[bool] - # TODO: should we used a relevance score for ordering? - #: value between 0 (likely not relevant) and 100 (likely relevant); default is 50. - # relevance: NotRequired[float] - -class SimpleMatcherResult(_MatcherResultBase): +class SimpleMatcherResult(MatcherResultBase): """Result of new-style completion matcher.""" #: list of candidate completions completions: Sequence[SimpleCompletion] -class _JediMatcherResult(_MatcherResultBase): +class _JediMatcherResult(MatcherResultBase): """Matching result returned by Jedi (will be processed differently)""" #: list of candidate completions @@ -535,6 +554,8 @@ class _JediMatcherResult(_MatcherResultBase): class CompletionContext(NamedTuple): + """Completion context provided as an argument to matchers in the Matcher API v2.""" + # rationale: many legacy matchers relied on completer state (`self.text_until_cursor`) # which was not explicitly visible as an argument of the matcher, making any refactor # prone to errors; by explicitly passing `cursor_position` we can decouple the matchers @@ -546,14 +567,20 @@ class CompletionContext(NamedTuple): #: (by switching the greedy mode). token: str + #: The full available content of the editor or buffer full_text: str - #: Cursor position in the line (the same for ``full_text`` and `text``). + #: Cursor position in the line (the same for ``full_text`` and ``text``). cursor_position: int #: Cursor line in ``full_text``. cursor_line: int + #: The maximum number of completions that will be used downstream. + #: Matchers can use this information to abort early. + #: The built-in Jedi matcher is currently excepted from this limit. + limit: int + @property @lru_cache(maxsize=None) # TODO change to @cache after dropping Python 3.7 def text_until_cursor(self) -> str: @@ -573,7 +600,7 @@ def line_with_cursor(self) -> str: def completion_matcher( - *, priority: float = None, identifier: str = None, api_version=1 + *, priority: float = None, identifier: str = None, api_version: int = 1 ): """Adds attributes describing the matcher. @@ -581,7 +608,7 @@ def completion_matcher( ---------- priority : Optional[float] The priority of the matcher, determines the order of execution of matchers. - Higher priority means that the matcher will be executed first. Defaults to 50. + Higher priority means that the matcher will be executed first. Defaults to 0. identifier : Optional[str] identifier of the matcher allowing users to modify the behaviour via traitlets, and also used to for debugging (will be passed as ``origin`` with the completions). @@ -593,14 +620,23 @@ def completion_matcher( """ def wrapper(func: Matcher): - func.matcher_priority = priority + func.matcher_priority = priority or 0 func.matcher_identifier = identifier or func.__qualname__ func.matcher_api_version = api_version + if TYPE_CHECKING: + if api_version == 1: + func = cast(func, MatcherAPIv1) + elif api_version == 2: + func = cast(func, MatcherAPIv2) return func return wrapper +def _get_matcher_priority(matcher: Matcher): + return getattr(matcher, "matcher_priority", 0) + + def _get_matcher_id(matcher: Matcher): return getattr(matcher, "matcher_identifier", matcher.__qualname__) @@ -1118,6 +1154,10 @@ def _safe_isinstance(obj, module, class_name): @context_matcher() def back_unicode_name_matcher(context): + """Match Unicode characters back to Unicode name + + Same as ``back_unicode_name_matches``, but adopted to new Matcher API. + """ fragment, matches = back_unicode_name_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True @@ -1166,6 +1206,10 @@ def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]: @context_matcher() def back_latex_name_matcher(context): + """Match latex characters back to unicode name + + Same as ``back_latex_name_matches``, but adopted to new Matcher API. + """ fragment, matches = back_latex_name_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="latex", fragment=fragment, suppress_if_matches=True @@ -1263,9 +1307,7 @@ def _convert_matcher_v1_result_to_v2( """Utility to help with transition""" result = { "completions": [SimpleCompletion(text=match, type=type) for match in matches], - "suppress_others": (True if matches else False) - if suppress_if_matches - else False, + "suppress": (True if matches else False) if suppress_if_matches else False, } if fragment is not None: result["matched_fragment"] = fragment @@ -1297,7 +1339,7 @@ def _greedy_changed(self, change): suppress_competing_matchers = UnionTrait( [Bool(), DictTrait(Bool(None, allow_none=True))], help=""" - Whether to suppress completions from other `Matchers`_. + Whether to suppress completions from other *Matchers*. When set to ``None`` (default) the matchers will attempt to auto-detect whether suppression of other matchers is desirable. For example, at @@ -1312,7 +1354,7 @@ def _greedy_changed(self, change): Set ``IPCompleter.suppress_competing_matchers = True`` to limit completions to the set of matchers with the highest priority; - this is equivalent to ``IPCompleter.merge_completions`` and + this is equivalent to ``IPCompleter.merge_completions`` and can be beneficial for performance, but will sometimes omit relevant candidates from matchers further down the priority list. """, @@ -1325,7 +1367,7 @@ def _greedy_changed(self, change): If False, only the completion results from the first non-empty completer will be returned. - As of version 8.5.0, setting the value to ``False`` is an alias for: + As of version 8.6.0, setting the value to ``False`` is an alias for: ``IPCompleter.suppress_competing_matchers = True.``. """, ).tag(config=True) @@ -1469,9 +1511,6 @@ def __init__( if not self.merge_completions: self.suppress_competing_matchers = True - if self.dict_keys_only: - self.disable_matchers.append(self.dict_key_matcher.matcher_identifier) - @property def matchers(self) -> List[Matcher]: """All active matcher routines for completion""" @@ -1523,6 +1562,7 @@ def _clean_glob_win32(self, text:str): @context_matcher() def file_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Same as ``file_matches``, but adopted to new Matcher API.""" matches = self.file_matches(context.token) # TODO: add a heuristic for suppressing (e.g. if it has OS-specific delimiter, # starts with `/home/`, `C:\`, etc) @@ -1540,7 +1580,10 @@ def file_matches(self, text: str) -> List[str]: only the parts after what's already been typed (instead of the full completions, as is normally done). I don't think with the current (as of Python 2.3) Python readline it's possible to do - better.""" + better. + + DEPRECATED: Deprecated since 8.6. Use ``file_matcher`` instead. + """ # chars that require escaping with backslash - i.e. chars # that readline treats incorrectly as delimiters, but we @@ -1616,11 +1659,14 @@ def magic_matcher(self, context: CompletionContext) -> SimpleMatcherResult: matches = self.magic_matches(text) result = _convert_matcher_v1_result_to_v2(matches, type="magic") is_magic_prefix = len(text) > 0 and text[0] == "%" - result["suppress_others"] = is_magic_prefix and bool(result["completions"]) + result["suppress"] = is_magic_prefix and bool(result["completions"]) return result def magic_matches(self, text: str): - """Match magics""" + """Match magics. + + DEPRECATED: Deprecated since 8.6. Use ``magic_matcher`` instead. + """ # Get all shell magics now rather than statically, so magics loaded at # runtime show up too. lsm = self.shell.magics_manager.lsmagic() @@ -1663,12 +1709,16 @@ def matches(magic): @context_matcher() def magic_config_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match class names and attributes for %config magic.""" # NOTE: uses `line_buffer` equivalent for compatibility matches = self.magic_config_matches(context.line_with_cursor) return _convert_matcher_v1_result_to_v2(matches, type="param") def magic_config_matches(self, text: str) -> List[str]: - """Match class names and attributes for %config magic""" + """Match class names and attributes for %config magic. + + DEPRECATED: Deprecated since 8.6. Use ``magic_config_matcher`` instead. + """ texts = text.strip().split() if len(texts) > 0 and (texts[0] == 'config' or texts[0] == '%config'): @@ -1704,12 +1754,16 @@ def magic_config_matches(self, text: str) -> List[str]: @context_matcher() def magic_color_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match color schemes for %colors magic.""" # NOTE: uses `line_buffer` equivalent for compatibility matches = self.magic_color_matches(context.line_with_cursor) return _convert_matcher_v1_result_to_v2(matches, type="param") def magic_color_matches(self, text: str) -> List[str]: - """Match color schemes for %colors magic""" + """Match color schemes for %colors magic. + + DEPRECATED: Deprecated since 8.6. Use ``magic_color_matcher`` instead. + """ texts = text.split() if text.endswith(' '): # .split() strips off the trailing whitespace. Add '' back @@ -1731,8 +1785,8 @@ def _jedi_matcher(self, context: CompletionContext) -> _JediMatcherResult: ) return { "completions": matches, - # statis analysis should not suppress other matchers - "suppress_others": False, + # static analysis should not suppress other matchers + "suppress": False, } def _jedi_matches( @@ -1755,6 +1809,8 @@ def _jedi_matches( ----- If ``IPCompleter.debug`` is ``True`` may return a :any:`_FakeJediCompletion` object containing a string with the Jedi debug information attached. + + DEPRECATED: Deprecated since 8.6. Use ``_jedi_matcher`` instead. """ namespaces = [self.namespace] if self.global_namespace is not None: @@ -1893,11 +1949,15 @@ def _default_arguments(self, obj): @context_matcher() def python_func_kw_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match named parameters (kwargs) of the last open function.""" matches = self.python_func_kw_matches(context.token) return _convert_matcher_v1_result_to_v2(matches, type="param") def python_func_kw_matches(self, text): - """Match named parameters (kwargs) of the last open function""" + """Match named parameters (kwargs) of the last open function. + + DEPRECATED: Deprecated since 8.6. Use ``magic_config_matcher`` instead. + """ if "." in text: # a parameter cannot be dotted return [] @@ -1994,6 +2054,7 @@ def _get_keys(obj: Any) -> List[Any]: @context_matcher() def dict_key_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match string keys in a dictionary, after e.g. ``foo[``.""" matches = self.dict_key_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="dict key", suppress_if_matches=True @@ -2002,7 +2063,7 @@ def dict_key_matcher(self, context: CompletionContext) -> SimpleMatcherResult: def dict_key_matches(self, text: str) -> List[str]: """Match string keys in a dictionary, after e.g. ``foo[``. - DEPRECATED: Deprecated since 8.5. Use ``dict_key_matcher`` instead. + DEPRECATED: Deprecated since 8.6. Use `dict_key_matcher` instead. """ if self.__dict_key_regexps is not None: @@ -2136,6 +2197,10 @@ def unicode_name_matches(text: str) -> Tuple[str, List[str]]: @context_matcher() def latex_name_matcher(self, context): + """Match Latex syntax for unicode characters. + + This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α`` + """ fragment, matches = self.latex_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="latex", fragment=fragment, suppress_if_matches=True @@ -2145,6 +2210,8 @@ def latex_matches(self, text: str) -> Tuple[str, Sequence[str]]: """Match Latex syntax for unicode characters. This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α`` + + DEPRECATED: Deprecated since 8.6. Use `latex_matcher` instead. """ slashpos = text.rfind('\\') if slashpos > -1: @@ -2168,9 +2235,13 @@ def custom_completer_matcher(self, context): matches, type="", suppress_if_matches=True ) result["ordered"] = True + result["do_not_suppress"] = {_get_matcher_id(self._jedi_matcher)} return result def dispatch_custom_completer(self, text): + """ + DEPRECATED: Deprecated since 8.6. Use `custom_completer_matcher` instead. + """ if not self.custom_completers: return @@ -2418,8 +2489,9 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com ) container.append(completion) - yield from self._deduplicate(ordered + self._sort(sortable)) - + yield from list(self._deduplicate(ordered + self._sort(sortable)))[ + :MATCHES_LIMIT + ] def complete(self, text=None, line_buffer=None, cursor_pos=None) -> Tuple[str, Sequence[str]]: """Find completions for the given text and line context. @@ -2490,6 +2562,8 @@ def _arrange_and_extract( for identifier, result in results.items(): if identifier in skip_matchers: continue + if not result["completions"]: + continue if not most_recent_fragment: most_recent_fragment = result["matched_fragment"] if ( @@ -2578,34 +2652,42 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, cursor_position=cursor_pos, cursor_line=cursor_line, token=text, + limit=MATCHES_LIMIT, ) # Start with a clean slate of completions results = {} - custom_completer_matcher_id = _get_matcher_id(self.custom_completer_matcher) jedi_matcher_id = _get_matcher_id(self._jedi_matcher) - for matcher in self.matchers: + suppressed_matchers = set() + + matchers = { + _get_matcher_id(matcher): matcher + for matcher in sorted( + self.matchers, key=_get_matcher_priority, reverse=True + ) + } + + for matcher_id, matcher in matchers.items(): api_version = _get_matcher_api_version(matcher) matcher_id = _get_matcher_id(matcher) + if matcher_id in self.disable_matchers: + continue + if matcher_id in results: warnings.warn(f"Duplicate matcher ID: {matcher_id}.") + if matcher_id in suppressed_matchers: + continue + try: if api_version == 1: result = _convert_matcher_v1_result_to_v2( matcher(text), type=_UNKNOWN_TYPE ) elif api_version == 2: - # TODO: MATCHES_LIMIT was used inconsistently in previous version - # (applied individually to latex/unicode and magic arguments matcher, - # but not Jedi, paths, magics, etc). Jedi did not have a limit here at - # all, but others had a total limit (retained in `_deduplicate_and_sort`). - # 1) Was that deliberate or an omission? - # 2) Should we include the limit in the API v2 signature to allow - # more expensive matchers to return early? result = cast(matcher, MatcherAPIv2)(context) else: raise ValueError(f"Unsupported API version {api_version}") @@ -2618,27 +2700,31 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, # set default value for matched fragment if suffix was not selected. result["matched_fragment"] = result.get("matched_fragment", context.token) - suppression_recommended = result.get("suppress_others", False) + if not suppressed_matchers: + suppression_recommended = result.get("suppress", False) - should_suppress = ( - self.suppress_competing_matchers is True - or suppression_recommended - or ( - isinstance(self.suppress_competing_matchers, dict) - and self.suppress_competing_matchers[matcher_id] - ) - ) and len(result["completions"]) - - if should_suppress: - new_results = {matcher_id: result} - if ( - matcher_id == custom_completer_matcher_id - and jedi_matcher_id in results - ): - # custom completer does not suppress Jedi (this may change in future versions). - new_results[jedi_matcher_id] = results[jedi_matcher_id] - results = new_results - break + should_suppress = ( + self.suppress_competing_matchers is True + or suppression_recommended + or ( + isinstance(self.suppress_competing_matchers, dict) + and self.suppress_competing_matchers[matcher_id] + ) + ) and len(result["completions"]) + + if should_suppress: + suppression_exceptions = result.get("do_not_suppress", set()) + try: + to_suppress = set(suppression_recommended) + except TypeError: + to_suppress = set(matchers) + suppressed_matchers = to_suppress - suppression_exceptions + + new_results = {} + for previous_matcher_id, previous_result in results.items(): + if previous_matcher_id not in suppressed_matchers: + new_results[previous_matcher_id] = previous_result + results = new_results results[matcher_id] = result @@ -2676,6 +2762,7 @@ def _sort(matches: Sequence[SimpleCompletion]): @context_matcher() def fwd_unicode_matcher(self, context): + """Same as ``fwd_unicode_match``, but adopted to new Matcher API.""" fragment, matches = self.latex_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True @@ -2693,6 +2780,8 @@ def fwd_unicode_match(self, text: str) -> Tuple[str, Sequence[str]]: At tuple with: - matched text (empty if no matches) - list of potential completions, empty tuple otherwise) + + DEPRECATED: Deprecated since 8.6. Use `fwd_unicode_matcher` instead. """ # TODO: self.unicode_names is here a list we traverse each time with ~100k elements. # We could do a faster match using a Trie. diff --git a/IPython/core/magics/config.py b/IPython/core/magics/config.py index efa992bbb79..f93561aa622 100644 --- a/IPython/core/magics/config.py +++ b/IPython/core/magics/config.py @@ -105,7 +105,7 @@ def config(self, s): Whether to merge completion results into a single list If False, only the completion results from the first non-empty completer will be returned. - As of version 8.5.0, setting the value to ``False`` is an alias for: + As of version 8.6.0, setting the value to ``False`` is an alias for: ``IPCompleter.suppress_competing_matchers = True.``. Current: True IPCompleter.omit__names= @@ -123,7 +123,7 @@ def config(self, s): Template for path at which to output profile data for completions. Current: '.completion_profiles' IPCompleter.suppress_competing_matchers= - Whether to suppress completions from other `Matchers`_. + Whether to suppress completions from other *Matchers*. When set to ``None`` (default) the matchers will attempt to auto-detect whether suppression of other matchers is desirable. For example, at the beginning of a line followed by `%` we expect a magic completion to be the @@ -136,9 +136,9 @@ def config(self, s): False}``. Set ``IPCompleter.suppress_competing_matchers = True`` to limit completions to the set of matchers with the highest priority; this is equivalent to - ``IPCompleter.merge_completions`` and can be beneficial for - performance, but will sometimes omit relevant candidates from matchers - further down the priority list. + ``IPCompleter.merge_completions`` and can be beneficial for performance, but + will sometimes omit relevant candidates from matchers further down the + priority list. Current: False IPCompleter.use_jedi= Experimental: Use Jedi to generate autocompletions. Default to True if jedi diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 2643816cceb..e47cf1a7206 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -24,6 +24,9 @@ provisionalcompleter, match_dict_keys, _deduplicate_completions, + completion_matcher, + SimpleCompletion, + CompletionContext, ) # ----------------------------------------------------------------------------- @@ -109,6 +112,16 @@ def greedy_completion(): ip.Completer.greedy = greedy_original +@contextmanager +def custom_matchers(matchers): + ip = get_ipython() + try: + ip.Completer.custom_matchers.extend(matchers) + yield + finally: + ip.Completer.custom_matchers.clear() + + def test_protect_filename(): if sys.platform == "win32": pairs = [ @@ -1281,3 +1294,97 @@ def test_percent_symbol_restrict_to_magic_completions(self): completions = completer.completions(text, len(text)) for c in completions: self.assertEqual(c.text[0], "%") + + def test_matcher_suppression(self): + @completion_matcher(identifier="a_matcher") + def a_matcher(text): + return ["completion_a"] + + @completion_matcher(identifier="b_matcher", api_version=2) + def b_matcher(context: CompletionContext): + text = context.token + result = {"completions": [SimpleCompletion("completion_b")]} + + if text == "suppress c": + result["suppress"] = {"c_matcher"} + + if text.startswith("suppress all"): + result["suppress"] = True + if text == "suppress all but c": + result["do_not_suppress"] = {"c_matcher"} + if text == "suppress all but a": + result["do_not_suppress"] = {"a_matcher"} + + return result + + @completion_matcher(identifier="c_matcher") + def c_matcher(text): + return ["completion_c"] + + with custom_matchers([a_matcher, b_matcher, c_matcher]): + ip = get_ipython() + c = ip.Completer + + def _(text, expected): + with provisionalcompleter(): + c.use_jedi = False + s, matches = c.complete(text) + self.assertEqual(expected, matches) + + _("do not suppress", ["completion_a", "completion_b", "completion_c"]) + _("suppress all", ["completion_b"]) + _("suppress all but a", ["completion_a", "completion_b"]) + _("suppress all but c", ["completion_b", "completion_c"]) + + def test_matcher_disabling(self): + @completion_matcher(identifier="a_matcher") + def a_matcher(text): + return ["completion_a"] + + @completion_matcher(identifier="b_matcher") + def b_matcher(text): + return ["completion_b"] + + def _(expected): + with provisionalcompleter(): + c.use_jedi = False + s, matches = c.complete("completion_") + self.assertEqual(expected, matches) + + with custom_matchers([a_matcher, b_matcher]): + ip = get_ipython() + c = ip.Completer + + _(["completion_a", "completion_b"]) + + cfg = Config() + cfg.IPCompleter.disable_matchers = ["b_matcher"] + c.update_config(cfg) + + _(["completion_a"]) + + cfg.IPCompleter.disable_matchers = [] + c.update_config(cfg) + + def test_matcher_priority(self): + @completion_matcher(identifier="a_matcher", priority=0, api_version=2) + def a_matcher(text): + return {"completions": [SimpleCompletion("completion_a")], "suppress": True} + + @completion_matcher(identifier="b_matcher", priority=2, api_version=2) + def b_matcher(text): + return {"completions": [SimpleCompletion("completion_b")], "suppress": True} + + def _(expected): + with provisionalcompleter(): + c.use_jedi = False + s, matches = c.complete("completion_") + self.assertEqual(expected, matches) + + with custom_matchers([a_matcher, b_matcher]): + ip = get_ipython() + c = ip.Completer + + _(["completion_b"]) + a_matcher.matcher_priority = 3 + _(["completion_a"]) From 21f1467bc36d067eb813c96e216a4cdda2d047d5 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 7 Sep 2022 04:00:23 +0100 Subject: [PATCH 0214/1752] Correct suppression defaults, add a test for #13735 --- IPython/core/completer.py | 16 ++++--- IPython/core/magics/config.py | 2 +- IPython/core/tests/test_completer.py | 68 +++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index f15fabb7d7e..e00d81e6719 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -1337,7 +1337,8 @@ def _greedy_changed(self, change): ) suppress_competing_matchers = UnionTrait( - [Bool(), DictTrait(Bool(None, allow_none=True))], + [Bool(allow_none=True), DictTrait(Bool(None, allow_none=True))], + default_value=None, help=""" Whether to suppress completions from other *Matchers*. @@ -2703,13 +2704,14 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, if not suppressed_matchers: suppression_recommended = result.get("suppress", False) + suppression_config = ( + self.suppress_competing_matchers.get(matcher_id, None) + if isinstance(self.suppress_competing_matchers, dict) + else self.suppress_competing_matchers + ) should_suppress = ( - self.suppress_competing_matchers is True - or suppression_recommended - or ( - isinstance(self.suppress_competing_matchers, dict) - and self.suppress_competing_matchers[matcher_id] - ) + (suppression_config is True) + or (suppression_recommended and (suppression_config is not False)) ) and len(result["completions"]) if should_suppress: diff --git a/IPython/core/magics/config.py b/IPython/core/magics/config.py index f93561aa622..f442ba15259 100644 --- a/IPython/core/magics/config.py +++ b/IPython/core/magics/config.py @@ -139,7 +139,7 @@ def config(self, s): ``IPCompleter.merge_completions`` and can be beneficial for performance, but will sometimes omit relevant candidates from matchers further down the priority list. - Current: False + Current: None IPCompleter.use_jedi= Experimental: Use Jedi to generate autocompletions. Default to True if jedi is installed. diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index e47cf1a7206..c675a9ab161 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -1295,6 +1295,36 @@ def test_percent_symbol_restrict_to_magic_completions(self): for c in completions: self.assertEqual(c.text[0], "%") + def test_dict_key_restrict_to_dicts(self): + """Test that dict key suppresses non-dict completion items""" + ip = get_ipython() + c = ip.Completer + d = {"abc": None} + ip.user_ns["d"] = d + + text = 'd["a' + + def _(): + with provisionalcompleter(): + c.use_jedi = True + return [ + completion.text for completion in c.completions(text, len(text)) + ] + + completions = _() + self.assertEqual(completions, ["abc"]) + + # check that it can be disabled in granular manner: + cfg = Config() + cfg.IPCompleter.suppress_competing_matchers = { + "IPCompleter.dict_key_matcher": False + } + c.update_config(cfg) + + completions = _() + self.assertIn("abc", completions) + self.assertGreater(len(completions), 1) + def test_matcher_suppression(self): @completion_matcher(identifier="a_matcher") def a_matcher(text): @@ -1326,16 +1356,34 @@ def c_matcher(text): c = ip.Completer def _(text, expected): - with provisionalcompleter(): - c.use_jedi = False - s, matches = c.complete(text) - self.assertEqual(expected, matches) + c.use_jedi = False + s, matches = c.complete(text) + self.assertEqual(expected, matches) _("do not suppress", ["completion_a", "completion_b", "completion_c"]) _("suppress all", ["completion_b"]) _("suppress all but a", ["completion_a", "completion_b"]) _("suppress all but c", ["completion_b", "completion_c"]) + def configure(suppression_config): + cfg = Config() + cfg.IPCompleter.suppress_competing_matchers = suppression_config + c.update_config(cfg) + + # test that configuration takes priority over the run-time decisions + + configure(False) + _("suppress all", ["completion_a", "completion_b", "completion_c"]) + + configure({"b_matcher": False}) + _("suppress all", ["completion_a", "completion_b", "completion_c"]) + + configure({"a_matcher": False}) + _("suppress all", ["completion_b"]) + + configure({"b_matcher": True}) + _("do not suppress", ["completion_b"]) + def test_matcher_disabling(self): @completion_matcher(identifier="a_matcher") def a_matcher(text): @@ -1346,10 +1394,8 @@ def b_matcher(text): return ["completion_b"] def _(expected): - with provisionalcompleter(): - c.use_jedi = False - s, matches = c.complete("completion_") - self.assertEqual(expected, matches) + s, matches = c.complete("completion_") + self.assertEqual(expected, matches) with custom_matchers([a_matcher, b_matcher]): ip = get_ipython() @@ -1376,10 +1422,8 @@ def b_matcher(text): return {"completions": [SimpleCompletion("completion_b")], "suppress": True} def _(expected): - with provisionalcompleter(): - c.use_jedi = False - s, matches = c.complete("completion_") - self.assertEqual(expected, matches) + s, matches = c.complete("completion_") + self.assertEqual(expected, matches) with custom_matchers([a_matcher, b_matcher]): ip = get_ipython() From d137c7abd87ca6ede43f30d0c94fa5764cc3cd2b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 7 Sep 2022 22:13:42 +0100 Subject: [PATCH 0215/1752] Improve type hinting and documentation --- IPython/core/completer.py | 185 +++++++++++++++++++++++++++--------- IPython/utils/decorators.py | 27 +++++- IPython/utils/docs.py | 7 ++ docs/source/conf.py | 8 ++ docs/sphinxext/apigen.py | 43 +++++---- 5 files changed, 206 insertions(+), 64 deletions(-) create mode 100644 IPython/utils/docs.py diff --git a/IPython/core/completer.py b/IPython/core/completer.py index b4a0215961c..dfe03629454 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -109,21 +109,48 @@ The built-in matchers include: -- ``IPCompleter.dict_key_matcher``: dictionary key completions, -- ``IPCompleter.magic_matcher``: completions for magics, -- ``IPCompleter.unicode_name_matcher``, ``IPCompleter.fwd_unicode_matcher`` and ``IPCompleter.latex_matcher``: see `Forward latex/unicode completion`_, -- ``back_unicode_name_matcher`` and ``back_latex_name_matcher``: see `Backward latex completion`_, -- ``IPCompleter.file_matcher``: paths to files and directories, -- ``IPCompleter.python_func_kw_matcher`` - function keywords, -- ``IPCompleter.python_matches`` - globals and attributes (v1 API), +- :any:`IPCompleter.dict_key_matcher`: dictionary key completions, +- :any:`IPCompleter.magic_matcher`: completions for magics, +- :any:`IPCompleter.unicode_name_matcher`, + :any:`IPCompleter.fwd_unicode_matcher` + and :any:`IPCompleter.latex_name_matcher`: see `Forward latex/unicode completion`_, +- :any:`back_unicode_name_matcher` and :any:`back_latex_name_matcher`: see `Backward latex completion`_, +- :any:`IPCompleter.file_matcher`: paths to files and directories, +- :any:`IPCompleter.python_func_kw_matcher` - function keywords, +- :any:`IPCompleter.python_matches` - globals and attributes (v1 API), - ``IPCompleter.jedi_matcher`` - static analysis with Jedi, -- ``IPCompleter.custom_completer_matcher`` - pluggable completer with a default implementation in any:`core.InteractiveShell` - which uses uses IPython hooks system (`complete_command`) with string dispatch (including regular expressions). - Differently to other matchers, ``custom_completer_matcher`` will not suppress Jedi results to match - behaviour in earlier IPython versions. +- :any:`IPCompleter.custom_completer_matcher` - pluggable completer with a default + implementation in :any:`InteractiveShell` which uses IPython hooks system + (`complete_command`) with string dispatch (including regular expressions). + Differently to other matchers, ``custom_completer_matcher`` will not suppress + Jedi results to match behaviour in earlier IPython versions. Custom matchers can be added by appending to ``IPCompleter.custom_matchers`` list. +Matcher API +----------- + +Simplifying some details, the ``Matcher`` interface can described as + +.. highlight:: + + MatcherAPIv1 = Callable[[str], list[str]] + MatcherAPIv2 = Callable[[CompletionContext], SimpleMatcherResult] + + Matcher = MatcherAPIv1 | MatcherAPIv2 + +The ``MatcherAPIv1`` reflects the matcher API as available prior to IPython 8.6.0 +and remains supported as a simplest way for generating completions. This is also +currently the only API supported by the IPython hooks system `complete_command`. + +To distinguish between matcher versions ``matcher_api_version`` attribute is used. +More precisely, the API allows to omit ``matcher_api_version`` for v1 Matchers, +and requires a literal ``2`` for v2 Matchers. + +Once the API stabilises future versions may relax the requirement for specifying +``matcher_api_version`` by switching to :any:`functools.singledispatch`, therefore +please do not rely on the presence of ``matcher_api_version`` for any purposes. + Suppression of competing matchers --------------------------------- @@ -137,6 +164,9 @@ Sometimes it is desirable to suppress most but not all other matchers; this can be achieved by adding a list of identifiers of matchers which should not be suppressed to ``MatcherResult`` under ``do_not_suppress`` key. + +The suppression behaviour can is user-configurable via +:any:`IPCompleter.suppress_competing_matchers`. """ @@ -146,7 +176,7 @@ # Some of this code originated from rlcompleter in the Python standard library # Copyright (C) 2001 Python Software Foundation, www.python.org - +from __future__ import annotations import builtins as builtin_mod import glob import inspect @@ -176,9 +206,9 @@ NamedTuple, Pattern, Optional, - Callable, TYPE_CHECKING, Set, + Literal, ) from IPython.core.error import TryNext @@ -187,7 +217,9 @@ from IPython.core.oinspect import InspectColors from IPython.testing.skipdoctest import skip_doctest from IPython.utils import generics +from IPython.utils.decorators import sphinx_options from IPython.utils.dir2 import dir2, get_real_method +from IPython.utils.docs import GENERATING_DOCUMENTATION from IPython.utils.path import ensure_dir_exists from IPython.utils.process import arg_split from traitlets import ( @@ -218,16 +250,23 @@ except ImportError: JEDI_INSTALLED = False -if TYPE_CHECKING: + +if TYPE_CHECKING or GENERATING_DOCUMENTATION: from typing import cast - from typing_extensions import TypedDict, NotRequired + from typing_extensions import TypedDict, NotRequired, Protocol, TypeAlias else: - def cast(obj, _type): + def cast(obj, type_): + """Workaround for `TypeError: MatcherAPIv2() takes no arguments`""" return obj - TypedDict = Dict - NotRequired = Tuple + # do not require on runtime + NotRequired = Tuple # requires Python >=3.11 + TypedDict = Dict # by extension of `NotRequired` requires 3.11 too + Protocol = object # requires Python >=3.8 + TypeAlias = Any # requires Python >=3.10 +if GENERATING_DOCUMENTATION: + from typing import TypedDict # ----------------------------------------------------------------------------- # Globals @@ -522,31 +561,36 @@ def __repr__(self): return f"" -class MatcherResultBase(TypedDict): +class _MatcherResultBase(TypedDict): """Definition of dictionary to be returned by new-style Matcher (API v2).""" - #: suffix of the provided ``CompletionContext.token``, if not given defaults to full token. + #: Suffix of the provided ``CompletionContext.token``, if not given defaults to full token. matched_fragment: NotRequired[str] - #: whether to suppress results from all other matchers (True), some + #: Whether to suppress results from all other matchers (True), some #: matchers (set of identifiers) or none (False); default is False. suppress: NotRequired[Union[bool, Set[str]]] - #: identifiers of matchers which should NOT be suppressed + #: Identifiers of matchers which should NOT be suppressed when this matcher + #: requests to suppress all other matchers; defaults to an empty set. do_not_suppress: NotRequired[Set[str]] - #: are completions already ordered and should be left as-is? default is False. + #: Are completions already ordered and should be left as-is? default is False. ordered: NotRequired[bool] -class SimpleMatcherResult(MatcherResultBase): +@sphinx_options(show_inherited_members=True, exclude_inherited_from=["dict"]) +class SimpleMatcherResult(_MatcherResultBase, TypedDict): """Result of new-style completion matcher.""" - #: list of candidate completions + # note: TypedDict is added again to the inheritance chain + # in order to get __orig_bases__ for documentation + + #: List of candidate completions completions: Sequence[SimpleCompletion] -class _JediMatcherResult(MatcherResultBase): +class _JediMatcherResult(_MatcherResultBase): """Matching result returned by Jedi (will be processed differently)""" #: list of candidate completions @@ -592,11 +636,38 @@ def line_with_cursor(self) -> str: return self.full_text.split("\n")[self.cursor_line] +#: Matcher results for API v2. MatcherResult = Union[SimpleMatcherResult, _JediMatcherResult] -MatcherAPIv1 = Callable[[str], List[str]] -MatcherAPIv2 = Callable[[CompletionContext], MatcherResult] -Matcher = Union[MatcherAPIv1, MatcherAPIv2] + +class _MatcherAPIv1Base(Protocol): + def __call__(self, text: str) -> list[str]: + """Call signature.""" + + +class _MatcherAPIv1Total(_MatcherAPIv1Base, Protocol): + #: API version + matcher_api_version: Optional[Literal[1]] + + def __call__(self, text: str) -> list[str]: + """Call signature.""" + + +#: Protocol describing Matcher API v1. +MatcherAPIv1: TypeAlias = Union[_MatcherAPIv1Base, _MatcherAPIv1Total] + + +class MatcherAPIv2(Protocol): + """Protocol describing Matcher API v2.""" + + #: API version + matcher_api_version: Literal[2] = 2 + + def __call__(self, context: CompletionContext) -> MatcherResult: + """Call signature.""" + + +Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2] def completion_matcher( @@ -1160,7 +1231,7 @@ def _safe_isinstance(obj, module, class_name): def back_unicode_name_matcher(context): """Match Unicode characters back to Unicode name - Same as ``back_unicode_name_matches``, but adopted to new Matcher API. + Same as :any:`back_unicode_name_matches`, but adopted to new Matcher API. """ fragment, matches = back_unicode_name_matches(context.token) return _convert_matcher_v1_result_to_v2( @@ -1178,6 +1249,9 @@ def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]: This will not either back-complete standard sequences like \\n, \\b ... + .. deprecated:: 8.6 + You can use :meth:`back_unicode_name_matcher` instead. + Returns ======= @@ -1187,7 +1261,6 @@ def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]: empty string, - a sequence (of 1), name for the match Unicode character, preceded by backslash, or empty if no match. - """ if len(text)<2: return '', () @@ -1212,7 +1285,7 @@ def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]: def back_latex_name_matcher(context): """Match latex characters back to unicode name - Same as ``back_latex_name_matches``, but adopted to new Matcher API. + Same as :any:`back_latex_name_matches`, but adopted to new Matcher API. """ fragment, matches = back_latex_name_matches(context.token) return _convert_matcher_v1_result_to_v2( @@ -1225,6 +1298,8 @@ def back_latex_name_matches(text: str) -> Tuple[str, Sequence[str]]: This does ``\\ℵ`` -> ``\\aleph`` + .. deprecated:: 8.6 + You can use :meth:`back_latex_name_matcher` instead. """ if len(text)<2: return '', () @@ -1567,7 +1642,7 @@ def _clean_glob_win32(self, text:str): @context_matcher() def file_matcher(self, context: CompletionContext) -> SimpleMatcherResult: - """Same as ``file_matches``, but adopted to new Matcher API.""" + """Same as :any:`file_matches`, but adopted to new Matcher API.""" matches = self.file_matches(context.token) # TODO: add a heuristic for suppressing (e.g. if it has OS-specific delimiter, # starts with `/home/`, `C:\`, etc) @@ -1587,7 +1662,8 @@ def file_matches(self, text: str) -> List[str]: current (as of Python 2.3) Python readline it's possible to do better. - DEPRECATED: Deprecated since 8.6. Use ``file_matcher`` instead. + .. deprecated:: 8.6 + You can use :meth:`file_matcher` instead. """ # chars that require escaping with backslash - i.e. chars @@ -1660,6 +1736,7 @@ def file_matches(self, text: str) -> List[str]: @context_matcher() def magic_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match magics.""" text = context.token matches = self.magic_matches(text) result = _convert_matcher_v1_result_to_v2(matches, type="magic") @@ -1670,7 +1747,8 @@ def magic_matcher(self, context: CompletionContext) -> SimpleMatcherResult: def magic_matches(self, text: str): """Match magics. - DEPRECATED: Deprecated since 8.6. Use ``magic_matcher`` instead. + .. deprecated:: 8.6 + You can use :meth:`magic_matcher` instead. """ # Get all shell magics now rather than statically, so magics loaded at # runtime show up too. @@ -1722,7 +1800,8 @@ def magic_config_matcher(self, context: CompletionContext) -> SimpleMatcherResul def magic_config_matches(self, text: str) -> List[str]: """Match class names and attributes for %config magic. - DEPRECATED: Deprecated since 8.6. Use ``magic_config_matcher`` instead. + .. deprecated:: 8.6 + You can use :meth:`magic_config_matcher` instead. """ texts = text.strip().split() @@ -1767,7 +1846,8 @@ def magic_color_matcher(self, context: CompletionContext) -> SimpleMatcherResult def magic_color_matches(self, text: str) -> List[str]: """Match color schemes for %colors magic. - DEPRECATED: Deprecated since 8.6. Use ``magic_color_matcher`` instead. + .. deprecated:: 8.6 + You can use :meth:`magic_color_matcher` instead. """ texts = text.split() if text.endswith(' '): @@ -1815,7 +1895,8 @@ def _jedi_matches( If ``IPCompleter.debug`` is ``True`` may return a :any:`_FakeJediCompletion` object containing a string with the Jedi debug information attached. - DEPRECATED: Deprecated since 8.6. Use ``_jedi_matcher`` instead. + .. deprecated:: 8.6 + You can use :meth:`_jedi_matcher` instead. """ namespaces = [self.namespace] if self.global_namespace is not None: @@ -1961,7 +2042,8 @@ def python_func_kw_matcher(self, context: CompletionContext) -> SimpleMatcherRes def python_func_kw_matches(self, text): """Match named parameters (kwargs) of the last open function. - DEPRECATED: Deprecated since 8.6. Use ``magic_config_matcher`` instead. + .. deprecated:: 8.6 + You can use :meth:`python_func_kw_matcher` instead. """ if "." in text: # a parameter cannot be dotted @@ -2068,7 +2150,8 @@ def dict_key_matcher(self, context: CompletionContext) -> SimpleMatcherResult: def dict_key_matches(self, text: str) -> List[str]: """Match string keys in a dictionary, after e.g. ``foo[``. - DEPRECATED: Deprecated since 8.6. Use `dict_key_matcher` instead. + .. deprecated:: 8.6 + You can use :meth:`dict_key_matcher` instead. """ if self.__dict_key_regexps is not None: @@ -2173,6 +2256,7 @@ def dict_key_matches(self, text: str) -> List[str]: @context_matcher() def unicode_name_matcher(self, context): + """Same as :any:`unicode_name_matches`, but adopted to new Matcher API.""" fragment, matches = self.unicode_name_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True @@ -2216,7 +2300,8 @@ def latex_matches(self, text: str) -> Tuple[str, Sequence[str]]: This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α`` - DEPRECATED: Deprecated since 8.6. Use `latex_matcher` instead. + .. deprecated:: 8.6 + You can use :meth:`latex_name_matcher` instead. """ slashpos = text.rfind('\\') if slashpos > -1: @@ -2235,9 +2320,13 @@ def latex_matches(self, text: str) -> Tuple[str, Sequence[str]]: @context_matcher() def custom_completer_matcher(self, context): + """Dispatch custom completer. + + If a match is found, suppresses all other matchers except for Jedi. + """ matches = self.dispatch_custom_completer(context.token) or [] result = _convert_matcher_v1_result_to_v2( - matches, type="", suppress_if_matches=True + matches, type=_UNKNOWN_TYPE, suppress_if_matches=True ) result["ordered"] = True result["do_not_suppress"] = {_get_matcher_id(self._jedi_matcher)} @@ -2245,7 +2334,8 @@ def custom_completer_matcher(self, context): def dispatch_custom_completer(self, text): """ - DEPRECATED: Deprecated since 8.6. Use `custom_completer_matcher` instead. + .. deprecated:: 8.6 + You can use :meth:`custom_completer_matcher` instead. """ if not self.custom_completers: return @@ -2768,7 +2858,7 @@ def _sort(matches: Sequence[SimpleCompletion]): @context_matcher() def fwd_unicode_matcher(self, context): - """Same as ``fwd_unicode_match``, but adopted to new Matcher API.""" + """Same as :any:`fwd_unicode_match`, but adopted to new Matcher API.""" fragment, matches = self.latex_matches(context.token) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True @@ -2779,15 +2869,16 @@ def fwd_unicode_match(self, text: str) -> Tuple[str, Sequence[str]]: Forward match a string starting with a backslash with a list of potential Unicode completions. - Will compute list list of Unicode character names on first call and cache it. + Will compute list of Unicode character names on first call and cache it. + + .. deprecated:: 8.6 + You can use :meth:`fwd_unicode_matcher` instead. Returns ------- At tuple with: - matched text (empty if no matches) - list of potential completions, empty tuple otherwise) - - DEPRECATED: Deprecated since 8.6. Use `fwd_unicode_matcher` instead. """ # TODO: self.unicode_names is here a list we traverse each time with ~100k elements. # We could do a faster match using a Trie. diff --git a/IPython/utils/decorators.py b/IPython/utils/decorators.py index 47791d7ca65..bc7589cd35c 100644 --- a/IPython/utils/decorators.py +++ b/IPython/utils/decorators.py @@ -2,7 +2,7 @@ """Decorators that don't go anywhere else. This module contains misc. decorators that don't really go with another module -in :mod:`IPython.utils`. Beore putting something here please see if it should +in :mod:`IPython.utils`. Before putting something here please see if it should go into another topical module in :mod:`IPython.utils`. """ @@ -16,6 +16,10 @@ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- +from typing import Sequence + +from IPython.utils.docs import GENERATING_DOCUMENTATION + #----------------------------------------------------------------------------- # Code @@ -48,6 +52,7 @@ def wrapper(*args,**kw): wrapper.__doc__ = func.__doc__ return wrapper + def undoc(func): """Mark a function or class as undocumented. @@ -56,3 +61,23 @@ def undoc(func): """ return func + +def sphinx_options( + show_inheritance: bool = True, + show_inherited_members: bool = False, + exclude_inherited_from: Sequence[str] = tuple(), +): + """Set sphinx options""" + + def wrapper(func): + if not GENERATING_DOCUMENTATION: + return func + + func._sphinx_options = dict( + show_inheritance=show_inheritance, + show_inherited_members=show_inherited_members, + exclude_inherited_from=exclude_inherited_from, + ) + return func + + return wrapper diff --git a/IPython/utils/docs.py b/IPython/utils/docs.py new file mode 100644 index 00000000000..ee1aaeaa2f9 --- /dev/null +++ b/IPython/utils/docs.py @@ -0,0 +1,7 @@ +# encoding: utf-8 + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. +import os + +GENERATING_DOCUMENTATION = os.environ.get("IN_SPHINX_RUN", None) == "True" diff --git a/docs/source/conf.py b/docs/source/conf.py index 29212af8bf7..d04d4637ba7 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,6 +41,14 @@ html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# Allow Python scripts to change behaviour during sphinx run +os.environ["IN_SPHINX_RUN"] = "True" + +autodoc_type_aliases = { + "Matcher": " IPython.core.completer.Matcher", + "MatcherAPIv1": " IPython.core.completer.MatcherAPIv1", +} + # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. diff --git a/docs/sphinxext/apigen.py b/docs/sphinxext/apigen.py index e58493b17fd..47dc1101933 100644 --- a/docs/sphinxext/apigen.py +++ b/docs/sphinxext/apigen.py @@ -24,14 +24,9 @@ import os import re from importlib import import_module +from types import SimpleNamespace as Obj -class Obj(object): - '''Namespace to hold arbitrary information.''' - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, v) - class FuncClsScanner(ast.NodeVisitor): """Scan a module for top-level functions and classes. @@ -42,7 +37,7 @@ def __init__(self): self.classes = [] self.classes_seen = set() self.functions = [] - + @staticmethod def has_undoc_decorator(node): return any(isinstance(d, ast.Name) and d.id == 'undoc' \ @@ -62,11 +57,15 @@ def visit_FunctionDef(self, node): self.functions.append(node.name) def visit_ClassDef(self, node): - if not (node.name.startswith('_') or self.has_undoc_decorator(node)) \ - and node.name not in self.classes_seen: - cls = Obj(name=node.name) - cls.has_init = any(isinstance(n, ast.FunctionDef) and \ - n.name=='__init__' for n in node.body) + if ( + not (node.name.startswith("_") or self.has_undoc_decorator(node)) + and node.name not in self.classes_seen + ): + cls = Obj(name=node.name, sphinx_options={}) + cls.has_init = any( + isinstance(n, ast.FunctionDef) and n.name == "__init__" + for n in node.body + ) self.classes.append(cls) self.classes_seen.add(node.name) @@ -221,7 +220,11 @@ def _import_funcs_classes(self, uri): funcs, classes = [], [] for name, obj in ns.items(): if inspect.isclass(obj): - cls = Obj(name=name, has_init='__init__' in obj.__dict__) + cls = Obj( + name=name, + has_init="__init__" in obj.__dict__, + sphinx_options=getattr(obj, "_sphinx_options", {}), + ) classes.append(cls) elif inspect.isfunction(obj): funcs.append(name) @@ -279,10 +282,18 @@ def generate_api_doc(self, uri): self.rst_section_levels[2] * len(subhead) + '\n' for c in classes: - ad += '\n.. autoclass:: ' + c.name + '\n' + opts = c.sphinx_options + ad += "\n.. autoclass:: " + c.name + "\n" # must NOT exclude from index to keep cross-refs working - ad += ' :members:\n' \ - ' :show-inheritance:\n' + ad += " :members:\n" + if opts.get("show_inheritance", True): + ad += " :show-inheritance:\n" + if opts.get("show_inherited_members", False): + exclusions_list = opts.get("exclude_inherited_from", []) + exclusions = ( + (" " + " ".join(exclusions_list)) if exclusions_list else "" + ) + ad += f" :inherited-members:{exclusions}\n" if c.has_init: ad += '\n .. automethod:: __init__\n' From b61b12e7a06b841c22fb89d4d70f55b02478098b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 7 Sep 2022 22:16:59 +0100 Subject: [PATCH 0216/1752] Move `typing_extensions` optional dependency --- docs/requirements.txt | 1 + setup.cfg | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 587288c2a0f..5d3295b0f3a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -7,3 +7,4 @@ docrepr matplotlib stack_data pytest<7 +typing_extensions diff --git a/setup.cfg b/setup.cfg index 99b3498d0b3..274e338d0ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,7 +73,6 @@ test_extra = numpy>=1.19 pandas trio - typing_extensions all = %(black)s %(doc)s From 56b6489db7006249abf67e9d8b7f9ee4d7ed681a Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 7 Sep 2022 22:23:58 +0100 Subject: [PATCH 0217/1752] =?UTF-8?q?`highlight`=20=E2=86=92=20`code-block?= =?UTF-8?q?`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IPython/core/completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index dfe03629454..a379e0e0935 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -132,7 +132,7 @@ Simplifying some details, the ``Matcher`` interface can described as -.. highlight:: +.. code-block:: MatcherAPIv1 = Callable[[str], list[str]] MatcherAPIv2 = Callable[[CompletionContext], SimpleMatcherResult] From 9eecab41318397815082d29814cce75b2cd34e45 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 8 Sep 2022 11:48:03 +0200 Subject: [PATCH 0218/1752] DOC/SEC/MAINT: Update security.md with Tidelift. Also add badge to readme. --- README.rst | 3 +++ SECURITY.md | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 5107eae9b5d..e38b7120e6b 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,9 @@ .. image:: https://raster.shields.io/badge/Follows-NEP29-brightgreen.png :target: https://numpy.org/neps/nep-0029-deprecation_policy.html +.. image:: https://tidelift.com/subscription/pkg/pypi-ipython + :target: https://tidelift.com/badges/package/pypi/ipython?style=flat + =========================================== IPython: Productive Interactive Computing diff --git a/SECURITY.md b/SECURITY.md index dc5db66e2a2..1208990a74f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,3 +4,8 @@ All IPython and Jupyter security are handled via security@ipython.org. You can find more information on the Jupyter website. https://jupyter.org/security + +## Tidelift + +We are also lifting IPython via Tidelift, you can also report security concern via the [tidelift platform]( +https://tidelift.com/security). From ef9f0dec2b28978d70e1aadd1e37e1052f90c77c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 8 Sep 2022 14:17:08 +0200 Subject: [PATCH 0219/1752] SEC: force workflows to be read-only. Via tidelift and suggested by https://deps.dev/pypi/ipython --- .github/workflows/docs.yml | 3 +++ .github/workflows/downstream.yml | 2 ++ .github/workflows/mypy.yml | 3 +++ .github/workflows/python-package.yml | 3 +++ 4 files changed, 11 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e4be71c0ddb..f18fb39392f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,9 @@ name: Build docs on: [push, pull_request] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index ae2dbe5efe9..e6206ae71f1 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -8,6 +8,8 @@ on: - cron: '23 1 * * 1' workflow_dispatch: +permissions: + contents: read jobs: test: diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 2725c92923c..8d1927d6b36 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main, 7.x] +permissions: + contents: read + jobs: build: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fc28ac8e4c5..62667b48d38 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -3,6 +3,9 @@ name: Python package +permissions: + contents: read + on: push: branches: [ main, 7.x ] From 37590bda0f176f0548f3f47a2d5a26aa120f734d Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:13:07 +0100 Subject: [PATCH 0220/1752] Fix backslash combining matchers (they require `text_until_cursor`). --- IPython/core/completer.py | 23 +++++++++++++---------- IPython/core/tests/test_completer.py | 12 ++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index a379e0e0935..50b3e5e0180 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -1228,12 +1228,12 @@ def _safe_isinstance(obj, module, class_name): @context_matcher() -def back_unicode_name_matcher(context): +def back_unicode_name_matcher(context: CompletionContext): """Match Unicode characters back to Unicode name Same as :any:`back_unicode_name_matches`, but adopted to new Matcher API. """ - fragment, matches = back_unicode_name_matches(context.token) + fragment, matches = back_unicode_name_matches(context.text_until_cursor) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True ) @@ -1282,12 +1282,12 @@ def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]: @context_matcher() -def back_latex_name_matcher(context): +def back_latex_name_matcher(context: CompletionContext): """Match latex characters back to unicode name Same as :any:`back_latex_name_matches`, but adopted to new Matcher API. """ - fragment, matches = back_latex_name_matches(context.token) + fragment, matches = back_latex_name_matches(context.text_until_cursor) return _convert_matcher_v1_result_to_v2( matches, type="latex", fragment=fragment, suppress_if_matches=True ) @@ -2255,9 +2255,9 @@ def dict_key_matches(self, text: str) -> List[str]: return [leading + k + suf for k in matches] @context_matcher() - def unicode_name_matcher(self, context): + def unicode_name_matcher(self, context: CompletionContext): """Same as :any:`unicode_name_matches`, but adopted to new Matcher API.""" - fragment, matches = self.unicode_name_matches(context.token) + fragment, matches = self.unicode_name_matches(context.text_until_cursor) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True ) @@ -2285,12 +2285,12 @@ def unicode_name_matches(text: str) -> Tuple[str, List[str]]: return '', [] @context_matcher() - def latex_name_matcher(self, context): + def latex_name_matcher(self, context: CompletionContext): """Match Latex syntax for unicode characters. This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α`` """ - fragment, matches = self.latex_matches(context.token) + fragment, matches = self.latex_matches(context.text_until_cursor) return _convert_matcher_v1_result_to_v2( matches, type="latex", fragment=fragment, suppress_if_matches=True ) @@ -2857,9 +2857,12 @@ def _sort(matches: Sequence[SimpleCompletion]): return sorted(matches, key=lambda x: completions_sorting_key(x.text)) @context_matcher() - def fwd_unicode_matcher(self, context): + def fwd_unicode_matcher(self, context: CompletionContext): """Same as :any:`fwd_unicode_match`, but adopted to new Matcher API.""" - fragment, matches = self.latex_matches(context.token) + # TODO: use `context.limit` to terminate early once we matched the maximum + # number that will be used downstream; can be added as an optional to + # `fwd_unicode_match(text: str, limit: int = None)` or we could re-implement here. + fragment, matches = self.fwd_unicode_match(context.text_until_cursor) return _convert_matcher_v1_result_to_v2( matches, type="unicode", fragment=fragment, suppress_if_matches=True ) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index c675a9ab161..fd72cf7d57a 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -1295,6 +1295,18 @@ def test_percent_symbol_restrict_to_magic_completions(self): for c in completions: self.assertEqual(c.text[0], "%") + def test_fwd_unicode_restricts(self): + ip = get_ipython() + completer = ip.Completer + text = "\\ROMAN NUMERAL FIVE" + + with provisionalcompleter(): + completer.use_jedi = True + completions = [ + completion.text for completion in completer.completions(text, len(text)) + ] + self.assertEqual(completions, ["\u2164"]) + def test_dict_key_restrict_to_dicts(self): """Test that dict key suppresses non-dict completion items""" ip = get_ipython() From 3e5275d3a7ad2ee6efc916d5019626e77e516501 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 8 Sep 2022 09:22:20 -0700 Subject: [PATCH 0221/1752] Update SECURITY.md Co-authored-by: Jason Weill <93281816+jweill-aws@users.noreply.github.com> --- SECURITY.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1208990a74f..86c88c328c0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,5 +7,4 @@ You can find more information on the Jupyter website. https://jupyter.org/securi ## Tidelift -We are also lifting IPython via Tidelift, you can also report security concern via the [tidelift platform]( -https://tidelift.com/security). +You can report security concerns for IPython via the [Tidelift platform](https://tidelift.com/security). From 62af0b0aefaf75d9175ca77a7771d42674502770 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:26:23 +0100 Subject: [PATCH 0222/1752] Relax constraint on limit to allow no limit --- IPython/core/completer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 50b3e5e0180..ef99c979f61 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -623,7 +623,8 @@ class CompletionContext(NamedTuple): #: The maximum number of completions that will be used downstream. #: Matchers can use this information to abort early. #: The built-in Jedi matcher is currently excepted from this limit. - limit: int + # If not given, return all possible completions. + limit: Optional[int] @property @lru_cache(maxsize=None) # TODO change to @cache after dropping Python 3.7 From b33c7213c79c36af4fdd388f1cdbe6001bfcb9e2 Mon Sep 17 00:00:00 2001 From: nouman <55525217+noumxn@users.noreply.github.com> Date: Sat, 10 Sep 2022 10:52:55 +0530 Subject: [PATCH 0223/1752] Update autoreload.py Fixes ipython#13651 --- IPython/extensions/autoreload.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index a0a8c27f134..f485ac38b56 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -95,6 +95,10 @@ before it is reloaded are not upgraded. - C extension modules cannot be reloaded, and so cannot be autoreloaded. + +- While comparing Enum and Flag, the 'is' Identity Operator is used (even in the case '==' has been used (Similar to the 'None' keyword)). + +- Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not. """ __skip_doctest__ = True From 4c855f4e60cacfb74e9417e07366c8ad49493f36 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 12 Sep 2022 21:26:00 +0200 Subject: [PATCH 0224/1752] Quick hack to make tb highlighting configurable. For example put from IPython.core.ultratb import VerboseTB VerboseTB._tb_highlight = "bg:#039dfc" In your ipython_config.py Mitigate #13446 and #13486 --- IPython/core/ultratb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 125ee9a78a4..8f40c63b758 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -610,6 +610,8 @@ class VerboseTB(TBTools): traceback, to be used with alternate interpreters (because their own code would appear in the traceback).""" + _tb_highlight = "bg:ansiyellow" + def __init__( self, color_scheme: str = "Linux", @@ -836,7 +838,7 @@ def get_records( before = context - after if self.has_colors: style = get_style_by_name("default") - style = stack_data.style_with_executing_node(style, "bg:ansiyellow") + style = stack_data.style_with_executing_node(style, self._tb_highlight) formatter = Terminal256Formatter(style=style) else: formatter = None From 2dfda2f86249899ae61deb61bcb707847ee449a0 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 16 Sep 2022 14:42:14 +0200 Subject: [PATCH 0225/1752] MAINT: Nep 29- drop support for Numpy 1.19. > On Jun 21, 2022 drop support for NumPy 1.19 (initially released on Jun 20, 2020) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6004f3a4d3d..fa6a1665d52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -78,7 +78,7 @@ test_extra = curio matplotlib!=3.2.0 nbformat - numpy>=1.19 + numpy>=1.20 pandas trio all = From 7ee7665834919ceab5658088d39c10df059cf5c1 Mon Sep 17 00:00:00 2001 From: "Wenhan Zhu (Cosmos)" Date: Mon, 19 Sep 2022 14:44:16 -0400 Subject: [PATCH 0226/1752] fixed cpaste session error --- IPython/terminal/magics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/terminal/magics.py b/IPython/terminal/magics.py index 206ff20a0f8..66d532511b3 100644 --- a/IPython/terminal/magics.py +++ b/IPython/terminal/magics.py @@ -41,7 +41,7 @@ class TerminalMagics(Magics): def __init__(self, shell): super(TerminalMagics, self).__init__(shell) - def store_or_execute(self, block, name): + def store_or_execute(self, block, name, store_history=False): """ Execute a block, or store it in a variable, per the user's request. """ if name: @@ -53,7 +53,7 @@ def store_or_execute(self, block, name): self.shell.user_ns['pasted_block'] = b self.shell.using_paste_magics = True try: - self.shell.run_cell(b, store_history=True) + self.shell.run_cell(b, store_history) finally: self.shell.using_paste_magics = False @@ -147,7 +147,7 @@ def cpaste(self, parameter_s=''): sentinel = opts.get('s', u'--') block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet)) - self.store_or_execute(block, name) + self.store_or_execute(block, name, store_history=False) @line_magic def paste(self, parameter_s=''): @@ -203,7 +203,7 @@ def paste(self, parameter_s=''): sys.stdout.write("\n") sys.stdout.write("## -- End pasted text --\n") - self.store_or_execute(block, name) + self.store_or_execute(block, name, store_history=True) # Class-level: add a '%cls' magic only on Windows if sys.platform == 'win32': From 5bb0259a747651290f91c0384ca93492a423c82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Sat, 24 Sep 2022 21:15:53 +0100 Subject: [PATCH 0227/1752] Remove outdated header as suggested Co-authored-by: Matthias Bussonnier --- IPython/utils/docs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/IPython/utils/docs.py b/IPython/utils/docs.py index ee1aaeaa2f9..6a97815cdc7 100644 --- a/IPython/utils/docs.py +++ b/IPython/utils/docs.py @@ -1,7 +1,3 @@ -# encoding: utf-8 - -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. import os GENERATING_DOCUMENTATION = os.environ.get("IN_SPHINX_RUN", None) == "True" From ab60d3197788b943db0cf81d9279c28e52ec9225 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 24 Sep 2022 21:39:35 +0100 Subject: [PATCH 0228/1752] Switch `CompletionContext` to `dataclass`, use `cached_property` Data class has narrower API than named tuple (does not expose item getters) which means it is less likely that the downstream code would be broken in the future if we introduce new attributes. Also, compared to other places where memory-efficient named tuple is used, `CompletionContext` is only created once per completion and thus does not require as low overhead. For the same reason we can forgo slightly more memory-efficient `@property @cache` stack and just use `@cached_property`. --- IPython/core/completer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index ef99c979f61..fc3aea7b611 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -191,7 +191,8 @@ import uuid import warnings from contextlib import contextmanager -from functools import lru_cache, partial +from dataclasses import dataclass +from functools import cached_property, partial from importlib import import_module from types import SimpleNamespace from typing import ( @@ -597,7 +598,8 @@ class _JediMatcherResult(_MatcherResultBase): completions: Iterable[_JediCompletionLike] -class CompletionContext(NamedTuple): +@dataclass +class CompletionContext: """Completion context provided as an argument to matchers in the Matcher API v2.""" # rationale: many legacy matchers relied on completer state (`self.text_until_cursor`) @@ -626,13 +628,11 @@ class CompletionContext(NamedTuple): # If not given, return all possible completions. limit: Optional[int] - @property - @lru_cache(maxsize=None) # TODO change to @cache after dropping Python 3.7 + @cached_property def text_until_cursor(self) -> str: return self.line_with_cursor[: self.cursor_position] - @property - @lru_cache(maxsize=None) # TODO change to @cache after dropping Python 3.7 + @cached_property def line_with_cursor(self) -> str: return self.full_text.split("\n")[self.cursor_line] From 1c5ecd6fc3c010595a324ef2fa2a2e23f9566503 Mon Sep 17 00:00:00 2001 From: Petri Salminen Date: Wed, 28 Sep 2022 19:47:12 +0300 Subject: [PATCH 0229/1752] Allow spaces in file paths when using sphinx directive Closes: #11606 --- IPython/sphinxext/ipython_directive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py index 9e3c7b2276a..e55ba126c8a 100644 --- a/IPython/sphinxext/ipython_directive.py +++ b/IPython/sphinxext/ipython_directive.py @@ -981,7 +981,7 @@ def setup(self): self.shell.warning_is_error = warning_is_error # setup bookmark for saving figures directory - self.shell.process_input_line('bookmark ipy_savedir %s'%savefig_dir, + self.shell.process_input_line('bookmark ipy_savedir "%s"'%savefig_dir, store_history=False) self.shell.clear_cout() From 81898815733f8f666fab4767970c1bc61901e955 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 10:23:21 -0700 Subject: [PATCH 0230/1752] Remove copy-paste error --- IPython/extensions/autoreload.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index a0a8c27f134..4b6b5cfdc1e 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -97,6 +97,7 @@ - C extension modules cannot be reloaded, and so cannot be autoreloaded. """ +from IPython.core.magic import Magics, magics_class, line_magic __skip_doctest__ = True # ----------------------------------------------------------------------------- @@ -491,8 +492,6 @@ def superreload(module, reload=reload, old_objects=None, shell=None): # IPython connectivity # ------------------------------------------------------------------------------ -from IPython.core.magic import Magics, magics_class, line_magic - @magics_class class AutoreloadMagics(Magics): @@ -558,7 +557,6 @@ def autoreload(self, parameter_s=""): elif parameter_s == "2": self._reloader.check_all = True self._reloader.enabled = True - self._reloader.enabled = True elif parameter_s == "3": self._reloader.check_all = True self._reloader.enabled = True From a7c76db7d6dbb961af374b2f34c996eaef5ad62b Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 12:35:56 -0700 Subject: [PATCH 0231/1752] Verbose version of `%autoreload` command --- IPython/extensions/autoreload.py | 41 ++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 4b6b5cfdc1e..72f6ec436e7 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -29,29 +29,30 @@ The following magic commands are provided: -``%autoreload`` +``%autoreload``, ``%autoreload now`` Reload all modules (except those excluded by ``%aimport``) automatically now. -``%autoreload 0`` +``%autoreload 0``, ``%autoreload off`` Disable automatic reloading. -``%autoreload 1`` +``%autoreload 1``, ``%autoreload explicit`` Reload all modules imported with ``%aimport`` every time before executing the Python code typed. -``%autoreload 2`` +``%autoreload 2``, ``%autoreload all`` Reload all modules (except those excluded by ``%aimport``) every time before executing the Python code typed. -``%autoreload 3`` +``%autoreload 3``, ``%autoreload complete`` - Reload all modules AND autoload newly added objects - every time before executing the Python code typed. + Reload all modules (except those excluded by ``%aimport``) AND + autoload newly added objects every time before executing the + Python code typed. ``%aimport`` @@ -506,18 +507,22 @@ def __init__(self, *a, **kw): def autoreload(self, parameter_s=""): r"""%autoreload => Reload modules automatically - %autoreload + %autoreload or %autoreload now Reload all modules (except those excluded by %aimport) automatically now. - %autoreload 0 + %autoreload 0 or %autoreload off Disable automatic reloading. - %autoreload 1 - Reload all modules imported with %aimport every time before executing + %autoreload 1 or %autoreload explicit + Reload only modules imported with %aimport every time before executing the Python code typed. - %autoreload 2 + %autoreload 2 or %autoreload all + Reload all modules (except those excluded by %aimport) every time + before executing the Python code typed. + + %autoreload 3 or %autoreload complete Reload all modules (except those excluded by %aimport) every time before executing the Python code typed. @@ -547,17 +552,17 @@ def autoreload(self, parameter_s=""): autoreloaded. """ - if parameter_s == "": + if parameter_s == "" or parameter_s.lower() == "now": self._reloader.check(True) - elif parameter_s == "0": + elif parameter_s == "0" or parameter_s.lower() == "off": self._reloader.enabled = False - elif parameter_s == "1": + elif parameter_s == "1" or parameter_s.lower() == "explicit": self._reloader.check_all = False self._reloader.enabled = True - elif parameter_s == "2": + elif parameter_s == "2" or parameter_s.lower() == "all": self._reloader.check_all = True self._reloader.enabled = True - elif parameter_s == "3": + elif parameter_s == "3" or parameter_s.lower() == "complete": self._reloader.check_all = True self._reloader.enabled = True self._reloader.autoload_obj = True @@ -576,7 +581,7 @@ def aimport(self, parameter_s="", stream=None): Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload 1 %aimport -foo - Mark module 'foo' to not be autoreloaded for %autoreload 1 + Mark module 'foo' to not be autoreloaded for %autoreload 1, 2, or 3 """ modname = parameter_s if not modname: From 8a66e854a87b5147d811bd3bc92c5c2a382633e1 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 12:40:09 -0700 Subject: [PATCH 0232/1752] Improve parsing for `%aimport` --- IPython/extensions/autoreload.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 72f6ec436e7..215877c6be1 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -580,8 +580,9 @@ def aimport(self, parameter_s="", stream=None): %aimport foo, bar Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload 1 - %aimport -foo - Mark module 'foo' to not be autoreloaded for %autoreload 1, 2, or 3 + %aimport -foo, bar + Mark module 'foo' to not be autoreloaded for %autoreload 1, 2, or 3, and 'bar' + to be autoreloaded for 1. """ modname = parameter_s if not modname: @@ -594,15 +595,16 @@ def aimport(self, parameter_s="", stream=None): else: stream.write("Modules to reload:\n%s\n" % " ".join(to_reload)) stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip)) - elif modname.startswith("-"): - modname = modname[1:] - self._reloader.mark_module_skipped(modname) else: for _module in [_.strip() for _ in modname.split(",")]: - top_module, top_name = self._reloader.aimport_module(_module) - - # Inject module to user namespace - self.shell.push({top_name: top_module}) + if _module.startswith("-"): + _module = _module[1:].strip() + self._reloader.mark_module_skipped(_module) + else: + top_module, top_name = self._reloader.aimport_module(_module) + + # Inject module to user namespace + self.shell.push({top_name: top_module}) def pre_run_cell(self): if self._reloader.enabled: From cde0178a80d1d9bb07fba136fdb4eb1659ad481f Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 12:48:43 -0700 Subject: [PATCH 0233/1752] Raise error on bad parameter --- IPython/extensions/autoreload.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 215877c6be1..472c74e5d48 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -566,6 +566,8 @@ def autoreload(self, parameter_s=""): self._reloader.check_all = True self._reloader.enabled = True self._reloader.autoload_obj = True + else: + raise ValueError(f'Unrecognized parameter "{parameter_s}".') @line_magic def aimport(self, parameter_s="", stream=None): From dc75b256829ba6cdd9848fed21b0a513814c1a9f Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 12:57:11 -0700 Subject: [PATCH 0234/1752] Add `%averbose` magic command. --- IPython/extensions/autoreload.py | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 472c74e5d48..5e0ccb415d9 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -70,6 +70,18 @@ Mark module 'foo' to not be autoreloaded. +``%averbose off`` + + Perform autoreload tasks quietly + +``%averbose on`` + + Report activity with `print` statements. + +``%averbose log`` + + Report activity with the logger. + Caveats ======= @@ -123,6 +135,7 @@ import types import weakref import gc +import logging from importlib import import_module, reload from importlib.util import source_from_cache @@ -154,6 +167,9 @@ def __init__(self, shell=None): self.modules_mtimes = {} self.shell = shell + # Reporting callable for verbosity + self._report = lambda msg: None # by default, be quiet. + # Cache module modification times self.check(check_all=True, do_reload=False) @@ -252,6 +268,7 @@ def check(self, check_all=False, do_reload=True): # If we've reached this point, we should try to reload the module if do_reload: + self._report(f"Reloading '{modname}'.") try: if self.autoload_obj: superreload(m, reload, self.old_objects, self.shell) @@ -608,6 +625,29 @@ def aimport(self, parameter_s="", stream=None): # Inject module to user namespace self.shell.push({top_name: top_module}) + @line_magic + def averbose(self, parameter_s=""): + r"""%averbose => Turn verbosity on/off for autoreloading. + + %averbose 0 or %averbose off + Turn off any reporting during autoreload. + + %averbose 1 or %averbose on + Report autoreload activity via print statements. + + %averbose 2 or %averbose log + Report autoreload activity via logging. + """ + + if parameter_s == "0" or parameter_s.lower() == "off": + self._reloader._report = lambda msg: None + elif parameter_s == "1" or parameter_s.lower() == "on": + self._reloader._report = lambda msg: print(msg) + elif parameter_s == "2" or parameter_s.lower() == "log": + self._reloader._report = lambda msg: logging.getLogger('autoreload').info(msg) + else: + raise ValueError(f'Unrecognized parameter "{parameter_s}".') + def pre_run_cell(self): if self._reloader.enabled: try: From b8f77d95627e993c20510d1a4d3e026bf5b0a783 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 13:21:46 -0700 Subject: [PATCH 0235/1752] Correct documentation for mode 3 --- IPython/extensions/autoreload.py | 9 ++++----- IPython/extensions/tests/test_autoreload.py | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 5e0ccb415d9..595bd7e5a99 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -50,9 +50,8 @@ ``%autoreload 3``, ``%autoreload complete`` - Reload all modules (except those excluded by ``%aimport``) AND - autoload newly added objects every time before executing the - Python code typed. + Same as 2/all, but also adds any new objects in the module. See + unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects ``%aimport`` @@ -540,8 +539,8 @@ def autoreload(self, parameter_s=""): before executing the Python code typed. %autoreload 3 or %autoreload complete - Reload all modules (except those excluded by %aimport) every time - before executing the Python code typed. + Same as 2/all, but also but also adds any new objects in the module. See + unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects Reloading Python modules in a reliable way is in general difficult, and unexpected things may occur. %autoreload tries to diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 88637fbab9c..c7b7280a91a 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -310,6 +310,7 @@ class MyClass: self.shell.run_code("pass") # trigger another reload def test_autoload_newly_added_objects(self): + # All of these fail with %autoreload 2 self.shell.magic_autoreload("3") mod_code = """ def func1(): pass From 70787c6a226ee1ef772201959ffd82b059954812 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 13:54:09 -0700 Subject: [PATCH 0236/1752] Test for verbose mode names --- IPython/extensions/tests/test_autoreload.py | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index c7b7280a91a..d384519067b 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -22,6 +22,7 @@ import random import time from io import StringIO +from dataclasses import dataclass import IPython.testing.tools as tt @@ -393,6 +394,31 @@ def meth(self): self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'") self.shell.run_code("assert ext_int == 2") + def test_verbose_names(self): + # Asserts correspondense between original mode names and their verbose equivalents. + @dataclass + class AutoreloadSettings: + check_all: bool + enabled: bool + autoload_obj: bool + + def gather_settings(mode): + self.shell.magic_autoreload(mode) + module_reloader = self.shell.auto_magics._reloader + return AutoreloadSettings(module_reloader.check_all, + module_reloader.enabled, + module_reloader.autoload_obj + ) + assert gather_settings("0") == gather_settings("off") + assert gather_settings("0") == gather_settings("OFF") # Case insensitive + assert gather_settings("1") == gather_settings("explicit") + assert gather_settings("2") == gather_settings("all") + assert gather_settings("3") == gather_settings("complete") + + # And an invalid mode name raises an exception. + with self.assertRaises(ValueError): + self.shell.magic_autoreload('4') + def _check_smoketest(self, use_aimport=True): """ Functional test for the automatic reloader using either From a7f4a900c3a009bfc40b44096023110b632d3284 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 14:49:04 -0700 Subject: [PATCH 0237/1752] Test for new parsing of `%aimport` --- IPython/extensions/tests/test_autoreload.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index d384519067b..6c3e75a2c31 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -419,6 +419,23 @@ def gather_settings(mode): with self.assertRaises(ValueError): self.shell.magic_autoreload('4') + def test_aimport_parsing(self): + # Modules can be included or excluded all in one line. + module_reloader = self.shell.auto_magics._reloader + self.shell.magic_aimport('os') # import and mark `os` for auto-reload. + assert module_reloader.modules['os'] is True + assert 'os' not in module_reloader.skip_modules.keys() + + self.shell.magic_aimport('-math') # forbid autoreloading of `math` + assert module_reloader.skip_modules['math'] is True + assert 'math' not in module_reloader.modules.keys() + + self.shell.magic_aimport('-os, math') # Can do this all in one line; wasn't possible before. + assert module_reloader.modules['math'] is True + assert 'math' not in module_reloader.skip_modules.keys() + assert module_reloader.skip_modules['os'] is True + assert 'os' not in module_reloader.modules.keys() + def _check_smoketest(self, use_aimport=True): """ Functional test for the automatic reloader using either From 4436f4601e623a41b4504d5b92bc3ab3c913b4de Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 15:33:49 -0700 Subject: [PATCH 0238/1752] Test of `%averbose` Cannot get logging output to show in tests, TBD if that's needed --- IPython/extensions/tests/test_autoreload.py | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 6c3e75a2c31..346733e92e0 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -71,6 +71,8 @@ def magic_aimport(self, parameter, stream=None): self.auto_magics.aimport(parameter, stream=stream) self.auto_magics.post_execute_hook() + def magic_averbose(self, parameter): + self.auto_magics.averbose(parameter) class Fixture(TestCase): """Fixture for creating test module files""" @@ -436,6 +438,42 @@ def test_aimport_parsing(self): assert module_reloader.skip_modules['os'] is True assert 'os' not in module_reloader.modules.keys() + def test_averbose(self): + self.shell.magic_averbose("off") + self.shell.magic_autoreload("complete") + mod_code = """ + def func1(): pass + """ + mod_name, mod_fn = self.new_module(mod_code) + self.shell.run_code(f"import {mod_name}") + with tt.AssertPrints('', channel="stdout"): # no output. + self.shell.run_code("pass") + + self.write_file(mod_fn, mod_code) # "modify" the module + self.shell.magic_averbose("on") # Should now see a print statement. + with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): + self.shell.run_code("pass") + + self.write_file(mod_fn, mod_code) # "modify" the module + self.shell.magic_averbose("off") # Should not see anything on next call + with tt.AssertPrints('', channel="stdout"): + self.shell.run_code("pass") + + # TODO: test logging. Why won't this work? + # with tt.AssertPrints('LOGGER: '): + # self.shell.run_code("import logging; logging.basicConfig(format='LOGGER: %(message)s');" + # "logger = logging.getLogger(); logger.setLevel(logging.DEBUG);" + # "logger.info('test')") + + # self.shell.magic_averbose("log") # Should see it formatted as per our logging config + # self.write_file(mod_fn, mod_code) # "modify" the module + # with tt.AssertPrints(f"LOGGER: Reloading '{mod_name}'.", channel="stdout"): + # self.shell.run_code("pass") + + # And an invalid mode name raises an exception. + with self.assertRaises(ValueError): + self.shell.magic_averbose('fax') + def _check_smoketest(self, use_aimport=True): """ Functional test for the automatic reloader using either From 097e82fe580fd76bae6257aa3be742cb84f80aca Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 15:51:30 -0700 Subject: [PATCH 0239/1752] RST file documentation --- .../whatsnew/pr/autoreload-verbosity.rst | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/source/whatsnew/pr/autoreload-verbosity.rst diff --git a/docs/source/whatsnew/pr/autoreload-verbosity.rst b/docs/source/whatsnew/pr/autoreload-verbosity.rst new file mode 100644 index 00000000000..dc5a9aa9479 --- /dev/null +++ b/docs/source/whatsnew/pr/autoreload-verbosity.rst @@ -0,0 +1,25 @@ +Autoreload verbosity +==================== + +We introduce more descriptive names for the `%autoreload` parameter: +- `%autoreload now` (also `%autoreload`) - perform autoreload immediately. +- `%autoreload off` (also `%autoreload 0`) - turn off autoreload. +- `%autoreload explicit` (also `%autoreload 1`) - turn on autoreload only for modules + whitelisted by `%aimport` statements. +- `%autoreload all` (also `%autoreload 2`) - turn on autoreload for all modules except those + blacklisted by `%aimport` statements. +- `%autoreload complete` (also `%autoreload 3`) - all the fatures of `all` but also adding new + objects from the imported modules (see + IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects). + +The original designations (e.g. "2") still work, and these new ones are case-insensitive. + +The parsing logic for `%aimport` is now improved such that modules can be whitelisted and +blacklisted in the same line, e.g. it's now possible to call `%aimport os, -math` to include `os` +for `%autoreload explicit` and exclude `math` for modes 2 and 3. + +A new magic command `%averbose` controls printing of the names of modules about to be autoreloaded. +- `%averbose off` / `%averbose 0` - turns off all output (default behavior) +- `%averbose on` / `%averbose 1` - uses `print` to display module name +- `%averbose log` / `%averbose 2` - logs an `INFO` message with the module name + From 29ac662dee739fb58034fc7009aaa5cbfb2078b0 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 16:02:15 -0700 Subject: [PATCH 0240/1752] Fixes from `darker` --- IPython/extensions/autoreload.py | 5 ++- IPython/extensions/tests/test_autoreload.py | 46 +++++++++++---------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 595bd7e5a99..ea0b54cbdcf 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -110,6 +110,7 @@ """ from IPython.core.magic import Magics, magics_class, line_magic + __skip_doctest__ = True # ----------------------------------------------------------------------------- @@ -643,7 +644,9 @@ def averbose(self, parameter_s=""): elif parameter_s == "1" or parameter_s.lower() == "on": self._reloader._report = lambda msg: print(msg) elif parameter_s == "2" or parameter_s.lower() == "log": - self._reloader._report = lambda msg: logging.getLogger('autoreload').info(msg) + self._reloader._report = lambda msg: logging.getLogger("autoreload").info( + msg + ) else: raise ValueError(f'Unrecognized parameter "{parameter_s}".') diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 346733e92e0..1523867593e 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -407,10 +407,12 @@ class AutoreloadSettings: def gather_settings(mode): self.shell.magic_autoreload(mode) module_reloader = self.shell.auto_magics._reloader - return AutoreloadSettings(module_reloader.check_all, - module_reloader.enabled, - module_reloader.autoload_obj - ) + return AutoreloadSettings( + module_reloader.check_all, + module_reloader.enabled, + module_reloader.autoload_obj, + ) + assert gather_settings("0") == gather_settings("off") assert gather_settings("0") == gather_settings("OFF") # Case insensitive assert gather_settings("1") == gather_settings("explicit") @@ -419,24 +421,26 @@ def gather_settings(mode): # And an invalid mode name raises an exception. with self.assertRaises(ValueError): - self.shell.magic_autoreload('4') + self.shell.magic_autoreload("4") def test_aimport_parsing(self): # Modules can be included or excluded all in one line. module_reloader = self.shell.auto_magics._reloader - self.shell.magic_aimport('os') # import and mark `os` for auto-reload. - assert module_reloader.modules['os'] is True - assert 'os' not in module_reloader.skip_modules.keys() - - self.shell.magic_aimport('-math') # forbid autoreloading of `math` - assert module_reloader.skip_modules['math'] is True - assert 'math' not in module_reloader.modules.keys() - - self.shell.magic_aimport('-os, math') # Can do this all in one line; wasn't possible before. - assert module_reloader.modules['math'] is True - assert 'math' not in module_reloader.skip_modules.keys() - assert module_reloader.skip_modules['os'] is True - assert 'os' not in module_reloader.modules.keys() + self.shell.magic_aimport("os") # import and mark `os` for auto-reload. + assert module_reloader.modules["os"] is True + assert "os" not in module_reloader.skip_modules.keys() + + self.shell.magic_aimport("-math") # forbid autoreloading of `math` + assert module_reloader.skip_modules["math"] is True + assert "math" not in module_reloader.modules.keys() + + self.shell.magic_aimport( + "-os, math" + ) # Can do this all in one line; wasn't possible before. + assert module_reloader.modules["math"] is True + assert "math" not in module_reloader.skip_modules.keys() + assert module_reloader.skip_modules["os"] is True + assert "os" not in module_reloader.modules.keys() def test_averbose(self): self.shell.magic_averbose("off") @@ -446,7 +450,7 @@ def func1(): pass """ mod_name, mod_fn = self.new_module(mod_code) self.shell.run_code(f"import {mod_name}") - with tt.AssertPrints('', channel="stdout"): # no output. + with tt.AssertPrints("", channel="stdout"): # no output. self.shell.run_code("pass") self.write_file(mod_fn, mod_code) # "modify" the module @@ -456,7 +460,7 @@ def func1(): pass self.write_file(mod_fn, mod_code) # "modify" the module self.shell.magic_averbose("off") # Should not see anything on next call - with tt.AssertPrints('', channel="stdout"): + with tt.AssertPrints("", channel="stdout"): self.shell.run_code("pass") # TODO: test logging. Why won't this work? @@ -472,7 +476,7 @@ def func1(): pass # And an invalid mode name raises an exception. with self.assertRaises(ValueError): - self.shell.magic_averbose('fax') + self.shell.magic_averbose("fax") def _check_smoketest(self, use_aimport=True): """ From 3eb6aaf5406008c4fdc550eaf966ebd97e5ef720 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 4 Oct 2022 16:04:04 -0700 Subject: [PATCH 0241/1752] doc formatting fixes --- docs/source/whatsnew/pr/autoreload-verbosity.rst | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/source/whatsnew/pr/autoreload-verbosity.rst b/docs/source/whatsnew/pr/autoreload-verbosity.rst index dc5a9aa9479..677c07a26d6 100644 --- a/docs/source/whatsnew/pr/autoreload-verbosity.rst +++ b/docs/source/whatsnew/pr/autoreload-verbosity.rst @@ -4,13 +4,9 @@ Autoreload verbosity We introduce more descriptive names for the `%autoreload` parameter: - `%autoreload now` (also `%autoreload`) - perform autoreload immediately. - `%autoreload off` (also `%autoreload 0`) - turn off autoreload. -- `%autoreload explicit` (also `%autoreload 1`) - turn on autoreload only for modules - whitelisted by `%aimport` statements. -- `%autoreload all` (also `%autoreload 2`) - turn on autoreload for all modules except those - blacklisted by `%aimport` statements. -- `%autoreload complete` (also `%autoreload 3`) - all the fatures of `all` but also adding new - objects from the imported modules (see - IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects). +- `%autoreload explicit` (also `%autoreload 1`) - turn on autoreload only for modules whitelisted by `%aimport` statements. +- `%autoreload all` (also `%autoreload 2`) - turn on autoreload for all modules except those blacklisted by `%aimport` statements. +- `%autoreload complete` (also `%autoreload 3`) - all the fatures of `all` but also adding new objects from the imported modules (see IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects). The original designations (e.g. "2") still work, and these new ones are case-insensitive. From 0238c413f52790562bd5f55e978b6e261ec27446 Mon Sep 17 00:00:00 2001 From: ChandanChainani Date: Fri, 14 Oct 2022 10:39:16 +0530 Subject: [PATCH 0242/1752] Fix doc for `IPython.display.TextDisplayObject` (#13770) --- IPython/core/display.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index 933295ad6ce..a095f3cb5e0 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -389,7 +389,19 @@ def reload(self): class TextDisplayObject(DisplayObject): - """Validate that display data is text""" + """Create a text display object given raw data. + + Parameters + ---------- + data : str or unicode + The raw data or a URL or file to load the data from. + url : unicode + A URL to download the data from. + filename : unicode + Path to a local file to load the data from. + metadata : dict + Dict of metadata associated to be the object when displayed + """ def _check_data(self): if self.data is not None and not isinstance(self.data, str): raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data)) From e2cd7aca43afd18d8294816d39ce800366eab8d3 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 16 Oct 2022 15:01:08 +0200 Subject: [PATCH 0243/1752] MAINT: convert ip.magic to to run_line magic in many places. Too many warnings during test suite make it hard to spot issues. --- IPython/core/tests/test_magic.py | 111 ++++++++++++++++--------------- IPython/core/tests/test_run.py | 94 +++++++++++++------------- 2 files changed, 106 insertions(+), 99 deletions(-) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 1c793ca2915..509dd66dd28 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -84,7 +84,7 @@ def test_extract_symbols_raises_exception_with_non_python_code(): def test_magic_not_found(): # magic not found raises UsageError with pytest.raises(UsageError): - _ip.magic('doesntexist') + _ip.run_line_magic("doesntexist", "") # ensure result isn't success when a magic isn't found result = _ip.run_cell('%doesntexist') @@ -116,13 +116,14 @@ def test_config(): magic. """ ## should not raise. - _ip.magic('config') + _ip.run_line_magic("config", "") + def test_config_available_configs(): """ test that config magic prints available configs in unique and sorted order. """ with capture_output() as captured: - _ip.magic('config') + _ip.run_line_magic("config", "") stdout = captured.stdout config_classes = stdout.strip().split('\n')[1:] @@ -131,7 +132,7 @@ def test_config_available_configs(): def test_config_print_class(): """ test that config with a classname prints the class's options. """ with capture_output() as captured: - _ip.magic('config TerminalInteractiveShell') + _ip.run_line_magic("config", "TerminalInteractiveShell") stdout = captured.stdout assert re.match( @@ -144,7 +145,7 @@ def test_rehashx(): _ip.alias_manager.clear_aliases() del _ip.db['syscmdlist'] - _ip.magic('rehashx') + _ip.run_line_magic("rehashx", "") # Practically ALL ipython development systems will have more than 10 aliases assert len(_ip.alias_manager.aliases) > 10 @@ -277,11 +278,11 @@ def test_macro(): cmds = ["a=1", "def b():\n return a**2", "print(a,b())"] for i, cmd in enumerate(cmds, start=1): ip.history_manager.store_inputs(i, cmd) - ip.magic("macro test 1-3") + ip.run_line_magic("macro", "test 1-3") assert ip.user_ns["test"].value == "\n".join(cmds) + "\n" # List macros - assert "test" in ip.magic("macro") + assert "test" in ip.run_line_magic("macro", "") def test_macro_run(): @@ -302,7 +303,7 @@ def test_magic_magic(): """Test %magic""" ip = get_ipython() with capture_output() as captured: - ip.magic("magic") + ip.run_line_magic("magic", "") stdout = captured.stdout assert "%magic" in stdout @@ -316,7 +317,7 @@ def test_numpy_reset_array_undec(): _ip.ex("import numpy as np") _ip.ex("a = np.empty(2)") assert "a" in _ip.user_ns - _ip.magic("reset -f array") + _ip.run_line_magic("reset", "-f array") assert "a" not in _ip.user_ns @@ -326,7 +327,7 @@ def test_reset_out(): # test '%reset -f out', make an Out prompt _ip.run_cell("parrot", store_history=True) assert "dead" in [_ip.user_ns[x] for x in ("_", "__", "___")] - _ip.magic("reset -f out") + _ip.run_line_magic("reset", "-f out") assert "dead" not in [_ip.user_ns[x] for x in ("_", "__", "___")] assert len(_ip.user_ns["Out"]) == 0 @@ -336,7 +337,7 @@ def test_reset_in(): # test '%reset -f in' _ip.run_cell("parrot", store_history=True) assert "parrot" in [_ip.user_ns[x] for x in ("_i", "_ii", "_iii")] - _ip.magic("%reset -f in") + _ip.run_line_magic("reset", "-f in") assert "parrot" not in [_ip.user_ns[x] for x in ("_i", "_ii", "_iii")] assert len(set(_ip.user_ns["In"])) == 1 @@ -344,10 +345,10 @@ def test_reset_in(): def test_reset_dhist(): "Test '%reset dhist' magic" _ip.run_cell("tmp = [d for d in _dh]") # copy before clearing - _ip.magic("cd " + os.path.dirname(pytest.__file__)) - _ip.magic("cd -") + _ip.run_line_magic("cd", os.path.dirname(pytest.__file__)) + _ip.run_line_magic("cd", "-") assert len(_ip.user_ns["_dh"]) > 0 - _ip.magic("reset -f dhist") + _ip.run_line_magic("reset", "-f dhist") assert len(_ip.user_ns["_dh"]) == 0 _ip.run_cell("_dh = [d for d in tmp]") # restore @@ -472,8 +473,8 @@ def test_time_local_ns(): def test_doctest_mode(): "Toggle doctest_mode twice, it should be a no-op and run without error" - _ip.magic('doctest_mode') - _ip.magic('doctest_mode') + _ip.run_line_magic("doctest_mode", "") + _ip.run_line_magic("doctest_mode", "") def test_parse_options(): @@ -498,7 +499,9 @@ def test_parse_options_preserve_non_option_string(): def test_run_magic_preserve_code_block(): """Test to assert preservation of non-option part of magic-block, while running magic.""" _ip.user_ns["spaces"] = [] - _ip.magic("timeit -n1 -r1 spaces.append([s.count(' ') for s in ['document']])") + _ip.run_line_magic( + "timeit", "-n1 -r1 spaces.append([s.count(' ') for s in ['document']])" + ) assert _ip.user_ns["spaces"] == [[0]] @@ -509,13 +512,13 @@ def test_dirops(): startdir = os.getcwd() ipdir = os.path.realpath(_ip.ipython_dir) try: - _ip.magic('cd "%s"' % ipdir) + _ip.run_line_magic("cd", '"%s"' % ipdir) assert curpath() == ipdir - _ip.magic('cd -') + _ip.run_line_magic("cd", "-") assert curpath() == startdir - _ip.magic('pushd "%s"' % ipdir) + _ip.run_line_magic("pushd", '"%s"' % ipdir) assert curpath() == ipdir - _ip.magic('popd') + _ip.run_line_magic("popd", "") assert curpath() == startdir finally: os.chdir(startdir) @@ -542,7 +545,7 @@ def test_xmode(): # Calling xmode three times should be a no-op xmode = _ip.InteractiveTB.mode for i in range(4): - _ip.magic("xmode") + _ip.run_line_magic("xmode", "") assert _ip.InteractiveTB.mode == xmode def test_reset_hard(): @@ -557,7 +560,7 @@ def __repr__(self): _ip.run_cell("a") assert monitor == [] - _ip.magic("reset -f") + _ip.run_line_magic("reset", "-f") assert monitor == [1] class TestXdel(tt.TempFileMixin): @@ -570,14 +573,14 @@ def test_xdel(self): "a = A()\n") self.mktmp(src) # %run creates some hidden references... - _ip.magic("run %s" % self.fname) + _ip.run_line_magic("run", "%s" % self.fname) # ... as does the displayhook. _ip.run_cell("a") monitor = _ip.user_ns["A"].monitor assert monitor == [] - _ip.magic("xdel a") + _ip.run_line_magic("xdel", "a") # Check that a's __del__ method has been called. gc.collect(0) @@ -614,7 +617,7 @@ class A(object): def __repr__(self): raise Exception() _ip.user_ns['a'] = A() - _ip.magic("whos") + _ip.run_line_magic("whos", "") def doctest_precision(): """doctest for %precision @@ -655,12 +658,12 @@ def test_psearch(): def test_timeit_shlex(): """test shlex issues with timeit (#1109)""" _ip.ex("def f(*a,**kw): pass") - _ip.magic('timeit -n1 "this is a bug".count(" ")') - _ip.magic('timeit -r1 -n1 f(" ", 1)') - _ip.magic('timeit -r1 -n1 f(" ", 1, " ", 2, " ")') - _ip.magic('timeit -r1 -n1 ("a " + "b")') - _ip.magic('timeit -r1 -n1 f("a " + "b")') - _ip.magic('timeit -r1 -n1 f("a " + "b ")') + _ip.run_line_magic("timeit", '-n1 "this is a bug".count(" ")') + _ip.run_line_magic("timeit", '-r1 -n1 f(" ", 1)') + _ip.run_line_magic("timeit", '-r1 -n1 f(" ", 1, " ", 2, " ")') + _ip.run_line_magic("timeit", '-r1 -n1 ("a " + "b")') + _ip.run_line_magic("timeit", '-r1 -n1 f("a " + "b")') + _ip.run_line_magic("timeit", '-r1 -n1 f("a " + "b ")') def test_timeit_special_syntax(): @@ -738,9 +741,9 @@ def test_extension(): try: _ip.user_ns.pop('arq', None) invalidate_caches() # Clear import caches - _ip.magic("load_ext daft_extension") + _ip.run_line_magic("load_ext", "daft_extension") assert _ip.user_ns["arq"] == 185 - _ip.magic("unload_ext daft_extension") + _ip.run_line_magic("unload_ext", "daft_extension") assert 'arq' not in _ip.user_ns finally: sys.path.remove(daft_path) @@ -755,17 +758,17 @@ def test_notebook_export_json(): _ip.history_manager.store_inputs(i, cmd) with TemporaryDirectory() as td: outfile = os.path.join(td, "nb.ipynb") - _ip.magic("notebook %s" % outfile) + _ip.run_line_magic("notebook", "%s" % outfile) class TestEnv(TestCase): def test_env(self): - env = _ip.magic("env") + env = _ip.run_line_magic("env", "") self.assertTrue(isinstance(env, dict)) def test_env_secret(self): - env = _ip.magic("env") + env = _ip.run_line_magic("env", "") hidden = "" with mock.patch.dict( os.environ, @@ -776,35 +779,35 @@ def test_env_secret(self): "VAR": "abc" } ): - env = _ip.magic("env") + env = _ip.run_line_magic("env", "") assert env["API_KEY"] == hidden assert env["SECRET_THING"] == hidden assert env["JUPYTER_TOKEN"] == hidden assert env["VAR"] == "abc" def test_env_get_set_simple(self): - env = _ip.magic("env var val1") + env = _ip.run_line_magic("env", "var val1") self.assertEqual(env, None) - self.assertEqual(os.environ['var'], 'val1') - self.assertEqual(_ip.magic("env var"), 'val1') - env = _ip.magic("env var=val2") + self.assertEqual(os.environ["var"], "val1") + self.assertEqual(_ip.run_line_magic("env", "var"), "val1") + env = _ip.run_line_magic("env", "var=val2") self.assertEqual(env, None) self.assertEqual(os.environ['var'], 'val2') def test_env_get_set_complex(self): - env = _ip.magic("env var 'val1 '' 'val2") + env = _ip.run_line_magic("env", "var 'val1 '' 'val2") self.assertEqual(env, None) self.assertEqual(os.environ['var'], "'val1 '' 'val2") - self.assertEqual(_ip.magic("env var"), "'val1 '' 'val2") - env = _ip.magic('env var=val2 val3="val4') + self.assertEqual(_ip.run_line_magic("env", "var"), "'val1 '' 'val2") + env = _ip.run_line_magic("env", 'var=val2 val3="val4') self.assertEqual(env, None) self.assertEqual(os.environ['var'], 'val2 val3="val4') def test_env_set_bad_input(self): - self.assertRaises(UsageError, lambda: _ip.magic("set_env var")) + self.assertRaises(UsageError, lambda: _ip.run_line_magic("set_env", "var")) def test_env_set_whitespace(self): - self.assertRaises(UsageError, lambda: _ip.magic("env var A=B")) + self.assertRaises(UsageError, lambda: _ip.run_line_magic("env", "var A=B")) class CellMagicTestCase(TestCase): @@ -1308,7 +1311,7 @@ def test_ls_magic(): ip = get_ipython() json_formatter = ip.display_formatter.formatters['application/json'] json_formatter.enabled = True - lsmagic = ip.magic('lsmagic') + lsmagic = ip.run_line_magic("lsmagic", "") with warnings.catch_warnings(record=True) as w: j = json_formatter(lsmagic) assert sorted(j) == ["cell", "line"] @@ -1358,16 +1361,16 @@ def test_logging_magic_not_quiet(): def test_time_no_var_expand(): - _ip.user_ns['a'] = 5 - _ip.user_ns['b'] = [] - _ip.magic('time b.append("{a}")') - assert _ip.user_ns['b'] == ['{a}'] + _ip.user_ns["a"] = 5 + _ip.user_ns["b"] = [] + _ip.run_line_magic("time", 'b.append("{a}")') + assert _ip.user_ns["b"] == ["{a}"] # this is slow, put at the end for local testing. def test_timeit_arguments(): "Test valid timeit arguments, should not cause SyntaxError (GH #1269)" - _ip.magic("timeit -n1 -r1 a=('#')") + _ip.run_line_magic("timeit", "-n1 -r1 a=('#')") MINIMAL_LAZY_MAGIC = """ @@ -1442,7 +1445,7 @@ def get_data(self, path): sys.meta_path.insert(0, MyTempImporter()) with capture_output() as captured: - _ip.magic("run -m my_tmp") + _ip.run_line_magic("run", "-m my_tmp") _ip.run_cell("import my_tmp") output = "Loaded my_tmp\nI just ran a script\nLoaded my_tmp\n" diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py index ae20ce6096b..e27ba2cade8 100644 --- a/IPython/core/tests/test_run.py +++ b/IPython/core/tests/test_run.py @@ -180,13 +180,13 @@ def run_tmpfile(self): _ip = get_ipython() # This fails on Windows if self.tmpfile.name has spaces or "~" in it. # See below and ticket https://bugs.launchpad.net/bugs/366353 - _ip.magic('run %s' % self.fname) + _ip.run_line_magic("run", self.fname) def run_tmpfile_p(self): _ip = get_ipython() # This fails on Windows if self.tmpfile.name has spaces or "~" in it. # See below and ticket https://bugs.launchpad.net/bugs/366353 - _ip.magic('run -p %s' % self.fname) + _ip.run_line_magic("run", "-p %s" % self.fname) def test_builtins_id(self): """Check that %run doesn't damage __builtins__ """ @@ -216,20 +216,20 @@ def test_run_profile(self): def test_run_debug_twice(self): # https://github.com/ipython/ipython/issues/10028 _ip = get_ipython() - with tt.fake_input(['c']): - _ip.magic('run -d %s' % self.fname) - with tt.fake_input(['c']): - _ip.magic('run -d %s' % self.fname) + with tt.fake_input(["c"]): + _ip.run_line_magic("run", "-d %s" % self.fname) + with tt.fake_input(["c"]): + _ip.run_line_magic("run", "-d %s" % self.fname) def test_run_debug_twice_with_breakpoint(self): """Make a valid python temp file.""" _ip = get_ipython() - with tt.fake_input(['b 2', 'c', 'c']): - _ip.magic('run -d %s' % self.fname) + with tt.fake_input(["b 2", "c", "c"]): + _ip.run_line_magic("run", "-d %s" % self.fname) - with tt.fake_input(['c']): - with tt.AssertNotPrints('KeyError'): - _ip.magic('run -d %s' % self.fname) + with tt.fake_input(["c"]): + with tt.AssertNotPrints("KeyError"): + _ip.run_line_magic("run", "-d %s" % self.fname) class TestMagicRunSimple(tt.TempFileMixin): @@ -239,7 +239,7 @@ def test_simpledef(self): src = ("class foo: pass\n" "def f(): return foo()") self.mktmp(src) - _ip.magic("run %s" % self.fname) + _ip.run_line_magic("run", str(self.fname)) _ip.run_cell("t = isinstance(f(), foo)") assert _ip.user_ns["t"] is True @@ -277,7 +277,7 @@ def test_aggressive_namespace_cleanup(self): " break\n" % ("run " + empty.fname) ) self.mktmp(src) - _ip.magic("run %s" % self.fname) + _ip.run_line_magic("run", str(self.fname)) _ip.run_cell("ip == get_ipython()") assert _ip.user_ns["i"] == 4 @@ -288,8 +288,8 @@ def test_run_second(self): with tt.TempFileMixin() as empty: empty.mktmp("") - _ip.magic("run %s" % self.fname) - _ip.magic("run %s" % empty.fname) + _ip.run_line_magic("run", self.fname) + _ip.run_line_magic("run", empty.fname) assert _ip.user_ns["afunc"]() == 1 def test_tclass(self): @@ -323,23 +323,23 @@ def test_run_i_after_reset(self): self.mktmp(src) _ip.run_cell("zz = 23") try: - _ip.magic("run -i %s" % self.fname) + _ip.run_line_magic("run", "-i %s" % self.fname) assert _ip.user_ns["yy"] == 23 finally: - _ip.magic('reset -f') + _ip.run_line_magic("reset", "-f") _ip.run_cell("zz = 23") try: - _ip.magic("run -i %s" % self.fname) + _ip.run_line_magic("run", "-i %s" % self.fname) assert _ip.user_ns["yy"] == 23 finally: - _ip.magic('reset -f') + _ip.run_line_magic("reset", "-f") def test_unicode(self): """Check that files in odd encodings are accepted.""" mydir = os.path.dirname(__file__) - na = os.path.join(mydir, 'nonascii.py') - _ip.magic('run "%s"' % na) + na = os.path.join(mydir, "nonascii.py") + _ip.run_line_magic("run", "%r" % na) assert _ip.user_ns["u"] == "Ўт№Ф" def test_run_py_file_attribute(self): @@ -347,9 +347,9 @@ def test_run_py_file_attribute(self): src = "t = __file__\n" self.mktmp(src) _missing = object() - file1 = _ip.user_ns.get('__file__', _missing) - _ip.magic('run %s' % self.fname) - file2 = _ip.user_ns.get('__file__', _missing) + file1 = _ip.user_ns.get("__file__", _missing) + _ip.run_line_magic("run", self.fname) + file2 = _ip.user_ns.get("__file__", _missing) # Check that __file__ was equal to the filename in the script's # namespace. @@ -363,9 +363,9 @@ def test_run_ipy_file_attribute(self): src = "t = __file__\n" self.mktmp(src, ext='.ipy') _missing = object() - file1 = _ip.user_ns.get('__file__', _missing) - _ip.magic('run %s' % self.fname) - file2 = _ip.user_ns.get('__file__', _missing) + file1 = _ip.user_ns.get("__file__", _missing) + _ip.run_line_magic("run", self.fname) + file2 = _ip.user_ns.get("__file__", _missing) # Check that __file__ was equal to the filename in the script's # namespace. @@ -378,18 +378,18 @@ def test_run_formatting(self): """ Test that %run -t -N does not raise a TypeError for N > 1.""" src = "pass" self.mktmp(src) - _ip.magic('run -t -N 1 %s' % self.fname) - _ip.magic('run -t -N 10 %s' % self.fname) + _ip.run_line_magic("run", "-t -N 1 %s" % self.fname) + _ip.run_line_magic("run", "-t -N 10 %s" % self.fname) def test_ignore_sys_exit(self): """Test the -e option to ignore sys.exit()""" src = "import sys; sys.exit(1)" self.mktmp(src) - with tt.AssertPrints('SystemExit'): - _ip.magic('run %s' % self.fname) + with tt.AssertPrints("SystemExit"): + _ip.run_line_magic("run", self.fname) - with tt.AssertNotPrints('SystemExit'): - _ip.magic('run -e %s' % self.fname) + with tt.AssertNotPrints("SystemExit"): + _ip.run_line_magic("run", "-e %s" % self.fname) def test_run_nb(self): """Test %run notebook.ipynb""" @@ -404,7 +404,7 @@ def test_run_nb(self): src = writes(nb, version=4) self.mktmp(src, ext='.ipynb') - _ip.magic("run %s" % self.fname) + _ip.run_line_magic("run", self.fname) assert _ip.user_ns["answer"] == 42 @@ -478,12 +478,16 @@ def tearDown(self): sys.path[:] = [p for p in sys.path if p != self.tempdir.name] self.tempdir.cleanup() - def check_run_submodule(self, submodule, opts=''): - _ip.user_ns.pop('x', None) - _ip.magic('run {2} -m {0}.{1}'.format(self.package, submodule, opts)) - self.assertEqual(_ip.user_ns['x'], self.value, - 'Variable `x` is not loaded from module `{0}`.' - .format(submodule)) + def check_run_submodule(self, submodule, opts=""): + _ip.user_ns.pop("x", None) + _ip.run_line_magic( + "run", "{2} -m {0}.{1}".format(self.package, submodule, opts) + ) + self.assertEqual( + _ip.user_ns["x"], + self.value, + "Variable `x` is not loaded from module `{0}`.".format(submodule), + ) def test_run_submodule_with_absolute_import(self): self.check_run_submodule('absolute') @@ -533,17 +537,17 @@ def test_run__name__(): f.write("q = __name__") _ip.user_ns.pop("q", None) - _ip.magic("run {}".format(path)) + _ip.run_line_magic("run", "{}".format(path)) assert _ip.user_ns.pop("q") == "__main__" - _ip.magic("run -n {}".format(path)) + _ip.run_line_magic("run", "-n {}".format(path)) assert _ip.user_ns.pop("q") == "foo" try: - _ip.magic("run -i -n {}".format(path)) + _ip.run_line_magic("run", "-i -n {}".format(path)) assert _ip.user_ns.pop("q") == "foo" finally: - _ip.magic('reset -f') + _ip.run_line_magic("reset", "-f") def test_run_tb(): @@ -563,7 +567,7 @@ def test_run_tb(): ) ) with capture_output() as io: - _ip.magic('run {}'.format(path)) + _ip.run_line_magic("run", "{}".format(path)) out = io.stdout assert "execfile" not in out assert "RuntimeError" in out From 5e67aa3b5849d9d2cf708a75cbf951b02b1ae2d9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 16 Oct 2022 14:28:25 +0200 Subject: [PATCH 0244/1752] Support square bracket indexing with literal with pinfo/?/pinfo2/?? We add limited support for evaluating `[xxx]` where xxx is a numeric literal. This still abit dangerous as __getindex__ can run arbitrary code. We could support a wider range of constructs, but this would start to reach into complex and dangerous territory. Alredy this with lead to weird suff like >>> def foo(*arg, **kwargs):pass >>> c = [foo] >>> c[0]? Signature: c[0](*args, **kwargs) Instead of >>> c[0]? Signature: foo(*args, **kwargs) As the name of the object is used in the signature is the one if the main namespace. This should fix https://github.com/ipython/ipython/issues/13780 --- IPython/core/inputtransformer2.py | 24 ++++++--- IPython/core/interactiveshell.py | 62 ++++++++++++++++------ IPython/core/tests/test_oinspect.py | 53 +++++++++++++++++- IPython/testing/plugin/pytest_ipdoctest.py | 2 +- 4 files changed, 116 insertions(+), 25 deletions(-) diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py index e4e385a95ff..432e091fcdb 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -429,9 +429,11 @@ def transform(self, lines): return lines_before + [new_line] + lines_after -_help_end_re = re.compile(r"""(%{0,2} + +_help_end_re = re.compile( + r"""(%{0,2} (?!\d)[\w*]+ # Variable name - (\.(?!\d)[\w*]+)* # .etc.etc + (\.(?!\d)[\w*]+|\[[0-9]+\])* # .etc.etc or [0], we only support literal integers. ) (\?\??)$ # ? or ?? """, @@ -464,10 +466,11 @@ def find(cls, tokens_by_line): def transform(self, lines): """Transform a help command found by the ``find()`` classmethod. """ - piece = ''.join(lines[self.start_line:self.q_line+1]) - indent, content = piece[:self.start_col], piece[self.start_col:] - lines_before = lines[:self.start_line] - lines_after = lines[self.q_line + 1:] + + piece = "".join(lines[self.start_line : self.q_line + 1]) + indent, content = piece[: self.start_col], piece[self.start_col :] + lines_before = lines[: self.start_line] + lines_after = lines[self.q_line + 1 :] m = _help_end_re.search(content) if not m: @@ -543,8 +546,13 @@ def has_sunken_brackets(tokens: List[tokenize.TokenInfo]): def show_linewise_tokens(s: str): """For investigation and debugging""" - if not s.endswith('\n'): - s += '\n' + warnings.warn( + "show_linewise_tokens is deprecated since IPython 8.6", + DeprecationWarning, + stacklevel=2, + ) + if not s.endswith("\n"): + s += "\n" lines = s.splitlines(keepends=True) for line in make_tokens_by_line(lines): print("Line -------") diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 06912640673..1c422a59975 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -213,14 +213,17 @@ def __repr__(self): raw_cell = ( (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell ) - return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % ( - name, - id(self), - raw_cell, - self.store_history, - self.silent, - self.shell_futures, - self.cell_id, + return ( + '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' + % ( + name, + id(self), + raw_cell, + self.store_history, + self.silent, + self.shell_futures, + self.cell_id, + ) ) @@ -1537,10 +1540,33 @@ def _ofind(self, oname, namespaces=None): Has special code to detect magic functions. """ oname = oname.strip() - if not oname.startswith(ESC_MAGIC) and \ - not oname.startswith(ESC_MAGIC2) and \ - not all(a.isidentifier() for a in oname.split(".")): - return {'found': False} + raw_parts = oname.split(".") + parts = [] + parts_ok = True + for p in raw_parts: + if p.endswith("]"): + var, *indices = p.split("[") + if not var.isidentifier(): + parts_ok = False + break + parts.append(var) + for ind in indices: + if ind[-1] != "]" and not ind[:-1].isnumeric(): + parts_ok = False + break + parts.append(ind[:-1]) + continue + + if not p.isidentifier(): + parts_ok = False + parts.append(p) + + if ( + not oname.startswith(ESC_MAGIC) + and not oname.startswith(ESC_MAGIC2) + and not parts_ok + ): + return {"found": False} if namespaces is None: # Namespaces to search in: @@ -1562,7 +1588,7 @@ def _ofind(self, oname, namespaces=None): # Look for the given name by splitting it in parts. If the head is # found, then we look for all the remaining parts as members, and only # declare success if we can find them all. - oname_parts = oname.split('.') + oname_parts = parts oname_head, oname_rest = oname_parts[0],oname_parts[1:] for nsname,ns in namespaces: try: @@ -1579,7 +1605,10 @@ def _ofind(self, oname, namespaces=None): if idx == len(oname_rest) - 1: obj = self._getattr_property(obj, part) else: - obj = getattr(obj, part) + if part.isnumeric(): + obj = obj[int(part)] + else: + obj = getattr(obj, part) except: # Blanket except b/c some badly implemented objects # allow __getattr__ to raise exceptions other than @@ -1643,7 +1672,10 @@ def _getattr_property(obj, attrname): # # The universal alternative is to traverse the mro manually # searching for attrname in class dicts. - attr = getattr(type(obj), attrname) + if attrname.isnumeric(): + return obj[int(attrname)] + else: + attr = getattr(type(obj), attrname) except AttributeError: pass else: diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index 94deb356a88..f2312804acd 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -5,6 +5,7 @@ # Distributed under the terms of the Modified BSD License. +from contextlib import contextmanager from inspect import signature, Signature, Parameter import inspect import os @@ -43,7 +44,7 @@ class SourceModuleMainTest: # defined, if any code is inserted above, the following line will need to be # updated. Do NOT insert any whitespace between the next line and the function # definition below. -THIS_LINE_NUMBER = 46 # Put here the actual number of this line +THIS_LINE_NUMBER = 47 # Put here the actual number of this line def test_find_source_lines(): @@ -345,6 +346,56 @@ def foo(): pass inspector.pdef(foo, 'foo') +@contextmanager +def cleanup_user_ns(**kwargs): + """ + On exit delete all the keys that were not in user_ns before entering. + + It does not restore old values ! + + Parameters + ---------- + + **kwargs + used to update ip.user_ns + + """ + try: + known = set(ip.user_ns.keys()) + ip.user_ns.update(kwargs) + yield + finally: + added = set(ip.user_ns.keys()) - known + for k in added: + del ip.user_ns[k] + + +def test_pinfo_getindex(): + def dummy(): + """ + MARKER + """ + + container = [dummy] + with cleanup_user_ns(container=container): + with AssertPrints("MARKER"): + ip._inspect("pinfo", "container[0]", detail_level=0) + assert "container" not in ip.user_ns.keys() + + +def test_qmark_getindex(): + def dummy(): + """ + MARKER 2 + """ + + container = [dummy] + with cleanup_user_ns(container=container): + with AssertPrints("MARKER 2"): + ip.run_cell("container[0]?") + assert "container" not in ip.user_ns.keys() + + def test_pinfo_nonascii(): # See gh-1177 from . import nonascii2 diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 809713d7c8e..4ba2f1adf8e 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + if float(w.group()) == approx(float(g.group()), abs=10**-precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. From 44a6127199f95c09792dfe097270ec124ae0bf0e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 16 Oct 2022 15:35:59 +0200 Subject: [PATCH 0245/1752] revert fialing on windows --- IPython/core/tests/test_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py index e27ba2cade8..9687786b46a 100644 --- a/IPython/core/tests/test_run.py +++ b/IPython/core/tests/test_run.py @@ -339,7 +339,7 @@ def test_unicode(self): """Check that files in odd encodings are accepted.""" mydir = os.path.dirname(__file__) na = os.path.join(mydir, "nonascii.py") - _ip.run_line_magic("run", "%r" % na) + _ip.magic('run "%s"' % na) assert _ip.user_ns["u"] == "Ўт№Ф" def test_run_py_file_attribute(self): From 6c9912fc5b73171c1c362a90fa3f6e36e81fa375 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 16 Oct 2022 15:45:03 +0200 Subject: [PATCH 0246/1752] reformat with same version of balck --- IPython/core/interactiveshell.py | 19 ++++++++----------- IPython/testing/plugin/pytest_ipdoctest.py | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 1c422a59975..9b3cc06ea13 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -213,17 +213,14 @@ def __repr__(self): raw_cell = ( (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell ) - return ( - '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' - % ( - name, - id(self), - raw_cell, - self.store_history, - self.silent, - self.shell_futures, - self.cell_id, - ) + return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % ( + name, + id(self), + raw_cell, + self.store_history, + self.silent, + self.shell_futures, + self.cell_id, ) diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 4ba2f1adf8e..809713d7c8e 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10**-precision): + if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. From 56c8a1e6f82ea7032ff8435cee3c5b3c9ab00b56 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Oct 2022 09:05:00 +0200 Subject: [PATCH 0247/1752] support negative indexing --- IPython/core/inputtransformer2.py | 14 ++++++++------ IPython/core/interactiveshell.py | 17 ++++++++++++++--- IPython/core/tests/test_oinspect.py | 13 +++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py index 432e091fcdb..37f0e7699c4 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -432,12 +432,14 @@ def transform(self, lines): _help_end_re = re.compile( r"""(%{0,2} - (?!\d)[\w*]+ # Variable name - (\.(?!\d)[\w*]+|\[[0-9]+\])* # .etc.etc or [0], we only support literal integers. - ) - (\?\??)$ # ? or ?? - """, - re.VERBOSE) + (?!\d)[\w*]+ # Variable name + (\.(?!\d)[\w*]+|\[-?[0-9]+\])* # .etc.etc or [0], we only support literal integers. + ) + (\?\??)$ # ? or ?? + """, + re.VERBOSE, +) + class HelpEnd(TokenTransformBase): """Transformer for help syntax: obj? and obj??""" diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 9b3cc06ea13..04094d74b06 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -147,6 +147,17 @@ class ProvisionalWarning(DeprecationWarning): # Utilities #----------------------------------------------------------------------------- +def is_integer_string(s:str): + """ + Variant of "str.isnumeric()" that allow negative values and other ints. + """ + try: + int(s) + return True + except ValueError: + return False + raise ValueError('Unexpected error') + @undoc def softspace(file, newvalue): """Copied from code.py, to remove the dependency""" @@ -1548,7 +1559,7 @@ def _ofind(self, oname, namespaces=None): break parts.append(var) for ind in indices: - if ind[-1] != "]" and not ind[:-1].isnumeric(): + if ind[-1] != "]" and not is_integer_string(ind[:-1]): parts_ok = False break parts.append(ind[:-1]) @@ -1602,7 +1613,7 @@ def _ofind(self, oname, namespaces=None): if idx == len(oname_rest) - 1: obj = self._getattr_property(obj, part) else: - if part.isnumeric(): + if is_integer_string(part): obj = obj[int(part)] else: obj = getattr(obj, part) @@ -1669,7 +1680,7 @@ def _getattr_property(obj, attrname): # # The universal alternative is to traverse the mro manually # searching for attrname in class dicts. - if attrname.isnumeric(): + if is_integer_string(attrname): return obj[int(attrname)] else: attr = getattr(type(obj), attrname) diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index f2312804acd..0c7c0da6793 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -395,6 +395,19 @@ def dummy(): ip.run_cell("container[0]?") assert "container" not in ip.user_ns.keys() +def test_qmark_getindex_negatif(): + def dummy(): + """ + MARKER 3 + """ + + container = [dummy] + with cleanup_user_ns(container=container): + with AssertPrints("MARKER 3"): + ip.run_cell("container[-1]?") + assert "container" not in ip.user_ns.keys() + + def test_pinfo_nonascii(): # See gh-1177 From 7fec05a42311e9d37e5d9467ffc5924abae4feb2 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 17 Oct 2022 09:09:29 +0200 Subject: [PATCH 0248/1752] formatting --- IPython/core/interactiveshell.py | 6 ++++-- IPython/core/tests/test_oinspect.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 04094d74b06..649ceba903c 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -147,7 +147,8 @@ class ProvisionalWarning(DeprecationWarning): # Utilities #----------------------------------------------------------------------------- -def is_integer_string(s:str): + +def is_integer_string(s: str): """ Variant of "str.isnumeric()" that allow negative values and other ints. """ @@ -156,7 +157,8 @@ def is_integer_string(s:str): return True except ValueError: return False - raise ValueError('Unexpected error') + raise ValueError("Unexpected error") + @undoc def softspace(file, newvalue): diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index 0c7c0da6793..8ae146fa49a 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -395,6 +395,7 @@ def dummy(): ip.run_cell("container[0]?") assert "container" not in ip.user_ns.keys() + def test_qmark_getindex_negatif(): def dummy(): """ From 1dca7fe12fb98a79256d6eb8212ec400eaa240b9 Mon Sep 17 00:00:00 2001 From: jcollins1983 Date: Tue, 18 Oct 2022 07:34:44 +1100 Subject: [PATCH 0249/1752] Update namespace.py Correct unattended to unintended in reset method docstring. --- IPython/core/magics/namespace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/magics/namespace.py b/IPython/core/magics/namespace.py index c86d3de9b65..5da8f7161a0 100644 --- a/IPython/core/magics/namespace.py +++ b/IPython/core/magics/namespace.py @@ -492,7 +492,7 @@ def reset(self, parameter_s=''): --aggressive Try to aggressively remove modules from sys.modules ; this may allow you to reimport Python modules that have been updated and - pick up changes, but can have unattended consequences. + pick up changes, but can have unintended consequences. in reset input history From 3a5c2e2e3b9671257534bad318ffa807a91dc55d Mon Sep 17 00:00:00 2001 From: Hassan Tahir Date: Thu, 20 Oct 2022 23:14:53 +0500 Subject: [PATCH 0250/1752] Fixed Tidelift Badge --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e38b7120e6b..0371848061e 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,8 @@ .. image:: https://raster.shields.io/badge/Follows-NEP29-brightgreen.png :target: https://numpy.org/neps/nep-0029-deprecation_policy.html -.. image:: https://tidelift.com/subscription/pkg/pypi-ipython - :target: https://tidelift.com/badges/package/pypi/ipython?style=flat +.. image:: https://tidelift.com/badges/package/pypi/ipython?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-ipython =========================================== From e3ae64a182e8628d192f3ab6b31eb330670dbf88 Mon Sep 17 00:00:00 2001 From: Frazer McLean Date: Fri, 28 Oct 2022 18:50:07 +0200 Subject: [PATCH 0251/1752] Use SPDX license expression in project metadata --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index a7e48a17b83..ae8b15ec39e 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -36,7 +36,7 @@ kernel_protocol_version_info = (5, 0) kernel_protocol_version = "%i.%i" % kernel_protocol_version_info -license = 'BSD' +license = 'BSD-3-Clause' authors = {'Fernando' : ('Fernando Perez','fperez.net@gmail.com'), 'Janko' : ('Janko Hauser','jhauser@zscout.de'), From de9895dc85c43b2d7719ce49a2a24127ac51f215 Mon Sep 17 00:00:00 2001 From: anakimluke Date: Fri, 28 Oct 2022 20:06:20 -0300 Subject: [PATCH 0252/1752] fix minor typo in details.rst --- docs/source/config/details.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/config/details.rst b/docs/source/config/details.rst index 9e63232d81d..3cc310a4b96 100644 --- a/docs/source/config/details.rst +++ b/docs/source/config/details.rst @@ -69,7 +69,7 @@ shell: /home/bob >>> # it works -See ``IPython/example/utils/cwd_prompt.py`` for an example of how to write an +See ``IPython/example/utils/cwd_prompt.py`` for an example of how to write extensions to customise prompts. Inside IPython or in a startup script, you can use a custom prompts class From 033545a951e839c9de6a2600f10a80aa7657f271 Mon Sep 17 00:00:00 2001 From: Maciej Goszczycki Date: Sun, 3 Apr 2022 05:25:25 +0200 Subject: [PATCH 0253/1752] Prevent the terminal title from being set if the output is not a tty Fixes #11482 --- IPython/terminal/interactiveshell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index b7739c885a4..cf2ab42776e 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -405,14 +405,14 @@ def _autosuggestions_provider_changed(self, change): @observe('term_title') def init_term_title(self, change=None): # Enable or disable the terminal title. - if self.term_title: + if self.term_title and _is_tty: toggle_set_term_title(True) set_term_title(self.term_title_format.format(cwd=abbrev_cwd())) else: toggle_set_term_title(False) def restore_term_title(self): - if self.term_title: + if self.term_title and _is_tty: restore_term_title() def init_display_formatter(self): From 71f7729bba60f8e3e175bc0201a3c2f738d7a395 Mon Sep 17 00:00:00 2001 From: Maciej Goszczycki Date: Sun, 3 Apr 2022 05:28:50 +0200 Subject: [PATCH 0254/1752] Restore terminal title when not running non-interactively --- IPython/terminal/ipapp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index a87eb2f4434..df4648b8914 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -318,6 +318,7 @@ def start(self): self.shell.mainloop() else: self.log.debug("IPython not interactive...") + self.shell.restore_term_title() if not self.shell.last_execution_succeeded: sys.exit(1) From d183ce0cc9e7194658fa042d910a0d2b56ea40ba Mon Sep 17 00:00:00 2001 From: Maciej Goszczycki Date: Sun, 3 Apr 2022 05:30:06 +0200 Subject: [PATCH 0255/1752] Only save the initial terminal title --- IPython/utils/terminal.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/IPython/utils/terminal.py b/IPython/utils/terminal.py index 49fd3fe1739..cba499b711c 100644 --- a/IPython/utils/terminal.py +++ b/IPython/utils/terminal.py @@ -62,15 +62,27 @@ def _restore_term_title(): pass +_xterm_term_title_saved = False + + def _set_term_title_xterm(title): """ Change virtual terminal title in xterm-workalikes """ - # save the current title to the xterm "stack" - sys.stdout.write('\033[22;0t') + global _xterm_term_title_saved + # Only save the title the first time we set, otherwise restore will only + # go back one title (probably undoing a %cd title change). + if not _xterm_term_title_saved: + # save the current title to the xterm "stack" + sys.stdout.write('\033[22;0t') + _xterm_term_title_saved = True sys.stdout.write('\033]0;%s\007' % title) def _restore_term_title_xterm(): + # Make sure the restore has at least one accompanying set. + global _xterm_term_title_saved + assert _xterm_term_title_saved sys.stdout.write('\033[23;0t') + _xterm_term_title_saved = False if os.name == 'posix': From ed2fdc3dc2ef5875fbd8e701cfec4a4061fbbe75 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 12:14:14 +0200 Subject: [PATCH 0256/1752] Fix formatting --- IPython/utils/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/utils/terminal.py b/IPython/utils/terminal.py index cba499b711c..161a9ae6042 100644 --- a/IPython/utils/terminal.py +++ b/IPython/utils/terminal.py @@ -72,7 +72,7 @@ def _set_term_title_xterm(title): # go back one title (probably undoing a %cd title change). if not _xterm_term_title_saved: # save the current title to the xterm "stack" - sys.stdout.write('\033[22;0t') + sys.stdout.write("\033[22;0t") _xterm_term_title_saved = True sys.stdout.write('\033]0;%s\007' % title) From e7d5820fd3fd8089b147482b85f06248f0e0a3b9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 12:54:05 +0200 Subject: [PATCH 0257/1752] pin darker, upgrade black --- .github/workflows/python-package.yml | 2 +- IPython/core/interactiveshell.py | 19 +++++++++++-------- IPython/core/release.py | 2 +- IPython/testing/plugin/pytest_ipdoctest.py | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 62667b48d38..fc1f19e0912 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install darker black==21.12b0 + pip install darker==1.5.1 black==22.10.0 - name: Lint with darker run: | darker -r 60625f241f298b5039cb2debc365db38aa7bb522 --check --diff . || ( diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 649ceba903c..6d04846081a 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -226,14 +226,17 @@ def __repr__(self): raw_cell = ( (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell ) - return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' % ( - name, - id(self), - raw_cell, - self.store_history, - self.silent, - self.shell_futures, - self.cell_id, + return ( + '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>' + % ( + name, + id(self), + raw_cell, + self.store_history, + self.silent, + self.shell_futures, + self.cell_id, + ) ) diff --git a/IPython/core/release.py b/IPython/core/release.py index ae8b15ec39e..bd0ac2161e3 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -36,7 +36,7 @@ kernel_protocol_version_info = (5, 0) kernel_protocol_version = "%i.%i" % kernel_protocol_version_info -license = 'BSD-3-Clause' +license = "BSD-3-Clause" authors = {'Fernando' : ('Fernando Perez','fperez.net@gmail.com'), 'Janko' : ('Janko Hauser','jhauser@zscout.de'), diff --git a/IPython/testing/plugin/pytest_ipdoctest.py b/IPython/testing/plugin/pytest_ipdoctest.py index 809713d7c8e..4ba2f1adf8e 100644 --- a/IPython/testing/plugin/pytest_ipdoctest.py +++ b/IPython/testing/plugin/pytest_ipdoctest.py @@ -782,7 +782,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + if float(w.group()) == approx(float(g.group()), abs=10**-precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. From 667f3cbe6879d6fbe63ce8cb7895e2db6e49bfb7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 12:56:56 +0200 Subject: [PATCH 0258/1752] Run black on all files that would lead to 3 lines changes or less --- IPython/core/tests/nonascii.py | 2 +- IPython/lib/tests/test_imports.py | 3 +++ IPython/terminal/pt_inputhooks/asyncio.py | 3 +-- IPython/terminal/pt_inputhooks/gtk.py | 2 ++ IPython/terminal/pt_inputhooks/gtk3.py | 2 ++ IPython/testing/plugin/simplevars.py | 2 +- IPython/utils/_sysinfo.py | 2 +- IPython/utils/tests/test_dir2.py | 3 +-- setupbase.py | 2 +- 9 files changed, 13 insertions(+), 8 deletions(-) diff --git a/IPython/core/tests/nonascii.py b/IPython/core/tests/nonascii.py index 78801dfd093..12738e3adc2 100644 --- a/IPython/core/tests/nonascii.py +++ b/IPython/core/tests/nonascii.py @@ -1,4 +1,4 @@ # coding: iso-8859-5 # (Unlikely to be the default encoding for most testers.) # <- Cyrillic characters -u = '' +u = "" diff --git a/IPython/lib/tests/test_imports.py b/IPython/lib/tests/test_imports.py index d2e1b877290..515cd4a8a58 100644 --- a/IPython/lib/tests/test_imports.py +++ b/IPython/lib/tests/test_imports.py @@ -1,11 +1,14 @@ # encoding: utf-8 from IPython.testing import decorators as dec + def test_import_backgroundjobs(): from IPython.lib import backgroundjobs + def test_import_deepreload(): from IPython.lib import deepreload + def test_import_demo(): from IPython.lib import demo diff --git a/IPython/terminal/pt_inputhooks/asyncio.py b/IPython/terminal/pt_inputhooks/asyncio.py index 2d8c128208e..d2499e11e68 100644 --- a/IPython/terminal/pt_inputhooks/asyncio.py +++ b/IPython/terminal/pt_inputhooks/asyncio.py @@ -31,8 +31,7 @@ from IPython.core.async_helpers import get_asyncio_loop -PTK3 = ptk_version.startswith('3.') - +PTK3 = ptk_version.startswith("3.") def inputhook(context): diff --git a/IPython/terminal/pt_inputhooks/gtk.py b/IPython/terminal/pt_inputhooks/gtk.py index 6e246ba8377..5c201b65d75 100644 --- a/IPython/terminal/pt_inputhooks/gtk.py +++ b/IPython/terminal/pt_inputhooks/gtk.py @@ -41,6 +41,7 @@ # Enable threading in GTK. (Otherwise, GTK will keep the GIL.) gtk.gdk.threads_init() + def inputhook(context): """ When the eventloop of prompt-toolkit is idle, call this inputhook. @@ -50,6 +51,7 @@ def inputhook(context): :param context: An `InputHookContext` instance. """ + def _main_quit(*a, **kw): gtk.main_quit() return False diff --git a/IPython/terminal/pt_inputhooks/gtk3.py b/IPython/terminal/pt_inputhooks/gtk3.py index ae82b4edaaa..b073bd94d99 100644 --- a/IPython/terminal/pt_inputhooks/gtk3.py +++ b/IPython/terminal/pt_inputhooks/gtk3.py @@ -3,10 +3,12 @@ from gi.repository import Gtk, GLib + def _main_quit(*args, **kwargs): Gtk.main_quit() return False + def inputhook(context): GLib.io_add_watch(context.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, _main_quit) Gtk.main() diff --git a/IPython/testing/plugin/simplevars.py b/IPython/testing/plugin/simplevars.py index cac0b753124..82a5edb028d 100644 --- a/IPython/testing/plugin/simplevars.py +++ b/IPython/testing/plugin/simplevars.py @@ -1,2 +1,2 @@ x = 1 -print('x is:',x) +print("x is:", x) diff --git a/IPython/utils/_sysinfo.py b/IPython/utils/_sysinfo.py index a80b0295e85..2e58242d561 100644 --- a/IPython/utils/_sysinfo.py +++ b/IPython/utils/_sysinfo.py @@ -1,2 +1,2 @@ # GENERATED BY setup.py -commit = u"" +commit = "" diff --git a/IPython/utils/tests/test_dir2.py b/IPython/utils/tests/test_dir2.py index d35b110e413..bf7f5e57ea2 100644 --- a/IPython/utils/tests/test_dir2.py +++ b/IPython/utils/tests/test_dir2.py @@ -19,7 +19,6 @@ def test_base(): def test_SubClass(): - class SubClass(Base): y = 2 @@ -53,7 +52,7 @@ def some_method(self): class SillierWithDir(MisbehavingGetattr): def __dir__(self): - return ['some_method'] + return ["some_method"] for bad_klass in (MisbehavingGetattr, SillierWithDir): obj = bad_klass() diff --git a/setupbase.py b/setupbase.py index 5db235cd7de..748b4dd6a8a 100644 --- a/setupbase.py +++ b/setupbase.py @@ -346,7 +346,7 @@ def _record_commit(self, base_dir): out_file.writelines( [ "# GENERATED BY setup.py\n", - 'commit = u"%s"\n' % repo_commit, + 'commit = "%s"\n' % repo_commit, ] ) From 5a72e3dbb24aa81c4a5062ac3b078a59a4e6f682 Mon Sep 17 00:00:00 2001 From: Ben Longbons Date: Tue, 4 Oct 2022 12:41:10 -0700 Subject: [PATCH 0259/1752] Stop monkeypatching `linecache` It turns out this has never been necessary if we just change the cache entry to forbid expiry in the first place. --- IPython/core/compilerop.py | 55 ++++++++++++++++---------------- IPython/core/interactiveshell.py | 3 +- IPython/core/ultratb.py | 6 ++-- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/IPython/core/compilerop.py b/IPython/core/compilerop.py index b43e570b3ad..e3d77b16d5c 100644 --- a/IPython/core/compilerop.py +++ b/IPython/core/compilerop.py @@ -73,25 +73,6 @@ class CachingCompiler(codeop.Compile): def __init__(self): codeop.Compile.__init__(self) - # This is ugly, but it must be done this way to allow multiple - # simultaneous ipython instances to coexist. Since Python itself - # directly accesses the data structures in the linecache module, and - # the cache therein is global, we must work with that data structure. - # We must hold a reference to the original checkcache routine and call - # that in our own check_cache() below, but the special IPython cache - # must also be shared by all IPython instances. If we were to hold - # separate caches (one in each CachingCompiler instance), any call made - # by Python itself to linecache.checkcache() would obliterate the - # cached data from the other IPython instances. - if not hasattr(linecache, '_ipython_cache'): - linecache._ipython_cache = {} - if not hasattr(linecache, '_checkcache_ori'): - linecache._checkcache_ori = linecache.checkcache - # Now, we must monkeypatch the linecache directly so that parts of the - # stdlib that call it outside our control go through our codepath - # (otherwise we'd lose our tracebacks). - linecache.checkcache = check_linecache_ipython - # Caching a dictionary { filename: execution_count } for nicely # rendered tracebacks. The filename corresponds to the filename # argument used for the builtins.compile function. @@ -161,14 +142,24 @@ def cache(self, transformed_code, number=0, raw_code=None): # Save the execution count self._filename_map[name] = number + # Since Python 2.5, setting mtime to `None` means the lines will + # never be removed by `linecache.checkcache`. This means all the + # monkeypatching has *never* been necessary, since this code was + # only added in 2010, at which point IPython had already stopped + # supporting Python 2.4. + # + # Note that `linecache.clearcache` and `linecache.updatecache` may + # still remove our code from the cache, but those show explicit + # intent, and we should not try to interfere. Normally the former + # is never called except when out of memory, and the latter is only + # called for lines *not* in the cache. entry = ( len(transformed_code), - time.time(), + None, [line + "\n" for line in transformed_code.splitlines()], name, ) linecache.cache[name] = entry - linecache._ipython_cache[name] = entry return name @contextmanager @@ -187,10 +178,20 @@ def extra_flags(self, flags): def check_linecache_ipython(*args): - """Call linecache.checkcache() safely protecting our cached values. + """Deprecated since IPython 8.6. Call linecache.checkcache() directly. + + It was already not necessary to call this function directly. If no + CachingCompiler had been created, this function would fail badly. If + an instance had been created, this function would've been monkeypatched + into place. + + As of IPython 8.6, the monkeypatching has gone away entirely. But there + were still internal callers of this function, so maybe external callers + also existed? """ - # First call the original checkcache as intended - linecache._checkcache_ori(*args) - # Then, update back the cache with our data, so that tracebacks related - # to our compiled codes can be produced. - linecache.cache.update(linecache._ipython_cache) + import warnings + warnings.warn( + 'Just call linecache.checkcache() directly.', + DeprecationWarning + ) + linecache.checkcache() diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 6d04846081a..c4eecff1fa6 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -61,7 +61,7 @@ from IPython.core.alias import Alias, AliasManager from IPython.core.autocall import ExitAutocall from IPython.core.builtin_trap import BuiltinTrap -from IPython.core.compilerop import CachingCompiler, check_linecache_ipython +from IPython.core.compilerop import CachingCompiler from IPython.core.debugger import InterruptiblePdb from IPython.core.display_trap import DisplayTrap from IPython.core.displayhook import DisplayHook @@ -1810,7 +1810,6 @@ def init_traceback_handlers(self, custom_exceptions): self.InteractiveTB = ultratb.AutoFormattedTB(mode = 'Plain', color_scheme='NoColor', tb_offset = 1, - check_cache=check_linecache_ipython, debugger_cls=self.debugger_cls, parent=self) # The instance will store a pointer to the system-wide exception hook, diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 8f40c63b758..e83e2b4a0c1 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -644,10 +644,8 @@ def __init__( self.long_header = long_header self.include_vars = include_vars # By default we use linecache.checkcache, but the user can provide a - # different check_cache implementation. This is used by the IPython - # kernel to provide tracebacks for interactive code that is cached, - # by a compiler instance that flushes the linecache but preserves its - # own code cache. + # different check_cache implementation. This was formerly used by the + # IPython kernel for interactive code, but is no longer necessary. if check_cache is None: check_cache = linecache.checkcache self.check_cache = check_cache From f712cf87436be380992485fc520663c425857c49 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 12:32:21 +0200 Subject: [PATCH 0260/1752] please formatter --- IPython/core/compilerop.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/IPython/core/compilerop.py b/IPython/core/compilerop.py index e3d77b16d5c..bffac9c86d5 100644 --- a/IPython/core/compilerop.py +++ b/IPython/core/compilerop.py @@ -190,8 +190,6 @@ def check_linecache_ipython(*args): also existed? """ import warnings - warnings.warn( - 'Just call linecache.checkcache() directly.', - DeprecationWarning - ) + + warnings.warn("Just call linecache.checkcache() directly.", DeprecationWarning) linecache.checkcache() From 516c43d451962bf0d25d5454657c0a57ebb5bea8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 12:33:58 +0200 Subject: [PATCH 0261/1752] add some infor in warning --- IPython/core/compilerop.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/IPython/core/compilerop.py b/IPython/core/compilerop.py index bffac9c86d5..228f705666f 100644 --- a/IPython/core/compilerop.py +++ b/IPython/core/compilerop.py @@ -191,5 +191,9 @@ def check_linecache_ipython(*args): """ import warnings - warnings.warn("Just call linecache.checkcache() directly.", DeprecationWarning) + warnings.warn( + "Deprecated Since IPython 8.6, Just call linecache.checkcache() directly.", + DeprecationWarning, + stacklevel=2, + ) linecache.checkcache() From 93265fc972971fea195643c5391baa3e97f49857 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 3 Apr 2022 19:15:43 -0700 Subject: [PATCH 0262/1752] display.Javascriptr should use `rel` in CSS link. Seeing errors when using `display.Javascript` with `css` parameters. The target css does not load in current chrome, due to the use of: `` rather than: `` in the display call. --- IPython/core/display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index a095f3cb5e0..bf5504481f6 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -626,7 +626,7 @@ def _repr_json_(self): return self._data_and_metadata() _css_t = """var link = document.createElement("link"); - link.ref = "stylesheet"; + link.rel = "stylesheet"; link.type = "text/css"; link.href = "%s"; document.head.appendChild(link); From 3fe68e1cb312a8aec1f4189220b017f60c6d45f7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 03:30:54 -0700 Subject: [PATCH 0263/1752] Update IPython/core/display.py --- IPython/core/display.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/display.py b/IPython/core/display.py index bf5504481f6..23d8636b507 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -625,6 +625,7 @@ def _data_and_metadata(self): def _repr_json_(self): return self._data_and_metadata() + _css_t = """var link = document.createElement("link"); link.rel = "stylesheet"; link.type = "text/css"; From 6a90b3afa67416a79d036a11880da4d33aad8e30 Mon Sep 17 00:00:00 2001 From: Osher De Paz Date: Sat, 1 Oct 2022 11:46:26 +0300 Subject: [PATCH 0264/1752] raise an error when user tries to open a standard stream Fixes #13718 --- IPython/core/interactiveshell.py | 9 +++++++++ IPython/core/tests/test_interactiveshell.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 6d04846081a..e9cc89a0881 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1255,6 +1255,15 @@ def prepare_user_module(self, user_module=None, user_ns=None): if user_ns is None: user_ns = user_module.__dict__ + @functools.wraps(io_open) + def modified_open(file, *args, **kwargs): + if file in {0, 1, 2}: + raise ValueError(f"IPython won't let you open fd={file} by default") + + return io_open(file, *args, **kwargs) + + user_ns["open"] = modified_open + return user_module, user_ns def init_sys_modules(self): diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 10827b5fa0f..b5efe98b85b 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -103,6 +103,21 @@ def test_syntax_error(self): res = ip.run_cell("raise = 3") self.assertIsInstance(res.error_before_exec, SyntaxError) + def test_open_standard_input_stream(self): + ip.init_create_namespaces() + res = ip.run_cell("open(0)") + self.assertIsInstance(res.error_in_exec, ValueError) + + def test_open_standard_output_stream(self): + ip.init_create_namespaces() + res = ip.run_cell("open(1)") + self.assertIsInstance(res.error_in_exec, ValueError) + + def test_open_standard_error_stream(self): + ip.init_create_namespaces() + res = ip.run_cell("open(2)") + self.assertIsInstance(res.error_in_exec, ValueError) + def test_In_variable(self): "Verify that In variable grows with user input (GH-284)" oldlen = len(ip.user_ns['In']) From 467f4a49105b74a1e55a2ab070f947b7b82fbecb Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 19 Oct 2022 06:37:22 -0700 Subject: [PATCH 0265/1752] Update IPython/core/interactiveshell.py --- IPython/core/interactiveshell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index e9cc89a0881..c6950453bf7 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1258,7 +1258,10 @@ def prepare_user_module(self, user_module=None, user_ns=None): @functools.wraps(io_open) def modified_open(file, *args, **kwargs): if file in {0, 1, 2}: - raise ValueError(f"IPython won't let you open fd={file} by default") + raise ValueError(f"IPython won't let you open fd={file} by default " + "as it is likely to crash IPython. If you know what you are doing, " + "you can use builtins' open." + ) return io_open(file, *args, **kwargs) From fac46998ce833b79138c26d839d06dfcf73e3d99 Mon Sep 17 00:00:00 2001 From: zhizheng1 <114848324+zhizheng1@users.noreply.github.com> Date: Sat, 8 Oct 2022 09:52:34 +0800 Subject: [PATCH 0266/1752] Update pylabtools.py --- IPython/core/pylabtools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 68e100f7d07..7c452182992 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -26,6 +26,7 @@ "qt": "Qt5Agg", "osx": "MacOSX", "nbagg": "nbAgg", + "webagg": "WebAgg", "notebook": "nbAgg", "agg": "agg", "svg": "svg", From 054fd6835659e6f3142f65eba5ac3bb4f95f3085 Mon Sep 17 00:00:00 2001 From: zhizheng1 <114848324+zhizheng1@users.noreply.github.com> Date: Sat, 8 Oct 2022 09:55:05 +0800 Subject: [PATCH 0267/1752] Update interactiveshell.py --- IPython/terminal/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index cf2ab42776e..50bc41cb8ad 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -711,7 +711,7 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): - if gui and (gui != 'inline') : + if gui and (gui not in ['inline', 'webagg']) : self.active_eventloop, self._inputhook =\ get_inputhook_name_and_func(gui) else: From 293ec35c126251e876e1a32bd04c55f9dcbf06d3 Mon Sep 17 00:00:00 2001 From: zhizheng1 <114848324+zhizheng1@users.noreply.github.com> Date: Thu, 27 Oct 2022 17:59:54 +0800 Subject: [PATCH 0268/1752] Cosmetic fix. --- IPython/terminal/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 50bc41cb8ad..0250184dff0 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -711,7 +711,7 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): - if gui and (gui not in ['inline', 'webagg']) : + if gui and (gui not in {'inline', 'webagg'}) : self.active_eventloop, self._inputhook =\ get_inputhook_name_and_func(gui) else: From 17ac9ea417bd1c308af4f01691159a83efa376a4 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 12:28:12 +0200 Subject: [PATCH 0269/1752] please formatter --- IPython/core/display.py | 1 + IPython/core/interactiveshell.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/IPython/core/display.py b/IPython/core/display.py index a095f3cb5e0..25b340fc8eb 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -625,6 +625,7 @@ def _data_and_metadata(self): def _repr_json_(self): return self._data_and_metadata() + _css_t = """var link = document.createElement("link"); link.ref = "stylesheet"; link.type = "text/css"; diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index c6950453bf7..54e5d547beb 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1258,9 +1258,10 @@ def prepare_user_module(self, user_module=None, user_ns=None): @functools.wraps(io_open) def modified_open(file, *args, **kwargs): if file in {0, 1, 2}: - raise ValueError(f"IPython won't let you open fd={file} by default " - "as it is likely to crash IPython. If you know what you are doing, " - "you can use builtins' open." + raise ValueError( + f"IPython won't let you open fd={file} by default " + "as it is likely to crash IPython. If you know what you are doing, " + "you can use builtins' open." ) return io_open(file, *args, **kwargs) From 5ab1ecc6ba59be4c774544670822c6894e0caf21 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 13:45:22 +0200 Subject: [PATCH 0270/1752] please formatter --- IPython/terminal/interactiveshell.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 0250184dff0..c867b553f2e 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -711,9 +711,8 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): - if gui and (gui not in {'inline', 'webagg'}) : - self.active_eventloop, self._inputhook =\ - get_inputhook_name_and_func(gui) + if gui and (gui not in {"inline", "webagg"}): + self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) else: self.active_eventloop = self._inputhook = None From f44e27095fd647cc22bf37874f183ec4db85949f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 29 Oct 2022 14:40:25 +0200 Subject: [PATCH 0271/1752] Refactor a bit of uniformity. Not-reiniting user_ns may also help fix issues on windows. --- IPython/core/interactiveshell.py | 24 ++++++++++----------- IPython/core/tests/test_interactiveshell.py | 3 --- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 54e5d547beb..214493b324c 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -270,6 +270,16 @@ def __repr__(self): return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s info=%s result=%s>' %\ (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.info), repr(self.result)) +@functools.wraps(io_open) +def _modified_open(file, *args, **kwargs): + if file in {0, 1, 2}: + raise ValueError( + f"IPython won't let you open fd={file} by default " + "as it is likely to crash IPython. If you know what you are doing, " + "you can use builtins' open." + ) + + return io_open(file, *args, **kwargs) class InteractiveShell(SingletonConfigurable): """An enhanced, interactive shell for Python.""" @@ -1255,19 +1265,6 @@ def prepare_user_module(self, user_module=None, user_ns=None): if user_ns is None: user_ns = user_module.__dict__ - @functools.wraps(io_open) - def modified_open(file, *args, **kwargs): - if file in {0, 1, 2}: - raise ValueError( - f"IPython won't let you open fd={file} by default " - "as it is likely to crash IPython. If you know what you are doing, " - "you can use builtins' open." - ) - - return io_open(file, *args, **kwargs) - - user_ns["open"] = modified_open - return user_module, user_ns def init_sys_modules(self): @@ -1336,6 +1333,7 @@ def init_user_ns(self): ns['exit'] = self.exiter ns['quit'] = self.exiter + ns["open"] = _modified_open # Sync what we've added so far to user_ns_hidden so these aren't seen # by %who diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index b5efe98b85b..982bd5a3b22 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -104,17 +104,14 @@ def test_syntax_error(self): self.assertIsInstance(res.error_before_exec, SyntaxError) def test_open_standard_input_stream(self): - ip.init_create_namespaces() res = ip.run_cell("open(0)") self.assertIsInstance(res.error_in_exec, ValueError) def test_open_standard_output_stream(self): - ip.init_create_namespaces() res = ip.run_cell("open(1)") self.assertIsInstance(res.error_in_exec, ValueError) def test_open_standard_error_stream(self): - ip.init_create_namespaces() res = ip.run_cell("open(2)") self.assertIsInstance(res.error_in_exec, ValueError) From 8ca7b420a29ad781cc6c701dd4a6af0dd21b35c4 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Oct 2022 09:10:13 +0100 Subject: [PATCH 0272/1752] fix stack-data 0.6 failing tests --- IPython/core/tests/test_iplib.py | 129 ++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 44 deletions(-) diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py index f12601823d1..dec429fbe35 100644 --- a/IPython/core/tests/test_iplib.py +++ b/IPython/core/tests/test_iplib.py @@ -1,15 +1,10 @@ """Tests for the key interactiveshell module, where the main ipython class is defined. """ -#----------------------------------------------------------------------------- -# Module imports -#----------------------------------------------------------------------------- +import stack_data -# our own packages +SV_VERSION = tuple([int(x) for x in stack_data.__version__.split(".")[0:2]]) -#----------------------------------------------------------------------------- -# Test functions -#----------------------------------------------------------------------------- def test_reset(): """reset must clear most namespaces.""" @@ -170,46 +165,92 @@ def doctest_tb_sysexit(): """ -def doctest_tb_sysexit_verbose(): - """ - In [18]: %run simpleerr.py exit - An exception has occurred, use %tb to see the full traceback. - SystemExit: (1, 'Mode = exit') +if SV_VERSION < (0, 6): - In [19]: %run simpleerr.py exit 2 - An exception has occurred, use %tb to see the full traceback. - SystemExit: (2, 'Mode = exit') + def doctest_tb_sysexit_verbose_stack_data_05(): + """ + In [18]: %run simpleerr.py exit + An exception has occurred, use %tb to see the full traceback. + SystemExit: (1, 'Mode = exit') + + In [19]: %run simpleerr.py exit 2 + An exception has occurred, use %tb to see the full traceback. + SystemExit: (2, 'Mode = exit') + + In [23]: %xmode verbose + Exception reporting mode: Verbose + + In [24]: %tb + --------------------------------------------------------------------------- + SystemExit Traceback (most recent call last) + + ... + 30 except IndexError: + 31 mode = 'div' + ---> 33 bar(mode) + mode = 'exit' + + ... in bar(mode='exit') + ... except: + ... stat = 1 + ---> ... sysexit(stat, mode) + mode = 'exit' + stat = 2 + ... else: + ... raise ValueError('Unknown mode') + + ... in sysexit(stat=2, mode='exit') + 10 def sysexit(stat, mode): + ---> 11 raise SystemExit(stat, f"Mode = {mode}") + stat = 2 + + SystemExit: (2, 'Mode = exit') + """ - In [23]: %xmode verbose - Exception reporting mode: Verbose - - In [24]: %tb - --------------------------------------------------------------------------- - SystemExit Traceback (most recent call last) - - ... - 30 except IndexError: - 31 mode = 'div' - ---> 33 bar(mode) - mode = 'exit' - - ... in bar(mode='exit') - ... except: - ... stat = 1 - ---> ... sysexit(stat, mode) - mode = 'exit' - stat = 2 - ... else: - ... raise ValueError('Unknown mode') - - ... in sysexit(stat=2, mode='exit') - 10 def sysexit(stat, mode): - ---> 11 raise SystemExit(stat, f"Mode = {mode}") - stat = 2 - - SystemExit: (2, 'Mode = exit') - """ +else: + # currently the only difference is + # + mode = 'exit' + def doctest_tb_sysexit_verbose_stack_data_06(): + """ + In [18]: %run simpleerr.py exit + An exception has occurred, use %tb to see the full traceback. + SystemExit: (1, 'Mode = exit') + + In [19]: %run simpleerr.py exit 2 + An exception has occurred, use %tb to see the full traceback. + SystemExit: (2, 'Mode = exit') + + In [23]: %xmode verbose + Exception reporting mode: Verbose + + In [24]: %tb + --------------------------------------------------------------------------- + SystemExit Traceback (most recent call last) + + ... + 30 except IndexError: + 31 mode = 'div' + ---> 33 bar(mode) + mode = 'exit' + + ... in bar(mode='exit') + ... except: + ... stat = 1 + ---> ... sysexit(stat, mode) + mode = 'exit' + stat = 2 + ... else: + ... raise ValueError('Unknown mode') + + ... in sysexit(stat=2, mode='exit') + 10 def sysexit(stat, mode): + ---> 11 raise SystemExit(stat, f"Mode = {mode}") + stat = 2 + mode = 'exit' + + SystemExit: (2, 'Mode = exit') + """ def test_run_cell(): import textwrap From fcdcddd5e528844672688e07bfa5188e48e37521 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Oct 2022 09:20:57 +0100 Subject: [PATCH 0273/1752] iterate --- IPython/core/tests/test_iplib.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py index dec429fbe35..8fdf2675ba1 100644 --- a/IPython/core/tests/test_iplib.py +++ b/IPython/core/tests/test_iplib.py @@ -229,22 +229,22 @@ def doctest_tb_sysexit_verbose_stack_data_06(): SystemExit Traceback (most recent call last) ... - 30 except IndexError: - 31 mode = 'div' + 30 except IndexError: + 31 mode = 'div' ---> 33 bar(mode) mode = 'exit' ... in bar(mode='exit') - ... except: - ... stat = 1 + ... except: + ... stat = 1 ---> ... sysexit(stat, mode) mode = 'exit' stat = 2 - ... else: - ... raise ValueError('Unknown mode') + ... else: + ... raise ValueError('Unknown mode') ... in sysexit(stat=2, mode='exit') - 10 def sysexit(stat, mode): + 10 def sysexit(stat, mode): ---> 11 raise SystemExit(stat, f"Mode = {mode}") stat = 2 mode = 'exit' From 98e3599e130e253f292679f74982a3a2cd3a7a7a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Oct 2022 09:27:22 +0100 Subject: [PATCH 0274/1752] exclude 3.8 import sys --- IPython/core/tests/test_iplib.py | 174 ++++++++++++++++--------------- 1 file changed, 88 insertions(+), 86 deletions(-) diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py index 8fdf2675ba1..c5e065005f0 100644 --- a/IPython/core/tests/test_iplib.py +++ b/IPython/core/tests/test_iplib.py @@ -2,6 +2,7 @@ """ import stack_data +import sys SV_VERSION = tuple([int(x) for x in stack_data.__version__.split(".")[0:2]]) @@ -165,92 +166,93 @@ def doctest_tb_sysexit(): """ -if SV_VERSION < (0, 6): - - def doctest_tb_sysexit_verbose_stack_data_05(): - """ - In [18]: %run simpleerr.py exit - An exception has occurred, use %tb to see the full traceback. - SystemExit: (1, 'Mode = exit') - - In [19]: %run simpleerr.py exit 2 - An exception has occurred, use %tb to see the full traceback. - SystemExit: (2, 'Mode = exit') - - In [23]: %xmode verbose - Exception reporting mode: Verbose - - In [24]: %tb - --------------------------------------------------------------------------- - SystemExit Traceback (most recent call last) - - ... - 30 except IndexError: - 31 mode = 'div' - ---> 33 bar(mode) - mode = 'exit' - - ... in bar(mode='exit') - ... except: - ... stat = 1 - ---> ... sysexit(stat, mode) - mode = 'exit' - stat = 2 - ... else: - ... raise ValueError('Unknown mode') - - ... in sysexit(stat=2, mode='exit') - 10 def sysexit(stat, mode): - ---> 11 raise SystemExit(stat, f"Mode = {mode}") - stat = 2 - - SystemExit: (2, 'Mode = exit') - """ - -else: - # currently the only difference is - # + mode = 'exit' - - def doctest_tb_sysexit_verbose_stack_data_06(): - """ - In [18]: %run simpleerr.py exit - An exception has occurred, use %tb to see the full traceback. - SystemExit: (1, 'Mode = exit') - - In [19]: %run simpleerr.py exit 2 - An exception has occurred, use %tb to see the full traceback. - SystemExit: (2, 'Mode = exit') - - In [23]: %xmode verbose - Exception reporting mode: Verbose - - In [24]: %tb - --------------------------------------------------------------------------- - SystemExit Traceback (most recent call last) - - ... - 30 except IndexError: - 31 mode = 'div' - ---> 33 bar(mode) - mode = 'exit' - - ... in bar(mode='exit') - ... except: - ... stat = 1 - ---> ... sysexit(stat, mode) - mode = 'exit' - stat = 2 - ... else: - ... raise ValueError('Unknown mode') - - ... in sysexit(stat=2, mode='exit') - 10 def sysexit(stat, mode): - ---> 11 raise SystemExit(stat, f"Mode = {mode}") - stat = 2 - mode = 'exit' - - SystemExit: (2, 'Mode = exit') - """ +if sys.version_info >= (3, 9): + if SV_VERSION < (0, 6): + + def doctest_tb_sysexit_verbose_stack_data_05(): + """ + In [18]: %run simpleerr.py exit + An exception has occurred, use %tb to see the full traceback. + SystemExit: (1, 'Mode = exit') + + In [19]: %run simpleerr.py exit 2 + An exception has occurred, use %tb to see the full traceback. + SystemExit: (2, 'Mode = exit') + + In [23]: %xmode verbose + Exception reporting mode: Verbose + + In [24]: %tb + --------------------------------------------------------------------------- + SystemExit Traceback (most recent call last) + + ... + 30 except IndexError: + 31 mode = 'div' + ---> 33 bar(mode) + mode = 'exit' + + ... in bar(mode='exit') + ... except: + ... stat = 1 + ---> ... sysexit(stat, mode) + mode = 'exit' + stat = 2 + ... else: + ... raise ValueError('Unknown mode') + + ... in sysexit(stat=2, mode='exit') + 10 def sysexit(stat, mode): + ---> 11 raise SystemExit(stat, f"Mode = {mode}") + stat = 2 + + SystemExit: (2, 'Mode = exit') + """ + + else: + # currently the only difference is + # + mode = 'exit' + + def doctest_tb_sysexit_verbose_stack_data_06(): + """ + In [18]: %run simpleerr.py exit + An exception has occurred, use %tb to see the full traceback. + SystemExit: (1, 'Mode = exit') + + In [19]: %run simpleerr.py exit 2 + An exception has occurred, use %tb to see the full traceback. + SystemExit: (2, 'Mode = exit') + + In [23]: %xmode verbose + Exception reporting mode: Verbose + + In [24]: %tb + --------------------------------------------------------------------------- + SystemExit Traceback (most recent call last) + + ... + 30 except IndexError: + 31 mode = 'div' + ---> 33 bar(mode) + mode = 'exit' + + ... in bar(mode='exit') + ... except: + ... stat = 1 + ---> ... sysexit(stat, mode) + mode = 'exit' + stat = 2 + ... else: + ... raise ValueError('Unknown mode') + + ... in sysexit(stat=2, mode='exit') + 10 def sysexit(stat, mode): + ---> 11 raise SystemExit(stat, f"Mode = {mode}") + stat = 2 + mode = 'exit' + + SystemExit: (2, 'Mode = exit') + """ def test_run_cell(): import textwrap From 0e83a6373574e28b7b8b1211ff49edfbb0119a37 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Oct 2022 08:35:15 +0100 Subject: [PATCH 0275/1752] What's new version 8.6 --- docs/source/whatsnew/version8.rst | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 59e71654faf..eee7af0aa48 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,70 @@ 8.x Series ============ +.. _version 8.6.0: + +IPython 8.6.0 +------------- + +Back to a more regular release schedule (at least I try), as Friday is +already over by more than 24h hours. This is a slightly bigger release with a +few new features that contain no less then 25 PRs. + +We'll notably found a couple of non negligible changes: + +The ``install_ext`` and related functions have been removed after being +deprecated for years. You can use pip to install extensions. ``pip`` did not +exists when ``install_ext`` was introduced. You can still load local extensions +without installing them. Just set your ``sys.path`` for example. :ghpull:`13744` + +IPython now have extra entry points that that the major *and minor* version of +python. For some of you this mean that you can do a quick ``ipython3.10`` to +launch IPython from the Python 3.10 interpreter, while still using Python 3.11 +as your main Python. :ghpull:`13743` + +The completer matcher API have been improved. See :ghpull:`13745`. This should +improve the type inference and improve dict keys completions in many use case. +Tanks ``@krassowski`` for all the works, and the D.E. Shaw group for sponsoring +it. + +The color of error nodes in tracebacks can now be customized. See +:ghpull:`13756`. This is a private attribute until someone find the time to +properly add a configuration option. Note that with Python 3.11 that also show +the relevant nodes in traceback, it would be good to leverage this informations +(plus the "did you mean" info added on attribute errors). But that's likely work +I won't have time to do before long, so contributions welcome. + +As we follow NEP 29, we removed support for numpy 1.19 :ghpull:`13760`. + + +The ``open()`` function present in the user namespace by default will now refuse +to open the file descriptors 0,1,2 (stdin, out, err), to avoid crashing IPython. +This mostly occurs in teaching context when incorrect values get passed around. + + +The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find +objects insides arrays. That is to say, the following now works:: + + + >>> def my_func(*arg, **kwargs):pass + >>> container = [my_func] + >>> container[0]? + + +If ``container`` define a custom ``getitem``, this __will__ trigger the custom +method. So don't put side effects in your ``getitems``. Thanks the D.E. Shaw +group for the request and sponsoring the work. + + +As usual you can find the full list of PRs on GitHub under `the 8.6 milestone +`__. + +Thanks to all hacktoberfest contributors, please contribute to +`closember.org `__. + +Thanks to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + .. _version 8.5.0: IPython 8.5.0 From f8d0e4c79a6c3587e6355f1354fff265f73f4938 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Oct 2022 10:30:09 +0100 Subject: [PATCH 0276/1752] release 8.6.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index bd0ac2161e3..d8eeae46901 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 8d2d355e76da23a696d429d9eb2e0a14ba778578 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 30 Oct 2022 10:32:00 +0100 Subject: [PATCH 0277/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index d8eeae46901..d891c34cc62 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 6 +_version_minor = 7 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From a146297a8ec7e17bd8633c77bce0c19ebd56160a Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 9 Nov 2022 06:25:20 -0700 Subject: [PATCH 0278/1752] Lint formatting --- IPython/sphinxext/ipython_directive.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py index e55ba126c8a..c428e7917fd 100644 --- a/IPython/sphinxext/ipython_directive.py +++ b/IPython/sphinxext/ipython_directive.py @@ -981,8 +981,9 @@ def setup(self): self.shell.warning_is_error = warning_is_error # setup bookmark for saving figures directory - self.shell.process_input_line('bookmark ipy_savedir "%s"'%savefig_dir, - store_history=False) + self.shell.process_input_line( + 'bookmark ipy_savedir "%s"' % savefig_dir, store_history=False + ) self.shell.clear_cout() return rgxin, rgxout, promptin, promptout From 724c9d70d5ae1d23d6121ba2e88fa987af1fd8c5 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 9 Nov 2022 00:36:57 -0700 Subject: [PATCH 0279/1752] Fix mypy error by being more explicit and verbose --- IPython/core/magics/basic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index af69b02676d..7dfa84ce2d5 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -297,7 +297,10 @@ def page(self, parameter_s=''): oname = args and args or '_' info = self.shell._ofind(oname) if info['found']: - txt = (raw and str or pformat)( info['obj'] ) + if raw: + txt = str(info["obj"]) + else: + txt = pformat(info["obj"]) page.page(txt) else: print('Object `%s` not found' % oname) From 03052c48c0a6b868d81f5f3678b218a079959f66 Mon Sep 17 00:00:00 2001 From: Bill Vineyard <71736216+wvineyard@users.noreply.github.com> Date: Sat, 29 Oct 2022 11:21:17 -0500 Subject: [PATCH 0280/1752] removed duplicate .vscode in gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index f4736530e10..3b6963b6317 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ __pycache__ .cache .coverage *.swp -.vscode .pytest_cache .python-version venv*/ From 1c3678bf1debbc6abd19fe94f4c574b329f3c749 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Tue, 8 Nov 2022 17:04:51 -0700 Subject: [PATCH 0281/1752] Make the formatting of a code block name extendable Currently the user display of a code block requires a tight coupling between the caching compiler and the ultratb file, i.e., ultratb needs to know internal private variables of the caching compiler. This change makes the user-visible display of the code block name the responsibility of the caching compiler. A nice result is that the caching compiler can be overridden to have custom terminology in different systems for code blocks executed. --- IPython/core/compilerop.py | 15 +++++++++++++++ IPython/core/ultratb.py | 26 ++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/IPython/core/compilerop.py b/IPython/core/compilerop.py index 228f705666f..7799a4fc99e 100644 --- a/IPython/core/compilerop.py +++ b/IPython/core/compilerop.py @@ -116,6 +116,21 @@ def get_code_name(self, raw_code, transformed_code, number): """ return code_name(transformed_code, number) + def format_code_name(self, name): + """Return a user-friendly label and name for a code block. + + Parameters + ---------- + name : str + The name for the code block returned from get_code_name + + Returns + ------- + A (label, name) pair that can be used in tracebacks, or None if the default formatting should be used. + """ + if name in self._filename_map: + return "Cell", "In[%s]" % self._filename_map[name] + def cache(self, transformed_code, number=0, raw_code=None): """Make a name for a block of code, and cache the code. diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index e83e2b4a0c1..18eff270829 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -173,7 +173,7 @@ def _format_traceback_lines(lines, Colors, has_colors: bool, lvals): def _format_filename(file, ColorFilename, ColorNormal, *, lineno=None): """ - Format filename lines with `In [n]` if it's the nth code cell or `File *.py` if it's a module. + Format filename lines with custom formatting from caching compiler or `File *.py` by default Parameters ---------- @@ -184,23 +184,29 @@ def _format_filename(file, ColorFilename, ColorNormal, *, lineno=None): ColorScheme's normal coloring to be used. """ ipinst = get_ipython() - - if ipinst is not None and file in ipinst.compile._filename_map: - file = "[%s]" % ipinst.compile._filename_map[file] + if ( + ipinst is not None + and (data := ipinst.compile.format_code_name(file)) is not None + ): + label, name = data if lineno is None: - tpl_link = f"Cell {ColorFilename}In {{file}}{ColorNormal}" + tpl_link = f"{{label}} {ColorFilename}{{name}}{ColorNormal}" else: - tpl_link = f"Cell {ColorFilename}In {{file}}, line {{lineno}}{ColorNormal}" + tpl_link = ( + f"{{label}} {ColorFilename}{{name}}, line {{lineno}}{ColorNormal}" + ) else: - file = util_path.compress_user( + label = "File" + name = util_path.compress_user( py3compat.cast_unicode(file, util_path.fs_encoding) ) if lineno is None: - tpl_link = f"File {ColorFilename}{{file}}{ColorNormal}" + tpl_link = f"{{label}} {ColorFilename}{{name}}{ColorNormal}" else: - tpl_link = f"File {ColorFilename}{{file}}:{{lineno}}{ColorNormal}" + # can we make this the more friendly ", line {{lineno}}", or do we need to preserve the formatting with the colon? + tpl_link = f"{{label}} {ColorFilename}{{name}}:{{lineno}}{ColorNormal}" - return tpl_link.format(file=file, lineno=lineno) + return tpl_link.format(label=label, name=name, lineno=lineno) #--------------------------------------------------------------------------- # Module classes From cd04814365c74d947b8ffc52bd21ea36a5f4fc8c Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 11 Nov 2022 20:18:03 +0000 Subject: [PATCH 0282/1752] Fix `merge_completions=False` error with Jedi Jedi uses filter to return an iterator hence we cannot just check `len()` to see if there are any completions. --- IPython/core/completer.py | 17 +++++++- IPython/core/tests/test_completer.py | 59 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index fc3aea7b611..25b780be3da 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -671,6 +671,19 @@ def __call__(self, context: CompletionContext) -> MatcherResult: Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2] +def has_any_completions(result: MatcherResult) -> bool: + """Check if any result includes any completions.""" + if hasattr(result["completions"], "__len__"): + return len(result["completions"]) != 0 + try: + old_iterator = result["completions"] + first = next(old_iterator) + result["completions"] = itertools.chain([first], old_iterator) + return True + except StopIteration: + return False + + def completion_matcher( *, priority: float = None, identifier: str = None, api_version: int = 1 ): @@ -1952,7 +1965,7 @@ def _jedi_matches( else: return [] - def python_matches(self, text:str)->List[str]: + def python_matches(self, text: str) -> Iterable[str]: """Match attributes or global python names""" if "." in text: try: @@ -2807,7 +2820,7 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, should_suppress = ( (suppression_config is True) or (suppression_recommended and (suppression_config is not False)) - ) and len(result["completions"]) + ) and has_any_completions(result) if should_suppress: suppression_exceptions = result.get("do_not_suppress", set()) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index fd72cf7d57a..98ec814a769 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -1396,6 +1396,65 @@ def configure(suppression_config): configure({"b_matcher": True}) _("do not suppress", ["completion_b"]) + configure(True) + _("do not suppress", ["completion_a"]) + + def test_matcher_suppression_with_iterator(self): + @completion_matcher(identifier="matcher_returning_iterator") + def matcher_returning_iterator(text): + return iter(["completion_iter"]) + + @completion_matcher(identifier="matcher_returning_list") + def matcher_returning_list(text): + return ["completion_list"] + + with custom_matchers([matcher_returning_iterator, matcher_returning_list]): + ip = get_ipython() + c = ip.Completer + + def _(text, expected): + c.use_jedi = False + s, matches = c.complete(text) + self.assertEqual(expected, matches) + + def configure(suppression_config): + cfg = Config() + cfg.IPCompleter.suppress_competing_matchers = suppression_config + c.update_config(cfg) + + configure(False) + _("---", ["completion_iter", "completion_list"]) + + configure(True) + _("---", ["completion_iter"]) + + configure(None) + _("--", ["completion_iter", "completion_list"]) + + def test_matcher_suppression_with_jedi(self): + ip = get_ipython() + c = ip.Completer + c.use_jedi = True + + def configure(suppression_config): + cfg = Config() + cfg.IPCompleter.suppress_competing_matchers = suppression_config + c.update_config(cfg) + + def _(): + with provisionalcompleter(): + matches = [completion.text for completion in c.completions("dict.", 5)] + self.assertIn("keys", matches) + + configure(False) + _() + + configure(True) + _() + + configure(None) + _() + def test_matcher_disabling(self): @completion_matcher(identifier="a_matcher") def a_matcher(text): From c5994eee0ff57d26cde7f7e14de0c92ae9520f25 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 12 Nov 2022 22:16:01 +0000 Subject: [PATCH 0283/1752] Explain expected format for `disable_matchers`/identifiers --- IPython/core/completer.py | 13 ++++++++++--- IPython/core/magics/config.py | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index fc3aea7b611..645f8b806e5 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -684,7 +684,10 @@ def completion_matcher( identifier : Optional[str] identifier of the matcher allowing users to modify the behaviour via traitlets, and also used to for debugging (will be passed as ``origin`` with the completions). - Defaults to matcher function ``__qualname__``. + + Defaults to matcher function's ``__qualname__`` (for example, + ``IPCompleter.file_matcher`` for the built-in matched defined + as a ``file_matcher`` method of the ``IPCompleter`` class). api_version: Optional[int] version of the Matcher API used by this matcher. Currently supported values are 1 and 2. @@ -1447,14 +1450,18 @@ def _greedy_changed(self, change): If False, only the completion results from the first non-empty completer will be returned. - + As of version 8.6.0, setting the value to ``False`` is an alias for: ``IPCompleter.suppress_competing_matchers = True.``. """, ).tag(config=True) disable_matchers = ListTrait( - Unicode(), help="""List of matchers to disable.""" + Unicode(), + help="""List of matchers to disable. + + The list should contain matcher identifiers (see :any:`completion_matcher`). + """, ).tag(config=True) omit__names = Enum( diff --git a/IPython/core/magics/config.py b/IPython/core/magics/config.py index f442ba15259..87fe3eed3a5 100644 --- a/IPython/core/magics/config.py +++ b/IPython/core/magics/config.py @@ -82,6 +82,8 @@ def config(self, s): Current: False IPCompleter.disable_matchers=... List of matchers to disable. + The list should contain matcher identifiers (see + :any:`completion_matcher`). Current: [] IPCompleter.greedy= Activate greedy completion From 2c55f54336afc3bae882220e483e3dbacb9248f1 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 15 Nov 2022 11:31:53 +0100 Subject: [PATCH 0284/1752] MAINT:add py.typed --- IPython/py.typed | 0 MANIFEST.in | 1 + 2 files changed, 1 insertion(+) create mode 100644 IPython/py.typed diff --git a/IPython/py.typed b/IPython/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/MANIFEST.in b/MANIFEST.in index c70c57d346f..970adeef334 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include LICENSE include setupbase.py include MANIFEST.in include pytest.ini +include py.typed include mypy.ini include .mailmap include .flake8 From cfb05c7ae1e0576d89ea75ccd6d0ba5ff863d8ba Mon Sep 17 00:00:00 2001 From: Hristo Georgiev Date: Fri, 18 Nov 2022 15:31:42 +0200 Subject: [PATCH 0285/1752] Pin minimum prompt-toolkit to 3.0.11 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b3a26586cf3..18af28885e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ install_requires = matplotlib-inline pexpect>4.3; sys_platform != "win32" pickleshare - prompt_toolkit>3.0.1,<3.1.0 + prompt_toolkit>=3.0.11,<3.1.0 pygments>=2.4.0 stack_data traitlets>=5 From bd008c4c8ac27f5f6779841c294074bb89f2ad4f Mon Sep 17 00:00:00 2001 From: nfgf Date: Fri, 25 Nov 2022 00:44:21 -0500 Subject: [PATCH 0286/1752] 1st proposal --- IPython/core/interactiveshell.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 21e428b54d4..689ddfe30d7 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2362,6 +2362,14 @@ def run_line_magic(self, magic_name: str, line, _stack_depth=1): kwargs['local_ns'] = self.get_local_scope(stack_depth) with self.builtin_trap: result = fn(*args, **kwargs) + + # The code below prevents output from being displayed + # when using magic %time. + # Output from '%time foo();', for instance, would never + # be displayed. + if magic_name == 'time' and len(magic_arg_s) > 0 and magic_arg_s[-1] == ';': + return None + return result def get_local_scope(self, stack_depth): From d0be67275d918e187de37943854f9db8e2ecec40 Mon Sep 17 00:00:00 2001 From: Nir Schulman Date: Fri, 25 Nov 2022 14:56:58 +0200 Subject: [PATCH 0287/1752] Reused the previously unused find_entry_points in setup.py --- setup.cfg | 3 --- setup.py | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index b3a26586cf3..5c7554a28fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -106,9 +106,6 @@ IPython.lib.tests = *.wav IPython.testing.plugin = *.txt [options.entry_points] -console_scripts = - ipython = IPython:start_ipython - ipython3 = IPython:start_ipython pygments.lexers = ipythonconsole = IPython.lib.lexers:IPythonConsoleLexer ipython = IPython.lib.lexers:IPythonLexer diff --git a/setup.py b/setup.py index bfdf5fb88bf..dca0cd3b3aa 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ # Our own imports sys.path.insert(0, ".") -from setupbase import target_update +from setupbase import target_update, find_entry_points from setupbase import ( setup_args, @@ -139,6 +139,9 @@ 'install_scripts_sym': install_scripts_for_symlink, 'unsymlink': unsymlink, } +setup_args["entry_points"] = { + "console_scripts": find_entry_points() +} #--------------------------------------------------------------------------- # Do the actual setup now From cfa39c55f44e0f117856cf64ea1ff895392d8f82 Mon Sep 17 00:00:00 2001 From: Nir Schulman Date: Fri, 25 Nov 2022 17:35:07 +0200 Subject: [PATCH 0288/1752] Fixed formatting --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index dca0cd3b3aa..4939ca53836 100644 --- a/setup.py +++ b/setup.py @@ -139,9 +139,7 @@ 'install_scripts_sym': install_scripts_for_symlink, 'unsymlink': unsymlink, } -setup_args["entry_points"] = { - "console_scripts": find_entry_points() -} +setup_args["entry_points"] = {"console_scripts": find_entry_points()} #--------------------------------------------------------------------------- # Do the actual setup now From f4081a6ff29cce329ec1da25599ba06c4fafe67e Mon Sep 17 00:00:00 2001 From: nfgf Date: Fri, 25 Nov 2022 10:52:05 -0500 Subject: [PATCH 0289/1752] Formatting fixes after running dark. --- IPython/core/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 689ddfe30d7..c8aacdc22cf 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2367,7 +2367,7 @@ def run_line_magic(self, magic_name: str, line, _stack_depth=1): # when using magic %time. # Output from '%time foo();', for instance, would never # be displayed. - if magic_name == 'time' and len(magic_arg_s) > 0 and magic_arg_s[-1] == ';': + if magic_name == "time" and len(magic_arg_s) > 0 and magic_arg_s[-1] == ";": return None return result From b6e1073c7f9f8e5cc20b4349b607f6324f7b7d06 Mon Sep 17 00:00:00 2001 From: nfgf Date: Sat, 26 Nov 2022 14:54:01 -0500 Subject: [PATCH 0290/1752] Adding test. --- IPython/core/tests/test_magic.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 509dd66dd28..ce0fbbea684 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -416,6 +416,21 @@ def test_time(): with tt.AssertPrints("hihi", suppress=False): ip.run_cell("f('hi')") +# ';' at the end of %time prevents instruction value to be printed. +# This tests fix for #13837 +def test_time_no_outputwith_semicolon(): + ip = get_ipython() + + with tt.AssertPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456") + + with tt.AssertNotPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456;") + def test_time_last_not_expression(): ip.run_cell("%%time\n" "var_1 = 1\n" From 31f22916c1fd38d6570df4c64539d2a2f1e976b5 Mon Sep 17 00:00:00 2001 From: nfgf Date: Sat, 26 Nov 2022 15:07:55 -0500 Subject: [PATCH 0291/1752] Change after running darker. --- IPython/core/tests/test_magic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index ce0fbbea684..9c44320740a 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -416,6 +416,7 @@ def test_time(): with tt.AssertPrints("hihi", suppress=False): ip.run_cell("f('hi')") + # ';' at the end of %time prevents instruction value to be printed. # This tests fix for #13837 def test_time_no_outputwith_semicolon(): @@ -431,6 +432,7 @@ def test_time_no_outputwith_semicolon(): with tt.AssertPrints("CPU times: ", suppress=False): ip.run_cell("%time 123000+456;") + def test_time_last_not_expression(): ip.run_cell("%%time\n" "var_1 = 1\n" From a07a31dc85da8ee493f1718728ae39bf50bc8ea9 Mon Sep 17 00:00:00 2001 From: nfgf Date: Sat, 26 Nov 2022 15:21:41 -0500 Subject: [PATCH 0292/1752] Again --- IPython/core/tests/test_magic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 9c44320740a..cb9890c4682 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -418,7 +418,7 @@ def test_time(): # ';' at the end of %time prevents instruction value to be printed. -# This tests fix for #13837 +# This tests fix for #13837. def test_time_no_outputwith_semicolon(): ip = get_ipython() From 12d1fb6d179b4d1a40bc6b8ce0adfd3ac536fd6e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 28 Nov 2022 10:06:35 +0100 Subject: [PATCH 0293/1752] What's new 8.7 --- docs/source/whatsnew/version8.rst | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index eee7af0aa48..d3c33704bad 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,32 @@ 8.x Series ============ + +.. _version 8.7.0: + +IPython 8.7.0 +------------- + + +Small release of IPython with a couple of bug fixes and new features for this +month. Next month is end of year, it is unclear if there will be a release close +the new year's eve, or if the next release will be at end of January. + +Here are a few of the relevant fixes, +as usual you can find the full list of PRs on GitHub under `the 8.7 milestone +`__. + + + - :ghpull:`13834` bump the minimum prompt toolkit to 3.0.11. + - IPython shipped with the ``py.typed`` marker now, and we are progressively + adding more types. :ghpull:`13831` + - :ghpull:`13817` add configuration of code blacks formatting. + + +Thanks to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + + .. _version 8.6.0: IPython 8.6.0 @@ -40,7 +66,7 @@ As we follow NEP 29, we removed support for numpy 1.19 :ghpull:`13760`. The ``open()`` function present in the user namespace by default will now refuse to open the file descriptors 0,1,2 (stdin, out, err), to avoid crashing IPython. -This mostly occurs in teaching context when incorrect values get passed around. +This mostly occurs in teaching context when incorrect values get passed around. The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find From ff770b25d03140e6f9355625601d609d4a5464e8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 28 Nov 2022 14:50:12 +0100 Subject: [PATCH 0294/1752] release 8.7.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index d891c34cc62..ff1acafac66 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 3f0bf05f072a91b2a3042d23ce250e5e906183fd Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 28 Nov 2022 14:51:08 +0100 Subject: [PATCH 0295/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index ff1acafac66..e2ce2eac2b4 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 7 +_version_minor = 8 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 81300ca6de7e50ea521073dbf49cc5eb4a276d66 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 28 Nov 2022 14:57:08 +0100 Subject: [PATCH 0296/1752] Make sure build is installed --- tools/release_helper.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/release_helper.sh b/tools/release_helper.sh index 697ed859e84..d221f551e66 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -8,6 +8,7 @@ python -c 'import twine' python -c 'import sphinx' python -c 'import sphinx_rtd_theme' python -c 'import pytest' +python -c 'import build' BLACK=$(tput setaf 1) From bd3807ac61ffb208e146bde78726e676e4092512 Mon Sep 17 00:00:00 2001 From: nfgf Date: Tue, 29 Nov 2022 17:59:33 -0500 Subject: [PATCH 0297/1752] Implementing decorator --- IPython/core/displayhook.py | 8 +++++++- IPython/core/interactiveshell.py | 12 ++++++------ IPython/core/magic.py | 11 ++++++++++- IPython/core/magics/execution.py | 2 ++ IPython/core/tests/test_magic.py | 17 ++++++++++++++++- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 578e783ab8e..25aa2f03274 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -91,7 +91,13 @@ def quiet(self): # some uses of ipshellembed may fail here return False - sio = _io.StringIO(cell) + return self.semicolon_at_end_of_expression(cell) + + @staticmethod + def semicolon_at_end_of_expression(expression): + """Parse Python expression and detects whether last token is ';'""" + + sio = _io.StringIO(expression) tokens = list(tokenize.generate_tokens(sio.readline)) for token in reversed(tokens): diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index c8aacdc22cf..69b441816a4 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2363,12 +2363,12 @@ def run_line_magic(self, magic_name: str, line, _stack_depth=1): with self.builtin_trap: result = fn(*args, **kwargs) - # The code below prevents output from being displayed - # when using magic %time. - # Output from '%time foo();', for instance, would never - # be displayed. - if magic_name == "time" and len(magic_arg_s) > 0 and magic_arg_s[-1] == ";": - return None + # The code below prevents the output from being displayed + # when using magics with decodator @output_can_be_disabled + # when the last Python token in the expression is a ';'. + if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_DISABLED, False): + if DisplayHook.semicolon_at_end_of_expression(magic_arg_s): + return None return result diff --git a/IPython/core/magic.py b/IPython/core/magic.py index cedba619378..0eadc179762 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -258,7 +258,7 @@ def mark(func, *a, **kw): MAGIC_NO_VAR_EXPAND_ATTR = '_ipython_magic_no_var_expand' - +MAGIC_OUTPUT_CAN_BE_DISABLED = '_ipython_magic_output_can_be_disabled' def no_var_expand(magic_func): """Mark a magic function as not needing variable expansion @@ -275,6 +275,15 @@ def no_var_expand(magic_func): setattr(magic_func, MAGIC_NO_VAR_EXPAND_ATTR, True) return magic_func +def output_can_be_disabled(magic_func): + """Mark a magic function so its output may be disabled. + + The output is disabled if the Python expression used as a parameter of + the magic ends in a semicolon, not counting a Python comment that can + follows it. + """ + setattr(magic_func, MAGIC_OUTPUT_CAN_BE_DISABLED, True) + return magic_func # Create the actual decorators for public use diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index da7f780b9cb..5d7942f6472 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -37,6 +37,7 @@ magics_class, needs_local_scope, no_var_expand, + output_can_be_disabled, on_off, ) from IPython.testing.skipdoctest import skip_doctest @@ -1194,6 +1195,7 @@ def timeit(self, line='', cell=None, local_ns=None): @no_var_expand @needs_local_scope @line_cell_magic + @output_can_be_disabled def time(self,line='', cell=None, local_ns=None): """Time execution of a Python statement or expression. diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index cb9890c4682..55408d4af1e 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -419,7 +419,7 @@ def test_time(): # ';' at the end of %time prevents instruction value to be printed. # This tests fix for #13837. -def test_time_no_outputwith_semicolon(): +def test_time_no_output_with_semicolon(): ip = get_ipython() with tt.AssertPrints(" 123456"): @@ -432,6 +432,21 @@ def test_time_no_outputwith_semicolon(): with tt.AssertPrints("CPU times: ", suppress=False): ip.run_cell("%time 123000+456;") + with tt.AssertPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456 # Comment") + + with tt.AssertNotPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456; # Comment") + + with tt.AssertPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456 # ;Comment") + def test_time_last_not_expression(): ip.run_cell("%%time\n" From a2de719356f944389f23a9e90747a9853d5a6e64 Mon Sep 17 00:00:00 2001 From: nfgf Date: Tue, 29 Nov 2022 18:07:19 -0500 Subject: [PATCH 0298/1752] Formatting to make darker happy. --- IPython/core/magic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 0eadc179762..2381bf96dac 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -275,6 +275,7 @@ def no_var_expand(magic_func): setattr(magic_func, MAGIC_NO_VAR_EXPAND_ATTR, True) return magic_func + def output_can_be_disabled(magic_func): """Mark a magic function so its output may be disabled. From 33f18f7c11f178a5610ce620151a1abf1abc53fb Mon Sep 17 00:00:00 2001 From: nfgf Date: Tue, 29 Nov 2022 18:16:15 -0500 Subject: [PATCH 0299/1752] Again --- IPython/core/magic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 2381bf96dac..a5a55e6fbcf 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -260,6 +260,7 @@ def mark(func, *a, **kw): MAGIC_NO_VAR_EXPAND_ATTR = '_ipython_magic_no_var_expand' MAGIC_OUTPUT_CAN_BE_DISABLED = '_ipython_magic_output_can_be_disabled' + def no_var_expand(magic_func): """Mark a magic function as not needing variable expansion From 10a27e1d026040a5f0415b052aa5e0614bbece46 Mon Sep 17 00:00:00 2001 From: nfgf Date: Tue, 29 Nov 2022 18:26:59 -0500 Subject: [PATCH 0300/1752] And again --- IPython/core/displayhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 25aa2f03274..aba4f904d8d 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -92,7 +92,7 @@ def quiet(self): return False return self.semicolon_at_end_of_expression(cell) - + @staticmethod def semicolon_at_end_of_expression(expression): """Parse Python expression and detects whether last token is ';'""" From a9f3943371cd8fd9e028f4e04d3bee0d43fc5dbe Mon Sep 17 00:00:00 2001 From: nfgf Date: Tue, 29 Nov 2022 18:35:18 -0500 Subject: [PATCH 0301/1752] sigh --- IPython/core/magic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index a5a55e6fbcf..82728cdebd1 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -257,8 +257,8 @@ def mark(func, *a, **kw): return magic_deco -MAGIC_NO_VAR_EXPAND_ATTR = '_ipython_magic_no_var_expand' -MAGIC_OUTPUT_CAN_BE_DISABLED = '_ipython_magic_output_can_be_disabled' +MAGIC_NO_VAR_EXPAND_ATTR = "_ipython_magic_no_var_expand" +MAGIC_OUTPUT_CAN_BE_DISABLED = "_ipython_magic_output_can_be_disabled" def no_var_expand(magic_func): From 4b0aed94df9573566e87941eec4e23bd58727097 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:23:01 +0000 Subject: [PATCH 0302/1752] Implement guarded evaluation, replace greedy, implement: - completion of integer keys - completion in pandas for loc indexer `.loc[:, ` --- IPython/core/completer.py | 243 +++++++---- IPython/core/guarded_eval.py | 541 ++++++++++++++++++++++++ IPython/core/tests/test_completer.py | 56 ++- IPython/core/tests/test_guarded_eval.py | 286 +++++++++++++ 4 files changed, 1027 insertions(+), 99 deletions(-) create mode 100644 IPython/core/guarded_eval.py create mode 100644 IPython/core/tests/test_guarded_eval.py diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 2dff9efbbfe..a497f12f01c 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -190,6 +190,7 @@ import unicodedata import uuid import warnings +from ast import literal_eval from contextlib import contextmanager from dataclasses import dataclass from functools import cached_property, partial @@ -212,6 +213,7 @@ Literal, ) +from IPython.core.guarded_eval import guarded_eval, EvaluationContext from IPython.core.error import TryNext from IPython.core.inputtransformer2 import ESC_MAGIC from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol @@ -296,6 +298,9 @@ def cast(obj, type_): # Completion type reported when no type can be inferred. _UNKNOWN_TYPE = "" +# sentinel value to signal lack of a match +not_found = object() + class ProvisionalCompleterWarning(FutureWarning): """ Exception raise by an experimental feature in this module. @@ -902,12 +907,33 @@ def split_line(self, line, cursor_pos=None): class Completer(Configurable): - greedy = Bool(False, - help="""Activate greedy completion - PENDING DEPRECATION. this is now mostly taken care of with Jedi. + greedy = Bool( + False, + help="""Activate greedy completion. + + .. deprecated:: 8.8 + Use :any:`evaluation` instead. + + As of IPython 8.8 proxy for ``evaluation = 'unsafe'`` when set to ``True``, + and for ``'forbidden'`` when set to ``False``. + """, + ).tag(config=True) - This will enable completion on elements of lists, results of function calls, etc., - but can be unsafe because the code is actually evaluated on TAB. + evaluation = Enum( + ('forbidden', 'minimal', 'limitted', 'unsafe', 'dangerous'), + default_value='limitted', + help="""Code evaluation under completion. + + Successive options allow to enable more eager evaluation for more accurate completion suggestions, + including for nested dictionaries, nested lists, or even results of function calls. Setting `unsafe` + or higher can lead to evaluation of arbitrary user code on TAB with potentially dangerous side effects. + + Allowed values are: + - `forbidden`: no evaluation at all + - `minimal`: evaluation of literals and access to built-in namespaces; no item/attribute evaluation nor access to locals/globals + - `limitted` (default): access to all namespaces, evaluation of hard-coded methods (``keys()``, ``__getattr__``, ``__getitems__``, etc) on allow-listed objects (e.g. ``dict``, ``list``, ``tuple``, ``pandas.Series``) + - `unsafe`: evaluation of all methods and function calls but not of syntax with side-effects like `del x`, + - `dangerous`: completely arbitrary evaluation """, ).tag(config=True) @@ -1029,28 +1055,16 @@ def attr_matches(self, text): with a __getattr__ hook is evaluated. """ + m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) + if not m2: + return [] + expr, attr = m2.group(1,2) - # Another option, seems to work great. Catches things like ''. - m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text) + obj = self._evaluate_expr(expr) - if m: - expr, attr = m.group(1, 3) - elif self.greedy: - m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) - if not m2: - return [] - expr, attr = m2.group(1,2) - else: + if obj is not_found: return [] - try: - obj = eval(expr, self.namespace) - except: - try: - obj = eval(expr, self.global_namespace) - except: - return [] - if self.limit_to__all__ and hasattr(obj, '__all__'): words = get__all__entries(obj) else: @@ -1068,9 +1082,33 @@ def attr_matches(self, text): pass # Build match list to return n = len(attr) - return [u"%s.%s" % (expr, w) for w in words if w[:n] == attr ] + return ["%s.%s" % (expr, w) for w in words if w[:n] == attr ] + def _evaluate_expr(self, expr): + obj = not_found + done = False + while not done and expr: + try: + obj = guarded_eval( + expr, + EvaluationContext( + globals_=self.global_namespace, + locals_=self.namespace, + evaluation=self.evaluation + ) + ) + done = True + except Exception as e: + if self.debug: + print('Evaluation exception', e) + # trim the expression to remove any invalid prefix + # e.g. user starts `(d[`, so we get `expr = '(d'`, + # where parenthesis is not closed. + # TODO: make this faster by reusing parts of the computation? + expr = expr[1:] + return obj + def get__all__entries(obj): """returns the strings in the __all__ attribute""" try: @@ -1081,8 +1119,8 @@ def get__all__entries(obj): return [w for w in words if isinstance(w, str)] -def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes]]]], prefix: str, delims: str, - extra_prefix: Optional[Tuple[str, bytes]]=None) -> Tuple[str, int, List[str]]: +def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], prefix: str, delims: str, + extra_prefix: Optional[Tuple[Union[str, bytes], ...]]=None) -> Tuple[str, int, List[str]]: """Used by dict_key_matches, matching the prefix to a list of keys Parameters @@ -1106,25 +1144,28 @@ def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes]]]], pre """ prefix_tuple = extra_prefix if extra_prefix else () + Nprefix = len(prefix_tuple) + text_serializable_types = (str, bytes, int, float, slice) def filter_prefix_tuple(key): # Reject too short keys if len(key) <= Nprefix: return False - # Reject keys with non str/bytes in it + # Reject keys which cannot be serialised to text for k in key: - if not isinstance(k, (str, bytes)): + if not isinstance(k, text_serializable_types): return False # Reject keys that do not match the prefix for k, pt in zip(key, prefix_tuple): - if k != pt: + if k != pt and not isinstance(pt, slice): return False # All checks passed! return True - filtered_keys:List[Union[str,bytes]] = [] + filtered_keys: List[Union[str, bytes, int, float, slice]] = [] + def _add_to_filtered_keys(key): - if isinstance(key, (str, bytes)): + if isinstance(key, text_serializable_types): filtered_keys.append(key) for k in keys: @@ -1140,7 +1181,7 @@ def _add_to_filtered_keys(key): assert quote_match is not None # silence mypy quote = quote_match.group() try: - prefix_str = eval(prefix + quote, {}) + prefix_str = literal_eval(prefix + quote) except Exception: return '', 0, [] @@ -1150,17 +1191,18 @@ def _add_to_filtered_keys(key): token_start = token_match.start() token_prefix = token_match.group() - matched:List[str] = [] + matched: List[str] = [] for key in filtered_keys: + str_key = key if isinstance(key, (str, bytes)) else str(key) try: - if not key.startswith(prefix_str): + if not str_key.startswith(prefix_str): continue except (AttributeError, TypeError, UnicodeError): # Python 3+ TypeError on b'a'.startswith('a') or vice-versa continue # reformat remainder of key to begin with prefix - rem = key[len(prefix_str):] + rem = str_key[len(prefix_str):] # force repr wrapped in ' rem_repr = repr(rem + '"') if isinstance(rem, str) else repr(rem + b'"') rem_repr = rem_repr[1 + rem_repr.index("'"):-2] @@ -1237,11 +1279,14 @@ def position_to_cursor(text:str, offset:int)->Tuple[int, int]: return line, col -def _safe_isinstance(obj, module, class_name): +def _safe_isinstance(obj, module, class_name, *attrs): """Checks if obj is an instance of module.class_name if loaded """ - return (module in sys.modules and - isinstance(obj, getattr(import_module(module), class_name))) + if module in sys.modules: + m = sys.modules[module] + for attr in [class_name, *attrs]: + m = getattr(m, attr) + return isinstance(obj, m) @context_matcher() @@ -1394,6 +1439,37 @@ def _make_signature(completion)-> str: _CompleteResult = Dict[str, MatcherResult] +DICT_MATCHER_REGEX = re.compile(r"""(?x) +( # match dict-referring - or any get item object - expression + .+ +) +\[ # open bracket +\s* # and optional whitespace +# Capture any number of serializable objects (e.g. "a", "b", 'c') +# and slices +((?:[uUbB]? # string prefix (r not handled) + (?: + '(?:[^']|(? List[Any]: return method() # Special case some common in-memory dict-like types - if isinstance(obj, dict) or\ - _safe_isinstance(obj, 'pandas', 'DataFrame'): + if (isinstance(obj, dict) or + _safe_isinstance(obj, 'pandas', 'DataFrame')): try: return list(obj.keys()) except Exception: return [] + elif _safe_isinstance(obj, 'pandas', 'core', 'indexing', '_LocIndexer'): + try: + return list(obj.obj.keys()) + except Exception: + return [] elif _safe_isinstance(obj, 'numpy', 'ndarray') or\ _safe_isinstance(obj, 'numpy', 'void'): return obj.dtype.names or [] @@ -2175,65 +2256,43 @@ def dict_key_matches(self, text: str) -> List[str]: You can use :meth:`dict_key_matcher` instead. """ - if self.__dict_key_regexps is not None: - regexps = self.__dict_key_regexps - else: - dict_key_re_fmt = r'''(?x) - ( # match dict-referring expression wrt greedy setting - %s - ) - \[ # open bracket - \s* # and optional whitespace - # Capture any number of str-like objects (e.g. "a", "b", 'c') - ((?:[uUbB]? # string prefix (r not handled) - (?: - '(?:[^']|(? List[str]: # - the start of the key text # - the start of the completion text_start = len(self.text_until_cursor) - len(text) - if prefix: + if key_prefix: key_start = match.start(3) completion_start = key_start + token_offset else: diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py new file mode 100644 index 00000000000..f477c6bc2c1 --- /dev/null +++ b/IPython/core/guarded_eval.py @@ -0,0 +1,541 @@ +from typing import Callable, Protocol, Set, Tuple, NamedTuple, Literal, Union +import collections +import sys +import ast +import types +from functools import cached_property +from dataclasses import dataclass, field + + +class HasGetItem(Protocol): + def __getitem__(self, key) -> None: ... + + +class InstancesHaveGetItem(Protocol): + def __call__(self) -> HasGetItem: ... + + +class HasGetAttr(Protocol): + def __getattr__(self, key) -> None: ... + + +class DoesNotHaveGetAttr(Protocol): + pass + +# By default `__getattr__` is not explicitly implemented on most objects +MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr] + + +def unbind_method(func: Callable) -> Union[Callable, None]: + """Get unbound method for given bound method. + + Returns None if cannot get unbound method.""" + owner = getattr(func, '__self__', None) + owner_class = type(owner) + name = getattr(func, '__name__', None) + instance_dict_overrides = getattr(owner, '__dict__', None) + if ( + owner is not None + and + name + and + ( + not instance_dict_overrides + or + ( + instance_dict_overrides + and name not in instance_dict_overrides + ) + ) + ): + return getattr(owner_class, name) + + +@dataclass +class EvaluationPolicy: + allow_locals_access: bool = False + allow_globals_access: bool = False + allow_item_access: bool = False + allow_attr_access: bool = False + allow_builtins_access: bool = False + allow_any_calls: bool = False + allowed_calls: Set[Callable] = field(default_factory=set) + + def can_get_item(self, value, item): + return self.allow_item_access + + def can_get_attr(self, value, attr): + return self.allow_attr_access + + def can_call(self, func): + if self.allow_any_calls: + return True + + if func in self.allowed_calls: + return True + + owner_method = unbind_method(func) + if owner_method and owner_method in self.allowed_calls: + return True + +def has_original_dunder_external(value, module_name, access_path, method_name,): + try: + if module_name not in sys.modules: + return False + member_type = sys.modules[module_name] + for attr in access_path: + member_type = getattr(member_type, attr) + value_type = type(value) + if type(value) == member_type: + return True + if isinstance(value, member_type): + method = getattr(value_type, method_name, None) + member_method = getattr(member_type, method_name, None) + if member_method == method: + return True + except (AttributeError, KeyError): + return False + + +def has_original_dunder( + value, + allowed_types, + allowed_methods, + allowed_external, + method_name +): + # note: Python ignores `__getattr__`/`__getitem__` on instances, + # we only need to check at class level + value_type = type(value) + + # strict type check passes → no need to check method + if value_type in allowed_types: + return True + + method = getattr(value_type, method_name, None) + + if not method: + return None + + if method in allowed_methods: + return True + + for module_name, *access_path in allowed_external: + if has_original_dunder_external(value, module_name, access_path, method_name): + return True + + return False + + +@dataclass +class SelectivePolicy(EvaluationPolicy): + allowed_getitem: Set[HasGetItem] = field(default_factory=set) + allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set) + allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set) + allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set) + + def can_get_attr(self, value, attr): + has_original_attribute = has_original_dunder( + value, + allowed_types=self.allowed_getattr, + allowed_methods=self._getattribute_methods, + allowed_external=self.allowed_getattr_external, + method_name='__getattribute__' + ) + has_original_attr = has_original_dunder( + value, + allowed_types=self.allowed_getattr, + allowed_methods=self._getattr_methods, + allowed_external=self.allowed_getattr_external, + method_name='__getattr__' + ) + # Many objects do not have `__getattr__`, this is fine + if has_original_attr is None and has_original_attribute: + return True + + # Accept objects without modifications to `__getattr__` and `__getattribute__` + return has_original_attr and has_original_attribute + + def get_attr(self, value, attr): + if self.can_get_attr(value, attr): + return getattr(value, attr) + + + def can_get_item(self, value, item): + """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" + return has_original_dunder( + value, + allowed_types=self.allowed_getitem, + allowed_methods=self._getitem_methods, + allowed_external=self.allowed_getitem_external, + method_name='__getitem__' + ) + + @cached_property + def _getitem_methods(self) -> Set[Callable]: + return self._safe_get_methods( + self.allowed_getitem, + '__getitem__' + ) + + @cached_property + def _getattr_methods(self) -> Set[Callable]: + return self._safe_get_methods( + self.allowed_getattr, + '__getattr__' + ) + + @cached_property + def _getattribute_methods(self) -> Set[Callable]: + return self._safe_get_methods( + self.allowed_getattr, + '__getattribute__' + ) + + def _safe_get_methods(self, classes, name) -> Set[Callable]: + return { + method + for class_ in classes + for method in [getattr(class_, name, None)] + if method + } + + +class DummyNamedTuple(NamedTuple): + pass + + +class EvaluationContext(NamedTuple): + locals_: dict + globals_: dict + evaluation: Literal['forbidden', 'minimal', 'limitted', 'unsafe', 'dangerous'] = 'forbidden' + in_subscript: bool = False + + +class IdentitySubscript: + def __getitem__(self, key): + return key + +IDENTITY_SUBSCRIPT = IdentitySubscript() +SUBSCRIPT_MARKER = '__SUBSCRIPT_SENTINEL__' + +class GuardRejection(ValueError): + pass + + +def guarded_eval( + code: str, + context: EvaluationContext +): + locals_ = context.locals_ + + if context.evaluation == 'forbidden': + raise GuardRejection('Forbidden mode') + + # note: not using `ast.literal_eval` as it does not implement + # getitem at all, for example it fails on simple `[0][1]` + + if context.in_subscript: + # syntatic sugar for ellipsis (:) is only available in susbcripts + # so we need to trick the ast parser into thinking that we have + # a subscript, but we need to be able to later recognise that we did + # it so we can ignore the actual __getitem__ operation + if not code: + return tuple() + locals_ = locals_.copy() + locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT + code = SUBSCRIPT_MARKER + '[' + code + ']' + context = EvaluationContext(**{ + **context._asdict(), + **{'locals_': locals_} + }) + + if context.evaluation == 'dangerous': + return eval(code, context.globals_, context.locals_) + + expression = ast.parse(code, mode='eval') + + return eval_node(expression, context) + +def eval_node(node: Union[ast.AST, None], context: EvaluationContext): + """ + Evaluate AST node in provided context. + + Applies evaluation restrictions defined in the context. + + Currently does not support evaluation of functions with arguments. + + Does not evaluate actions which always have side effects: + - class definitions (`class sth: ...`) + - function definitions (`def sth: ...`) + - variable assignments (`x = 1`) + - augumented assignments (`x += 1`) + - deletions (`del x`) + + Does not evaluate operations which do not return values: + - assertions (`assert x`) + - pass (`pass`) + - imports (`import x`) + - control flow + - conditionals (`if x:`) except for terenary IfExp (`a if x else b`) + - loops (`for` and `while`) + - exception handling + """ + policy = EVALUATION_POLICIES[context.evaluation] + if node is None: + return None + if isinstance(node, ast.Expression): + return eval_node(node.body, context) + if isinstance(node, ast.BinOp): + # TODO: add guards + left = eval_node(node.left, context) + right = eval_node(node.right, context) + if isinstance(node.op, ast.Add): + return left + right + if isinstance(node.op, ast.Sub): + return left - right + if isinstance(node.op, ast.Mult): + return left * right + if isinstance(node.op, ast.Div): + return left / right + if isinstance(node.op, ast.FloorDiv): + return left // right + if isinstance(node.op, ast.Mod): + return left % right + if isinstance(node.op, ast.Pow): + return left ** right + if isinstance(node.op, ast.LShift): + return left << right + if isinstance(node.op, ast.RShift): + return left >> right + if isinstance(node.op, ast.BitOr): + return left | right + if isinstance(node.op, ast.BitXor): + return left ^ right + if isinstance(node.op, ast.BitAnd): + return left & right + if isinstance(node.op, ast.MatMult): + return left @ right + if isinstance(node, ast.Constant): + return node.value + if isinstance(node, ast.Index): + return eval_node(node.value, context) + if isinstance(node, ast.Tuple): + return tuple( + eval_node(e, context) + for e in node.elts + ) + if isinstance(node, ast.List): + return [ + eval_node(e, context) + for e in node.elts + ] + if isinstance(node, ast.Set): + return { + eval_node(e, context) + for e in node.elts + } + if isinstance(node, ast.Dict): + return dict(zip( + [eval_node(k, context) for k in node.keys], + [eval_node(v, context) for v in node.values] + )) + if isinstance(node, ast.Slice): + return slice( + eval_node(node.lower, context), + eval_node(node.upper, context), + eval_node(node.step, context) + ) + if isinstance(node, ast.ExtSlice): + return tuple([ + eval_node(dim, context) + for dim in node.dims + ]) + if isinstance(node, ast.UnaryOp): + # TODO: add guards + value = eval_node(node.operand, context) + if isinstance(node.op, ast.USub): + return -value + if isinstance(node.op, ast.UAdd): + return +value + if isinstance(node.op, ast.Invert): + return ~value + if isinstance(node.op, ast.Not): + return not value + raise ValueError('Unhandled unary operation:', node.op) + if isinstance(node, ast.Subscript): + value = eval_node(node.value, context) + slice_ = eval_node(node.slice, context) + if policy.can_get_item(value, slice_): + return value[slice_] + raise GuardRejection( + 'Subscript access (`__getitem__`) for', + type(value), # not joined to avoid calling `repr` + f' not allowed in {context.evaluation} mode' + ) + if isinstance(node, ast.Name): + if policy.allow_locals_access and node.id in context.locals_: + return context.locals_[node.id] + if policy.allow_globals_access and node.id in context.globals_: + return context.globals_[node.id] + if policy.allow_builtins_access and node.id in __builtins__: + return __builtins__[node.id] + if not policy.allow_globals_access and not policy.allow_locals_access: + raise GuardRejection( + f'Namespace access not allowed in {context.evaluation} mode' + ) + else: + raise NameError(f'{node.id} not found in locals nor globals') + if isinstance(node, ast.Attribute): + value = eval_node(node.value, context) + if policy.can_get_attr(value, node.attr): + return getattr(value, node.attr) + raise GuardRejection( + 'Attribute access (`__getattr__`) for', + type(value), # not joined to avoid calling `repr` + f'not allowed in {context.evaluation} mode' + ) + if isinstance(node, ast.IfExp): + test = eval_node(node.test, context) + if test: + return eval_node(node.body, context) + else: + return eval_node(node.orelse, context) + if isinstance(node, ast.Call): + func = eval_node(node.func, context) + print(node.keywords) + if policy.can_call(func) and not node.keywords: + args = [ + eval_node(arg, context) + for arg in node.args + ] + return func(*args) + raise GuardRejection( + 'Call for', + func, # not joined to avoid calling `repr` + f'not allowed in {context.evaluation} mode' + ) + raise ValueError('Unhandled node', node) + + +SUPPORTED_EXTERNAL_GETITEM = { + ('pandas', 'core', 'indexing', '_iLocIndexer'), + ('pandas', 'core', 'indexing', '_LocIndexer'), + ('pandas', 'DataFrame'), + ('pandas', 'Series'), + ('numpy', 'ndarray'), + ('numpy', 'void') +} + +BUILTIN_GETITEM = { + dict, + str, + bytes, + list, + tuple, + collections.defaultdict, + collections.deque, + collections.OrderedDict, + collections.ChainMap, + collections.UserDict, + collections.UserList, + collections.UserString, + DummyNamedTuple, + IdentitySubscript +} + + +def _list_methods(cls, source=None): + """For use on immutable objects or with methods returning a copy""" + return [ + getattr(cls, k) + for k in (source if source else dir(cls)) + ] + + +dict_non_mutating_methods = ('copy', 'keys', 'values', 'items') +list_non_mutating_methods = ('copy', 'index', 'count') +set_non_mutating_methods = set(dir(set)) & set(dir(frozenset)) + + +dict_keys = type({}.keys()) +method_descriptor = type(list.copy) + +ALLOWED_CALLS = { + bytes, + *_list_methods(bytes), + dict, + *_list_methods(dict, dict_non_mutating_methods), + dict_keys.isdisjoint, + list, + *_list_methods(list, list_non_mutating_methods), + set, + *_list_methods(set, set_non_mutating_methods), + frozenset, + *_list_methods(frozenset), + range, + str, + *_list_methods(str), + tuple, + *_list_methods(tuple), + collections.deque, + *_list_methods(collections.deque, list_non_mutating_methods), + collections.defaultdict, + *_list_methods(collections.defaultdict, dict_non_mutating_methods), + collections.OrderedDict, + *_list_methods(collections.OrderedDict, dict_non_mutating_methods), + collections.UserDict, + *_list_methods(collections.UserDict, dict_non_mutating_methods), + collections.UserList, + *_list_methods(collections.UserList, list_non_mutating_methods), + collections.UserString, + *_list_methods(collections.UserString, dir(str)), + collections.Counter, + *_list_methods(collections.Counter, dict_non_mutating_methods), + collections.Counter.elements, + collections.Counter.most_common +} + +EVALUATION_POLICIES = { + 'minimal': EvaluationPolicy( + allow_builtins_access=True, + allow_locals_access=False, + allow_globals_access=False, + allow_item_access=False, + allow_attr_access=False, + allowed_calls=set(), + allow_any_calls=False + ), + 'limitted': SelectivePolicy( + # TODO: + # - should reject binary and unary operations if custom methods would be dispatched + allowed_getitem=BUILTIN_GETITEM, + allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM, + allowed_getattr={ + *BUILTIN_GETITEM, + set, + frozenset, + object, + type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. + dict_keys, + method_descriptor + }, + allowed_getattr_external={ + # pandas Series/Frame implements custom `__getattr__` + ('pandas', 'DataFrame'), + ('pandas', 'Series') + }, + allow_builtins_access=True, + allow_locals_access=True, + allow_globals_access=True, + allowed_calls=ALLOWED_CALLS + ), + 'unsafe': EvaluationPolicy( + allow_builtins_access=True, + allow_locals_access=True, + allow_globals_access=True, + allow_attr_access=True, + allow_item_access=True, + allow_any_calls=True + ) +} \ No newline at end of file diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 98ec814a769..7a99a2655ab 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -112,6 +112,17 @@ def greedy_completion(): ip.Completer.greedy = greedy_original +@contextmanager +def evaluation_level(evaluation: str): + ip = get_ipython() + evaluation_original = ip.Completer.evaluation + try: + ip.Completer.evaluation = evaluation + yield + finally: + ip.Completer.evaluation = evaluation_original + + @contextmanager def custom_matchers(matchers): ip = get_ipython() @@ -522,10 +533,10 @@ class Z: def test_greedy_completions(self): """ - Test the capability of the Greedy completer. + Test the capability of the Greedy completer. Most of the test here does not really show off the greedy completer, for proof - each of the text below now pass with Jedi. The greedy completer is capable of more. + each of the text below now pass with Jedi. The greedy completer is capable of more. See the :any:`test_dict_key_completion_contexts` @@ -852,15 +863,13 @@ def test_match_dict_keys(self): assert match_dict_keys(keys, '"', delims=delims) == ('"', 1, ["foo"]) assert match_dict_keys(keys, '"f', delims=delims) == ('"', 1, ["foo"]) - match_dict_keys - def test_match_dict_keys_tuple(self): """ Test that match_dict_keys called with extra prefix works on a couple of use case, does return what expected, and does not crash. """ delims = " \t\n`!@#$^&*()=+[{]}\\|;:'\",<>?" - + keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] # Completion on first key == "foo" @@ -883,6 +892,11 @@ def test_match_dict_keys_tuple(self): assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) + keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, 'test')] + assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["1111", "2222"]) + assert match_dict_keys(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar", "test"]) + assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) + def test_dict_key_completion_string(self): """Test dictionary key completion for string keys""" ip = get_ipython() @@ -1050,6 +1064,7 @@ class C: ip.user_ns["C"] = C ip.user_ns["get"] = lambda: d + ip.user_ns["nested"] = {'x': d} def assert_no_completion(**kwargs): _, matches = complete(**kwargs) @@ -1075,6 +1090,13 @@ def assert_completion(**kwargs): assert_completion(line_buffer="(d[") assert_completion(line_buffer="C.data[") + # nested dict completion + assert_completion(line_buffer="nested['x'][") + + with evaluation_level('minimal'): + with pytest.raises(AssertionError): + assert_completion(line_buffer="nested['x'][") + # greedy flag def assert_completion(**kwargs): _, matches = complete(**kwargs) @@ -1162,12 +1184,21 @@ def test_struct_array_key_completion(self): _, matches = complete(line_buffer="d['") self.assertIn("my_head", matches) self.assertIn("my_data", matches) - # complete on a nested level - with greedy_completion(): + def completes_on_nested(): ip.user_ns["d"] = numpy.zeros(2, dtype=dt) _, matches = complete(line_buffer="d[1]['my_head']['") self.assertTrue(any(["my_dt" in m for m in matches])) self.assertTrue(any(["my_df" in m for m in matches])) + # complete on a nested level + with greedy_completion(): + completes_on_nested() + + with evaluation_level('limitted'): + completes_on_nested() + + with evaluation_level('minimal'): + with pytest.raises(AssertionError): + completes_on_nested() @dec.skip_without("pandas") def test_dataframe_key_completion(self): @@ -1180,6 +1211,17 @@ def test_dataframe_key_completion(self): _, matches = complete(line_buffer="d['") self.assertIn("hello", matches) self.assertIn("world", matches) + _, matches = complete(line_buffer="d.loc[:, '") + self.assertIn("hello", matches) + self.assertIn("world", matches) + _, matches = complete(line_buffer="d.loc[1:, '") + self.assertIn("hello", matches) + _, matches = complete(line_buffer="d.loc[1:1, '") + self.assertIn("hello", matches) + _, matches = complete(line_buffer="d.loc[1:1:-1, '") + self.assertIn("hello", matches) + _, matches = complete(line_buffer="d.loc[::, '") + self.assertIn("hello", matches) def test_dict_key_completion_invalids(self): """Smoke test cases dict key completion can't handle""" diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py new file mode 100644 index 00000000000..5c89a68f637 --- /dev/null +++ b/IPython/core/tests/test_guarded_eval.py @@ -0,0 +1,286 @@ +from typing import NamedTuple +from IPython.core.guarded_eval import EvaluationContext, GuardRejection, guarded_eval, unbind_method +from IPython.testing import decorators as dec +import pytest + + +def limitted(**kwargs): + return EvaluationContext( + locals_=kwargs, + globals_={}, + evaluation='limitted' + ) + + +def unsafe(**kwargs): + return EvaluationContext( + locals_=kwargs, + globals_={}, + evaluation='unsafe' + ) + +@dec.skip_without('pandas') +def test_pandas_series_iloc(): + import pandas as pd + series = pd.Series([1], index=['a']) + context = limitted(data=series) + assert guarded_eval('data.iloc[0]', context) == 1 + + +@dec.skip_without('pandas') +def test_pandas_series(): + import pandas as pd + context = limitted(data=pd.Series([1], index=['a'])) + assert guarded_eval('data["a"]', context) == 1 + with pytest.raises(KeyError): + guarded_eval('data["c"]', context) + + +@dec.skip_without('pandas') +def test_pandas_bad_series(): + import pandas as pd + class BadItemSeries(pd.Series): + def __getitem__(self, key): + return 'CUSTOM_ITEM' + + class BadAttrSeries(pd.Series): + def __getattr__(self, key): + return 'CUSTOM_ATTR' + + bad_series = BadItemSeries([1], index=['a']) + context = limitted(data=bad_series) + + with pytest.raises(GuardRejection): + guarded_eval('data["a"]', context) + with pytest.raises(GuardRejection): + guarded_eval('data["c"]', context) + + # note: here result is a bit unexpected because + # pandas `__getattr__` calls `__getitem__`; + # FIXME - special case to handle it? + assert guarded_eval('data.a', context) == 'CUSTOM_ITEM' + + context = unsafe(data=bad_series) + assert guarded_eval('data["a"]', context) == 'CUSTOM_ITEM' + + bad_attr_series = BadAttrSeries([1], index=['a']) + context = limitted(data=bad_attr_series) + assert guarded_eval('data["a"]', context) == 1 + with pytest.raises(GuardRejection): + guarded_eval('data.a', context) + + +@dec.skip_without('pandas') +def test_pandas_dataframe_loc(): + import pandas as pd + from pandas.testing import assert_series_equal + data = pd.DataFrame([{'a': 1}]) + context = limitted(data=data) + assert_series_equal( + guarded_eval('data.loc[:, "a"]', context), + data['a'] + ) + + +def test_named_tuple(): + + class GoodNamedTuple(NamedTuple): + a: str + pass + + class BadNamedTuple(NamedTuple): + a: str + def __getitem__(self, key): + return None + + good = GoodNamedTuple(a='x') + bad = BadNamedTuple(a='x') + + context = limitted(data=good) + assert guarded_eval('data[0]', context) == 'x' + + context = limitted(data=bad) + with pytest.raises(GuardRejection): + guarded_eval('data[0]', context) + + +def test_dict(): + context = limitted( + data={'a': 1, 'b': {'x': 2}, ('x', 'y'): 3} + ) + assert guarded_eval('data["a"]', context) == 1 + assert guarded_eval('data["b"]', context) == {'x': 2} + assert guarded_eval('data["b"]["x"]', context) == 2 + assert guarded_eval('data["x", "y"]', context) == 3 + + assert guarded_eval('data.keys', context) + + +def test_set(): + context = limitted(data={'a', 'b'}) + assert guarded_eval('data.difference', context) + + +def test_list(): + context = limitted(data=[1, 2, 3]) + assert guarded_eval('data[1]', context) == 2 + assert guarded_eval('data.copy', context) + + +def test_dict_literal(): + context = limitted() + assert guarded_eval('{}', context) == {} + assert guarded_eval('{"a": 1}', context) == {"a": 1} + + +def test_list_literal(): + context = limitted() + assert guarded_eval('[]', context) == [] + assert guarded_eval('[1, "a"]', context) == [1, "a"] + + +def test_set_literal(): + context = limitted() + assert guarded_eval('set()', context) == set() + assert guarded_eval('{"a"}', context) == {"a"} + + +def test_if_expression(): + context = limitted() + assert guarded_eval('2 if True else 3', context) == 2 + assert guarded_eval('4 if False else 5', context) == 5 + + +def test_object(): + obj = object() + context = limitted(obj=obj) + assert guarded_eval('obj.__dir__', context) == obj.__dir__ + + +@pytest.mark.parametrize( + "code,expected", + [ + [ + 'int.numerator', + int.numerator + ], + [ + 'float.is_integer', + float.is_integer + ], + [ + 'complex.real', + complex.real + ] + ] +) +def test_number_attributes(code, expected): + assert guarded_eval(code, limitted()) == expected + + +def test_method_descriptor(): + context = limitted() + assert guarded_eval('list.copy.__name__', context) == 'copy' + + +@pytest.mark.parametrize( + "data,good,bad,expected", + [ + [ + [1, 2, 3], + 'data.index(2)', + 'data.append(4)', + 1 + ], + [ + {'a': 1}, + 'data.keys().isdisjoint({})', + 'data.update()', + True + ] + ] +) +def test_calls(data, good, bad, expected): + context = limitted(data=data) + assert guarded_eval(good, context) == expected + + with pytest.raises(GuardRejection): + guarded_eval(bad, context) + + +@pytest.mark.parametrize( + "code,expected", + [ + [ + '(1\n+\n1)', + 2 + ], + [ + 'list(range(10))[-1:]', + [9] + ], + [ + 'list(range(20))[3:-2:3]', + [3, 6, 9, 12, 15] + ] + ] +) +def test_literals(code, expected): + context = limitted() + assert guarded_eval(code, context) == expected + + +def test_subscript(): + context = EvaluationContext( + locals_={}, + globals_={}, + evaluation='limitted', + in_subscript=True + ) + empty_slice = slice(None, None, None) + assert guarded_eval('', context) == tuple() + assert guarded_eval(':', context) == empty_slice + assert guarded_eval('1:2:3', context) == slice(1, 2, 3) + assert guarded_eval(':, "a"', context) == (empty_slice, "a") + + +def test_unbind_method(): + class X(list): + def index(self, k): + return 'CUSTOM' + x = X() + assert unbind_method(x.index) is X.index + assert unbind_method([].index) is list.index + + +def test_assumption_instance_attr_do_not_matter(): + """This is semi-specified in Python documentation. + + However, since the specification says 'not guaranted + to work' rather than 'is forbidden to work', future + versions could invalidate this assumptions. This test + is meant to catch such a change if it ever comes true. + """ + class T: + def __getitem__(self, k): + return 'a' + def __getattr__(self, k): + return 'a' + t = T() + t.__getitem__ = lambda f: 'b' + t.__getattr__ = lambda f: 'b' + assert t[1] == 'a' + assert t[1] == 'a' + + +def test_assumption_named_tuples_share_getitem(): + """Check assumption on named tuples sharing __getitem__""" + from typing import NamedTuple + + class A(NamedTuple): + pass + + class B(NamedTuple): + pass + + assert A.__getitem__ == B.__getitem__ From 4c580c1497d56a21acf380031b2b60a22219913a Mon Sep 17 00:00:00 2001 From: nfgf Date: Wed, 30 Nov 2022 19:27:49 -0500 Subject: [PATCH 0303/1752] Terminology: output is silenced, not disabled. --- IPython/core/interactiveshell.py | 4 ++-- IPython/core/magic.py | 12 ++++++------ IPython/core/magics/execution.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 69b441816a4..12503e9d916 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2364,9 +2364,9 @@ def run_line_magic(self, magic_name: str, line, _stack_depth=1): result = fn(*args, **kwargs) # The code below prevents the output from being displayed - # when using magics with decodator @output_can_be_disabled + # when using magics with decodator @output_can_be_silenced # when the last Python token in the expression is a ';'. - if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_DISABLED, False): + if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False): if DisplayHook.semicolon_at_end_of_expression(magic_arg_s): return None diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 82728cdebd1..95653dc7893 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -258,7 +258,7 @@ def mark(func, *a, **kw): MAGIC_NO_VAR_EXPAND_ATTR = "_ipython_magic_no_var_expand" -MAGIC_OUTPUT_CAN_BE_DISABLED = "_ipython_magic_output_can_be_disabled" +MAGIC_OUTPUT_CAN_BE_SILENCED = "_ipython_magic_output_can_be_silenced" def no_var_expand(magic_func): @@ -277,14 +277,14 @@ def no_var_expand(magic_func): return magic_func -def output_can_be_disabled(magic_func): - """Mark a magic function so its output may be disabled. +def output_can_be_silenced(magic_func): + """Mark a magic function so its output may be silenced. - The output is disabled if the Python expression used as a parameter of + The output is silenced if the Python expression used as a parameter of the magic ends in a semicolon, not counting a Python comment that can - follows it. + follow it. """ - setattr(magic_func, MAGIC_OUTPUT_CAN_BE_DISABLED, True) + setattr(magic_func, MAGIC_OUTPUT_CAN_BE_SILENCED, True) return magic_func # Create the actual decorators for public use diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index 5d7942f6472..7b558d5bc6a 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -37,7 +37,7 @@ magics_class, needs_local_scope, no_var_expand, - output_can_be_disabled, + output_can_be_silenced, on_off, ) from IPython.testing.skipdoctest import skip_doctest @@ -1195,7 +1195,7 @@ def timeit(self, line='', cell=None, local_ns=None): @no_var_expand @needs_local_scope @line_cell_magic - @output_can_be_disabled + @output_can_be_silenced def time(self,line='', cell=None, local_ns=None): """Time execution of a Python statement or expression. From 61b0fb8f67cdc870ae116b9efbdc6fab76119cc1 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Thu, 1 Dec 2022 07:07:14 -0600 Subject: [PATCH 0304/1752] move all entry_point definitions to setup.py --- setup.cfg | 6 ------ setup.py | 10 +++++++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/setup.cfg b/setup.cfg index 226506f08f0..769bfda14a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -105,12 +105,6 @@ IPython.core.tests = *.png, *.jpg, daft_extension/*.py IPython.lib.tests = *.wav IPython.testing.plugin = *.txt -[options.entry_points] -pygments.lexers = - ipythonconsole = IPython.lib.lexers:IPythonConsoleLexer - ipython = IPython.lib.lexers:IPythonLexer - ipython3 = IPython.lib.lexers:IPython3Lexer - [velin] ignore_patterns = IPython/core/tests diff --git a/setup.py b/setup.py index 4939ca53836..454c297524f 100644 --- a/setup.py +++ b/setup.py @@ -139,7 +139,15 @@ 'install_scripts_sym': install_scripts_for_symlink, 'unsymlink': unsymlink, } -setup_args["entry_points"] = {"console_scripts": find_entry_points()} + +setup_args["entry_points"] = { + "console_scripts": find_entry_points(), + "pygments.lexers": [ + "ipythonconsole = IPython.lib.lexers:IPythonConsoleLexer", + "ipython = IPython.lib.lexers:IPythonLexer", + "ipython3 = IPython.lib.lexers:IPython3Lexer", + ] +} #--------------------------------------------------------------------------- # Do the actual setup now From 5a611b0080ff68ad344125c27dc52b8ea041abc5 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Fri, 2 Dec 2022 21:47:31 +0000 Subject: [PATCH 0305/1752] docs: remove mention of `_ipython_display_` being ignored in the REPL --- docs/source/config/integrating.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/config/integrating.rst b/docs/source/config/integrating.rst index 07429ef1792..23cc1e58875 100644 --- a/docs/source/config/integrating.rst +++ b/docs/source/config/integrating.rst @@ -128,7 +128,6 @@ More powerful methods Displays the object as a side effect; the return value is ignored. If this is defined, all other display methods are ignored. - This method is ignored in the REPL. Metadata From c986c9eddefc3363d98a27c9cd6aa8d58fe8053b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 00:37:34 +0000 Subject: [PATCH 0306/1752] Re-implement key closing behaviour with a setting, improve number handling and static typing --- IPython/core/completer.py | 307 +++++++++++++++++++++------ IPython/core/guarded_eval.py | 35 +-- IPython/core/tests/test_completer.py | 191 ++++++++++++++--- 3 files changed, 430 insertions(+), 103 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index a497f12f01c..e53e83b38e9 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -178,6 +178,7 @@ from __future__ import annotations import builtins as builtin_mod +import enum import glob import inspect import itertools @@ -186,15 +187,16 @@ import re import string import sys +import tokenize import time import unicodedata import uuid import warnings from ast import literal_eval +from collections import defaultdict from contextlib import contextmanager from dataclasses import dataclass from functools import cached_property, partial -from importlib import import_module from types import SimpleNamespace from typing import ( Iterable, @@ -205,8 +207,6 @@ Any, Sequence, Dict, - NamedTuple, - Pattern, Optional, TYPE_CHECKING, Set, @@ -233,7 +233,6 @@ Unicode, Dict as DictTrait, Union as UnionTrait, - default, observe, ) from traitlets.config.configurable import Configurable @@ -559,7 +558,7 @@ class SimpleCompletion: __slots__ = ["text", "type"] - def __init__(self, text: str, *, type: str = None): + def __init__(self, text: str, *, type: Optional[str] = None): self.text = text self.type = type @@ -647,16 +646,18 @@ def line_with_cursor(self) -> str: class _MatcherAPIv1Base(Protocol): - def __call__(self, text: str) -> list[str]: + def __call__(self, text: str) -> List[str]: """Call signature.""" + ... class _MatcherAPIv1Total(_MatcherAPIv1Base, Protocol): #: API version matcher_api_version: Optional[Literal[1]] - def __call__(self, text: str) -> list[str]: + def __call__(self, text: str) -> List[str]: """Call signature.""" + ... #: Protocol describing Matcher API v1. @@ -671,6 +672,7 @@ class MatcherAPIv2(Protocol): def __call__(self, context: CompletionContext) -> MatcherResult: """Call signature.""" + ... Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2] @@ -912,10 +914,11 @@ class Completer(Configurable): help="""Activate greedy completion. .. deprecated:: 8.8 - Use :any:`evaluation` instead. + Use :any:`evaluation` and :any:`auto_close_dict_keys` instead. - As of IPython 8.8 proxy for ``evaluation = 'unsafe'`` when set to ``True``, - and for ``'forbidden'`` when set to ``False``. + Whent enabled in IPython 8.8+ activates following settings for compatibility: + - ``evaluation = 'unsafe'`` + - ``auto_close_dict_keys = True`` """, ).tag(config=True) @@ -957,6 +960,11 @@ class Completer(Configurable): "Includes completion of latex commands, unicode names, and expanding " "unicode characters back to latex commands.").tag(config=True) + auto_close_dict_keys = Bool( + False, + help="""Enable auto-closing dictionary keys.""" + ).tag(config=True) + def __init__(self, namespace=None, global_namespace=None, **kwargs): """Create a new completer for the command line. @@ -1119,8 +1127,80 @@ def get__all__entries(obj): return [w for w in words if isinstance(w, str)] -def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], prefix: str, delims: str, - extra_prefix: Optional[Tuple[Union[str, bytes], ...]]=None) -> Tuple[str, int, List[str]]: +class DictKeyState(enum.Flag): + """Represent state of the key match in context of other possible matches. + + - given `d1 = {'a': 1}` completion on `d1['` will yield `{'a': END_OF_ITEM}` as there is no tuple. + - given `d2 = {('a', 'b'): 1}`: `d2['a', '` will yield `{'b': END_OF_TUPLE}` as there is no tuple members to add beyond `'b'`. + - given `d3 = {('a', 'b'): 1}`: `d3['` will yield `{'a': IN_TUPLE}` as `'a'` can be added. + - given `d4 = {'a': 1, ('a', 'b'): 2}`: `d4['` will yield `{'a': END_OF_ITEM & END_OF_TUPLE}` + """ + BASELINE = 0 + END_OF_ITEM = enum.auto() + END_OF_TUPLE = enum.auto() + IN_TUPLE = enum.auto() + + +def _parse_tokens(c): + tokens = [] + token_generator = tokenize.generate_tokens(iter(c.splitlines()).__next__) + while True: + try: + tokens.append(next(token_generator)) + except tokenize.TokenError: + return tokens + except StopIteration: + return tokens + + +def _match_number_in_dict_key_prefix(prefix: str) -> Union[str, None]: + """Match any valid Python numeric literal in a prefix of dictionary keys. + + References: + - https://docs.python.org/3/reference/lexical_analysis.html#numeric-literals + - https://docs.python.org/3/library/tokenize.html + """ + if prefix[-1].isspace(): + # if user typed a space we do not have anything to complete + # even if there was a valid number token before + return None + tokens = _parse_tokens(prefix) + rev_tokens = reversed(tokens) + skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} + number = None + for token in rev_tokens: + if token.type in skip_over: + continue + if number is None: + if token.type == tokenize.NUMBER: + number = token.string + continue + else: + # we did not match a number + return None + if token.type == tokenize.OP: + if token.string == ',': + break + if token.string in {'+', '-'}: + number = token.string + number + else: + return None + return number + + +_INT_FORMATS = { + '0b': bin, + '0o': oct, + '0x': hex, +} + + +def match_dict_keys( + keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], + prefix: str, + delims: str, + extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None +) -> Tuple[str, int, Dict[str, DictKeyState]]: """Used by dict_key_matches, matching the prefix to a list of keys Parameters @@ -1140,16 +1220,21 @@ def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]] A tuple of three elements: ``quote``, ``token_start``, ``matched``, with ``quote`` being the quote that need to be used to close current string. ``token_start`` the position where the replacement should start occurring, - ``matches`` a list of replacement/completion - + ``matches`` a dictionary of replacement/completion keys on keys and values + indicating whether the state. """ prefix_tuple = extra_prefix if extra_prefix else () - Nprefix = len(prefix_tuple) + prefix_tuple_size = sum([ + # for pandas, do not count slices as taking space + not isinstance(k, slice) + for k in prefix_tuple + ]) text_serializable_types = (str, bytes, int, float, slice) + def filter_prefix_tuple(key): # Reject too short keys - if len(key) <= Nprefix: + if len(key) <= prefix_tuple_size: return False # Reject keys which cannot be serialised to text for k in key: @@ -1162,28 +1247,58 @@ def filter_prefix_tuple(key): # All checks passed! return True - filtered_keys: List[Union[str, bytes, int, float, slice]] = [] - - def _add_to_filtered_keys(key): - if isinstance(key, text_serializable_types): - filtered_keys.append(key) + filtered_key_is_final: Dict[Union[str, bytes, int, float], DictKeyState] = defaultdict(lambda: DictKeyState.BASELINE) for k in keys: + # If at least one of the matches is not final, mark as undetermined. + # This can happen with `d = {111: 'b', (111, 222): 'a'}` where + # `111` appears final on first match but is not final on the second. + if isinstance(k, tuple): if filter_prefix_tuple(k): - _add_to_filtered_keys(k[Nprefix]) + key_fragment = k[prefix_tuple_size] + filtered_key_is_final[key_fragment] |= ( + DictKeyState.END_OF_TUPLE + if len(k) == prefix_tuple_size + 1 else + DictKeyState.IN_TUPLE + ) + elif prefix_tuple_size > 0: + # we are completing a tuple but this key is not a tuple, + # so we should ignore it + pass else: - _add_to_filtered_keys(k) + if isinstance(k, text_serializable_types): + filtered_key_is_final[k] |= DictKeyState.END_OF_ITEM + + filtered_keys = filtered_key_is_final.keys() if not prefix: - return '', 0, [repr(k) for k in filtered_keys] - quote_match = re.search('["\']', prefix) - assert quote_match is not None # silence mypy - quote = quote_match.group() - try: - prefix_str = literal_eval(prefix + quote) - except Exception: - return '', 0, [] + return '', 0, {repr(k): v for k, v in filtered_key_is_final.items()} + + quote_match = re.search('(?:"|\')', prefix) + is_user_prefix_numeric = False + + if quote_match: + quote = quote_match.group() + valid_prefix = prefix + quote + try: + prefix_str = literal_eval(valid_prefix) + except Exception: + return '', 0, {} + else: + # If it does not look like a string, let's assume + # we are dealing with a number or variable. + number_match = _match_number_in_dict_key_prefix(prefix) + + # We do not want the key matcher to suggest variable names so we yield: + if number_match is None: + # The alternative would be to assume that user forgort the quote + # and if the substring matches, suggest adding it at the start. + return '', 0, {} + + prefix_str = number_match + is_user_prefix_numeric = True + quote = '' pattern = '[^' + ''.join('\\' + c for c in delims) + ']*$' token_match = re.search(pattern, prefix, re.UNICODE) @@ -1191,13 +1306,29 @@ def _add_to_filtered_keys(key): token_start = token_match.start() token_prefix = token_match.group() - matched: List[str] = [] + matched: Dict[str, DictKeyState] = {} + for key in filtered_keys: - str_key = key if isinstance(key, (str, bytes)) else str(key) + if isinstance(key, (int, float)): + # User typed a number but this key is not a number. + if not is_user_prefix_numeric: + continue + str_key = str(key) + if isinstance(key, int): + int_base = prefix_str[:2].lower() + # if user typed integer using binary/oct/hex notation: + if int_base in _INT_FORMATS: + int_format = _INT_FORMATS[int_base] + str_key = int_format(key) + else: + # User typed a string but this key is a number. + if is_user_prefix_numeric: + continue + str_key = key try: if not str_key.startswith(prefix_str): continue - except (AttributeError, TypeError, UnicodeError): + except (AttributeError, TypeError, UnicodeError) as e: # Python 3+ TypeError on b'a'.startswith('a') or vice-versa continue @@ -1213,7 +1344,9 @@ def _add_to_filtered_keys(key): rem_repr = rem_repr.replace('"', '\\"') # then reinsert prefix from start of token - matched.append('%s%s' % (token_prefix, rem_repr)) + match = '%s%s' % (token_prefix, rem_repr) + + matched[match] = filtered_key_is_final[key] return quote, token_start, matched @@ -1447,24 +1580,39 @@ def _make_signature(completion)-> str: \s* # and optional whitespace # Capture any number of serializable objects (e.g. "a", "b", 'c') # and slices -((?:[uUbB]? # string prefix (r not handled) - (?: - '(?:[^']|(? str: def _convert_matcher_v1_result_to_v2( matches: Sequence[str], type: str, - fragment: str = None, + fragment: Optional[str] = None, suppress_if_matches: bool = False, ) -> SimpleMatcherResult: """Utility to help with transition""" @@ -1494,9 +1642,11 @@ def _greedy_changed(self, change): """update the splitter and readline delims when greedy is changed""" if change['new']: self.evaluation = 'unsafe' + self.auto_close_dict_keys = True self.splitter.delims = GREEDY_DELIMS else: self.evaluation = 'limitted' + self.auto_close_dict_keys = False self.splitter.delims = DELIMS dict_keys_only = Bool( @@ -2294,7 +2444,7 @@ def dict_key_matches(self, text: str) -> List[str]: extra_prefix=tuple_prefix ) if not matches: - return matches + return [] # get the cursor position of # - the text being completed @@ -2313,26 +2463,55 @@ def dict_key_matches(self, text: str) -> List[str]: else: leading = text[text_start:completion_start] - # the index of the `[` character - bracket_idx = match.end(1) - # append closing quote and bracket as appropriate # this is *not* appropriate if the opening quote or bracket is outside - # the text given to this method - suf = '' - continuation = self.line_buffer[len(self.text_until_cursor):] - if key_start > text_start and closing_quote: - # quotes were opened inside text, maybe close them - if continuation.startswith(closing_quote): - continuation = continuation[len(closing_quote):] - else: - suf += closing_quote - if bracket_idx > text_start: - # brackets were opened inside text, maybe close them - if not continuation.startswith(']'): - suf += ']' + # the text given to this method, e.g. `d["""a\nt + can_close_quote = False + can_close_bracket = False + + continuation = self.line_buffer[len(self.text_until_cursor):].strip() + + if continuation.startswith(closing_quote): + # do not close if already closed, e.g. `d['a'` + continuation = continuation[len(closing_quote):] + else: + can_close_quote = True + + continuation = continuation.strip() + + # e.g. `pandas.DataFrame` has different tuple indexer behaviour, + # handling it is out of scope, so let's avoid appending suffixes. + has_known_tuple_handling = isinstance(obj, dict) - return [leading + k + suf for k in matches] + can_close_bracket = not continuation.startswith(']') and self.auto_close_dict_keys + can_close_tuple_item = not continuation.startswith(',') and has_known_tuple_handling and self.auto_close_dict_keys + can_close_quote = can_close_quote and self.auto_close_dict_keys + + # fast path if closing qoute should be appended but not suffix is allowed + if not can_close_quote and not can_close_bracket and closing_quote: + return [leading + k for k in matches] + + results = [] + + end_of_tuple_or_item = DictKeyState.END_OF_TUPLE | DictKeyState.END_OF_ITEM + + for k, state_flag in matches.items(): + result = leading + k + if can_close_quote and closing_quote: + result += closing_quote + + if state_flag == end_of_tuple_or_item: + # We do not know which suffix to add, + # e.g. both tuple item and string + # match this item. + pass + + if state_flag in end_of_tuple_or_item and can_close_bracket: + result += ']' + if state_flag == DictKeyState.IN_TUPLE and can_close_tuple_item: + result += ', ' + results.append(result) + return results @context_matcher() def unicode_name_matcher(self, context: CompletionContext): diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index f477c6bc2c1..d420ca80980 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -1,11 +1,19 @@ -from typing import Callable, Protocol, Set, Tuple, NamedTuple, Literal, Union +from typing import Callable, Set, Tuple, NamedTuple, Literal, Union, TYPE_CHECKING import collections import sys import ast -import types from functools import cached_property from dataclasses import dataclass, field +from IPython.utils.docs import GENERATING_DOCUMENTATION + + +if TYPE_CHECKING or GENERATING_DOCUMENTATION: + from typing_extensions import Protocol +else: + # do not require on runtime + Protocol = object # requires Python >=3.8 + class HasGetItem(Protocol): def __getitem__(self, key) -> None: ... @@ -266,20 +274,23 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): Currently does not support evaluation of functions with arguments. Does not evaluate actions which always have side effects: - - class definitions (`class sth: ...`) - - function definitions (`def sth: ...`) - - variable assignments (`x = 1`) - - augumented assignments (`x += 1`) - - deletions (`del x`) + - class definitions (``class sth: ...``) + - function definitions (``def sth: ...``) + - variable assignments (``x = 1``) + - augumented assignments (``x += 1``) + - deletions (``del x``) Does not evaluate operations which do not return values: - - assertions (`assert x`) - - pass (`pass`) - - imports (`import x`) + - assertions (``assert x``) + - pass (``pass``) + - imports (``import x``) - control flow - - conditionals (`if x:`) except for terenary IfExp (`a if x else b`) - - loops (`for` and `while`) + - conditionals (``if x:``) except for terenary IfExp (``a if x else b``) + - loops (``for`` and `while``) - exception handling + + The purpose of this function is to guard against unwanted side-effects; + it does not give guarantees on protection from malicious code execution. """ policy = EVALUATION_POLICIES[context.evaluation] if node is None: diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 7a99a2655ab..4d8eecec1f8 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -24,6 +24,7 @@ provisionalcompleter, match_dict_keys, _deduplicate_completions, + _match_number_in_dict_key_prefix, completion_matcher, SimpleCompletion, CompletionContext, @@ -181,7 +182,6 @@ def check_line_split(splitter, test_specs): out = splitter.split_line(line, cursor_pos) assert out == split - def test_line_split(): """Basic line splitter test with default specs.""" sp = completer.CompletionSplitter() @@ -852,16 +852,37 @@ def test_match_dict_keys(self): """ delims = " \t\n`!@#$^&*()=+[{]}\\|;:'\",<>?" + def match(*args, **kwargs): + quote, offset, matches = match_dict_keys(*args, **kwargs) + return quote, offset, list(matches) + keys = ["foo", b"far"] - assert match_dict_keys(keys, "b'", delims=delims) == ("'", 2, ["far"]) - assert match_dict_keys(keys, "b'f", delims=delims) == ("'", 2, ["far"]) - assert match_dict_keys(keys, 'b"', delims=delims) == ('"', 2, ["far"]) - assert match_dict_keys(keys, 'b"f', delims=delims) == ('"', 2, ["far"]) + assert match(keys, "b'", delims=delims) == ("'", 2, ["far"]) + assert match(keys, "b'f", delims=delims) == ("'", 2, ["far"]) + assert match(keys, 'b"', delims=delims) == ('"', 2, ["far"]) + assert match(keys, 'b"f', delims=delims) == ('"', 2, ["far"]) - assert match_dict_keys(keys, "'", delims=delims) == ("'", 1, ["foo"]) - assert match_dict_keys(keys, "'f", delims=delims) == ("'", 1, ["foo"]) - assert match_dict_keys(keys, '"', delims=delims) == ('"', 1, ["foo"]) - assert match_dict_keys(keys, '"f', delims=delims) == ('"', 1, ["foo"]) + assert match(keys, "'", delims=delims) == ("'", 1, ["foo"]) + assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) + assert match(keys, '"', delims=delims) == ('"', 1, ["foo"]) + assert match(keys, '"f', delims=delims) == ('"', 1, ["foo"]) + + # Completion on first item of tuple + keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, 'test')] + assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) + assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) + + # Completion on numbers + keys = [ + 0xdeadbeef, # 3735928559 + 1111, 1234, "1999", + 0b10101, # 21 + 22 + ] + assert match(keys, "0xdead", delims=delims) == ("", 0, ["0xdeadbeef"]) + assert match(keys, "1", delims=delims) == ("", 0, ["1111", "1234"]) + assert match(keys, "2", delims=delims) == ("", 0, ["21", "22"]) + assert match(keys, "0b101", delims=delims) == ("", 0, ['0b10101', '0b10110']) def test_match_dict_keys_tuple(self): """ @@ -872,30 +893,85 @@ def test_match_dict_keys_tuple(self): keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] + def match(*args, **kwargs): + quote, offset, matches = match_dict_keys(*args, **kwargs) + return quote, offset, list(matches) + # Completion on first key == "foo" - assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["bar", "oof"]) - assert match_dict_keys(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) - assert match_dict_keys(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) - assert match_dict_keys(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) - assert match_dict_keys(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) - assert match_dict_keys(keys, "b\"", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) - assert match_dict_keys(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) - assert match_dict_keys(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) + assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["bar", "oof"]) + assert match(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) + assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) + assert match(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) + assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) + assert match(keys, "b\"", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) + assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) + assert match(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) # No Completion - assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) - assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) + assert match(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) + assert match(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) keys = [('foo1', 'foo2', 'foo3', 'foo4'), ('foo1', 'foo2', 'bar', 'foo4')] - assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1',)) == ("'", 1, ["foo2", "foo2"]) - assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) - assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) - assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) + assert match(keys, "'foo", delims=delims, extra_prefix=('foo1',)) == ("'", 1, ["foo2"]) + assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) + assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) + assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) + + keys = [("foo", 1111), ("foo", "2222"), (3333, "bar"), (3333, 4444)] + assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["2222"]) + assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ("", 0, ["1111", "'2222'"]) + assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar"]) + assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ("", 0, ["'bar'", "4444"]) + assert match(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) + assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) + + def test_dict_key_completion_closures(self): + ip = get_ipython() + complete = ip.Completer.complete + ip.Completer.auto_close_dict_keys = True - keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, 'test')] - assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["1111", "2222"]) - assert match_dict_keys(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar", "test"]) - assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) + ip.user_ns["d"] = { + # tuple only + ('aa', 11): None, + # tuple and non-tuple + ('bb', 22): None, + 'bb': None, + # non-tuple only + 'cc': None, + # numeric tuple only + (77, 'x'): None, + # numeric tuple and non-tuple + (88, 'y'): None, + 88: None, + # numeric non-tuple only + 99: None, + } + + _, matches = complete(line_buffer="d[") + # should append `, ` if matches a tuple only + self.assertIn("'aa', ", matches) + # should not append anything if matches a tuple and an item + self.assertIn("'bb'", matches) + # should append `]` if matches and item only + self.assertIn("'cc']", matches) + + # should append `, ` if matches a tuple only + self.assertIn("77, ", matches) + # should not append anything if matches a tuple and an item + self.assertIn("88", matches) + # should append `]` if matches and item only + self.assertIn("99]", matches) + + _, matches = complete(line_buffer="d['aa', ") + # should restrict matches to those matching tuple prefix + self.assertIn("11]", matches) + self.assertNotIn("'bb'", matches) + self.assertNotIn("'bb', ", matches) + self.assertNotIn("'bb']", matches) + self.assertNotIn("'cc'", matches) + self.assertNotIn("'cc', ", matches) + self.assertNotIn("'cc']", matches) + ip.Completer.auto_close_dict_keys = False def test_dict_key_completion_string(self): """Test dictionary key completion for string keys""" @@ -1052,6 +1128,35 @@ def test_dict_key_completion_string(self): self.assertNotIn("foo", matches) self.assertNotIn("bar", matches) + def test_dict_key_completion_numbers(self): + ip = get_ipython() + complete = ip.Completer.complete + + ip.user_ns["d"] = { + 0xdeadbeef: None, # 3735928559 + 1111: None, + 1234: None, + "1999": None, + 0b10101: None, # 21 + 22: None + } + _, matches = complete(line_buffer="d[1") + self.assertIn("1111", matches) + self.assertIn("1234", matches) + self.assertNotIn("1999", matches) + self.assertNotIn("'1999'", matches) + + _, matches = complete(line_buffer="d[0xdead") + self.assertIn("0xdeadbeef", matches) + + _, matches = complete(line_buffer="d[2") + self.assertIn("21", matches) + self.assertIn("22", matches) + + _, matches = complete(line_buffer="d[0b101") + self.assertIn("0b10101", matches) + self.assertIn("0b10110", matches) + def test_dict_key_completion_contexts(self): """Test expression contexts in which dict key completion occurs""" ip = get_ipython() @@ -1545,3 +1650,35 @@ def _(expected): _(["completion_b"]) a_matcher.matcher_priority = 3 _(["completion_a"]) + + +@pytest.mark.parametrize( + 'input, expected', + [ + ['1.234', '1.234'], + # should match signed numbers + ['+1', '+1'], + ['-1', '-1'], + ['-1.0', '-1.0'], + ['-1.', '-1.'], + ['+1.', '+1.'], + ['.1', '.1'], + # should not match non-numbers + ['1..', None], + ['..', None], + ['.1.', None], + # should match after comma + [',1', '1'], + [', 1', '1'], + [', .1', '.1'], + [', +.1', '+.1'], + # should not match after trailing spaces + ['.1 ', None], + # some complex cases + ['0b_0011_1111_0100_1110', '0b_0011_1111_0100_1110'], + ['0xdeadbeef', '0xdeadbeef'], + ['0b_1110_0101', '0b_1110_0101'] + ] +) +def test_match_numeric_literal_for_dict_key(input, expected): + assert _match_number_in_dict_key_prefix(input) == expected From 21ae8802bd0d3b62a7e4e5fdc64648069616997d Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 00:41:40 +0000 Subject: [PATCH 0307/1752] You Want It Darker --- IPython/core/completer.py | 112 +++++++------ IPython/core/guarded_eval.py | 207 +++++++++++------------- IPython/core/tests/test_completer.py | 172 +++++++++++++------- IPython/core/tests/test_guarded_eval.py | 178 +++++++++----------- 4 files changed, 338 insertions(+), 331 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index e53e83b38e9..3ea3dd99258 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -923,8 +923,8 @@ class Completer(Configurable): ).tag(config=True) evaluation = Enum( - ('forbidden', 'minimal', 'limitted', 'unsafe', 'dangerous'), - default_value='limitted', + ("forbidden", "minimal", "limitted", "unsafe", "dangerous"), + default_value="limitted", help="""Code evaluation under completion. Successive options allow to enable more eager evaluation for more accurate completion suggestions, @@ -961,8 +961,7 @@ class Completer(Configurable): "unicode characters back to latex commands.").tag(config=True) auto_close_dict_keys = Bool( - False, - help="""Enable auto-closing dictionary keys.""" + False, help="""Enable auto-closing dictionary keys.""" ).tag(config=True) def __init__(self, namespace=None, global_namespace=None, **kwargs): @@ -1066,7 +1065,7 @@ def attr_matches(self, text): m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) if not m2: return [] - expr, attr = m2.group(1,2) + expr, attr = m2.group(1, 2) obj = self._evaluate_expr(expr) @@ -1090,8 +1089,7 @@ def attr_matches(self, text): pass # Build match list to return n = len(attr) - return ["%s.%s" % (expr, w) for w in words if w[:n] == attr ] - + return ["%s.%s" % (expr, w) for w in words if w[:n] == attr] def _evaluate_expr(self, expr): obj = not_found @@ -1103,13 +1101,13 @@ def _evaluate_expr(self, expr): EvaluationContext( globals_=self.global_namespace, locals_=self.namespace, - evaluation=self.evaluation - ) + evaluation=self.evaluation, + ), ) done = True except Exception as e: if self.debug: - print('Evaluation exception', e) + print("Evaluation exception", e) # trim the expression to remove any invalid prefix # e.g. user starts `(d[`, so we get `expr = '(d'`, # where parenthesis is not closed. @@ -1135,6 +1133,7 @@ class DictKeyState(enum.Flag): - given `d3 = {('a', 'b'): 1}`: `d3['` will yield `{'a': IN_TUPLE}` as `'a'` can be added. - given `d4 = {'a': 1, ('a', 'b'): 2}`: `d4['` will yield `{'a': END_OF_ITEM & END_OF_TUPLE}` """ + BASELINE = 0 END_OF_ITEM = enum.auto() END_OF_TUPLE = enum.auto() @@ -1179,9 +1178,9 @@ def _match_number_in_dict_key_prefix(prefix: str) -> Union[str, None]: # we did not match a number return None if token.type == tokenize.OP: - if token.string == ',': + if token.string == ",": break - if token.string in {'+', '-'}: + if token.string in {"+", "-"}: number = token.string + number else: return None @@ -1189,9 +1188,9 @@ def _match_number_in_dict_key_prefix(prefix: str) -> Union[str, None]: _INT_FORMATS = { - '0b': bin, - '0o': oct, - '0x': hex, + "0b": bin, + "0o": oct, + "0x": hex, } @@ -1199,7 +1198,7 @@ def match_dict_keys( keys: List[Union[str, bytes, Tuple[Union[str, bytes], ...]]], prefix: str, delims: str, - extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None + extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None, ) -> Tuple[str, int, Dict[str, DictKeyState]]: """Used by dict_key_matches, matching the prefix to a list of keys @@ -1225,11 +1224,13 @@ def match_dict_keys( """ prefix_tuple = extra_prefix if extra_prefix else () - prefix_tuple_size = sum([ - # for pandas, do not count slices as taking space - not isinstance(k, slice) - for k in prefix_tuple - ]) + prefix_tuple_size = sum( + [ + # for pandas, do not count slices as taking space + not isinstance(k, slice) + for k in prefix_tuple + ] + ) text_serializable_types = (str, bytes, int, float, slice) def filter_prefix_tuple(key): @@ -1247,7 +1248,9 @@ def filter_prefix_tuple(key): # All checks passed! return True - filtered_key_is_final: Dict[Union[str, bytes, int, float], DictKeyState] = defaultdict(lambda: DictKeyState.BASELINE) + filtered_key_is_final: Dict[ + Union[str, bytes, int, float], DictKeyState + ] = defaultdict(lambda: DictKeyState.BASELINE) for k in keys: # If at least one of the matches is not final, mark as undetermined. @@ -1259,8 +1262,8 @@ def filter_prefix_tuple(key): key_fragment = k[prefix_tuple_size] filtered_key_is_final[key_fragment] |= ( DictKeyState.END_OF_TUPLE - if len(k) == prefix_tuple_size + 1 else - DictKeyState.IN_TUPLE + if len(k) == prefix_tuple_size + 1 + else DictKeyState.IN_TUPLE ) elif prefix_tuple_size > 0: # we are completing a tuple but this key is not a tuple, @@ -1273,9 +1276,9 @@ def filter_prefix_tuple(key): filtered_keys = filtered_key_is_final.keys() if not prefix: - return '', 0, {repr(k): v for k, v in filtered_key_is_final.items()} + return "", 0, {repr(k): v for k, v in filtered_key_is_final.items()} - quote_match = re.search('(?:"|\')', prefix) + quote_match = re.search("(?:\"|')", prefix) is_user_prefix_numeric = False if quote_match: @@ -1284,7 +1287,7 @@ def filter_prefix_tuple(key): try: prefix_str = literal_eval(valid_prefix) except Exception: - return '', 0, {} + return "", 0, {} else: # If it does not look like a string, let's assume # we are dealing with a number or variable. @@ -1294,11 +1297,11 @@ def filter_prefix_tuple(key): if number_match is None: # The alternative would be to assume that user forgort the quote # and if the substring matches, suggest adding it at the start. - return '', 0, {} + return "", 0, {} prefix_str = number_match is_user_prefix_numeric = True - quote = '' + quote = "" pattern = '[^' + ''.join('\\' + c for c in delims) + ']*$' token_match = re.search(pattern, prefix, re.UNICODE) @@ -1333,7 +1336,7 @@ def filter_prefix_tuple(key): continue # reformat remainder of key to begin with prefix - rem = str_key[len(prefix_str):] + rem = str_key[len(prefix_str) :] # force repr wrapped in ' rem_repr = repr(rem + '"') if isinstance(rem, str) else repr(rem + b'"') rem_repr = rem_repr[1 + rem_repr.index("'"):-2] @@ -1344,7 +1347,7 @@ def filter_prefix_tuple(key): rem_repr = rem_repr.replace('"', '\\"') # then reinsert prefix from start of token - match = '%s%s' % (token_prefix, rem_repr) + match = "%s%s" % (token_prefix, rem_repr) matched[match] = filtered_key_is_final[key] return quote, token_start, matched @@ -1572,7 +1575,8 @@ def _make_signature(completion)-> str: _CompleteResult = Dict[str, MatcherResult] -DICT_MATCHER_REGEX = re.compile(r"""(?x) +DICT_MATCHER_REGEX = re.compile( + r"""(?x) ( # match dict-referring - or any get item object - expression .+ ) @@ -1616,7 +1620,9 @@ def _make_signature(completion)-> str: ) )? $ -""") +""" +) + def _convert_matcher_v1_result_to_v2( matches: Sequence[str], @@ -1640,12 +1646,12 @@ class IPCompleter(Completer): @observe('greedy') def _greedy_changed(self, change): """update the splitter and readline delims when greedy is changed""" - if change['new']: - self.evaluation = 'unsafe' + if change["new"]: + self.evaluation = "unsafe" self.auto_close_dict_keys = True self.splitter.delims = GREEDY_DELIMS else: - self.evaluation = 'limitted' + self.evaluation = "limitted" self.auto_close_dict_keys = False self.splitter.delims = DELIMS @@ -2375,13 +2381,12 @@ def _get_keys(obj: Any) -> List[Any]: return method() # Special case some common in-memory dict-like types - if (isinstance(obj, dict) or - _safe_isinstance(obj, 'pandas', 'DataFrame')): + if isinstance(obj, dict) or _safe_isinstance(obj, "pandas", "DataFrame"): try: return list(obj.keys()) except Exception: return [] - elif _safe_isinstance(obj, 'pandas', 'core', 'indexing', '_LocIndexer'): + elif _safe_isinstance(obj, "pandas", "core", "indexing", "_LocIndexer"): try: return list(obj.obj.keys()) except Exception: @@ -2408,7 +2413,7 @@ def dict_key_matches(self, text: str) -> List[str]: # Short-circuit on closed dictionary (regular expression would # not match anyway, but would take quite a while). - if self.text_until_cursor.strip().endswith(']'): + if self.text_until_cursor.strip().endswith("]"): return [] match = DICT_MATCHER_REGEX.search(self.text_until_cursor) @@ -2433,15 +2438,12 @@ def dict_key_matches(self, text: str) -> List[str]: globals_=self.global_namespace, locals_=self.namespace, evaluation=self.evaluation, - in_subscript=True - ) + in_subscript=True, + ), ) closing_quote, token_offset, matches = match_dict_keys( - keys, - key_prefix, - self.splitter.delims, - extra_prefix=tuple_prefix + keys, key_prefix, self.splitter.delims, extra_prefix=tuple_prefix ) if not matches: return [] @@ -2469,11 +2471,11 @@ def dict_key_matches(self, text: str) -> List[str]: can_close_quote = False can_close_bracket = False - continuation = self.line_buffer[len(self.text_until_cursor):].strip() + continuation = self.line_buffer[len(self.text_until_cursor) :].strip() if continuation.startswith(closing_quote): # do not close if already closed, e.g. `d['a'` - continuation = continuation[len(closing_quote):] + continuation = continuation[len(closing_quote) :] else: can_close_quote = True @@ -2483,8 +2485,14 @@ def dict_key_matches(self, text: str) -> List[str]: # handling it is out of scope, so let's avoid appending suffixes. has_known_tuple_handling = isinstance(obj, dict) - can_close_bracket = not continuation.startswith(']') and self.auto_close_dict_keys - can_close_tuple_item = not continuation.startswith(',') and has_known_tuple_handling and self.auto_close_dict_keys + can_close_bracket = ( + not continuation.startswith("]") and self.auto_close_dict_keys + ) + can_close_tuple_item = ( + not continuation.startswith(",") + and has_known_tuple_handling + and self.auto_close_dict_keys + ) can_close_quote = can_close_quote and self.auto_close_dict_keys # fast path if closing qoute should be appended but not suffix is allowed @@ -2507,9 +2515,9 @@ def dict_key_matches(self, text: str) -> List[str]: pass if state_flag in end_of_tuple_or_item and can_close_bracket: - result += ']' + result += "]" if state_flag == DictKeyState.IN_TUPLE and can_close_tuple_item: - result += ', ' + result += ", " results.append(result) return results diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index d420ca80980..2c278a238ae 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -16,20 +16,24 @@ class HasGetItem(Protocol): - def __getitem__(self, key) -> None: ... + def __getitem__(self, key) -> None: + ... class InstancesHaveGetItem(Protocol): - def __call__(self) -> HasGetItem: ... + def __call__(self) -> HasGetItem: + ... class HasGetAttr(Protocol): - def __getattr__(self, key) -> None: ... + def __getattr__(self, key) -> None: + ... class DoesNotHaveGetAttr(Protocol): pass + # By default `__getattr__` is not explicitly implemented on most objects MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr] @@ -38,22 +42,16 @@ def unbind_method(func: Callable) -> Union[Callable, None]: """Get unbound method for given bound method. Returns None if cannot get unbound method.""" - owner = getattr(func, '__self__', None) + owner = getattr(func, "__self__", None) owner_class = type(owner) - name = getattr(func, '__name__', None) - instance_dict_overrides = getattr(owner, '__dict__', None) + name = getattr(func, "__name__", None) + instance_dict_overrides = getattr(owner, "__dict__", None) if ( owner is not None - and - name - and - ( + and name + and ( not instance_dict_overrides - or - ( - instance_dict_overrides - and name not in instance_dict_overrides - ) + or (instance_dict_overrides and name not in instance_dict_overrides) ) ): return getattr(owner_class, name) @@ -86,7 +84,13 @@ def can_call(self, func): if owner_method and owner_method in self.allowed_calls: return True -def has_original_dunder_external(value, module_name, access_path, method_name,): + +def has_original_dunder_external( + value, + module_name, + access_path, + method_name, +): try: if module_name not in sys.modules: return False @@ -106,11 +110,7 @@ def has_original_dunder_external(value, module_name, access_path, method_name,): def has_original_dunder( - value, - allowed_types, - allowed_methods, - allowed_external, - method_name + value, allowed_types, allowed_methods, allowed_external, method_name ): # note: Python ignores `__getattr__`/`__getitem__` on instances, # we only need to check at class level @@ -148,14 +148,14 @@ def can_get_attr(self, value, attr): allowed_types=self.allowed_getattr, allowed_methods=self._getattribute_methods, allowed_external=self.allowed_getattr_external, - method_name='__getattribute__' + method_name="__getattribute__", ) has_original_attr = has_original_dunder( value, allowed_types=self.allowed_getattr, allowed_methods=self._getattr_methods, allowed_external=self.allowed_getattr_external, - method_name='__getattr__' + method_name="__getattr__", ) # Many objects do not have `__getattr__`, this is fine if has_original_attr is None and has_original_attribute: @@ -168,7 +168,6 @@ def get_attr(self, value, attr): if self.can_get_attr(value, attr): return getattr(value, attr) - def can_get_item(self, value, item): """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" return has_original_dunder( @@ -176,29 +175,20 @@ def can_get_item(self, value, item): allowed_types=self.allowed_getitem, allowed_methods=self._getitem_methods, allowed_external=self.allowed_getitem_external, - method_name='__getitem__' + method_name="__getitem__", ) @cached_property def _getitem_methods(self) -> Set[Callable]: - return self._safe_get_methods( - self.allowed_getitem, - '__getitem__' - ) + return self._safe_get_methods(self.allowed_getitem, "__getitem__") @cached_property def _getattr_methods(self) -> Set[Callable]: - return self._safe_get_methods( - self.allowed_getattr, - '__getattr__' - ) + return self._safe_get_methods(self.allowed_getattr, "__getattr__") @cached_property def _getattribute_methods(self) -> Set[Callable]: - return self._safe_get_methods( - self.allowed_getattr, - '__getattribute__' - ) + return self._safe_get_methods(self.allowed_getattr, "__getattribute__") def _safe_get_methods(self, classes, name) -> Set[Callable]: return { @@ -216,7 +206,9 @@ class DummyNamedTuple(NamedTuple): class EvaluationContext(NamedTuple): locals_: dict globals_: dict - evaluation: Literal['forbidden', 'minimal', 'limitted', 'unsafe', 'dangerous'] = 'forbidden' + evaluation: Literal[ + "forbidden", "minimal", "limitted", "unsafe", "dangerous" + ] = "forbidden" in_subscript: bool = False @@ -224,21 +216,20 @@ class IdentitySubscript: def __getitem__(self, key): return key + IDENTITY_SUBSCRIPT = IdentitySubscript() -SUBSCRIPT_MARKER = '__SUBSCRIPT_SENTINEL__' +SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__" + class GuardRejection(ValueError): pass -def guarded_eval( - code: str, - context: EvaluationContext -): +def guarded_eval(code: str, context: EvaluationContext): locals_ = context.locals_ - if context.evaluation == 'forbidden': - raise GuardRejection('Forbidden mode') + if context.evaluation == "forbidden": + raise GuardRejection("Forbidden mode") # note: not using `ast.literal_eval` as it does not implement # getitem at all, for example it fails on simple `[0][1]` @@ -252,19 +243,17 @@ def guarded_eval( return tuple() locals_ = locals_.copy() locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT - code = SUBSCRIPT_MARKER + '[' + code + ']' - context = EvaluationContext(**{ - **context._asdict(), - **{'locals_': locals_} - }) + code = SUBSCRIPT_MARKER + "[" + code + "]" + context = EvaluationContext(**{**context._asdict(), **{"locals_": locals_}}) - if context.evaluation == 'dangerous': + if context.evaluation == "dangerous": return eval(code, context.globals_, context.locals_) - expression = ast.parse(code, mode='eval') + expression = ast.parse(code, mode="eval") return eval_node(expression, context) + def eval_node(node: Union[ast.AST, None], context: EvaluationContext): """ Evaluate AST node in provided context. @@ -314,7 +303,7 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): if isinstance(node.op, ast.Mod): return left % right if isinstance(node.op, ast.Pow): - return left ** right + return left**right if isinstance(node.op, ast.LShift): return left << right if isinstance(node.op, ast.RShift): @@ -332,36 +321,26 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): if isinstance(node, ast.Index): return eval_node(node.value, context) if isinstance(node, ast.Tuple): - return tuple( - eval_node(e, context) - for e in node.elts - ) + return tuple(eval_node(e, context) for e in node.elts) if isinstance(node, ast.List): - return [ - eval_node(e, context) - for e in node.elts - ] + return [eval_node(e, context) for e in node.elts] if isinstance(node, ast.Set): - return { - eval_node(e, context) - for e in node.elts - } + return {eval_node(e, context) for e in node.elts} if isinstance(node, ast.Dict): - return dict(zip( - [eval_node(k, context) for k in node.keys], - [eval_node(v, context) for v in node.values] - )) + return dict( + zip( + [eval_node(k, context) for k in node.keys], + [eval_node(v, context) for v in node.values], + ) + ) if isinstance(node, ast.Slice): return slice( eval_node(node.lower, context), eval_node(node.upper, context), - eval_node(node.step, context) + eval_node(node.step, context), ) if isinstance(node, ast.ExtSlice): - return tuple([ - eval_node(dim, context) - for dim in node.dims - ]) + return tuple([eval_node(dim, context) for dim in node.dims]) if isinstance(node, ast.UnaryOp): # TODO: add guards value = eval_node(node.operand, context) @@ -373,16 +352,16 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): return ~value if isinstance(node.op, ast.Not): return not value - raise ValueError('Unhandled unary operation:', node.op) + raise ValueError("Unhandled unary operation:", node.op) if isinstance(node, ast.Subscript): value = eval_node(node.value, context) slice_ = eval_node(node.slice, context) if policy.can_get_item(value, slice_): return value[slice_] raise GuardRejection( - 'Subscript access (`__getitem__`) for', - type(value), # not joined to avoid calling `repr` - f' not allowed in {context.evaluation} mode' + "Subscript access (`__getitem__`) for", + type(value), # not joined to avoid calling `repr` + f" not allowed in {context.evaluation} mode", ) if isinstance(node, ast.Name): if policy.allow_locals_access and node.id in context.locals_: @@ -393,49 +372,46 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): return __builtins__[node.id] if not policy.allow_globals_access and not policy.allow_locals_access: raise GuardRejection( - f'Namespace access not allowed in {context.evaluation} mode' + f"Namespace access not allowed in {context.evaluation} mode" ) else: - raise NameError(f'{node.id} not found in locals nor globals') + raise NameError(f"{node.id} not found in locals nor globals") if isinstance(node, ast.Attribute): value = eval_node(node.value, context) if policy.can_get_attr(value, node.attr): return getattr(value, node.attr) raise GuardRejection( - 'Attribute access (`__getattr__`) for', - type(value), # not joined to avoid calling `repr` - f'not allowed in {context.evaluation} mode' + "Attribute access (`__getattr__`) for", + type(value), # not joined to avoid calling `repr` + f"not allowed in {context.evaluation} mode", ) if isinstance(node, ast.IfExp): test = eval_node(node.test, context) if test: - return eval_node(node.body, context) + return eval_node(node.body, context) else: return eval_node(node.orelse, context) if isinstance(node, ast.Call): func = eval_node(node.func, context) print(node.keywords) if policy.can_call(func) and not node.keywords: - args = [ - eval_node(arg, context) - for arg in node.args - ] + args = [eval_node(arg, context) for arg in node.args] return func(*args) raise GuardRejection( - 'Call for', - func, # not joined to avoid calling `repr` - f'not allowed in {context.evaluation} mode' + "Call for", + func, # not joined to avoid calling `repr` + f"not allowed in {context.evaluation} mode", ) - raise ValueError('Unhandled node', node) + raise ValueError("Unhandled node", node) SUPPORTED_EXTERNAL_GETITEM = { - ('pandas', 'core', 'indexing', '_iLocIndexer'), - ('pandas', 'core', 'indexing', '_LocIndexer'), - ('pandas', 'DataFrame'), - ('pandas', 'Series'), - ('numpy', 'ndarray'), - ('numpy', 'void') + ("pandas", "core", "indexing", "_iLocIndexer"), + ("pandas", "core", "indexing", "_LocIndexer"), + ("pandas", "DataFrame"), + ("pandas", "Series"), + ("numpy", "ndarray"), + ("numpy", "void"), } BUILTIN_GETITEM = { @@ -452,20 +428,17 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): collections.UserList, collections.UserString, DummyNamedTuple, - IdentitySubscript + IdentitySubscript, } def _list_methods(cls, source=None): """For use on immutable objects or with methods returning a copy""" - return [ - getattr(cls, k) - for k in (source if source else dir(cls)) - ] + return [getattr(cls, k) for k in (source if source else dir(cls))] -dict_non_mutating_methods = ('copy', 'keys', 'values', 'items') -list_non_mutating_methods = ('copy', 'index', 'count') +dict_non_mutating_methods = ("copy", "keys", "values", "items") +list_non_mutating_methods = ("copy", "index", "count") set_non_mutating_methods = set(dir(set)) & set(dir(frozenset)) @@ -504,20 +477,20 @@ def _list_methods(cls, source=None): collections.Counter, *_list_methods(collections.Counter, dict_non_mutating_methods), collections.Counter.elements, - collections.Counter.most_common + collections.Counter.most_common, } EVALUATION_POLICIES = { - 'minimal': EvaluationPolicy( + "minimal": EvaluationPolicy( allow_builtins_access=True, allow_locals_access=False, allow_globals_access=False, allow_item_access=False, allow_attr_access=False, allowed_calls=set(), - allow_any_calls=False + allow_any_calls=False, ), - 'limitted': SelectivePolicy( + "limitted": SelectivePolicy( # TODO: # - should reject binary and unary operations if custom methods would be dispatched allowed_getitem=BUILTIN_GETITEM, @@ -529,24 +502,24 @@ def _list_methods(cls, source=None): object, type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. dict_keys, - method_descriptor + method_descriptor, }, allowed_getattr_external={ # pandas Series/Frame implements custom `__getattr__` - ('pandas', 'DataFrame'), - ('pandas', 'Series') + ("pandas", "DataFrame"), + ("pandas", "Series"), }, allow_builtins_access=True, allow_locals_access=True, allow_globals_access=True, - allowed_calls=ALLOWED_CALLS + allowed_calls=ALLOWED_CALLS, ), - 'unsafe': EvaluationPolicy( + "unsafe": EvaluationPolicy( allow_builtins_access=True, allow_locals_access=True, allow_globals_access=True, allow_attr_access=True, allow_item_access=True, - allow_any_calls=True - ) -} \ No newline at end of file + allow_any_calls=True, + ), +} diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 4d8eecec1f8..4e385d546a0 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -868,21 +868,16 @@ def match(*args, **kwargs): assert match(keys, '"f', delims=delims) == ('"', 1, ["foo"]) # Completion on first item of tuple - keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, 'test')] + keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, "test")] assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) # Completion on numbers - keys = [ - 0xdeadbeef, # 3735928559 - 1111, 1234, "1999", - 0b10101, # 21 - 22 - ] + keys = [0xDEADBEEF, 1111, 1234, "1999", 0b10101, 22] # 3735928559 # 21 assert match(keys, "0xdead", delims=delims) == ("", 0, ["0xdeadbeef"]) assert match(keys, "1", delims=delims) == ("", 0, ["1111", "1234"]) assert match(keys, "2", delims=delims) == ("", 0, ["21", "22"]) - assert match(keys, "0b101", delims=delims) == ("", 0, ['0b10101', '0b10110']) + assert match(keys, "0b101", delims=delims) == ("", 0, ["0b10101", "0b10110"]) def test_match_dict_keys_tuple(self): """ @@ -898,30 +893,90 @@ def match(*args, **kwargs): return quote, offset, list(matches) # Completion on first key == "foo" - assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["bar", "oof"]) - assert match(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) - assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) - assert match(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) - assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) - assert match(keys, "b\"", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) - assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) - assert match(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) + assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( + "'", + 1, + ["bar", "oof"], + ) + assert match(keys, '"', delims=delims, extra_prefix=("foo",)) == ( + '"', + 1, + ["bar", "oof"], + ) + assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ( + "'", + 1, + ["oof"], + ) + assert match(keys, '"o', delims=delims, extra_prefix=("foo",)) == ( + '"', + 1, + ["oof"], + ) + assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ( + "'", + 2, + ["bar"], + ) + assert match(keys, 'b"', delims=delims, extra_prefix=("foo",)) == ( + '"', + 2, + ["bar"], + ) + assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ( + "'", + 2, + ["bar"], + ) + assert match(keys, 'b"b', delims=delims, extra_prefix=("foo",)) == ( + '"', + 2, + ["bar"], + ) # No Completion assert match(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) assert match(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) - keys = [('foo1', 'foo2', 'foo3', 'foo4'), ('foo1', 'foo2', 'bar', 'foo4')] - assert match(keys, "'foo", delims=delims, extra_prefix=('foo1',)) == ("'", 1, ["foo2"]) - assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) - assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) - assert match(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) + keys = [("foo1", "foo2", "foo3", "foo4"), ("foo1", "foo2", "bar", "foo4")] + assert match(keys, "'foo", delims=delims, extra_prefix=("foo1",)) == ( + "'", + 1, + ["foo2"], + ) + assert match(keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2")) == ( + "'", + 1, + ["foo3"], + ) + assert match( + keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2", "foo3") + ) == ("'", 1, ["foo4"]) + assert match( + keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2", "foo3", "foo4") + ) == ("'", 1, []) keys = [("foo", 1111), ("foo", "2222"), (3333, "bar"), (3333, 4444)] - assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["2222"]) - assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ("", 0, ["1111", "'2222'"]) - assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ("'", 1, ["bar"]) - assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ("", 0, ["'bar'", "4444"]) + assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( + "'", + 1, + ["2222"], + ) + assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ( + "", + 0, + ["1111", "'2222'"], + ) + assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ( + "'", + 1, + ["bar"], + ) + assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ( + "", + 0, + ["'bar'", "4444"], + ) assert match(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) @@ -932,16 +987,16 @@ def test_dict_key_completion_closures(self): ip.user_ns["d"] = { # tuple only - ('aa', 11): None, + ("aa", 11): None, # tuple and non-tuple - ('bb', 22): None, - 'bb': None, + ("bb", 22): None, + "bb": None, # non-tuple only - 'cc': None, + "cc": None, # numeric tuple only - (77, 'x'): None, + (77, "x"): None, # numeric tuple and non-tuple - (88, 'y'): None, + (88, "y"): None, 88: None, # numeric non-tuple only 99: None, @@ -1133,12 +1188,12 @@ def test_dict_key_completion_numbers(self): complete = ip.Completer.complete ip.user_ns["d"] = { - 0xdeadbeef: None, # 3735928559 + 0xDEADBEEF: None, # 3735928559 1111: None, 1234: None, "1999": None, - 0b10101: None, # 21 - 22: None + 0b10101: None, # 21 + 22: None, } _, matches = complete(line_buffer="d[1") self.assertIn("1111", matches) @@ -1169,7 +1224,7 @@ class C: ip.user_ns["C"] = C ip.user_ns["get"] = lambda: d - ip.user_ns["nested"] = {'x': d} + ip.user_ns["nested"] = {"x": d} def assert_no_completion(**kwargs): _, matches = complete(**kwargs) @@ -1198,7 +1253,7 @@ def assert_completion(**kwargs): # nested dict completion assert_completion(line_buffer="nested['x'][") - with evaluation_level('minimal'): + with evaluation_level("minimal"): with pytest.raises(AssertionError): assert_completion(line_buffer="nested['x'][") @@ -1289,6 +1344,7 @@ def test_struct_array_key_completion(self): _, matches = complete(line_buffer="d['") self.assertIn("my_head", matches) self.assertIn("my_data", matches) + def completes_on_nested(): ip.user_ns["d"] = numpy.zeros(2, dtype=dt) _, matches = complete(line_buffer="d[1]['my_head']['") @@ -1298,10 +1354,10 @@ def completes_on_nested(): with greedy_completion(): completes_on_nested() - with evaluation_level('limitted'): + with evaluation_level("limitted"): completes_on_nested() - with evaluation_level('minimal'): + with evaluation_level("minimal"): with pytest.raises(AssertionError): completes_on_nested() @@ -1653,32 +1709,32 @@ def _(expected): @pytest.mark.parametrize( - 'input, expected', + "input, expected", [ - ['1.234', '1.234'], + ["1.234", "1.234"], # should match signed numbers - ['+1', '+1'], - ['-1', '-1'], - ['-1.0', '-1.0'], - ['-1.', '-1.'], - ['+1.', '+1.'], - ['.1', '.1'], + ["+1", "+1"], + ["-1", "-1"], + ["-1.0", "-1.0"], + ["-1.", "-1."], + ["+1.", "+1."], + [".1", ".1"], # should not match non-numbers - ['1..', None], - ['..', None], - ['.1.', None], + ["1..", None], + ["..", None], + [".1.", None], # should match after comma - [',1', '1'], - [', 1', '1'], - [', .1', '.1'], - [', +.1', '+.1'], + [",1", "1"], + [", 1", "1"], + [", .1", ".1"], + [", +.1", "+.1"], # should not match after trailing spaces - ['.1 ', None], + [".1 ", None], # some complex cases - ['0b_0011_1111_0100_1110', '0b_0011_1111_0100_1110'], - ['0xdeadbeef', '0xdeadbeef'], - ['0b_1110_0101', '0b_1110_0101'] - ] + ["0b_0011_1111_0100_1110", "0b_0011_1111_0100_1110"], + ["0xdeadbeef", "0xdeadbeef"], + ["0b_1110_0101", "0b_1110_0101"], + ], ) def test_match_numeric_literal_for_dict_key(input, expected): assert _match_number_in_dict_key_prefix(input) == expected diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 5c89a68f637..129112ff4db 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -1,53 +1,54 @@ from typing import NamedTuple -from IPython.core.guarded_eval import EvaluationContext, GuardRejection, guarded_eval, unbind_method +from IPython.core.guarded_eval import ( + EvaluationContext, + GuardRejection, + guarded_eval, + unbind_method, +) from IPython.testing import decorators as dec import pytest def limitted(**kwargs): - return EvaluationContext( - locals_=kwargs, - globals_={}, - evaluation='limitted' - ) + return EvaluationContext(locals_=kwargs, globals_={}, evaluation="limitted") def unsafe(**kwargs): - return EvaluationContext( - locals_=kwargs, - globals_={}, - evaluation='unsafe' - ) + return EvaluationContext(locals_=kwargs, globals_={}, evaluation="unsafe") + -@dec.skip_without('pandas') +@dec.skip_without("pandas") def test_pandas_series_iloc(): import pandas as pd - series = pd.Series([1], index=['a']) + + series = pd.Series([1], index=["a"]) context = limitted(data=series) - assert guarded_eval('data.iloc[0]', context) == 1 + assert guarded_eval("data.iloc[0]", context) == 1 -@dec.skip_without('pandas') +@dec.skip_without("pandas") def test_pandas_series(): import pandas as pd - context = limitted(data=pd.Series([1], index=['a'])) + + context = limitted(data=pd.Series([1], index=["a"])) assert guarded_eval('data["a"]', context) == 1 with pytest.raises(KeyError): guarded_eval('data["c"]', context) -@dec.skip_without('pandas') +@dec.skip_without("pandas") def test_pandas_bad_series(): import pandas as pd + class BadItemSeries(pd.Series): def __getitem__(self, key): - return 'CUSTOM_ITEM' + return "CUSTOM_ITEM" class BadAttrSeries(pd.Series): def __getattr__(self, key): - return 'CUSTOM_ATTR' + return "CUSTOM_ATTR" - bad_series = BadItemSeries([1], index=['a']) + bad_series = BadItemSeries([1], index=["a"]) context = limitted(data=bad_series) with pytest.raises(GuardRejection): @@ -58,121 +59,108 @@ def __getattr__(self, key): # note: here result is a bit unexpected because # pandas `__getattr__` calls `__getitem__`; # FIXME - special case to handle it? - assert guarded_eval('data.a', context) == 'CUSTOM_ITEM' + assert guarded_eval("data.a", context) == "CUSTOM_ITEM" context = unsafe(data=bad_series) - assert guarded_eval('data["a"]', context) == 'CUSTOM_ITEM' + assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM" - bad_attr_series = BadAttrSeries([1], index=['a']) + bad_attr_series = BadAttrSeries([1], index=["a"]) context = limitted(data=bad_attr_series) assert guarded_eval('data["a"]', context) == 1 with pytest.raises(GuardRejection): - guarded_eval('data.a', context) + guarded_eval("data.a", context) -@dec.skip_without('pandas') +@dec.skip_without("pandas") def test_pandas_dataframe_loc(): import pandas as pd from pandas.testing import assert_series_equal - data = pd.DataFrame([{'a': 1}]) + + data = pd.DataFrame([{"a": 1}]) context = limitted(data=data) - assert_series_equal( - guarded_eval('data.loc[:, "a"]', context), - data['a'] - ) + assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"]) def test_named_tuple(): - class GoodNamedTuple(NamedTuple): a: str pass class BadNamedTuple(NamedTuple): a: str + def __getitem__(self, key): return None - good = GoodNamedTuple(a='x') - bad = BadNamedTuple(a='x') + good = GoodNamedTuple(a="x") + bad = BadNamedTuple(a="x") context = limitted(data=good) - assert guarded_eval('data[0]', context) == 'x' + assert guarded_eval("data[0]", context) == "x" context = limitted(data=bad) with pytest.raises(GuardRejection): - guarded_eval('data[0]', context) + guarded_eval("data[0]", context) def test_dict(): - context = limitted( - data={'a': 1, 'b': {'x': 2}, ('x', 'y'): 3} - ) + context = limitted(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3}) assert guarded_eval('data["a"]', context) == 1 - assert guarded_eval('data["b"]', context) == {'x': 2} + assert guarded_eval('data["b"]', context) == {"x": 2} assert guarded_eval('data["b"]["x"]', context) == 2 assert guarded_eval('data["x", "y"]', context) == 3 - assert guarded_eval('data.keys', context) + assert guarded_eval("data.keys", context) def test_set(): - context = limitted(data={'a', 'b'}) - assert guarded_eval('data.difference', context) + context = limitted(data={"a", "b"}) + assert guarded_eval("data.difference", context) def test_list(): context = limitted(data=[1, 2, 3]) - assert guarded_eval('data[1]', context) == 2 - assert guarded_eval('data.copy', context) + assert guarded_eval("data[1]", context) == 2 + assert guarded_eval("data.copy", context) def test_dict_literal(): context = limitted() - assert guarded_eval('{}', context) == {} + assert guarded_eval("{}", context) == {} assert guarded_eval('{"a": 1}', context) == {"a": 1} def test_list_literal(): context = limitted() - assert guarded_eval('[]', context) == [] + assert guarded_eval("[]", context) == [] assert guarded_eval('[1, "a"]', context) == [1, "a"] def test_set_literal(): context = limitted() - assert guarded_eval('set()', context) == set() + assert guarded_eval("set()", context) == set() assert guarded_eval('{"a"}', context) == {"a"} def test_if_expression(): context = limitted() - assert guarded_eval('2 if True else 3', context) == 2 - assert guarded_eval('4 if False else 5', context) == 5 + assert guarded_eval("2 if True else 3", context) == 2 + assert guarded_eval("4 if False else 5", context) == 5 def test_object(): obj = object() context = limitted(obj=obj) - assert guarded_eval('obj.__dir__', context) == obj.__dir__ + assert guarded_eval("obj.__dir__", context) == obj.__dir__ @pytest.mark.parametrize( "code,expected", [ - [ - 'int.numerator', - int.numerator - ], - [ - 'float.is_integer', - float.is_integer - ], - [ - 'complex.real', - complex.real - ] - ] + ["int.numerator", int.numerator], + ["float.is_integer", float.is_integer], + ["complex.real", complex.real], + ], ) def test_number_attributes(code, expected): assert guarded_eval(code, limitted()) == expected @@ -180,25 +168,15 @@ def test_number_attributes(code, expected): def test_method_descriptor(): context = limitted() - assert guarded_eval('list.copy.__name__', context) == 'copy' + assert guarded_eval("list.copy.__name__", context) == "copy" @pytest.mark.parametrize( "data,good,bad,expected", [ - [ - [1, 2, 3], - 'data.index(2)', - 'data.append(4)', - 1 - ], - [ - {'a': 1}, - 'data.keys().isdisjoint({})', - 'data.update()', - True - ] - ] + [[1, 2, 3], "data.index(2)", "data.append(4)", 1], + [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True], + ], ) def test_calls(data, good, bad, expected): context = limitted(data=data) @@ -211,19 +189,10 @@ def test_calls(data, good, bad, expected): @pytest.mark.parametrize( "code,expected", [ - [ - '(1\n+\n1)', - 2 - ], - [ - 'list(range(10))[-1:]', - [9] - ], - [ - 'list(range(20))[3:-2:3]', - [3, 6, 9, 12, 15] - ] - ] + ["(1\n+\n1)", 2], + ["list(range(10))[-1:]", [9]], + ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]], + ], ) def test_literals(code, expected): context = limitted() @@ -232,22 +201,20 @@ def test_literals(code, expected): def test_subscript(): context = EvaluationContext( - locals_={}, - globals_={}, - evaluation='limitted', - in_subscript=True + locals_={}, globals_={}, evaluation="limitted", in_subscript=True ) empty_slice = slice(None, None, None) - assert guarded_eval('', context) == tuple() - assert guarded_eval(':', context) == empty_slice - assert guarded_eval('1:2:3', context) == slice(1, 2, 3) + assert guarded_eval("", context) == tuple() + assert guarded_eval(":", context) == empty_slice + assert guarded_eval("1:2:3", context) == slice(1, 2, 3) assert guarded_eval(':, "a"', context) == (empty_slice, "a") def test_unbind_method(): class X(list): def index(self, k): - return 'CUSTOM' + return "CUSTOM" + x = X() assert unbind_method(x.index) is X.index assert unbind_method([].index) is list.index @@ -261,16 +228,19 @@ def test_assumption_instance_attr_do_not_matter(): versions could invalidate this assumptions. This test is meant to catch such a change if it ever comes true. """ + class T: def __getitem__(self, k): - return 'a' + return "a" + def __getattr__(self, k): - return 'a' + return "a" + t = T() - t.__getitem__ = lambda f: 'b' - t.__getattr__ = lambda f: 'b' - assert t[1] == 'a' - assert t[1] == 'a' + t.__getitem__ = lambda f: "b" + t.__getattr__ = lambda f: "b" + assert t[1] == "a" + assert t[1] == "a" def test_assumption_named_tuples_share_getitem(): From b2af257cac51c788faeb2186fd9494f4f9fc8b1c Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 01:14:39 +0000 Subject: [PATCH 0308/1752] Fix typos --- IPython/core/completer.py | 10 +++--- IPython/core/guarded_eval.py | 11 +++---- IPython/core/tests/test_completer.py | 2 +- IPython/core/tests/test_guarded_eval.py | 44 ++++++++++++------------- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 3ea3dd99258..46ab19423cb 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -916,15 +916,15 @@ class Completer(Configurable): .. deprecated:: 8.8 Use :any:`evaluation` and :any:`auto_close_dict_keys` instead. - Whent enabled in IPython 8.8+ activates following settings for compatibility: + When enabled in IPython 8.8+ activates following settings for compatibility: - ``evaluation = 'unsafe'`` - ``auto_close_dict_keys = True`` """, ).tag(config=True) evaluation = Enum( - ("forbidden", "minimal", "limitted", "unsafe", "dangerous"), - default_value="limitted", + ("forbidden", "minimal", "limited", "unsafe", "dangerous"), + default_value="limited", help="""Code evaluation under completion. Successive options allow to enable more eager evaluation for more accurate completion suggestions, @@ -934,7 +934,7 @@ class Completer(Configurable): Allowed values are: - `forbidden`: no evaluation at all - `minimal`: evaluation of literals and access to built-in namespaces; no item/attribute evaluation nor access to locals/globals - - `limitted` (default): access to all namespaces, evaluation of hard-coded methods (``keys()``, ``__getattr__``, ``__getitems__``, etc) on allow-listed objects (e.g. ``dict``, ``list``, ``tuple``, ``pandas.Series``) + - `limited` (default): access to all namespaces, evaluation of hard-coded methods (``keys()``, ``__getattr__``, ``__getitems__``, etc) on allow-listed objects (e.g. ``dict``, ``list``, ``tuple``, ``pandas.Series``) - `unsafe`: evaluation of all methods and function calls but not of syntax with side-effects like `del x`, - `dangerous`: completely arbitrary evaluation """, @@ -1651,7 +1651,7 @@ def _greedy_changed(self, change): self.auto_close_dict_keys = True self.splitter.delims = GREEDY_DELIMS else: - self.evaluation = "limitted" + self.evaluation = "limited" self.auto_close_dict_keys = False self.splitter.delims = DELIMS diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index 2c278a238ae..0ed69dbb677 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -207,7 +207,7 @@ class EvaluationContext(NamedTuple): locals_: dict globals_: dict evaluation: Literal[ - "forbidden", "minimal", "limitted", "unsafe", "dangerous" + "forbidden", "minimal", "limited", "unsafe", "dangerous" ] = "forbidden" in_subscript: bool = False @@ -260,13 +260,13 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): Applies evaluation restrictions defined in the context. - Currently does not support evaluation of functions with arguments. + Currently does not support evaluation of functions with keyword arguments. Does not evaluate actions which always have side effects: - class definitions (``class sth: ...``) - function definitions (``def sth: ...``) - variable assignments (``x = 1``) - - augumented assignments (``x += 1``) + - augmented assignments (``x += 1``) - deletions (``del x``) Does not evaluate operations which do not return values: @@ -274,7 +274,7 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): - pass (``pass``) - imports (``import x``) - control flow - - conditionals (``if x:``) except for terenary IfExp (``a if x else b``) + - conditionals (``if x:``) except for ternary IfExp (``a if x else b``) - loops (``for`` and `while``) - exception handling @@ -393,7 +393,6 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): return eval_node(node.orelse, context) if isinstance(node, ast.Call): func = eval_node(node.func, context) - print(node.keywords) if policy.can_call(func) and not node.keywords: args = [eval_node(arg, context) for arg in node.args] return func(*args) @@ -490,7 +489,7 @@ def _list_methods(cls, source=None): allowed_calls=set(), allow_any_calls=False, ), - "limitted": SelectivePolicy( + "limited": SelectivePolicy( # TODO: # - should reject binary and unary operations if custom methods would be dispatched allowed_getitem=BUILTIN_GETITEM, diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 4e385d546a0..849d963905f 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -1354,7 +1354,7 @@ def completes_on_nested(): with greedy_completion(): completes_on_nested() - with evaluation_level("limitted"): + with evaluation_level("limited"): completes_on_nested() with evaluation_level("minimal"): diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 129112ff4db..2c9db81bb19 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -9,8 +9,8 @@ import pytest -def limitted(**kwargs): - return EvaluationContext(locals_=kwargs, globals_={}, evaluation="limitted") +def limited(**kwargs): + return EvaluationContext(locals_=kwargs, globals_={}, evaluation="limited") def unsafe(**kwargs): @@ -22,7 +22,7 @@ def test_pandas_series_iloc(): import pandas as pd series = pd.Series([1], index=["a"]) - context = limitted(data=series) + context = limited(data=series) assert guarded_eval("data.iloc[0]", context) == 1 @@ -30,7 +30,7 @@ def test_pandas_series_iloc(): def test_pandas_series(): import pandas as pd - context = limitted(data=pd.Series([1], index=["a"])) + context = limited(data=pd.Series([1], index=["a"])) assert guarded_eval('data["a"]', context) == 1 with pytest.raises(KeyError): guarded_eval('data["c"]', context) @@ -49,7 +49,7 @@ def __getattr__(self, key): return "CUSTOM_ATTR" bad_series = BadItemSeries([1], index=["a"]) - context = limitted(data=bad_series) + context = limited(data=bad_series) with pytest.raises(GuardRejection): guarded_eval('data["a"]', context) @@ -65,7 +65,7 @@ def __getattr__(self, key): assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM" bad_attr_series = BadAttrSeries([1], index=["a"]) - context = limitted(data=bad_attr_series) + context = limited(data=bad_attr_series) assert guarded_eval('data["a"]', context) == 1 with pytest.raises(GuardRejection): guarded_eval("data.a", context) @@ -77,7 +77,7 @@ def test_pandas_dataframe_loc(): from pandas.testing import assert_series_equal data = pd.DataFrame([{"a": 1}]) - context = limitted(data=data) + context = limited(data=data) assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"]) @@ -95,16 +95,16 @@ def __getitem__(self, key): good = GoodNamedTuple(a="x") bad = BadNamedTuple(a="x") - context = limitted(data=good) + context = limited(data=good) assert guarded_eval("data[0]", context) == "x" - context = limitted(data=bad) + context = limited(data=bad) with pytest.raises(GuardRejection): guarded_eval("data[0]", context) def test_dict(): - context = limitted(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3}) + context = limited(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3}) assert guarded_eval('data["a"]', context) == 1 assert guarded_eval('data["b"]', context) == {"x": 2} assert guarded_eval('data["b"]["x"]', context) == 2 @@ -114,43 +114,43 @@ def test_dict(): def test_set(): - context = limitted(data={"a", "b"}) + context = limited(data={"a", "b"}) assert guarded_eval("data.difference", context) def test_list(): - context = limitted(data=[1, 2, 3]) + context = limited(data=[1, 2, 3]) assert guarded_eval("data[1]", context) == 2 assert guarded_eval("data.copy", context) def test_dict_literal(): - context = limitted() + context = limited() assert guarded_eval("{}", context) == {} assert guarded_eval('{"a": 1}', context) == {"a": 1} def test_list_literal(): - context = limitted() + context = limited() assert guarded_eval("[]", context) == [] assert guarded_eval('[1, "a"]', context) == [1, "a"] def test_set_literal(): - context = limitted() + context = limited() assert guarded_eval("set()", context) == set() assert guarded_eval('{"a"}', context) == {"a"} def test_if_expression(): - context = limitted() + context = limited() assert guarded_eval("2 if True else 3", context) == 2 assert guarded_eval("4 if False else 5", context) == 5 def test_object(): obj = object() - context = limitted(obj=obj) + context = limited(obj=obj) assert guarded_eval("obj.__dir__", context) == obj.__dir__ @@ -163,11 +163,11 @@ def test_object(): ], ) def test_number_attributes(code, expected): - assert guarded_eval(code, limitted()) == expected + assert guarded_eval(code, limited()) == expected def test_method_descriptor(): - context = limitted() + context = limited() assert guarded_eval("list.copy.__name__", context) == "copy" @@ -179,7 +179,7 @@ def test_method_descriptor(): ], ) def test_calls(data, good, bad, expected): - context = limitted(data=data) + context = limited(data=data) assert guarded_eval(good, context) == expected with pytest.raises(GuardRejection): @@ -195,13 +195,13 @@ def test_calls(data, good, bad, expected): ], ) def test_literals(code, expected): - context = limitted() + context = limited() assert guarded_eval(code, context) == expected def test_subscript(): context = EvaluationContext( - locals_={}, globals_={}, evaluation="limitted", in_subscript=True + locals_={}, globals_={}, evaluation="limited", in_subscript=True ) empty_slice = slice(None, None, None) assert guarded_eval("", context) == tuple() From 467747d1e6716a0b5b796d2f9dcabe7474833d29 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 14:55:42 +0000 Subject: [PATCH 0309/1752] Check types with mypy --- .github/workflows/mypy.yml | 1 + IPython/core/guarded_eval.py | 49 ++++++++++++++++--------- IPython/core/tests/test_completer.py | 8 ++-- IPython/core/tests/test_guarded_eval.py | 5 +++ 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 8d1927d6b36..3ff6f86349e 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -31,6 +31,7 @@ jobs: run: | mypy -p IPython.terminal mypy -p IPython.core.magics + mypy -p IPython.core.guarded_eval - name: Lint with pyflakes run: | flake8 IPython/core/magics/script.py diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index 0ed69dbb677..a510d381485 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -1,4 +1,15 @@ -from typing import Callable, Set, Tuple, NamedTuple, Literal, Union, TYPE_CHECKING +from typing import ( + Any, + Callable, + Set, + Tuple, + NamedTuple, + Type, + Literal, + Union, + TYPE_CHECKING, +) +import builtins import collections import sys import ast @@ -21,7 +32,7 @@ def __getitem__(self, key) -> None: class InstancesHaveGetItem(Protocol): - def __call__(self) -> HasGetItem: + def __call__(self, *args, **kwargs) -> HasGetItem: ... @@ -55,6 +66,7 @@ def unbind_method(func: Callable) -> Union[Callable, None]: ) ): return getattr(owner_class, name) + return None @dataclass @@ -137,7 +149,7 @@ def has_original_dunder( @dataclass class SelectivePolicy(EvaluationPolicy): - allowed_getitem: Set[HasGetItem] = field(default_factory=set) + allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set) allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set) allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set) allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set) @@ -368,8 +380,9 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): return context.locals_[node.id] if policy.allow_globals_access and node.id in context.globals_: return context.globals_[node.id] - if policy.allow_builtins_access and node.id in __builtins__: - return __builtins__[node.id] + if policy.allow_builtins_access and hasattr(builtins, node.id): + # note: do not use __builtins__, it is implementation detail of Python + return getattr(builtins, node.id) if not policy.allow_globals_access and not policy.allow_locals_access: raise GuardRejection( f"Namespace access not allowed in {context.evaluation} mode" @@ -413,7 +426,7 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): ("numpy", "void"), } -BUILTIN_GETITEM = { +BUILTIN_GETITEM: Set[InstancesHaveGetItem] = { dict, str, bytes, @@ -441,8 +454,8 @@ def _list_methods(cls, source=None): set_non_mutating_methods = set(dir(set)) & set(dir(frozenset)) -dict_keys = type({}.keys()) -method_descriptor = type(list.copy) +dict_keys: Type[collections.abc.KeysView] = type({}.keys()) +method_descriptor: Any = type(list.copy) ALLOWED_CALLS = { bytes, @@ -479,6 +492,16 @@ def _list_methods(cls, source=None): collections.Counter.most_common, } +BUILTIN_GETATTR: Set[MayHaveGetattr] = { + *BUILTIN_GETITEM, + set, + frozenset, + object, + type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. + dict_keys, + method_descriptor, +} + EVALUATION_POLICIES = { "minimal": EvaluationPolicy( allow_builtins_access=True, @@ -494,15 +517,7 @@ def _list_methods(cls, source=None): # - should reject binary and unary operations if custom methods would be dispatched allowed_getitem=BUILTIN_GETITEM, allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM, - allowed_getattr={ - *BUILTIN_GETITEM, - set, - frozenset, - object, - type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. - dict_keys, - method_descriptor, - }, + allowed_getattr=BUILTIN_GETATTR, allowed_getattr_external={ # pandas Series/Frame implements custom `__getattr__` ("pandas", "DataFrame"), diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 849d963905f..bd2fa3cefdb 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -114,7 +114,7 @@ def greedy_completion(): @contextmanager -def evaluation_level(evaluation: str): +def evaluation_policy(evaluation: str): ip = get_ipython() evaluation_original = ip.Completer.evaluation try: @@ -1253,7 +1253,7 @@ def assert_completion(**kwargs): # nested dict completion assert_completion(line_buffer="nested['x'][") - with evaluation_level("minimal"): + with evaluation_policy("minimal"): with pytest.raises(AssertionError): assert_completion(line_buffer="nested['x'][") @@ -1354,10 +1354,10 @@ def completes_on_nested(): with greedy_completion(): completes_on_nested() - with evaluation_level("limited"): + with evaluation_policy("limited"): completes_on_nested() - with evaluation_level("minimal"): + with evaluation_policy("minimal"): with pytest.raises(AssertionError): completes_on_nested() diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 2c9db81bb19..b908f2af255 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -199,6 +199,11 @@ def test_literals(code, expected): assert guarded_eval(code, context) == expected +def test_access_builtins(): + context = limited() + assert guarded_eval("round", context) == round + + def test_subscript(): context = EvaluationContext( locals_={}, globals_={}, evaluation="limited", in_subscript=True From 79c46895a728bb4d3ccb8d3fa03a1a0ddd327bec Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 15:04:48 +0000 Subject: [PATCH 0310/1752] Compactify assertions spaghettified by black --- IPython/core/tests/test_completer.py | 143 +++++++++------------------ 1 file changed, 48 insertions(+), 95 deletions(-) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index bd2fa3cefdb..423979a297f 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -853,31 +853,38 @@ def test_match_dict_keys(self): delims = " \t\n`!@#$^&*()=+[{]}\\|;:'\",<>?" def match(*args, **kwargs): - quote, offset, matches = match_dict_keys(*args, **kwargs) + quote, offset, matches = match_dict_keys(*args, delims=delims, **kwargs) return quote, offset, list(matches) keys = ["foo", b"far"] - assert match(keys, "b'", delims=delims) == ("'", 2, ["far"]) - assert match(keys, "b'f", delims=delims) == ("'", 2, ["far"]) - assert match(keys, 'b"', delims=delims) == ('"', 2, ["far"]) - assert match(keys, 'b"f', delims=delims) == ('"', 2, ["far"]) + assert match(keys, "b'") == ("'", 2, ["far"]) + assert match(keys, "b'f") == ("'", 2, ["far"]) + assert match(keys, 'b"') == ('"', 2, ["far"]) + assert match(keys, 'b"f') == ('"', 2, ["far"]) - assert match(keys, "'", delims=delims) == ("'", 1, ["foo"]) - assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) - assert match(keys, '"', delims=delims) == ('"', 1, ["foo"]) - assert match(keys, '"f', delims=delims) == ('"', 1, ["foo"]) + assert match(keys, "'") == ("'", 1, ["foo"]) + assert match(keys, "'f") == ("'", 1, ["foo"]) + assert match(keys, '"') == ('"', 1, ["foo"]) + assert match(keys, '"f') == ('"', 1, ["foo"]) # Completion on first item of tuple keys = [("foo", 1111), ("foo", 2222), (3333, "bar"), (3333, "test")] - assert match(keys, "'f", delims=delims) == ("'", 1, ["foo"]) - assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) + assert match(keys, "'f") == ("'", 1, ["foo"]) + assert match(keys, "33") == ("", 0, ["3333"]) # Completion on numbers - keys = [0xDEADBEEF, 1111, 1234, "1999", 0b10101, 22] # 3735928559 # 21 - assert match(keys, "0xdead", delims=delims) == ("", 0, ["0xdeadbeef"]) - assert match(keys, "1", delims=delims) == ("", 0, ["1111", "1234"]) - assert match(keys, "2", delims=delims) == ("", 0, ["21", "22"]) - assert match(keys, "0b101", delims=delims) == ("", 0, ["0b10101", "0b10110"]) + keys = [ + 0xDEADBEEF, + 1111, + 1234, + "1999", + 0b10101, + 22, + ] # 0xDEADBEEF = 3735928559; 0b10101 = 21 + assert match(keys, "0xdead") == ("", 0, ["0xdeadbeef"]) + assert match(keys, "1") == ("", 0, ["1111", "1234"]) + assert match(keys, "2") == ("", 0, ["21", "22"]) + assert match(keys, "0b101") == ("", 0, ["0b10101", "0b10110"]) def test_match_dict_keys_tuple(self): """ @@ -888,97 +895,43 @@ def test_match_dict_keys_tuple(self): keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] - def match(*args, **kwargs): - quote, offset, matches = match_dict_keys(*args, **kwargs) + def match(*args, extra=None, **kwargs): + quote, offset, matches = match_dict_keys( + *args, delims=delims, extra_prefix=extra, **kwargs + ) return quote, offset, list(matches) # Completion on first key == "foo" - assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( - "'", - 1, - ["bar", "oof"], - ) - assert match(keys, '"', delims=delims, extra_prefix=("foo",)) == ( - '"', - 1, - ["bar", "oof"], - ) - assert match(keys, "'o", delims=delims, extra_prefix=("foo",)) == ( - "'", - 1, - ["oof"], - ) - assert match(keys, '"o', delims=delims, extra_prefix=("foo",)) == ( - '"', - 1, - ["oof"], - ) - assert match(keys, "b'", delims=delims, extra_prefix=("foo",)) == ( - "'", - 2, - ["bar"], - ) - assert match(keys, 'b"', delims=delims, extra_prefix=("foo",)) == ( - '"', - 2, - ["bar"], - ) - assert match(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ( - "'", - 2, - ["bar"], - ) - assert match(keys, 'b"b', delims=delims, extra_prefix=("foo",)) == ( - '"', - 2, - ["bar"], - ) + assert match(keys, "'", extra=("foo",)) == ("'", 1, ["bar", "oof"]) + assert match(keys, '"', extra=("foo",)) == ('"', 1, ["bar", "oof"]) + assert match(keys, "'o", extra=("foo",)) == ("'", 1, ["oof"]) + assert match(keys, '"o', extra=("foo",)) == ('"', 1, ["oof"]) + assert match(keys, "b'", extra=("foo",)) == ("'", 2, ["bar"]) + assert match(keys, 'b"', extra=("foo",)) == ('"', 2, ["bar"]) + assert match(keys, "b'b", extra=("foo",)) == ("'", 2, ["bar"]) + assert match(keys, 'b"b', extra=("foo",)) == ('"', 2, ["bar"]) # No Completion - assert match(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) - assert match(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) + assert match(keys, "'", extra=("no_foo",)) == ("'", 1, []) + assert match(keys, "'", extra=("fo",)) == ("'", 1, []) keys = [("foo1", "foo2", "foo3", "foo4"), ("foo1", "foo2", "bar", "foo4")] - assert match(keys, "'foo", delims=delims, extra_prefix=("foo1",)) == ( + assert match(keys, "'foo", extra=("foo1",)) == ("'", 1, ["foo2"]) + assert match(keys, "'foo", extra=("foo1", "foo2")) == ("'", 1, ["foo3"]) + assert match(keys, "'foo", extra=("foo1", "foo2", "foo3")) == ("'", 1, ["foo4"]) + assert match(keys, "'foo", extra=("foo1", "foo2", "foo3", "foo4")) == ( "'", 1, - ["foo2"], + [], ) - assert match(keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2")) == ( - "'", - 1, - ["foo3"], - ) - assert match( - keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2", "foo3") - ) == ("'", 1, ["foo4"]) - assert match( - keys, "'foo", delims=delims, extra_prefix=("foo1", "foo2", "foo3", "foo4") - ) == ("'", 1, []) keys = [("foo", 1111), ("foo", "2222"), (3333, "bar"), (3333, 4444)] - assert match(keys, "'", delims=delims, extra_prefix=("foo",)) == ( - "'", - 1, - ["2222"], - ) - assert match(keys, "", delims=delims, extra_prefix=("foo",)) == ( - "", - 0, - ["1111", "'2222'"], - ) - assert match(keys, "'", delims=delims, extra_prefix=(3333,)) == ( - "'", - 1, - ["bar"], - ) - assert match(keys, "", delims=delims, extra_prefix=(3333,)) == ( - "", - 0, - ["'bar'", "4444"], - ) - assert match(keys, "'", delims=delims, extra_prefix=("3333",)) == ("'", 1, []) - assert match(keys, "33", delims=delims) == ("", 0, ["3333"]) + assert match(keys, "'", extra=("foo",)) == ("'", 1, ["2222"]) + assert match(keys, "", extra=("foo",)) == ("", 0, ["1111", "'2222'"]) + assert match(keys, "'", extra=(3333,)) == ("'", 1, ["bar"]) + assert match(keys, "", extra=(3333,)) == ("", 0, ["'bar'", "4444"]) + assert match(keys, "'", extra=("3333",)) == ("'", 1, []) + assert match(keys, "33") == ("", 0, ["3333"]) def test_dict_key_completion_closures(self): ip = get_ipython() From 80b4bcf36ababf554d26b1f789460b911c5a97c3 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 16:56:09 +0000 Subject: [PATCH 0311/1752] Enable mypy typing checks for completer --- .github/workflows/mypy.yml | 1 + IPython/core/completer.py | 185 ++++++++++++++++++++++++++----------- 2 files changed, 133 insertions(+), 53 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 3ff6f86349e..e05678f724d 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -32,6 +32,7 @@ jobs: mypy -p IPython.terminal mypy -p IPython.core.magics mypy -p IPython.core.guarded_eval + mypy -p IPython.core.completer - name: Lint with pyflakes run: | flake8 IPython/core/magics/script.py diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 46ab19423cb..2f3b4f01457 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -210,6 +210,8 @@ Optional, TYPE_CHECKING, Set, + Sized, + TypeVar, Literal, ) @@ -255,10 +257,11 @@ if TYPE_CHECKING or GENERATING_DOCUMENTATION: from typing import cast - from typing_extensions import TypedDict, NotRequired, Protocol, TypeAlias + from typing_extensions import TypedDict, NotRequired, Protocol, TypeAlias, TypeGuard else: + from typing import Generic - def cast(obj, type_): + def cast(type_, obj): """Workaround for `TypeError: MatcherAPIv2() takes no arguments`""" return obj @@ -267,6 +270,7 @@ def cast(obj, type_): TypedDict = Dict # by extension of `NotRequired` requires 3.11 too Protocol = object # requires Python >=3.8 TypeAlias = Any # requires Python >=3.10 + TypeGuard = Generic # requires Python >=3.10 if GENERATING_DOCUMENTATION: from typing import TypedDict @@ -470,8 +474,9 @@ def __init__(self, name): self.complete = name self.type = 'crashed' self.name_with_symbols = name - self.signature = '' - self._origin = 'fake' + self.signature = "" + self._origin = "fake" + self.text = "crashed" def __repr__(self): return '' @@ -507,11 +512,23 @@ class Completion: __slots__ = ['start', 'end', 'text', 'type', 'signature', '_origin'] - def __init__(self, start: int, end: int, text: str, *, type: str=None, _origin='', signature='') -> None: - warnings.warn("``Completion`` is a provisional API (as of IPython 6.0). " - "It may change without warnings. " - "Use in corresponding context manager.", - category=ProvisionalCompleterWarning, stacklevel=2) + def __init__( + self, + start: int, + end: int, + text: str, + *, + type: Optional[str] = None, + _origin="", + signature="", + ) -> None: + warnings.warn( + "``Completion`` is a provisional API (as of IPython 6.0). " + "It may change without warnings. " + "Use in corresponding context manager.", + category=ProvisionalCompleterWarning, + stacklevel=2, + ) self.start = start self.end = end @@ -524,7 +541,7 @@ def __repr__(self): return '' % \ (self.start, self.end, self.text, self.type or '?', self.signature or '?') - def __eq__(self, other)->Bool: + def __eq__(self, other) -> bool: """ Equality and hash do not hash the type (as some completer may not be able to infer the type), but are use to (partially) de-duplicate @@ -592,14 +609,18 @@ class SimpleMatcherResult(_MatcherResultBase, TypedDict): # in order to get __orig_bases__ for documentation #: List of candidate completions - completions: Sequence[SimpleCompletion] + completions: Sequence[SimpleCompletion] | Iterator[SimpleCompletion] class _JediMatcherResult(_MatcherResultBase): """Matching result returned by Jedi (will be processed differently)""" #: list of candidate completions - completions: Iterable[_JediCompletionLike] + completions: Iterator[_JediCompletionLike] + + +AnyMatcherCompletion = Union[_JediCompletionLike, SimpleCompletion] +AnyCompletion = TypeVar("AnyCompletion", AnyMatcherCompletion, Completion) @dataclass @@ -650,6 +671,9 @@ def __call__(self, text: str) -> List[str]: """Call signature.""" ... + #: Used to construct the default matcher identifier + __qualname__: str + class _MatcherAPIv1Total(_MatcherAPIv1Base, Protocol): #: API version @@ -674,25 +698,59 @@ def __call__(self, context: CompletionContext) -> MatcherResult: """Call signature.""" ... + #: Used to construct the default matcher identifier + __qualname__: str + Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2] +def _is_matcher_v1(matcher: Matcher) -> TypeGuard[MatcherAPIv1]: + api_version = _get_matcher_api_version(matcher) + return api_version == 1 + + +def _is_matcher_v2(matcher: Matcher) -> TypeGuard[MatcherAPIv2]: + api_version = _get_matcher_api_version(matcher) + return api_version == 2 + + +def _is_sizable(value: Any) -> TypeGuard[Sized]: + """Determines whether objects is sizable""" + return hasattr(value, "__len__") + + +def _is_iterator(value: Any) -> TypeGuard[Iterator]: + """Determines whether objects is sizable""" + return hasattr(value, "__next__") + + def has_any_completions(result: MatcherResult) -> bool: """Check if any result includes any completions.""" - if hasattr(result["completions"], "__len__"): - return len(result["completions"]) != 0 - try: - old_iterator = result["completions"] - first = next(old_iterator) - result["completions"] = itertools.chain([first], old_iterator) - return True - except StopIteration: - return False + completions = result["completions"] + if _is_sizable(completions): + return len(completions) != 0 + if _is_iterator(completions): + try: + old_iterator = completions + first = next(old_iterator) + result["completions"] = cast( + Iterator[SimpleCompletion], + itertools.chain([first], old_iterator), + ) + return True + except StopIteration: + return False + raise ValueError( + "Completions returned by matcher need to be an Iterator or a Sizable" + ) def completion_matcher( - *, priority: float = None, identifier: str = None, api_version: int = 1 + *, + priority: Optional[float] = None, + identifier: Optional[str] = None, + api_version: int = 1, ): """Adds attributes describing the matcher. @@ -715,14 +773,14 @@ def completion_matcher( """ def wrapper(func: Matcher): - func.matcher_priority = priority or 0 - func.matcher_identifier = identifier or func.__qualname__ - func.matcher_api_version = api_version + func.matcher_priority = priority or 0 # type: ignore + func.matcher_identifier = identifier or func.__qualname__ # type: ignore + func.matcher_api_version = api_version # type: ignore if TYPE_CHECKING: if api_version == 1: - func = cast(func, MatcherAPIv1) + func = cast(MatcherAPIv1, func) elif api_version == 2: - func = cast(func, MatcherAPIv2) + func = cast(MatcherAPIv2, func) return func return wrapper @@ -1311,6 +1369,8 @@ def filter_prefix_tuple(key): matched: Dict[str, DictKeyState] = {} + str_key: Union[str, bytes] + for key in filtered_keys: if isinstance(key, (int, float)): # User typed a number but this key is not a number. @@ -1637,7 +1697,7 @@ def _convert_matcher_v1_result_to_v2( } if fragment is not None: result["matched_fragment"] = fragment - return result + return cast(SimpleMatcherResult, result) class IPCompleter(Completer): @@ -1839,7 +1899,7 @@ def __init__( if not self.backslash_combining_completions: for matcher in self._backslash_combining_matchers: - self.disable_matchers.append(matcher.matcher_identifier) + self.disable_matchers.append(_get_matcher_id(matcher)) if not self.merge_completions: self.suppress_competing_matchers = True @@ -2129,7 +2189,7 @@ def _jedi_matcher(self, context: CompletionContext) -> _JediMatcherResult: def _jedi_matches( self, cursor_column: int, cursor_line: int, text: str - ) -> Iterable[_JediCompletionLike]: + ) -> Iterator[_JediCompletionLike]: """ Return a list of :any:`jedi.api.Completion`s object from a ``text`` and cursor position. @@ -2195,15 +2255,23 @@ def _jedi_matches( print("Error detecting if completing a non-finished string :", e, '|') if not try_jedi: - return [] + return iter([]) try: return filter(completion_filter, interpreter.complete(column=cursor_column, line=cursor_line + 1)) except Exception as e: if self.debug: - return [_FakeJediCompletion('Oops Jedi has crashed, please report a bug with the following:\n"""\n%s\ns"""' % (e))] + return iter( + [ + _FakeJediCompletion( + 'Oops Jedi has crashed, please report a bug with the following:\n"""\n%s\ns"""' + % (e) + ) + ] + ) else: - return [] + return iter([]) + @completion_matcher(api_version=1) def python_matches(self, text: str) -> Iterable[str]: """Match attributes or global python names""" if "." in text: @@ -2762,17 +2830,23 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com jedi_matcher_id = _get_matcher_id(self._jedi_matcher) + def is_non_jedi_result( + result: MatcherResult, identifier: str + ) -> TypeGuard[SimpleMatcherResult]: + return identifier != jedi_matcher_id + results = self._complete( full_text=full_text, cursor_line=cursor_line, cursor_pos=cursor_column ) + non_jedi_results: Dict[str, SimpleMatcherResult] = { identifier: result for identifier, result in results.items() - if identifier != jedi_matcher_id + if is_non_jedi_result(result, identifier) } jedi_matches = ( - cast(results[jedi_matcher_id], _JediMatcherResult)["completions"] + cast(_JediMatcherResult, results[jedi_matcher_id])["completions"] if jedi_matcher_id in results else () ) @@ -2827,8 +2901,8 @@ def _completions(self, full_text: str, offset: int, *, _timeout) -> Iterator[Com signature="", ) - ordered = [] - sortable = [] + ordered: List[Completion] = [] + sortable: List[Completion] = [] for origin, result in non_jedi_results.items(): matched_text = result["matched_fragment"] @@ -2918,8 +2992,8 @@ def _arrange_and_extract( abort_if_offset_changes: bool, ): - sortable = [] - ordered = [] + sortable: List[AnyMatcherCompletion] = [] + ordered: List[AnyMatcherCompletion] = [] most_recent_fragment = None for identifier, result in results.items(): if identifier in skip_matchers: @@ -3018,11 +3092,11 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, ) # Start with a clean slate of completions - results = {} + results: Dict[str, MatcherResult] = {} jedi_matcher_id = _get_matcher_id(self._jedi_matcher) - suppressed_matchers = set() + suppressed_matchers: Set[str] = set() matchers = { _get_matcher_id(matcher): matcher @@ -3032,7 +3106,6 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, } for matcher_id, matcher in matchers.items(): - api_version = _get_matcher_api_version(matcher) matcher_id = _get_matcher_id(matcher) if matcher_id in self.disable_matchers: @@ -3044,14 +3117,16 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, if matcher_id in suppressed_matchers: continue + result: MatcherResult try: - if api_version == 1: + if _is_matcher_v1(matcher): result = _convert_matcher_v1_result_to_v2( matcher(text), type=_UNKNOWN_TYPE ) - elif api_version == 2: - result = cast(matcher, MatcherAPIv2)(context) + elif _is_matcher_v2(matcher): + result = matcher(context) else: + api_version = _get_matcher_api_version(matcher) raise ValueError(f"Unsupported API version {api_version}") except: # Show the ugly traceback if the matcher causes an @@ -3063,7 +3138,9 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, result["matched_fragment"] = result.get("matched_fragment", context.token) if not suppressed_matchers: - suppression_recommended = result.get("suppress", False) + suppression_recommended: Union[bool, Set[str]] = result.get( + "suppress", False + ) suppression_config = ( self.suppress_competing_matchers.get(matcher_id, None) @@ -3076,10 +3153,12 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, ) and has_any_completions(result) if should_suppress: - suppression_exceptions = result.get("do_not_suppress", set()) - try: + suppression_exceptions: Set[str] = result.get( + "do_not_suppress", set() + ) + if isinstance(suppression_recommended, Iterable): to_suppress = set(suppression_recommended) - except TypeError: + else: to_suppress = set(matchers) suppressed_matchers = to_suppress - suppression_exceptions @@ -3106,9 +3185,9 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None, @staticmethod def _deduplicate( - matches: Sequence[SimpleCompletion], - ) -> Iterable[SimpleCompletion]: - filtered_matches = {} + matches: Sequence[AnyCompletion], + ) -> Iterable[AnyCompletion]: + filtered_matches: Dict[str, AnyCompletion] = {} for match in matches: text = match.text if ( @@ -3120,7 +3199,7 @@ def _deduplicate( return filtered_matches.values() @staticmethod - def _sort(matches: Sequence[SimpleCompletion]): + def _sort(matches: Sequence[AnyCompletion]): return sorted(matches, key=lambda x: completions_sorting_key(x.text)) @context_matcher() From bbf990daf76dce132ac2f66e782d463f54a21f7f Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 20:27:08 +0000 Subject: [PATCH 0312/1752] Polish documentation, hide private functions --- IPython/core/completer.py | 80 ++++++++++++-------- IPython/core/guarded_eval.py | 98 +++++++++++++++++-------- IPython/core/magics/config.py | 88 ++-------------------- IPython/core/tests/test_guarded_eval.py | 12 +-- 4 files changed, 129 insertions(+), 149 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 2f3b4f01457..7dd585bce36 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -50,7 +50,7 @@ It is sometime challenging to know how to type a character, if you are using IPython, or any compatible frontend you can prepend backslash to the character -and press ```` to expand it to its latex form. +and press :kbd:`Tab` to expand it to its latex form. .. code:: @@ -59,7 +59,7 @@ Both forward and backward completions can be deactivated by setting the -``Completer.backslash_combining_completions`` option to ``False``. +:any:`Completer.backslash_combining_completions` option to ``False``. Experimental @@ -95,7 +95,7 @@ ... myvar[1].bi Tab completion will be able to infer that ``myvar[1]`` is a real number without -executing any code unlike the previously available ``IPCompleter.greedy`` +executing almost any code unlike the deprecated :any:`IPCompleter.greedy` option. Be sure to update :any:`jedi` to the latest stable version or to try the @@ -972,29 +972,38 @@ class Completer(Configurable): help="""Activate greedy completion. .. deprecated:: 8.8 - Use :any:`evaluation` and :any:`auto_close_dict_keys` instead. + Use :any:`Completer.evaluation` and :any:`Completer.auto_close_dict_keys` instead. - When enabled in IPython 8.8+ activates following settings for compatibility: - - ``evaluation = 'unsafe'`` - - ``auto_close_dict_keys = True`` + When enabled in IPython 8.8 or newer, changes configuration as follows: + + - ``Completer.evaluation = 'unsafe'`` + - ``Completer.auto_close_dict_keys = True`` """, ).tag(config=True) evaluation = Enum( ("forbidden", "minimal", "limited", "unsafe", "dangerous"), default_value="limited", - help="""Code evaluation under completion. + help="""Policy for code evaluation under completion. - Successive options allow to enable more eager evaluation for more accurate completion suggestions, - including for nested dictionaries, nested lists, or even results of function calls. Setting `unsafe` - or higher can lead to evaluation of arbitrary user code on TAB with potentially dangerous side effects. + Successive options allow to enable more eager evaluation for better + completion suggestions, including for nested dictionaries, nested lists, + or even results of function calls. + Setting ``unsafe`` or higher can lead to evaluation of arbitrary user + code on :kbd:`Tab` with potentially unwanted or dangerous side effects. Allowed values are: - - `forbidden`: no evaluation at all - - `minimal`: evaluation of literals and access to built-in namespaces; no item/attribute evaluation nor access to locals/globals - - `limited` (default): access to all namespaces, evaluation of hard-coded methods (``keys()``, ``__getattr__``, ``__getitems__``, etc) on allow-listed objects (e.g. ``dict``, ``list``, ``tuple``, ``pandas.Series``) - - `unsafe`: evaluation of all methods and function calls but not of syntax with side-effects like `del x`, - - `dangerous`: completely arbitrary evaluation + + - ``forbidden``: no evaluation of code is permitted, + - ``minimal``: evaluation of literals and access to built-in namespace; + no item/attribute evaluation nor access to locals/globals, + - ``limited``: access to all namespaces, evaluation of hard-coded methods + (for example: :any:`dict.keys`, :any:`object.__getattr__`, + :any:`object.__getitem__`) on allow-listed objects (for example: + :any:`dict`, :any:`list`, :any:`tuple`, ``pandas.Series``), + - ``unsafe``: evaluation of all methods and function calls but not of + syntax with side-effects like `del x`, + - ``dangerous``: completely arbitrary evaluation. """, ).tag(config=True) @@ -1019,7 +1028,15 @@ class Completer(Configurable): "unicode characters back to latex commands.").tag(config=True) auto_close_dict_keys = Bool( - False, help="""Enable auto-closing dictionary keys.""" + False, + help=""" + Enable auto-closing dictionary keys. + + When enabled string keys will be suffixed with a final quote + (matching the opening quote), tuple keys will also receive a + separating comma if needed, and keys which are final will + receive a closing bracket (``]``). + """, ).tag(config=True) def __init__(self, namespace=None, global_namespace=None, **kwargs): @@ -1157,8 +1174,8 @@ def _evaluate_expr(self, expr): obj = guarded_eval( expr, EvaluationContext( - globals_=self.global_namespace, - locals_=self.namespace, + globals=self.global_namespace, + locals=self.namespace, evaluation=self.evaluation, ), ) @@ -1183,7 +1200,7 @@ def get__all__entries(obj): return [w for w in words if isinstance(w, str)] -class DictKeyState(enum.Flag): +class _DictKeyState(enum.Flag): """Represent state of the key match in context of other possible matches. - given `d1 = {'a': 1}` completion on `d1['` will yield `{'a': END_OF_ITEM}` as there is no tuple. @@ -1199,6 +1216,7 @@ class DictKeyState(enum.Flag): def _parse_tokens(c): + """Parse tokens even if there is an error.""" tokens = [] token_generator = tokenize.generate_tokens(iter(c.splitlines()).__next__) while True: @@ -1257,7 +1275,7 @@ def match_dict_keys( prefix: str, delims: str, extra_prefix: Optional[Tuple[Union[str, bytes], ...]] = None, -) -> Tuple[str, int, Dict[str, DictKeyState]]: +) -> Tuple[str, int, Dict[str, _DictKeyState]]: """Used by dict_key_matches, matching the prefix to a list of keys Parameters @@ -1307,8 +1325,8 @@ def filter_prefix_tuple(key): return True filtered_key_is_final: Dict[ - Union[str, bytes, int, float], DictKeyState - ] = defaultdict(lambda: DictKeyState.BASELINE) + Union[str, bytes, int, float], _DictKeyState + ] = defaultdict(lambda: _DictKeyState.BASELINE) for k in keys: # If at least one of the matches is not final, mark as undetermined. @@ -1319,9 +1337,9 @@ def filter_prefix_tuple(key): if filter_prefix_tuple(k): key_fragment = k[prefix_tuple_size] filtered_key_is_final[key_fragment] |= ( - DictKeyState.END_OF_TUPLE + _DictKeyState.END_OF_TUPLE if len(k) == prefix_tuple_size + 1 - else DictKeyState.IN_TUPLE + else _DictKeyState.IN_TUPLE ) elif prefix_tuple_size > 0: # we are completing a tuple but this key is not a tuple, @@ -1329,7 +1347,7 @@ def filter_prefix_tuple(key): pass else: if isinstance(k, text_serializable_types): - filtered_key_is_final[k] |= DictKeyState.END_OF_ITEM + filtered_key_is_final[k] |= _DictKeyState.END_OF_ITEM filtered_keys = filtered_key_is_final.keys() @@ -1367,7 +1385,7 @@ def filter_prefix_tuple(key): token_start = token_match.start() token_prefix = token_match.group() - matched: Dict[str, DictKeyState] = {} + matched: Dict[str, _DictKeyState] = {} str_key: Union[str, bytes] @@ -2503,8 +2521,8 @@ def dict_key_matches(self, text: str) -> List[str]: tuple_prefix = guarded_eval( prior_tuple_keys, EvaluationContext( - globals_=self.global_namespace, - locals_=self.namespace, + globals=self.global_namespace, + locals=self.namespace, evaluation=self.evaluation, in_subscript=True, ), @@ -2569,7 +2587,7 @@ def dict_key_matches(self, text: str) -> List[str]: results = [] - end_of_tuple_or_item = DictKeyState.END_OF_TUPLE | DictKeyState.END_OF_ITEM + end_of_tuple_or_item = _DictKeyState.END_OF_TUPLE | _DictKeyState.END_OF_ITEM for k, state_flag in matches.items(): result = leading + k @@ -2584,7 +2602,7 @@ def dict_key_matches(self, text: str) -> List[str]: if state_flag in end_of_tuple_or_item and can_close_bracket: result += "]" - if state_flag == DictKeyState.IN_TUPLE and can_close_tuple_item: + if state_flag == _DictKeyState.IN_TUPLE and can_close_tuple_item: result += ", " results.append(result) return results diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index a510d381485..637d329a17e 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -17,6 +17,7 @@ from dataclasses import dataclass, field from IPython.utils.docs import GENERATING_DOCUMENTATION +from IPython.utils.decorators import undoc if TYPE_CHECKING or GENERATING_DOCUMENTATION: @@ -26,21 +27,25 @@ Protocol = object # requires Python >=3.8 +@undoc class HasGetItem(Protocol): def __getitem__(self, key) -> None: ... +@undoc class InstancesHaveGetItem(Protocol): def __call__(self, *args, **kwargs) -> HasGetItem: ... +@undoc class HasGetAttr(Protocol): def __getattr__(self, key) -> None: ... +@undoc class DoesNotHaveGetAttr(Protocol): pass @@ -49,7 +54,7 @@ class DoesNotHaveGetAttr(Protocol): MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr] -def unbind_method(func: Callable) -> Union[Callable, None]: +def _unbind_method(func: Callable) -> Union[Callable, None]: """Get unbound method for given bound method. Returns None if cannot get unbound method.""" @@ -69,8 +74,11 @@ def unbind_method(func: Callable) -> Union[Callable, None]: return None +@undoc @dataclass class EvaluationPolicy: + """Definition of evaluation policy.""" + allow_locals_access: bool = False allow_globals_access: bool = False allow_item_access: bool = False @@ -92,12 +100,12 @@ def can_call(self, func): if func in self.allowed_calls: return True - owner_method = unbind_method(func) + owner_method = _unbind_method(func) if owner_method and owner_method in self.allowed_calls: return True -def has_original_dunder_external( +def _has_original_dunder_external( value, module_name, access_path, @@ -121,7 +129,7 @@ def has_original_dunder_external( return False -def has_original_dunder( +def _has_original_dunder( value, allowed_types, allowed_methods, allowed_external, method_name ): # note: Python ignores `__getattr__`/`__getitem__` on instances, @@ -141,12 +149,13 @@ def has_original_dunder( return True for module_name, *access_path in allowed_external: - if has_original_dunder_external(value, module_name, access_path, method_name): + if _has_original_dunder_external(value, module_name, access_path, method_name): return True return False +@undoc @dataclass class SelectivePolicy(EvaluationPolicy): allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set) @@ -155,14 +164,14 @@ class SelectivePolicy(EvaluationPolicy): allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set) def can_get_attr(self, value, attr): - has_original_attribute = has_original_dunder( + has_original_attribute = _has_original_dunder( value, allowed_types=self.allowed_getattr, allowed_methods=self._getattribute_methods, allowed_external=self.allowed_getattr_external, method_name="__getattribute__", ) - has_original_attr = has_original_dunder( + has_original_attr = _has_original_dunder( value, allowed_types=self.allowed_getattr, allowed_methods=self._getattr_methods, @@ -182,7 +191,7 @@ def get_attr(self, value, attr): def can_get_item(self, value, item): """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" - return has_original_dunder( + return _has_original_dunder( value, allowed_types=self.allowed_getitem, allowed_methods=self._getitem_methods, @@ -211,34 +220,50 @@ def _safe_get_methods(self, classes, name) -> Set[Callable]: } -class DummyNamedTuple(NamedTuple): +class _DummyNamedTuple(NamedTuple): pass class EvaluationContext(NamedTuple): - locals_: dict - globals_: dict + #: Local namespace + locals: dict + #: Global namespace + globals: dict + #: Evaluation policy identifier evaluation: Literal[ "forbidden", "minimal", "limited", "unsafe", "dangerous" ] = "forbidden" + #: Whether the evalution of code takes place inside of a subscript. + #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``. in_subscript: bool = False -class IdentitySubscript: +class _IdentitySubscript: + """Returns the key itself when item is requested via subscript.""" + def __getitem__(self, key): return key -IDENTITY_SUBSCRIPT = IdentitySubscript() +IDENTITY_SUBSCRIPT = _IdentitySubscript() SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__" -class GuardRejection(ValueError): +class GuardRejection(Exception): + """Exception raised when guard rejects evaluation attempt.""" + pass def guarded_eval(code: str, context: EvaluationContext): - locals_ = context.locals_ + """Evaluate provided code in the evaluation context. + + If evaluation policy given by context is set to ``forbidden`` + no evaluation will be performed; if it is set to ``dangerous`` + standard :func:`eval` will be used; finally, for any other, + policy :func:`eval_node` will be called on parsed AST. + """ + locals_ = context.locals if context.evaluation == "forbidden": raise GuardRejection("Forbidden mode") @@ -256,10 +281,10 @@ def guarded_eval(code: str, context: EvaluationContext): locals_ = locals_.copy() locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT code = SUBSCRIPT_MARKER + "[" + code + "]" - context = EvaluationContext(**{**context._asdict(), **{"locals_": locals_}}) + context = EvaluationContext(**{**context._asdict(), **{"locals": locals_}}) if context.evaluation == "dangerous": - return eval(code, context.globals_, context.locals_) + return eval(code, context.globals, context.locals) expression = ast.parse(code, mode="eval") @@ -267,14 +292,12 @@ def guarded_eval(code: str, context: EvaluationContext): def eval_node(node: Union[ast.AST, None], context: EvaluationContext): - """ - Evaluate AST node in provided context. + """Evaluate AST node in provided context. - Applies evaluation restrictions defined in the context. + Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments. - Currently does not support evaluation of functions with keyword arguments. + Does not evaluate actions that always have side effects: - Does not evaluate actions which always have side effects: - class definitions (``class sth: ...``) - function definitions (``def sth: ...``) - variable assignments (``x = 1``) @@ -282,13 +305,15 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): - deletions (``del x``) Does not evaluate operations which do not return values: + - assertions (``assert x``) - pass (``pass``) - imports (``import x``) - - control flow - - conditionals (``if x:``) except for ternary IfExp (``a if x else b``) - - loops (``for`` and `while``) - - exception handling + - control flow: + + - conditionals (``if x:``) except for ternary IfExp (``a if x else b``) + - loops (``for`` and `while``) + - exception handling The purpose of this function is to guard against unwanted side-effects; it does not give guarantees on protection from malicious code execution. @@ -376,10 +401,10 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): f" not allowed in {context.evaluation} mode", ) if isinstance(node, ast.Name): - if policy.allow_locals_access and node.id in context.locals_: - return context.locals_[node.id] - if policy.allow_globals_access and node.id in context.globals_: - return context.globals_[node.id] + if policy.allow_locals_access and node.id in context.locals: + return context.locals[node.id] + if policy.allow_globals_access and node.id in context.globals: + return context.globals[node.id] if policy.allow_builtins_access and hasattr(builtins, node.id): # note: do not use __builtins__, it is implementation detail of Python return getattr(builtins, node.id) @@ -439,8 +464,8 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): collections.UserDict, collections.UserList, collections.UserString, - DummyNamedTuple, - IdentitySubscript, + _DummyNamedTuple, + _IdentitySubscript, } @@ -537,3 +562,12 @@ def _list_methods(cls, source=None): allow_any_calls=True, ), } + + +__all__ = [ + "guarded_eval", + "eval_node", + "GuardRejection", + "EvaluationContext", + "_unbind_method", +] diff --git a/IPython/core/magics/config.py b/IPython/core/magics/config.py index 87fe3eed3a5..9e1cb38c254 100644 --- a/IPython/core/magics/config.py +++ b/IPython/core/magics/config.py @@ -68,94 +68,22 @@ def config(self, s): To view what is configurable on a given class, just pass the class name:: - In [2]: %config IPCompleter - IPCompleter(Completer) options - ---------------------------- - IPCompleter.backslash_combining_completions= - Enable unicode completions, e.g. \\alpha . Includes completion of latex - commands, unicode names, and expanding unicode characters back to latex - commands. - Current: True - IPCompleter.debug= - Enable debug for the Completer. Mostly print extra information for - experimental jedi integration. + In [2]: %config LoggingMagics + LoggingMagics(Magics) options + --------------------------- + LoggingMagics.quiet= + Suppress output of log state when logging is enabled Current: False - IPCompleter.disable_matchers=... - List of matchers to disable. - The list should contain matcher identifiers (see - :any:`completion_matcher`). - Current: [] - IPCompleter.greedy= - Activate greedy completion - PENDING DEPRECATION. this is now mostly taken care of with Jedi. - This will enable completion on elements of lists, results of function calls, etc., - but can be unsafe because the code is actually evaluated on TAB. - Current: False - IPCompleter.jedi_compute_type_timeout= - Experimental: restrict time (in milliseconds) during which Jedi can compute types. - Set to 0 to stop computing types. Non-zero value lower than 100ms may hurt - performance by preventing jedi to build its cache. - Current: 400 - IPCompleter.limit_to__all__= - DEPRECATED as of version 5.0. - Instruct the completer to use __all__ for the completion - Specifically, when completing on ``object.``. - When True: only those names in obj.__all__ will be included. - When False [default]: the __all__ attribute is ignored - Current: False - IPCompleter.merge_completions= - Whether to merge completion results into a single list - If False, only the completion results from the first non-empty - completer will be returned. - As of version 8.6.0, setting the value to ``False`` is an alias for: - ``IPCompleter.suppress_competing_matchers = True.``. - Current: True - IPCompleter.omit__names= - Instruct the completer to omit private method names - Specifically, when completing on ``object.``. - When 2 [default]: all names that start with '_' will be excluded. - When 1: all 'magic' names (``__foo__``) will be excluded. - When 0: nothing will be excluded. - Choices: any of [0, 1, 2] - Current: 2 - IPCompleter.profile_completions= - If True, emit profiling data for completion subsystem using cProfile. - Current: False - IPCompleter.profiler_output_dir= - Template for path at which to output profile data for completions. - Current: '.completion_profiles' - IPCompleter.suppress_competing_matchers= - Whether to suppress completions from other *Matchers*. - When set to ``None`` (default) the matchers will attempt to auto-detect - whether suppression of other matchers is desirable. For example, at the - beginning of a line followed by `%` we expect a magic completion to be the - only applicable option, and after ``my_dict['`` we usually expect a - completion with an existing dictionary key. - If you want to disable this heuristic and see completions from all matchers, - set ``IPCompleter.suppress_competing_matchers = False``. To disable the - heuristic for specific matchers provide a dictionary mapping: - ``IPCompleter.suppress_competing_matchers = {'IPCompleter.dict_key_matcher': - False}``. - Set ``IPCompleter.suppress_competing_matchers = True`` to limit completions - to the set of matchers with the highest priority; this is equivalent to - ``IPCompleter.merge_completions`` and can be beneficial for performance, but - will sometimes omit relevant candidates from matchers further down the - priority list. - Current: None - IPCompleter.use_jedi= - Experimental: Use Jedi to generate autocompletions. Default to True if jedi - is installed. - Current: True but the real use is in setting values:: - In [3]: %config IPCompleter.greedy = True + In [3]: %config LoggingMagics.quiet = True and these values are read from the user_ns if they are variables:: - In [4]: feeling_greedy=False + In [4]: feeling_quiet=False - In [5]: %config IPCompleter.greedy = feeling_greedy + In [5]: %config LoggingMagics.quiet = feeling_quiet """ from traitlets.config.loader import Config diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index b908f2af255..9c98b7a8e2a 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -3,18 +3,18 @@ EvaluationContext, GuardRejection, guarded_eval, - unbind_method, + _unbind_method, ) from IPython.testing import decorators as dec import pytest def limited(**kwargs): - return EvaluationContext(locals_=kwargs, globals_={}, evaluation="limited") + return EvaluationContext(locals=kwargs, globals={}, evaluation="limited") def unsafe(**kwargs): - return EvaluationContext(locals_=kwargs, globals_={}, evaluation="unsafe") + return EvaluationContext(locals=kwargs, globals={}, evaluation="unsafe") @dec.skip_without("pandas") @@ -206,7 +206,7 @@ def test_access_builtins(): def test_subscript(): context = EvaluationContext( - locals_={}, globals_={}, evaluation="limited", in_subscript=True + locals={}, globals={}, evaluation="limited", in_subscript=True ) empty_slice = slice(None, None, None) assert guarded_eval("", context) == tuple() @@ -221,8 +221,8 @@ def index(self, k): return "CUSTOM" x = X() - assert unbind_method(x.index) is X.index - assert unbind_method([].index) is list.index + assert _unbind_method(x.index) is X.index + assert _unbind_method([].index) is list.index def test_assumption_instance_attr_do_not_matter(): From 8f4e32247605f1bb9180aadd6e02a1be62d011bc Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 3 Dec 2022 22:38:42 +0000 Subject: [PATCH 0313/1752] Add guards for binary, unary operators and comparators --- IPython/core/guarded_eval.py | 181 +++++++++++++++++++----- IPython/core/tests/test_guarded_eval.py | 70 +++++++++ 2 files changed, 214 insertions(+), 37 deletions(-) diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index 637d329a17e..215a36fab05 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -1,6 +1,7 @@ from typing import ( Any, Callable, + Dict, Set, Tuple, NamedTuple, @@ -9,10 +10,11 @@ Union, TYPE_CHECKING, ) +import ast import builtins import collections +import operator import sys -import ast from functools import cached_property from dataclasses import dataclass, field @@ -84,6 +86,7 @@ class EvaluationPolicy: allow_item_access: bool = False allow_attr_access: bool = False allow_builtins_access: bool = False + allow_all_operations: bool = False allow_any_calls: bool = False allowed_calls: Set[Callable] = field(default_factory=set) @@ -93,6 +96,10 @@ def can_get_item(self, value, item): def can_get_attr(self, value, attr): return self.allow_attr_access + def can_operate(self, dunders: Tuple[str, ...], a, b=None): + if self.allow_all_operations: + return True + def can_call(self, func): if self.allow_any_calls: return True @@ -160,9 +167,17 @@ def _has_original_dunder( class SelectivePolicy(EvaluationPolicy): allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set) allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set) + allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set) allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set) + allowed_operations: Set = field(default_factory=set) + allowed_operations_external: Set[Tuple[str, ...]] = field(default_factory=set) + + _operation_methods_cache: Dict[str, Set[Callable]] = field( + default_factory=dict, init=False + ) + def can_get_attr(self, value, attr): has_original_attribute = _has_original_dunder( value, @@ -199,6 +214,27 @@ def can_get_item(self, value, item): method_name="__getitem__", ) + def can_operate(self, dunders: Tuple[str, ...], a, b=None): + return all( + [ + _has_original_dunder( + a, + allowed_types=self.allowed_operations, + allowed_methods=self._dunder_methods(dunder), + allowed_external=self.allowed_operations_external, + method_name=dunder, + ) + for dunder in dunders + ] + ) + + def _dunder_methods(self, dunder: str) -> Set[Callable]: + if dunder not in self._operation_methods_cache: + self._operation_methods_cache[dunder] = self._safe_get_methods( + self.allowed_operations, dunder + ) + return self._operation_methods_cache[dunder] + @cached_property def _getitem_methods(self) -> Set[Callable]: return self._safe_get_methods(self.allowed_getitem, "__getitem__") @@ -291,6 +327,50 @@ def guarded_eval(code: str, context: EvaluationContext): return eval_node(expression, context) +BINARY_OP_DUNDERS: Dict[Type[ast.operator], Tuple[str]] = { + ast.Add: ("__add__",), + ast.Sub: ("__sub__",), + ast.Mult: ("__mul__",), + ast.Div: ("__truediv__",), + ast.FloorDiv: ("__floordiv__",), + ast.Mod: ("__mod__",), + ast.Pow: ("__pow__",), + ast.LShift: ("__lshift__",), + ast.RShift: ("__rshift__",), + ast.BitOr: ("__or__",), + ast.BitXor: ("__xor__",), + ast.BitAnd: ("__and__",), + ast.MatMult: ("__matmul__",), +} + +COMP_OP_DUNDERS: Dict[Type[ast.cmpop], Tuple[str, ...]] = { + ast.Eq: ("__eq__",), + ast.NotEq: ("__ne__", "__eq__"), + ast.Lt: ("__lt__", "__gt__"), + ast.LtE: ("__le__", "__ge__"), + ast.Gt: ("__gt__", "__lt__"), + ast.GtE: ("__ge__", "__le__"), + ast.In: ("__contains__",), + # Note: ast.Is, ast.IsNot, ast.NotIn are handled specially +} + +UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = { + ast.USub: ("__neg__",), + ast.UAdd: ("__pos__",), + # we have to check both __inv__ and __invert__! + ast.Invert: ("__invert__", "__inv__"), + ast.Not: ("__not__",), +} + + +def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]: + dunder = None + for op, candidate_dunder in dunders.items(): + if isinstance(node_op, op): + dunder = candidate_dunder + return dunder + + def eval_node(node: Union[ast.AST, None], context: EvaluationContext): """Evaluate AST node in provided context. @@ -324,35 +404,55 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): if isinstance(node, ast.Expression): return eval_node(node.body, context) if isinstance(node, ast.BinOp): - # TODO: add guards left = eval_node(node.left, context) right = eval_node(node.right, context) - if isinstance(node.op, ast.Add): - return left + right - if isinstance(node.op, ast.Sub): - return left - right - if isinstance(node.op, ast.Mult): - return left * right - if isinstance(node.op, ast.Div): - return left / right - if isinstance(node.op, ast.FloorDiv): - return left // right - if isinstance(node.op, ast.Mod): - return left % right - if isinstance(node.op, ast.Pow): - return left**right - if isinstance(node.op, ast.LShift): - return left << right - if isinstance(node.op, ast.RShift): - return left >> right - if isinstance(node.op, ast.BitOr): - return left | right - if isinstance(node.op, ast.BitXor): - return left ^ right - if isinstance(node.op, ast.BitAnd): - return left & right - if isinstance(node.op, ast.MatMult): - return left @ right + dunders = _find_dunder(node.op, BINARY_OP_DUNDERS) + if dunders: + if policy.can_operate(dunders, left, right): + return getattr(left, dunders[0])(right) + else: + raise GuardRejection( + f"Operation (`{dunders}`) for", + type(left), + f"not allowed in {context.evaluation} mode", + ) + if isinstance(node, ast.Compare): + left = eval_node(node.left, context) + all_true = True + negate = False + for op, right in zip(node.ops, node.comparators): + right = eval_node(right, context) + dunder = None + dunders = _find_dunder(op, COMP_OP_DUNDERS) + if not dunders: + if isinstance(op, ast.NotIn): + dunders = COMP_OP_DUNDERS[ast.In] + negate = True + if isinstance(op, ast.Is): + dunder = "is_" + if isinstance(op, ast.IsNot): + dunder = "is_" + negate = True + if not dunder and dunders: + dunder = dunders[0] + if dunder: + a, b = (right, left) if dunder == "__contains__" else (left, right) + if dunder == "is_" or dunders and policy.can_operate(dunders, a, b): + result = getattr(operator, dunder)(a, b) + if negate: + result = not result + if not result: + all_true = False + left = right + else: + raise GuardRejection( + f"Comparison (`{dunder}`) for", + type(left), + f"not allowed in {context.evaluation} mode", + ) + else: + raise ValueError(f"Comparison `{dunder}` not supported") + return all_true if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.Index): @@ -379,16 +479,17 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): if isinstance(node, ast.ExtSlice): return tuple([eval_node(dim, context) for dim in node.dims]) if isinstance(node, ast.UnaryOp): - # TODO: add guards value = eval_node(node.operand, context) - if isinstance(node.op, ast.USub): - return -value - if isinstance(node.op, ast.UAdd): - return +value - if isinstance(node.op, ast.Invert): - return ~value - if isinstance(node.op, ast.Not): - return not value + dunders = _find_dunder(node.op, UNARY_OP_DUNDERS) + if dunders: + if policy.can_operate(dunders, value): + return getattr(value, dunders[0])() + else: + raise GuardRejection( + f"Operation (`{dunders}`) for", + type(value), + f"not allowed in {context.evaluation} mode", + ) raise ValueError("Unhandled unary operation:", node.op) if isinstance(node, ast.Subscript): value = eval_node(node.value, context) @@ -527,6 +628,9 @@ def _list_methods(cls, source=None): method_descriptor, } + +BUILTIN_OPERATIONS = {int, float, complex, *BUILTIN_GETATTR} + EVALUATION_POLICIES = { "minimal": EvaluationPolicy( allow_builtins_access=True, @@ -536,6 +640,7 @@ def _list_methods(cls, source=None): allow_attr_access=False, allowed_calls=set(), allow_any_calls=False, + allow_all_operations=False, ), "limited": SelectivePolicy( # TODO: @@ -548,6 +653,7 @@ def _list_methods(cls, source=None): ("pandas", "DataFrame"), ("pandas", "Series"), }, + allowed_operations=BUILTIN_OPERATIONS, allow_builtins_access=True, allow_locals_access=True, allow_globals_access=True, @@ -560,6 +666,7 @@ def _list_methods(cls, source=None): allow_attr_access=True, allow_item_access=True, allow_any_calls=True, + allow_all_operations=True, ), } diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 9c98b7a8e2a..94f58298665 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -199,6 +199,76 @@ def test_literals(code, expected): assert guarded_eval(code, context) == expected +@pytest.mark.parametrize( + "code,expected", + [ + ["-5", -5], + ["+5", +5], + ["~5", -6], + ], +) +def test_unary_operations(code, expected): + context = limited() + assert guarded_eval(code, context) == expected + + +@pytest.mark.parametrize( + "code,expected", + [ + ["1 + 1", 2], + ["3 - 1", 2], + ["2 * 3", 6], + ["5 // 2", 2], + ["5 / 2", 2.5], + ["5**2", 25], + ["2 >> 1", 1], + ["2 << 1", 4], + ["1 | 2", 3], + ["1 & 1", 1], + ["1 & 2", 0], + ], +) +def test_binary_operations(code, expected): + context = limited() + assert guarded_eval(code, context) == expected + + +@pytest.mark.parametrize( + "code,expected", + [ + ["2 > 1", True], + ["2 < 1", False], + ["2 <= 1", False], + ["2 <= 2", True], + ["1 >= 2", False], + ["2 >= 2", True], + ["2 == 2", True], + ["1 == 2", False], + ["1 != 2", True], + ["1 != 1", False], + ["1 < 4 < 3", False], + ["(1 < 4) < 3", True], + ["4 > 3 > 2 > 1", True], + ["4 > 3 > 2 > 9", False], + ["1 < 2 < 3 < 4", True], + ["9 < 2 < 3 < 4", False], + ["1 < 2 > 1 > 0 > -1 < 1", True], + ["1 in [1] in [[1]]", True], + ["1 in [1] in [[2]]", False], + ["1 in [1]", True], + ["0 in [1]", False], + ["1 not in [1]", False], + ["0 not in [1]", True], + ["True is True", True], + ["False is False", True], + ["True is False", False], + ], +) +def test_comparisons(code, expected): + context = limited() + assert guarded_eval(code, context) == expected + + def test_access_builtins(): context = limited() assert guarded_eval("round", context) == round From 6250931515354a158dbda57ea082d65aec8a308b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 00:25:52 +0000 Subject: [PATCH 0314/1752] Increase coverage of `guard_eval` --- IPython/core/completer.py | 3 +- IPython/core/guarded_eval.py | 49 ++++-- IPython/core/tests/test_guarded_eval.py | 201 +++++++++++++++++++++--- 3 files changed, 215 insertions(+), 38 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 7dd585bce36..f2853d3c48a 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -996,7 +996,8 @@ class Completer(Configurable): - ``forbidden``: no evaluation of code is permitted, - ``minimal``: evaluation of literals and access to built-in namespace; - no item/attribute evaluation nor access to locals/globals, + no item/attribute evaluationm no access to locals/globals, + no evaluation of any operations or comparisons. - ``limited``: access to all namespaces, evaluation of hard-coded methods (for example: :any:`dict.keys`, :any:`object.__getattr__`, :any:`object.__getitem__`) on allow-listed objects (for example: diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index 215a36fab05..f71a73bea58 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -108,6 +108,7 @@ def can_call(self, func): return True owner_method = _unbind_method(func) + if owner_method and owner_method in self.allowed_calls: return True @@ -127,6 +128,10 @@ def _has_original_dunder_external( value_type = type(value) if type(value) == member_type: return True + if method_name == "__getattribute__": + # we have to short-circuit here due to an unresolved issue in + # `isinstance` implementation: https://bugs.python.org/issue32683 + return False if isinstance(value, member_type): method = getattr(value_type, method_name, None) member_method = getattr(member_type, method_name, None) @@ -149,7 +154,7 @@ def _has_original_dunder( method = getattr(value_type, method_name, None) - if not method: + if method is None: return None if method in allowed_methods: @@ -193,6 +198,7 @@ def can_get_attr(self, value, attr): allowed_external=self.allowed_getattr_external, method_name="__getattr__", ) + # Many objects do not have `__getattr__`, this is fine if has_original_attr is None and has_original_attribute: return True @@ -200,10 +206,6 @@ def can_get_attr(self, value, attr): # Accept objects without modifications to `__getattr__` and `__getattribute__` return has_original_attr and has_original_attribute - def get_attr(self, value, attr): - if self.can_get_attr(value, attr): - return getattr(value, attr) - def can_get_item(self, value, item): """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" return _has_original_dunder( @@ -215,20 +217,24 @@ def can_get_item(self, value, item): ) def can_operate(self, dunders: Tuple[str, ...], a, b=None): + objects = [a] + if b is not None: + objects.append(b) return all( [ _has_original_dunder( - a, + obj, allowed_types=self.allowed_operations, - allowed_methods=self._dunder_methods(dunder), + allowed_methods=self._operator_dunder_methods(dunder), allowed_external=self.allowed_operations_external, method_name=dunder, ) for dunder in dunders + for obj in objects ] ) - def _dunder_methods(self, dunder: str) -> Set[Callable]: + def _operator_dunder_methods(self, dunder: str) -> Set[Callable]: if dunder not in self._operation_methods_cache: self._operation_methods_cache[dunder] = self._safe_get_methods( self.allowed_operations, dunder @@ -257,7 +263,7 @@ def _safe_get_methods(self, classes, name) -> Set[Callable]: class _DummyNamedTuple(NamedTuple): - pass + """Used internally to retrieve methods of named tuple instance.""" class EvaluationContext(NamedTuple): @@ -451,12 +457,15 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): f"not allowed in {context.evaluation} mode", ) else: - raise ValueError(f"Comparison `{dunder}` not supported") + raise ValueError( + f"Comparison `{dunder}` not supported" + ) # pragma: no cover return all_true if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.Index): - return eval_node(node.value, context) + # deprecated since Python 3.9 + return eval_node(node.value, context) # pragma: no cover if isinstance(node, ast.Tuple): return tuple(eval_node(e, context) for e in node.elts) if isinstance(node, ast.List): @@ -477,7 +486,8 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): eval_node(node.step, context), ) if isinstance(node, ast.ExtSlice): - return tuple([eval_node(dim, context) for dim in node.dims]) + # deprecated since Python 3.9 + return tuple([eval_node(dim, context) for dim in node.dims]) # pragma: no cover if isinstance(node, ast.UnaryOp): value = eval_node(node.operand, context) dunders = _find_dunder(node.op, UNARY_OP_DUNDERS) @@ -490,7 +500,6 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): type(value), f"not allowed in {context.evaluation} mode", ) - raise ValueError("Unhandled unary operation:", node.op) if isinstance(node, ast.Subscript): value = eval_node(node.value, context) slice_ = eval_node(node.slice, context) @@ -507,14 +516,14 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): if policy.allow_globals_access and node.id in context.globals: return context.globals[node.id] if policy.allow_builtins_access and hasattr(builtins, node.id): - # note: do not use __builtins__, it is implementation detail of Python + # note: do not use __builtins__, it is implementation detail of cPython return getattr(builtins, node.id) if not policy.allow_globals_access and not policy.allow_locals_access: raise GuardRejection( f"Namespace access not allowed in {context.evaluation} mode" ) else: - raise NameError(f"{node.id} not found in locals nor globals") + raise NameError(f"{node.id} not found in locals, globals, nor builtins") if isinstance(node, ast.Attribute): value = eval_node(node.value, context) if policy.can_get_attr(value, node.attr): @@ -540,7 +549,7 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): func, # not joined to avoid calling `repr` f"not allowed in {context.evaluation} mode", ) - raise ValueError("Unhandled node", node) + raise ValueError("Unhandled node", ast.dump(node)) SUPPORTED_EXTERNAL_GETITEM = { @@ -552,6 +561,7 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext): ("numpy", "void"), } + BUILTIN_GETITEM: Set[InstancesHaveGetItem] = { dict, str, @@ -583,6 +593,8 @@ def _list_methods(cls, source=None): dict_keys: Type[collections.abc.KeysView] = type({}.keys()) method_descriptor: Any = type(list.copy) +NUMERICS = {int, float, complex} + ALLOWED_CALLS = { bytes, *_list_methods(bytes), @@ -600,6 +612,8 @@ def _list_methods(cls, source=None): *_list_methods(str), tuple, *_list_methods(tuple), + *NUMERICS, + *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)], collections.deque, *_list_methods(collections.deque, list_non_mutating_methods), collections.defaultdict, @@ -624,12 +638,13 @@ def _list_methods(cls, source=None): frozenset, object, type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. + *NUMERICS, dict_keys, method_descriptor, } -BUILTIN_OPERATIONS = {int, float, complex, *BUILTIN_GETATTR} +BUILTIN_OPERATIONS = {*BUILTIN_GETATTR} EVALUATION_POLICIES = { "minimal": EvaluationPolicy( diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 94f58298665..8d3495a3d6e 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -1,4 +1,5 @@ from typing import NamedTuple +from functools import partial from IPython.core.guarded_eval import ( EvaluationContext, GuardRejection, @@ -9,12 +10,19 @@ import pytest -def limited(**kwargs): - return EvaluationContext(locals=kwargs, globals={}, evaluation="limited") +def create_context(evaluation: str, **kwargs): + return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation) -def unsafe(**kwargs): - return EvaluationContext(locals=kwargs, globals={}, evaluation="unsafe") +forbidden = partial(create_context, "forbidden") +minimal = partial(create_context, "minimal") +limited = partial(create_context, "limited") +unsafe = partial(create_context, "unsafe") +dangerous = partial(create_context, "dangerous") + +LIMITED_OR_HIGHER = [limited, unsafe, dangerous] + +MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER] @dec.skip_without("pandas") @@ -142,7 +150,7 @@ def test_set_literal(): assert guarded_eval('{"a"}', context) == {"a"} -def test_if_expression(): +def test_evaluates_if_expression(): context = limited() assert guarded_eval("2 if True else 3", context) == 2 assert guarded_eval("4 if False else 5", context) == 5 @@ -178,7 +186,7 @@ def test_method_descriptor(): [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True], ], ) -def test_calls(data, good, bad, expected): +def test_evaluates_calls(data, good, bad, expected): context = limited(data=data) assert guarded_eval(good, context) == expected @@ -194,9 +202,26 @@ def test_calls(data, good, bad, expected): ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]], ], ) -def test_literals(code, expected): - context = limited() - assert guarded_eval(code, context) == expected +@pytest.mark.parametrize("context", LIMITED_OR_HIGHER) +def test_evaluates_complex_cases(code, expected, context): + assert guarded_eval(code, context()) == expected + + +@pytest.mark.parametrize( + "code,expected", + [ + ["1", 1], + ["1.0", 1.0], + ["0xdeedbeef", 0xDEEDBEEF], + ["True", True], + ["None", None], + ["{}", {}], + ["[]", []], + ], +) +@pytest.mark.parametrize("context", MINIMAL_OR_HIGHER) +def test_evaluates_literals(code, expected, context): + assert guarded_eval(code, context()) == expected @pytest.mark.parametrize( @@ -207,9 +232,9 @@ def test_literals(code, expected): ["~5", -6], ], ) -def test_unary_operations(code, expected): - context = limited() - assert guarded_eval(code, context) == expected +@pytest.mark.parametrize("context", LIMITED_OR_HIGHER) +def test_evaluates_unary_operations(code, expected, context): + assert guarded_eval(code, context()) == expected @pytest.mark.parametrize( @@ -228,9 +253,9 @@ def test_unary_operations(code, expected): ["1 & 2", 0], ], ) -def test_binary_operations(code, expected): - context = limited() - assert guarded_eval(code, context) == expected +@pytest.mark.parametrize("context", LIMITED_OR_HIGHER) +def test_evaluates_binary_operations(code, expected, context): + assert guarded_eval(code, context()) == expected @pytest.mark.parametrize( @@ -262,16 +287,152 @@ def test_binary_operations(code, expected): ["True is True", True], ["False is False", True], ["True is False", False], + ["True is not True", False], + ["False is not True", True], ], ) -def test_comparisons(code, expected): - context = limited() - assert guarded_eval(code, context) == expected +@pytest.mark.parametrize("context", LIMITED_OR_HIGHER) +def test_evaluates_comparisons(code, expected, context): + assert guarded_eval(code, context()) == expected + + +def test_guards_comparisons(): + class GoodEq(int): + pass + + class BadEq(int): + def __eq__(self, other): + assert False + + context = limited(bad=BadEq(1), good=GoodEq(1)) + + with pytest.raises(GuardRejection): + guarded_eval("bad == 1", context) + + with pytest.raises(GuardRejection): + guarded_eval("bad != 1", context) + + with pytest.raises(GuardRejection): + guarded_eval("1 == bad", context) + + with pytest.raises(GuardRejection): + guarded_eval("1 != bad", context) + + assert guarded_eval("good == 1", context) is True + assert guarded_eval("good != 1", context) is False + assert guarded_eval("1 == good", context) is True + assert guarded_eval("1 != good", context) is False + + +def test_guards_unary_operations(): + class GoodOp(int): + pass + + class BadOpInv(int): + def __inv__(self, other): + assert False + + class BadOpInverse(int): + def __inv__(self, other): + assert False + + context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1)) + + with pytest.raises(GuardRejection): + guarded_eval("~bad1", context) + + with pytest.raises(GuardRejection): + guarded_eval("~bad2", context) + + +def test_guards_binary_operations(): + class GoodOp(int): + pass + class BadOp(int): + def __add__(self, other): + assert False -def test_access_builtins(): + context = limited(good=GoodOp(1), bad=BadOp(1)) + + with pytest.raises(GuardRejection): + guarded_eval("1 + bad", context) + + with pytest.raises(GuardRejection): + guarded_eval("bad + 1", context) + + assert guarded_eval("good + 1", context) == 2 + assert guarded_eval("1 + good", context) == 2 + + +def test_guards_attributes(): + class GoodAttr(float): + pass + + class BadAttr1(float): + def __getattr__(self, key): + assert False + + class BadAttr2(float): + def __getattribute__(self, key): + assert False + + context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5)) + + with pytest.raises(GuardRejection): + guarded_eval("bad1.as_integer_ratio", context) + + with pytest.raises(GuardRejection): + guarded_eval("bad2.as_integer_ratio", context) + + assert guarded_eval("good.as_integer_ratio()", context) == (1, 2) + + +@pytest.mark.parametrize("context", MINIMAL_OR_HIGHER) +def test_access_builtins(context): + assert guarded_eval("round", context()) == round + + +def test_access_builtins_fails(): context = limited() - assert guarded_eval("round", context) == round + with pytest.raises(NameError): + guarded_eval("this_is_not_builtin", context) + + +def test_rejects_forbidden(): + context = forbidden() + with pytest.raises(GuardRejection): + guarded_eval("1", context) + + +def test_guards_locals_and_globals(): + context = EvaluationContext( + locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal" + ) + + with pytest.raises(GuardRejection): + guarded_eval("local_a", context) + + with pytest.raises(GuardRejection): + guarded_eval("global_b", context) + + +def test_access_locals_and_globals(): + context = EvaluationContext( + locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited" + ) + assert guarded_eval("local_a", context) == "a" + assert guarded_eval("global_b", context) == "b" + + +@pytest.mark.parametrize( + "code", + ["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"], +) +@pytest.mark.parametrize("context", [minimal(), limited(), unsafe()]) +def test_rejects_side_effect_syntax(code, context): + with pytest.raises(SyntaxError): + guarded_eval(code, context) def test_subscript(): From add04498bd106451679b75e0bc3a782ad5a13ea5 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 01:55:03 +0000 Subject: [PATCH 0315/1752] Fix code cov coverage reporting --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53ccb6f78ed..2f4677fb4d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,4 +77,7 @@ jobs: run: | pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 + with: + name: Test + files: /home/runner/work/ipython/ipython/coverage.xml From a6e74d58a693917677bfa4f35d94523a77cd3e35 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 11:31:50 +0000 Subject: [PATCH 0316/1752] Describe code style checks and working with docs locally --- CONTRIBUTING.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5826baf599c..10bf1efff8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,8 +66,9 @@ Some guidelines on contributing to IPython: If you're making functional changes, you can clean up the specific pieces of code you're working on. -[Travis](http://travis-ci.org/#!/ipython/ipython) does a pretty good job testing -IPython and Pull Requests, but it may make sense to manually perform tests, +[GitHub Actions](https://github.com/ipython/ipython/actions/workflows/test.yml) does +a pretty good job testing IPython and Pull Requests, +but it may make sense to manually perform tests, particularly for PRs that affect `IPython.parallel` or Windows. For more detailed information, see our [GitHub Workflow](https://github.com/ipython/ipython/wiki/Dev:-GitHub-workflow). @@ -88,3 +89,30 @@ Only a single test (for example **test_alias_lifecycle**) within a single file c ```shell pytest IPython/core/tests/test_alias.py::test_alias_lifecycle ``` + +## Code style + +* Before committing run `darker -r 60625f241f298b5039cb2debc365db38aa7bb522 ` to apply selective `black` formatting on modified regions using [darker](https://github.com/akaihola/darker) +* For newly added modules or refactors, please enable static typing analysis with `mypy` for the modified module by adding the file path in [`mypy.yml`](https://github.com/ipython/ipython/blob/main/.github/workflows/mypy.yml) workflow. +* As described in pull requests section, please avoid excessive formatting changes; if formatting-only commit is necessary consider adding its hash to [`.git-blame-ignore-revs`](https://github.com/ipython/ipython/blob/main/.git-blame-ignore-revs) file + +## Documentation + +Sphinx documentation can be built locally using standard sphinx `make` commands. To build HTML documentation from the root of the project, execute: + +```shell +pip install -r docs/requirements.txt # only needed once +make -C docs/ html SPHINXOPTS="-W" +``` + +To force update of the API documentation, precede the `make` command with: + +```shell +python3 docs/autogen_api.py +``` + +Similarly, to force-update the configuration, run: + +```shell +python3 docs/autogen_config.py +``` From 200dc32e519c12aa0bf9bdf6fce27e41c6a02dd0 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 11:32:23 +0000 Subject: [PATCH 0317/1752] Fix a typo in GH actions URL in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0371848061e..b004792e0e9 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ :target: https://pypi.python.org/pypi/ipython .. image:: https://github.com/ipython/ipython/actions/workflows/test.yml/badge.svg - :target: https://github.com/ipython/ipython/actions/workflows/test.yml) + :target: https://github.com/ipython/ipython/actions/workflows/test.yml .. image:: https://www.codetriage.com/ipython/ipython/badges/users.svg :target: https://www.codetriage.com/ipython/ipython/ From 6938ae1204891a9ebb9989f5b266007f1f18d82f Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 12:03:32 +0000 Subject: [PATCH 0318/1752] Remove unused pytest ignores (files have been deleted) --- pytest.ini | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pytest.ini b/pytest.ini index 81511e9ce51..5cc977692b8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -14,18 +14,10 @@ addopts = --durations=10 --ignore=IPython/sphinxext --ignore=IPython/terminal/pt_inputhooks --ignore=IPython/__main__.py - --ignore=IPython/config.py - --ignore=IPython/frontend.py - --ignore=IPython/html.py - --ignore=IPython/nbconvert.py - --ignore=IPython/nbformat.py - --ignore=IPython/parallel.py - --ignore=IPython/qt.py --ignore=IPython/external/qt_for_kernel.py --ignore=IPython/html/widgets/widget_link.py --ignore=IPython/html/widgets/widget_output.py --ignore=IPython/terminal/console.py - --ignore=IPython/terminal/ptshell.py --ignore=IPython/utils/_process_cli.py --ignore=IPython/utils/_process_posix.py --ignore=IPython/utils/_process_win32.py From a96912d4e98ea2da4f0e8d16e9d51714575b2bf2 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:16:04 +0000 Subject: [PATCH 0319/1752] Guard against custom properties --- IPython/core/guarded_eval.py | 65 ++++++++++++++++++++----- IPython/core/tests/test_guarded_eval.py | 49 ++++++++++++++++++- 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index f71a73bea58..9f391e35a69 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -3,6 +3,7 @@ Callable, Dict, Set, + Sequence, Tuple, NamedTuple, Type, @@ -113,18 +114,30 @@ def can_call(self, func): return True +def _get_external(module_name: str, access_path: Sequence[str]): + """Get value from external module given a dotted access path. + + Raises: + * `KeyError` if module is removed not found, and + * `AttributeError` if acess path does not match an exported object + """ + member_type = sys.modules[module_name] + for attr in access_path: + member_type = getattr(member_type, attr) + return member_type + + def _has_original_dunder_external( value, - module_name, - access_path, - method_name, + module_name: str, + access_path: Sequence[str], + method_name: str, ): + if module_name not in sys.modules: + # LBYLB as it is faster + return False try: - if module_name not in sys.modules: - return False - member_type = sys.modules[module_name] - for attr in access_path: - member_type = getattr(member_type, attr) + member_type = _get_external(module_name, access_path) value_type = type(value) if type(value) == member_type: return True @@ -199,12 +212,42 @@ def can_get_attr(self, value, attr): method_name="__getattr__", ) + accept = False + # Many objects do not have `__getattr__`, this is fine if has_original_attr is None and has_original_attribute: - return True + accept = True + else: + # Accept objects without modifications to `__getattr__` and `__getattribute__` + accept = has_original_attr and has_original_attribute + + if accept: + # We still need to check for overriden properties. - # Accept objects without modifications to `__getattr__` and `__getattribute__` - return has_original_attr and has_original_attribute + value_class = type(value) + if not hasattr(value_class, attr): + return True + + class_attr_val = getattr(value_class, attr) + is_property = isinstance(class_attr_val, property) + + if not is_property: + return True + + # Properties in allowed types are ok + if type(value) in self.allowed_getattr: + return True + + # Properties in subclasses of allowed types may be ok if not changed + for module_name, *access_path in self.allowed_getattr_external: + try: + external_class = _get_external(module_name, access_path) + external_class_attr_val = getattr(external_class, attr) + except (KeyError, AttributeError): + return False # pragma: no cover + return class_attr_val == external_class_attr_val + + return False def can_get_item(self, value, item): """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 8d3495a3d6e..1ee93ffe744 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from typing import NamedTuple from functools import partial from IPython.core.guarded_eval import ( @@ -25,6 +26,21 @@ def create_context(evaluation: str, **kwargs): MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER] +@contextmanager +def module_not_installed(module: str): + import sys + + try: + to_restore = sys.modules[module] + del sys.modules[module] + except KeyError: + to_restore = None + try: + yield + finally: + sys.modules[module] = to_restore + + @dec.skip_without("pandas") def test_pandas_series_iloc(): import pandas as pd @@ -34,6 +50,32 @@ def test_pandas_series_iloc(): assert guarded_eval("data.iloc[0]", context) == 1 +def test_rejects_custom_properties(): + class BadProperty: + @property + def iloc(self): + return [None] + + series = BadProperty() + context = limited(data=series) + + with pytest.raises(GuardRejection): + guarded_eval("data.iloc[0]", context) + + +@dec.skip_without("pandas") +def test_accepts_non_overriden_properties(): + import pandas as pd + + class GoodProperty(pd.Series): + pass + + series = GoodProperty([1], index=["a"]) + context = limited(data=series) + + assert guarded_eval("data.iloc[0]", context) == 1 + + @dec.skip_without("pandas") def test_pandas_series(): import pandas as pd @@ -472,9 +514,12 @@ def __getitem__(self, k): def __getattr__(self, k): return "a" + def f(self): + return "b" + t = T() - t.__getitem__ = lambda f: "b" - t.__getattr__ = lambda f: "b" + t.__getitem__ = f + t.__getattr__ = f assert t[1] == "a" assert t[1] == "a" From 52cda652f1a5c71f2117bf84f33f0cc99e59e7da Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:16:38 +0000 Subject: [PATCH 0320/1752] Increase coverage for completer tests --- IPython/core/tests/test_completer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 423979a297f..5e8cb35bc33 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -886,6 +886,12 @@ def match(*args, **kwargs): assert match(keys, "2") == ("", 0, ["21", "22"]) assert match(keys, "0b101") == ("", 0, ["0b10101", "0b10110"]) + # Should yield on variables + assert match(keys, "a_variable") == ("", 0, []) + + # Should pass over invalid literals + assert match(keys, "'' ''") == ("", 0, []) + def test_match_dict_keys_tuple(self): """ Test that match_dict_keys called with extra prefix works on a couple of use case, @@ -1687,6 +1693,9 @@ def _(expected): ["0b_0011_1111_0100_1110", "0b_0011_1111_0100_1110"], ["0xdeadbeef", "0xdeadbeef"], ["0b_1110_0101", "0b_1110_0101"], + # should not match if in an operation + ["1 + 1", None], + [", 1 + 1", None], ], ) def test_match_numeric_literal_for_dict_key(input, expected): From 7f95861a8736657bd78e36c14157c3962e65421c Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:17:33 +0000 Subject: [PATCH 0321/1752] Remove outdated TODO comment --- IPython/core/guarded_eval.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index 9f391e35a69..c2d88d0473a 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -701,8 +701,6 @@ def _list_methods(cls, source=None): allow_all_operations=False, ), "limited": SelectivePolicy( - # TODO: - # - should reject binary and unary operations if custom methods would be dispatched allowed_getitem=BUILTIN_GETITEM, allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM, allowed_getattr=BUILTIN_GETATTR, From abe32e8d2346f2badccfef6f84f7520365ece346 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:58:39 +0000 Subject: [PATCH 0322/1752] Add more tests --- IPython/core/guarded_eval.py | 10 ++++--- IPython/core/tests/test_guarded_eval.py | 35 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index c2d88d0473a..d60a5c5f1e3 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -60,7 +60,8 @@ class DoesNotHaveGetAttr(Protocol): def _unbind_method(func: Callable) -> Union[Callable, None]: """Get unbound method for given bound method. - Returns None if cannot get unbound method.""" + Returns None if cannot get unbound method, or method is already unbound. + """ owner = getattr(func, "__self__", None) owner_class = type(owner) name = getattr(func, "__name__", None) @@ -214,7 +215,7 @@ def can_get_attr(self, value, attr): accept = False - # Many objects do not have `__getattr__`, this is fine + # Many objects do not have `__getattr__`, this is fine. if has_original_attr is None and has_original_attribute: accept = True else: @@ -234,9 +235,10 @@ def can_get_attr(self, value, attr): if not is_property: return True - # Properties in allowed types are ok + # Properties in allowed types are ok (although we do not include any + # properties in our default allow list currently). if type(value) in self.allowed_getattr: - return True + return True # pragma: no cover # Properties in subclasses of allowed types may be ok if not changed for module_name, *access_path in self.allowed_getattr_external: diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 1ee93ffe744..905cf3ab8e3 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -22,7 +22,6 @@ def create_context(evaluation: str, **kwargs): dangerous = partial(create_context, "dangerous") LIMITED_OR_HIGHER = [limited, unsafe, dangerous] - MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER] @@ -41,6 +40,39 @@ def module_not_installed(module: str): sys.modules[module] = to_restore +def test_external_not_installed(): + """ + Because attribute check requires checking if object is not of allowed + external type, this tests logic for absence of external module. + """ + + class Custom: + def __init__(self): + self.test = 1 + + def __getattr__(self, key): + return key + + with module_not_installed("pandas"): + context = limited(x=Custom()) + with pytest.raises(GuardRejection): + guarded_eval("x.test", context) + + +@dec.skip_without("pandas") +def test_external_changed_api(monkeypatch): + """Check that the execution rejects if external API changed paths""" + import pandas as pd + + series = pd.Series([1], index=["a"]) + + with monkeypatch.context() as m: + m.delattr(pd, "Series") + context = limited(data=series) + with pytest.raises(GuardRejection): + guarded_eval("data.iloc[0]", context) + + @dec.skip_without("pandas") def test_pandas_series_iloc(): import pandas as pd @@ -496,6 +528,7 @@ def index(self, k): x = X() assert _unbind_method(x.index) is X.index assert _unbind_method([].index) is list.index + assert _unbind_method(list.index) is None def test_assumption_instance_attr_do_not_matter(): From d8bb14b60cce68058769dec4bb2a9e99b33d7150 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 5 Dec 2022 12:41:48 -0800 Subject: [PATCH 0323/1752] Call `lower` only once --- IPython/extensions/autoreload.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 13cb785fb7e..c8ea7e78e88 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -573,17 +573,18 @@ def autoreload(self, parameter_s=""): autoreloaded. """ - if parameter_s == "" or parameter_s.lower() == "now": + parameter_s_lower = parameter_s.lower() + if parameter_s == "" or parameter_s_lower == "now": self._reloader.check(True) - elif parameter_s == "0" or parameter_s.lower() == "off": + elif parameter_s == "0" or parameter_s_lower == "off": self._reloader.enabled = False - elif parameter_s == "1" or parameter_s.lower() == "explicit": + elif parameter_s == "1" or parameter_s_lower == "explicit": self._reloader.check_all = False self._reloader.enabled = True - elif parameter_s == "2" or parameter_s.lower() == "all": + elif parameter_s == "2" or parameter_s_lower == "all": self._reloader.check_all = True self._reloader.enabled = True - elif parameter_s == "3" or parameter_s.lower() == "complete": + elif parameter_s == "3" or parameter_s_lower == "complete": self._reloader.check_all = True self._reloader.enabled = True self._reloader.autoload_obj = True From 809ebdf7fe6b1a27de4f9a4417441b6814468666 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 5 Dec 2022 13:37:08 -0800 Subject: [PATCH 0324/1752] Replace `%averbose` with args to `%autoreload` --- IPython/extensions/autoreload.py | 105 ++++++++++-------- IPython/extensions/tests/test_autoreload.py | 42 ++++--- .../whatsnew/pr/autoreload-verbosity.rst | 8 +- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index c8ea7e78e88..106c452c7dc 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -53,6 +53,10 @@ Same as 2/all, but also adds any new objects in the module. See unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects + Adding ``--print`` or ``-p`` to the ``%autoreload`` line will print autoreload activity to + standard out. ``--log`` or ``-l`` will do it to the log at INFO level; both can be used + simultaneously. + ``%aimport`` List modules which are to be automatically imported or not to be imported. @@ -69,18 +73,6 @@ Mark module 'foo' to not be autoreloaded. -``%averbose off`` - - Perform autoreload tasks quietly - -``%averbose on`` - - Report activity with `print` statements. - -``%averbose log`` - - Report activity with the logger. - Caveats ======= @@ -113,6 +105,7 @@ - Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not. """ +from IPython.core import magic_arguments from IPython.core.magic import Magics, magics_class, line_magic __skip_doctest__ = True @@ -525,7 +518,28 @@ def __init__(self, *a, **kw): self.loaded_modules = set(sys.modules) @line_magic - def autoreload(self, parameter_s=""): + @magic_arguments.magic_arguments() + @magic_arguments.argument('mode', type=str, default='now', nargs='?', + help=""" + blank or 'now' - Reload all modules (except those excluded by + %%aimport) automaticallynow. + + '0' or 'off' - Disable automatic reloading. + + '1' or 'explicit' - Reload only modules imported with %%aimport every + time before executing the Python code typed. + + '2' or 'all' - Reload all modules (except those excluded by %%aimport) + every time before executing the Python code typed. + + '3' or 'complete' - Same as 2/all, but also but also adds any new + objects in the module. + """) + @magic_arguments.argument('-p', '--print', action='store_true', default=False, + help='Show autoreload activity using `print` statements') + @magic_arguments.argument('-l', '--log', action='store_true', default=False, + help='Show autoreload activity using the logger') + def autoreload(self, line=""): r"""%autoreload => Reload modules automatically %autoreload or %autoreload now @@ -547,6 +561,10 @@ def autoreload(self, parameter_s=""): Same as 2/all, but also but also adds any new objects in the module. See unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects + The optional arguments --print and --log control display of autoreload activity. The default + is to act silently; --print (or -p) will print out the names of modules that are being + reloaded, and --log (or -l) outputs them to the log at INFO level. + Reloading Python modules in a reliable way is in general difficult, and unexpected things may occur. %autoreload tries to work around common pitfalls by replacing function code objects and @@ -573,23 +591,45 @@ def autoreload(self, parameter_s=""): autoreloaded. """ - parameter_s_lower = parameter_s.lower() - if parameter_s == "" or parameter_s_lower == "now": + args = magic_arguments.parse_argstring(self.autoreload, line) + mode = args.mode.lower() + + def p(msg): + print(msg) + + def l(msg): + logging.getLogger("autoreload").info(msg) + + def pl(msg): + p(msg) + l(msg) + + if args.print is False and args.log is False: + self._reloader._report = lambda msg: None + elif args.print is True: + if args.log is True: + self._reloader._report = lambda msg: pl(msg) + else: + self._reloader._report = lambda msg: p(msg) + elif args.log is True: + self._reloader._report = lambda msg: l(msg) + + if mode == "" or mode == "now": self._reloader.check(True) - elif parameter_s == "0" or parameter_s_lower == "off": + elif mode == "0" or mode == "off": self._reloader.enabled = False - elif parameter_s == "1" or parameter_s_lower == "explicit": + elif mode == "1" or mode == "explicit": self._reloader.check_all = False self._reloader.enabled = True - elif parameter_s == "2" or parameter_s_lower == "all": + elif mode == "2" or mode == "all": self._reloader.check_all = True self._reloader.enabled = True - elif parameter_s == "3" or parameter_s_lower == "complete": + elif mode == "3" or mode == "complete": self._reloader.check_all = True self._reloader.enabled = True self._reloader.autoload_obj = True else: - raise ValueError(f'Unrecognized parameter "{parameter_s}".') + raise ValueError(f'Unrecognized autoreload mode "{mode}".') @line_magic def aimport(self, parameter_s="", stream=None): @@ -630,31 +670,6 @@ def aimport(self, parameter_s="", stream=None): # Inject module to user namespace self.shell.push({top_name: top_module}) - @line_magic - def averbose(self, parameter_s=""): - r"""%averbose => Turn verbosity on/off for autoreloading. - - %averbose 0 or %averbose off - Turn off any reporting during autoreload. - - %averbose 1 or %averbose on - Report autoreload activity via print statements. - - %averbose 2 or %averbose log - Report autoreload activity via logging. - """ - - if parameter_s == "0" or parameter_s.lower() == "off": - self._reloader._report = lambda msg: None - elif parameter_s == "1" or parameter_s.lower() == "on": - self._reloader._report = lambda msg: print(msg) - elif parameter_s == "2" or parameter_s.lower() == "log": - self._reloader._report = lambda msg: logging.getLogger("autoreload").info( - msg - ) - else: - raise ValueError(f'Unrecognized parameter "{parameter_s}".') - def pre_run_cell(self): if self._reloader.enabled: try: diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 1523867593e..8a6dae51ee5 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -71,9 +71,6 @@ def magic_aimport(self, parameter, stream=None): self.auto_magics.aimport(parameter, stream=stream) self.auto_magics.post_execute_hook() - def magic_averbose(self, parameter): - self.auto_magics.averbose(parameter) - class Fixture(TestCase): """Fixture for creating test module files""" @@ -442,41 +439,40 @@ def test_aimport_parsing(self): assert module_reloader.skip_modules["os"] is True assert "os" not in module_reloader.modules.keys() - def test_averbose(self): - self.shell.magic_averbose("off") + def test_autoreload_output(self): self.shell.magic_autoreload("complete") mod_code = """ def func1(): pass """ mod_name, mod_fn = self.new_module(mod_code) self.shell.run_code(f"import {mod_name}") - with tt.AssertPrints("", channel="stdout"): # no output. + with tt.AssertPrints("", channel="stdout"): # no output; this is default self.shell.run_code("pass") + self.shell.magic_autoreload("complete --print") self.write_file(mod_fn, mod_code) # "modify" the module - self.shell.magic_averbose("on") # Should now see a print statement. - with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): + with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out self.shell.run_code("pass") + self.shell.magic_autoreload("complete -p") self.write_file(mod_fn, mod_code) # "modify" the module - self.shell.magic_averbose("off") # Should not see anything on next call - with tt.AssertPrints("", channel="stdout"): + with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out self.shell.run_code("pass") - # TODO: test logging. Why won't this work? - # with tt.AssertPrints('LOGGER: '): - # self.shell.run_code("import logging; logging.basicConfig(format='LOGGER: %(message)s');" - # "logger = logging.getLogger(); logger.setLevel(logging.DEBUG);" - # "logger.info('test')") - - # self.shell.magic_averbose("log") # Should see it formatted as per our logging config - # self.write_file(mod_fn, mod_code) # "modify" the module - # with tt.AssertPrints(f"LOGGER: Reloading '{mod_name}'.", channel="stdout"): - # self.shell.run_code("pass") + self.shell.magic_autoreload("complete --print --log") + self.write_file(mod_fn, mod_code) # "modify" the module + with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out + self.shell.run_code("pass") - # And an invalid mode name raises an exception. - with self.assertRaises(ValueError): - self.shell.magic_averbose("fax") + # TODO: test logging, i.e. --log. Why won't this work? + # with tt.AssertPrints("LOGGER: test", channel="stdout"): + # # logger.info('test') + # self.shell.run_code("import logging; import sys;" + # "logging.basicConfig(format='LOGGER: %(message)s'," + # " stream=sys.stdout," + # " level=logging.DEBUG);" + # "logging.getLogger().info('test');" + # ) def _check_smoketest(self, use_aimport=True): """ diff --git a/docs/source/whatsnew/pr/autoreload-verbosity.rst b/docs/source/whatsnew/pr/autoreload-verbosity.rst index 677c07a26d6..155f890915a 100644 --- a/docs/source/whatsnew/pr/autoreload-verbosity.rst +++ b/docs/source/whatsnew/pr/autoreload-verbosity.rst @@ -10,12 +10,12 @@ We introduce more descriptive names for the `%autoreload` parameter: The original designations (e.g. "2") still work, and these new ones are case-insensitive. +Additionally, the option `--print` or `-p` can be added to the line to print the names of modules +being reloaded. Similarly, `--log` or `-l` will output the names to the logger at INFO level. Both +can be used simultaneously. + The parsing logic for `%aimport` is now improved such that modules can be whitelisted and blacklisted in the same line, e.g. it's now possible to call `%aimport os, -math` to include `os` for `%autoreload explicit` and exclude `math` for modes 2 and 3. -A new magic command `%averbose` controls printing of the names of modules about to be autoreloaded. -- `%averbose off` / `%averbose 0` - turns off all output (default behavior) -- `%averbose on` / `%averbose 1` - uses `print` to display module name -- `%averbose log` / `%averbose 2` - logs an `INFO` message with the module name From 97ae5668fedcfc038289330a0e60e70b2a539792 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 5 Dec 2022 13:52:13 -0800 Subject: [PATCH 0325/1752] Add logging tests --- IPython/extensions/tests/test_autoreload.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 8a6dae51ee5..483e72c07f3 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -464,15 +464,17 @@ def func1(): pass with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out self.shell.run_code("pass") - # TODO: test logging, i.e. --log. Why won't this work? - # with tt.AssertPrints("LOGGER: test", channel="stdout"): - # # logger.info('test') - # self.shell.run_code("import logging; import sys;" - # "logging.basicConfig(format='LOGGER: %(message)s'," - # " stream=sys.stdout," - # " level=logging.DEBUG);" - # "logging.getLogger().info('test');" - # ) + self.shell.magic_autoreload("complete --print --log") + self.write_file(mod_fn, mod_code) # "modify" the module + with self.assertLogs(logger='autoreload') as lo: # see something printed out + self.shell.run_code("pass") + assert lo.output==[f"INFO:autoreload:Reloading '{mod_name}'."] + + self.shell.magic_autoreload("complete -l") + self.write_file(mod_fn, mod_code) # "modify" the module + with self.assertLogs(logger='autoreload') as lo: # see something printed out + self.shell.run_code("pass") + assert lo.output==[f"INFO:autoreload:Reloading '{mod_name}'."] def _check_smoketest(self, use_aimport=True): """ From dff91e3bc358d09f00578c7c27f4b27eccc3a5c1 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 5 Dec 2022 14:10:17 -0800 Subject: [PATCH 0326/1752] Format with `black` --- IPython/extensions/autoreload.py | 58 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 106c452c7dc..9dc95b4f27f 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -519,26 +519,40 @@ def __init__(self, *a, **kw): @line_magic @magic_arguments.magic_arguments() - @magic_arguments.argument('mode', type=str, default='now', nargs='?', - help=""" - blank or 'now' - Reload all modules (except those excluded by - %%aimport) automaticallynow. - - '0' or 'off' - Disable automatic reloading. - - '1' or 'explicit' - Reload only modules imported with %%aimport every - time before executing the Python code typed. - - '2' or 'all' - Reload all modules (except those excluded by %%aimport) - every time before executing the Python code typed. - - '3' or 'complete' - Same as 2/all, but also but also adds any new - objects in the module. - """) - @magic_arguments.argument('-p', '--print', action='store_true', default=False, - help='Show autoreload activity using `print` statements') - @magic_arguments.argument('-l', '--log', action='store_true', default=False, - help='Show autoreload activity using the logger') + @magic_arguments.argument( + "mode", + type=str, + default="now", + nargs="?", + help="""blank or 'now' - Reload all modules (except those excluded by %%aimport) + automatically now. + + '0' or 'off' - Disable automatic reloading. + + '1' or 'explicit' - Reload only modules imported with %%aimport every + time before executing the Python code typed. + + '2' or 'all' - Reload all modules (except those excluded by %%aimport) + every time before executing the Python code typed. + + '3' or 'complete' - Same as 2/all, but also but also adds any new + objects in the module. + """, + ) + @magic_arguments.argument( + "-p", + "--print", + action="store_true", + default=False, + help="Show autoreload activity using `print` statements", + ) + @magic_arguments.argument( + "-l", + "--log", + action="store_true", + default=False, + help="Show autoreload activity using the logger", + ) def autoreload(self, line=""): r"""%autoreload => Reload modules automatically @@ -593,10 +607,10 @@ def autoreload(self, line=""): """ args = magic_arguments.parse_argstring(self.autoreload, line) mode = args.mode.lower() - + def p(msg): print(msg) - + def l(msg): logging.getLogger("autoreload").info(msg) From 39b2effaa6f50b555e2846f69b70d67c4d483ffa Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 5 Dec 2022 15:29:39 -0800 Subject: [PATCH 0327/1752] Reformat test with `black` --- IPython/extensions/tests/test_autoreload.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 483e72c07f3..d87f6dba0bf 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -71,6 +71,7 @@ def magic_aimport(self, parameter, stream=None): self.auto_magics.aimport(parameter, stream=stream) self.auto_magics.post_execute_hook() + class Fixture(TestCase): """Fixture for creating test module files""" @@ -451,30 +452,36 @@ def func1(): pass self.shell.magic_autoreload("complete --print") self.write_file(mod_fn, mod_code) # "modify" the module - with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out + with tt.AssertPrints( + f"Reloading '{mod_name}'.", channel="stdout" + ): # see something printed out self.shell.run_code("pass") self.shell.magic_autoreload("complete -p") self.write_file(mod_fn, mod_code) # "modify" the module - with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out + with tt.AssertPrints( + f"Reloading '{mod_name}'.", channel="stdout" + ): # see something printed out self.shell.run_code("pass") self.shell.magic_autoreload("complete --print --log") self.write_file(mod_fn, mod_code) # "modify" the module - with tt.AssertPrints(f"Reloading '{mod_name}'.", channel="stdout"): # see something printed out + with tt.AssertPrints( + f"Reloading '{mod_name}'.", channel="stdout" + ): # see something printed out self.shell.run_code("pass") self.shell.magic_autoreload("complete --print --log") self.write_file(mod_fn, mod_code) # "modify" the module - with self.assertLogs(logger='autoreload') as lo: # see something printed out + with self.assertLogs(logger="autoreload") as lo: # see something printed out self.shell.run_code("pass") - assert lo.output==[f"INFO:autoreload:Reloading '{mod_name}'."] + assert lo.output == [f"INFO:autoreload:Reloading '{mod_name}'."] self.shell.magic_autoreload("complete -l") self.write_file(mod_fn, mod_code) # "modify" the module - with self.assertLogs(logger='autoreload') as lo: # see something printed out + with self.assertLogs(logger="autoreload") as lo: # see something printed out self.shell.run_code("pass") - assert lo.output==[f"INFO:autoreload:Reloading '{mod_name}'."] + assert lo.output == [f"INFO:autoreload:Reloading '{mod_name}'."] def _check_smoketest(self, use_aimport=True): """ From 9632124e4e561e99d96304a672854be0d4cb6e16 Mon Sep 17 00:00:00 2001 From: Audrey Dutcher Date: Wed, 7 Dec 2022 10:22:54 -0700 Subject: [PATCH 0328/1752] Add py.typed to setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 226506f08f0..74bbd95193b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -100,6 +100,7 @@ exclude = setupext [options.package_data] +IPython = py.typed IPython.core = profile/README* IPython.core.tests = *.png, *.jpg, daft_extension/*.py IPython.lib.tests = *.wav From 86e1bdf022a2b181a690d77e97a78f20bd8ecab9 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 11:07:08 -0700 Subject: [PATCH 0329/1752] Refactor Inspector._get_info to make it easier to subclass and modify behavior. This factors out the logic to append an info field to the inspect_reply mimebundle and the logic for what information is in the mimebundle into separate functions that can easily be overridden by an inspector subclass. This allows a subclass to easily: * format information into yet another mimetype besides text/plain and text/html * modify or add to the default information without having to copy the default implementation. --- IPython/core/oinspect.py | 143 +++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index f1c454b2604..13d6268256d 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -552,56 +552,46 @@ def _mime_format(self, text:str, formatter=None) -> dict: def format_mime(self, bundle): - + """Format a mimebundle being created by _make_info_unformatted into a real mimebundle""" + # First, format the field names and values for the text/plain field text_plain = bundle['text/plain'] + if isinstance(text_plain, (list, tuple)): + text = '' + heads, bodies = list(zip(*text_plain)) + _len = max(len(h) for h in heads) - text = '' - heads, bodies = list(zip(*text_plain)) - _len = max(len(h) for h in heads) + for head, body in zip(heads, bodies): + body = body.strip('\n') + delim = '\n' if '\n' in body else ' ' + text += self.__head(head+':') + (_len - len(head))*' ' +delim + body +'\n' - for head, body in zip(heads, bodies): - body = body.strip('\n') - delim = '\n' if '\n' in body else ' ' - text += self.__head(head+':') + (_len - len(head))*' ' +delim + body +'\n' + bundle['text/plain'] = text - bundle['text/plain'] = text + # Next format the text/html value by joining strings if it is a list of strings + if isinstance(bundle['text/html'], (list, tuple)): + bundle['text/html'] = '\n'.join(bundle['text/html']) return bundle - def _get_info( - self, obj, oname="", formatter=None, info=None, detail_level=0, omit_sections=() - ): - """Retrieve an info dict and format it. - - Parameters - ---------- - obj : any - Object to inspect and return info from - oname : str (default: ''): - Name of the variable pointing to `obj`. - formatter : callable - info - already computed information - detail_level : integer - Granularity of detail level, if set to 1, give more information. - omit_sections : container[str] - Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`) - """ - - info = self.info(obj, oname=oname, info=info, detail_level=detail_level) - - _mime = { + def _append_info_field(self, bundle, title:str, key:str, info, omit_sections, formatter): + """Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted""" + if title in omit_sections or key in omit_sections: + return + field = info[key] + if field is not None: + formatted_field = self._mime_format(field, formatter) + bundle['text/plain'].append((title, formatted_field['text/plain'])) + bundle['text/html'] += '

' + title + '

\n' + formatted_field['text/html'] + + def _make_info_unformatted(self, info, formatter, detail_level, omit_sections): + """Assemble the mimebundle as unformatted lists of information""" + bundle = { 'text/plain': [], - 'text/html': '', + 'text/html': [], } + # A convenience function to simplify calls below def append_field(bundle, title:str, key:str, formatter=None): - if title in omit_sections or key in omit_sections: - return - field = info[key] - if field is not None: - formatted_field = self._mime_format(field, formatter) - bundle['text/plain'].append((title, formatted_field['text/plain'])) - bundle['text/html'] += '

' + title + '

\n' + formatted_field['text/html'] + '\n' + self._append_info_field(bundle, title=title, key=key, info=info, omit_sections=omit_sections, formatter=formatter) def code_formatter(text): return { @@ -610,56 +600,79 @@ def code_formatter(text): } if info['isalias']: - append_field(_mime, 'Repr', 'string_form') + append_field(bundle, 'Repr', 'string_form') elif info['ismagic']: if detail_level > 0: - append_field(_mime, 'Source', 'source', code_formatter) + append_field(bundle, 'Source', 'source', code_formatter) else: - append_field(_mime, 'Docstring', 'docstring', formatter) - append_field(_mime, 'File', 'file') + append_field(bundle, 'Docstring', 'docstring', formatter) + append_field(bundle, 'File', 'file') elif info['isclass'] or is_simple_callable(obj): # Functions, methods, classes - append_field(_mime, 'Signature', 'definition', code_formatter) - append_field(_mime, 'Init signature', 'init_definition', code_formatter) - append_field(_mime, 'Docstring', 'docstring', formatter) + append_field(bundle, 'Signature', 'definition', code_formatter) + append_field(bundle, 'Init signature', 'init_definition', code_formatter) + append_field(bundle, 'Docstring', 'docstring', formatter) if detail_level > 0 and info['source']: - append_field(_mime, 'Source', 'source', code_formatter) + append_field(bundle, 'Source', 'source', code_formatter) else: - append_field(_mime, 'Init docstring', 'init_docstring', formatter) + append_field(bundle, 'Init docstring', 'init_docstring', formatter) - append_field(_mime, 'File', 'file') - append_field(_mime, 'Type', 'type_name') - append_field(_mime, 'Subclasses', 'subclasses') + append_field(bundle, 'File', 'file') + append_field(bundle, 'Type', 'type_name') + append_field(bundle, 'Subclasses', 'subclasses') else: # General Python objects - append_field(_mime, 'Signature', 'definition', code_formatter) - append_field(_mime, 'Call signature', 'call_def', code_formatter) - append_field(_mime, 'Type', 'type_name') - append_field(_mime, 'String form', 'string_form') + append_field(bundle, 'Signature', 'definition', code_formatter) + append_field(bundle, 'Call signature', 'call_def', code_formatter) + append_field(bundle, 'Type', 'type_name') + append_field(bundle, 'String form', 'string_form') # Namespace if info['namespace'] != 'Interactive': - append_field(_mime, 'Namespace', 'namespace') + append_field(bundle, 'Namespace', 'namespace') - append_field(_mime, 'Length', 'length') - append_field(_mime, 'File', 'file') + append_field(bundle, 'Length', 'length') + append_field(bundle, 'File', 'file') # Source or docstring, depending on detail level and whether # source found. if detail_level > 0 and info['source']: - append_field(_mime, 'Source', 'source', code_formatter) + append_field(bundle, 'Source', 'source', code_formatter) else: - append_field(_mime, 'Docstring', 'docstring', formatter) + append_field(bundle, 'Docstring', 'docstring', formatter) + + append_field(bundle, 'Class docstring', 'class_docstring', formatter) + append_field(bundle, 'Init docstring', 'init_docstring', formatter) + append_field(bundle, 'Call docstring', 'call_docstring', formatter) + return bundle - append_field(_mime, 'Class docstring', 'class_docstring', formatter) - append_field(_mime, 'Init docstring', 'init_docstring', formatter) - append_field(_mime, 'Call docstring', 'call_docstring', formatter) + def _get_info( + self, obj, oname="", formatter=None, info=None, detail_level=0, omit_sections=() + ): + """Retrieve an info dict and format it. - return self.format_mime(_mime) + Parameters + ---------- + obj : any + Object to inspect and return info from + oname : str (default: ''): + Name of the variable pointing to `obj`. + formatter : callable + info + already computed information + detail_level : integer + Granularity of detail level, if set to 1, give more information. + omit_sections : container[str] + Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`) + """ + + info = self.info(obj, oname=oname, info=info, detail_level=detail_level) + bundle = self._make_info_unformatted(info, formatter, detail_level=detail_level, omit_sections=omit_sections) + return self.format_mime(bundle) def pinfo( self, From eae299e8c9d1471eca2c76f1e8e19765fde556a2 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 11:17:05 -0700 Subject: [PATCH 0330/1752] Move formatting of inspect reply html into the format_mime function Also simplify the formatting of the inspect reply text/plain logic --- IPython/core/oinspect.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 13d6268256d..3a80f05a4e1 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -553,23 +553,23 @@ def _mime_format(self, text:str, formatter=None) -> dict: def format_mime(self, bundle): """Format a mimebundle being created by _make_info_unformatted into a real mimebundle""" - # First, format the field names and values for the text/plain field - text_plain = bundle['text/plain'] - if isinstance(text_plain, (list, tuple)): - text = '' - heads, bodies = list(zip(*text_plain)) - _len = max(len(h) for h in heads) - - for head, body in zip(heads, bodies): + # Format text/plain mimetype + if isinstance(bundle['text/plain'], (list, tuple)): + # bundle['text/plain'] is a list of (head, formatted body) pairs + lines = [] + _len = max(len(h) for h,_ in bundle['text/plain']) + + for head, body in bundle['text/plain']: body = body.strip('\n') delim = '\n' if '\n' in body else ' ' - text += self.__head(head+':') + (_len - len(head))*' ' +delim + body +'\n' + lines.append(f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}") - bundle['text/plain'] = text + bundle['text/plain'] = '\n'.join(lines) - # Next format the text/html value by joining strings if it is a list of strings + # Format the text/html mimetype if isinstance(bundle['text/html'], (list, tuple)): - bundle['text/html'] = '\n'.join(bundle['text/html']) + # bundle['text/html'] is a list of (head, formatted body) pairs + bundle['text/html'] = '\n'.join((f'

{head}

\n{body}' for (head,body) in bundle['text/html'])) return bundle def _append_info_field(self, bundle, title:str, key:str, info, omit_sections, formatter): @@ -580,7 +580,7 @@ def _append_info_field(self, bundle, title:str, key:str, info, omit_sections, fo if field is not None: formatted_field = self._mime_format(field, formatter) bundle['text/plain'].append((title, formatted_field['text/plain'])) - bundle['text/html'] += '

' + title + '

\n' + formatted_field['text/html'] + bundle['text/html'].append((title, formatted_field['text/html'])) def _make_info_unformatted(self, info, formatter, detail_level, omit_sections): """Assemble the mimebundle as unformatted lists of information""" From 5e7b9d5506ff0f2891e10248255394592cc779b9 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 11:19:17 -0700 Subject: [PATCH 0331/1752] Lint --- IPython/core/oinspect.py | 105 ++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 3a80f05a4e1..1227fd07c66 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -554,44 +554,57 @@ def _mime_format(self, text:str, formatter=None) -> dict: def format_mime(self, bundle): """Format a mimebundle being created by _make_info_unformatted into a real mimebundle""" # Format text/plain mimetype - if isinstance(bundle['text/plain'], (list, tuple)): + if isinstance(bundle["text/plain"], (list, tuple)): # bundle['text/plain'] is a list of (head, formatted body) pairs lines = [] - _len = max(len(h) for h,_ in bundle['text/plain']) + _len = max(len(h) for h, _ in bundle["text/plain"]) - for head, body in bundle['text/plain']: - body = body.strip('\n') - delim = '\n' if '\n' in body else ' ' - lines.append(f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}") + for head, body in bundle["text/plain"]: + body = body.strip("\n") + delim = "\n" if "\n" in body else " " + lines.append( + f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}" + ) - bundle['text/plain'] = '\n'.join(lines) + bundle["text/plain"] = "\n".join(lines) # Format the text/html mimetype - if isinstance(bundle['text/html'], (list, tuple)): + if isinstance(bundle["text/html"], (list, tuple)): # bundle['text/html'] is a list of (head, formatted body) pairs - bundle['text/html'] = '\n'.join((f'

{head}

\n{body}' for (head,body) in bundle['text/html'])) + bundle["text/html"] = "\n".join( + (f"

{head}

\n{body}" for (head, body) in bundle["text/html"]) + ) return bundle - def _append_info_field(self, bundle, title:str, key:str, info, omit_sections, formatter): + def _append_info_field( + self, bundle, title: str, key: str, info, omit_sections, formatter + ): """Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted""" if title in omit_sections or key in omit_sections: return field = info[key] if field is not None: formatted_field = self._mime_format(field, formatter) - bundle['text/plain'].append((title, formatted_field['text/plain'])) - bundle['text/html'].append((title, formatted_field['text/html'])) + bundle["text/plain"].append((title, formatted_field["text/plain"])) + bundle["text/html"].append((title, formatted_field["text/html"])) def _make_info_unformatted(self, info, formatter, detail_level, omit_sections): """Assemble the mimebundle as unformatted lists of information""" bundle = { - 'text/plain': [], - 'text/html': [], + "text/plain": [], + "text/html": [], } # A convenience function to simplify calls below - def append_field(bundle, title:str, key:str, formatter=None): - self._append_info_field(bundle, title=title, key=key, info=info, omit_sections=omit_sections, formatter=formatter) + def append_field(bundle, title: str, key: str, formatter=None): + self._append_info_field( + bundle, + title=title, + key=key, + info=info, + omit_sections=omit_sections, + formatter=formatter, + ) def code_formatter(text): return { @@ -599,54 +612,54 @@ def code_formatter(text): 'text/html': pylight(text) } - if info['isalias']: - append_field(bundle, 'Repr', 'string_form') + if info["isalias"]: + append_field(bundle, "Repr", "string_form") elif info['ismagic']: if detail_level > 0: - append_field(bundle, 'Source', 'source', code_formatter) + append_field(bundle, "Source", "source", code_formatter) else: - append_field(bundle, 'Docstring', 'docstring', formatter) - append_field(bundle, 'File', 'file') + append_field(bundle, "Docstring", "docstring", formatter) + append_field(bundle, "File", "file") elif info['isclass'] or is_simple_callable(obj): # Functions, methods, classes - append_field(bundle, 'Signature', 'definition', code_formatter) - append_field(bundle, 'Init signature', 'init_definition', code_formatter) - append_field(bundle, 'Docstring', 'docstring', formatter) - if detail_level > 0 and info['source']: - append_field(bundle, 'Source', 'source', code_formatter) + append_field(bundle, "Signature", "definition", code_formatter) + append_field(bundle, "Init signature", "init_definition", code_formatter) + append_field(bundle, "Docstring", "docstring", formatter) + if detail_level > 0 and info["source"]: + append_field(bundle, "Source", "source", code_formatter) else: - append_field(bundle, 'Init docstring', 'init_docstring', formatter) + append_field(bundle, "Init docstring", "init_docstring", formatter) - append_field(bundle, 'File', 'file') - append_field(bundle, 'Type', 'type_name') - append_field(bundle, 'Subclasses', 'subclasses') + append_field(bundle, "File", "file") + append_field(bundle, "Type", "type_name") + append_field(bundle, "Subclasses", "subclasses") else: # General Python objects - append_field(bundle, 'Signature', 'definition', code_formatter) - append_field(bundle, 'Call signature', 'call_def', code_formatter) - append_field(bundle, 'Type', 'type_name') - append_field(bundle, 'String form', 'string_form') + append_field(bundle, "Signature", "definition", code_formatter) + append_field(bundle, "Call signature", "call_def", code_formatter) + append_field(bundle, "Type", "type_name") + append_field(bundle, "String form", "string_form") # Namespace - if info['namespace'] != 'Interactive': - append_field(bundle, 'Namespace', 'namespace') + if info["namespace"] != "Interactive": + append_field(bundle, "Namespace", "namespace") - append_field(bundle, 'Length', 'length') - append_field(bundle, 'File', 'file') + append_field(bundle, "Length", "length") + append_field(bundle, "File", "file") # Source or docstring, depending on detail level and whether # source found. - if detail_level > 0 and info['source']: - append_field(bundle, 'Source', 'source', code_formatter) + if detail_level > 0 and info["source"]: + append_field(bundle, "Source", "source", code_formatter) else: - append_field(bundle, 'Docstring', 'docstring', formatter) + append_field(bundle, "Docstring", "docstring", formatter) - append_field(bundle, 'Class docstring', 'class_docstring', formatter) - append_field(bundle, 'Init docstring', 'init_docstring', formatter) - append_field(bundle, 'Call docstring', 'call_docstring', formatter) + append_field(bundle, "Class docstring", "class_docstring", formatter) + append_field(bundle, "Init docstring", "init_docstring", formatter) + append_field(bundle, "Call docstring", "call_docstring", formatter) return bundle @@ -671,7 +684,9 @@ def _get_info( """ info = self.info(obj, oname=oname, info=info, detail_level=detail_level) - bundle = self._make_info_unformatted(info, formatter, detail_level=detail_level, omit_sections=omit_sections) + bundle = self._make_info_unformatted( + info, formatter, detail_level=detail_level, omit_sections=omit_sections + ) return self.format_mime(bundle) def pinfo( From 5db69068fe3256b29f575deb68ecca5287cefafc Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 11:24:40 -0700 Subject: [PATCH 0332/1752] Fix missing argument in _make_info_unformatted --- IPython/core/oinspect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 1227fd07c66..43f6707dd36 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -588,7 +588,7 @@ def _append_info_field( bundle["text/plain"].append((title, formatted_field["text/plain"])) bundle["text/html"].append((title, formatted_field["text/html"])) - def _make_info_unformatted(self, info, formatter, detail_level, omit_sections): + def _make_info_unformatted(self, obj, info, formatter, detail_level, omit_sections): """Assemble the mimebundle as unformatted lists of information""" bundle = { "text/plain": [], @@ -685,7 +685,7 @@ def _get_info( info = self.info(obj, oname=oname, info=info, detail_level=detail_level) bundle = self._make_info_unformatted( - info, formatter, detail_level=detail_level, omit_sections=omit_sections + obj, info, formatter, detail_level=detail_level, omit_sections=omit_sections ) return self.format_mime(bundle) From 99b83f140ac70d6307382b2fd238844baaf5c957 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 13:18:08 -0700 Subject: [PATCH 0333/1752] Escape html text in inspect replies by default Otherwise, the default docstring was not displayed in html because it was acting as an html tag. --- IPython/core/oinspect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 43f6707dd36..801cb880659 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -16,6 +16,7 @@ import ast import inspect from inspect import signature +import html import linecache import warnings import os @@ -531,7 +532,7 @@ def _mime_format(self, text:str, formatter=None) -> dict: """ defaults = { 'text/plain': text, - 'text/html': '
' + text + '
' + 'text/html': '
' + html.escape(text) + '
' } if formatter is None: From 08b767f70d80677fbd2205014f9b495c568738de Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 12:32:45 -0800 Subject: [PATCH 0334/1752] Minor formatting changes --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10bf1efff8f..164757fb350 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,9 +92,9 @@ pytest IPython/core/tests/test_alias.py::test_alias_lifecycle ## Code style -* Before committing run `darker -r 60625f241f298b5039cb2debc365db38aa7bb522 ` to apply selective `black` formatting on modified regions using [darker](https://github.com/akaihola/darker) +* Before committing, run `darker -r 60625f241f298b5039cb2debc365db38aa7bb522 ` to apply selective `black` formatting on modified regions using [darker](https://github.com/akaihola/darker). * For newly added modules or refactors, please enable static typing analysis with `mypy` for the modified module by adding the file path in [`mypy.yml`](https://github.com/ipython/ipython/blob/main/.github/workflows/mypy.yml) workflow. -* As described in pull requests section, please avoid excessive formatting changes; if formatting-only commit is necessary consider adding its hash to [`.git-blame-ignore-revs`](https://github.com/ipython/ipython/blob/main/.git-blame-ignore-revs) file +* As described in the pull requests section, please avoid excessive formatting changes; if a formatting-only commit is necessary, consider adding its hash to [`.git-blame-ignore-revs`](https://github.com/ipython/ipython/blob/main/.git-blame-ignore-revs) file. ## Documentation From ea239fb8c60c12e8db051cb64fd09d74e1684bc3 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 9 Dec 2022 13:51:11 -0700 Subject: [PATCH 0335/1752] Lint + change to f strings --- IPython/core/oinspect.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 801cb880659..bcaa95c97fa 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -531,8 +531,8 @@ def _mime_format(self, text:str, formatter=None) -> dict: """ defaults = { - 'text/plain': text, - 'text/html': '
' + html.escape(text) + '
' + "text/plain": text, + "text/html": f"
{html.escape(text)}
", } if formatter is None: @@ -543,10 +543,7 @@ def _mime_format(self, text:str, formatter=None) -> dict: if not isinstance(formatted, dict): # Handle the deprecated behavior of a formatter returning # a string instead of a mime bundle. - return { - 'text/plain': formatted, - 'text/html': '
' + formatted + '
' - } + return {"text/plain": formatted, "text/html": f"
{formatted}
"} else: return dict(defaults, **formatted) From fdb27756cf4b9bb7bbacb725e9eeb735a14bae42 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 17 Nov 2022 13:26:11 -0700 Subject: [PATCH 0336/1752] Make inspector class a configurable attribute in InteractiveShell. --- IPython/core/interactiveshell.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 21e428b54d4..fa0614d6139 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -389,6 +389,9 @@ def _import_runner(self, proposal): displayhook_class = Type(DisplayHook) display_pub_class = Type(DisplayPublisher) compiler_class = Type(CachingCompiler) + inspector_class = Type( + oinspect.Inspector, help="Class to use to instantiate the shell inspector" + ).tag(config=True) sphinxify_docstring = Bool(False, help= """ @@ -755,10 +758,12 @@ def init_builtins(self): @observe('colors') def init_inspector(self, changes=None): # Object inspector - self.inspector = oinspect.Inspector(oinspect.InspectColors, - PyColorize.ANSICodeColors, - self.colors, - self.object_info_string_level) + self.inspector = self.inspector_class( + oinspect.InspectColors, + PyColorize.ANSICodeColors, + self.colors, + self.object_info_string_level, + ) def init_io(self): # implemented in subclasses, TerminalInteractiveShell does call From 4182eee632b721ef5d70aa6bd6053920d8c1e42f Mon Sep 17 00:00:00 2001 From: nfgf Date: Sat, 10 Dec 2022 12:04:12 -0500 Subject: [PATCH 0337/1752] Semicolon at the end silence output of %%time too. --- IPython/core/interactiveshell.py | 9 +++++++++ IPython/core/tests/test_magic.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 12503e9d916..f158ef21c4f 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2423,6 +2423,14 @@ def run_cell_magic(self, magic_name, line, cell): with self.builtin_trap: args = (magic_arg_s, cell) result = fn(*args, **kwargs) + + # The code below prevents the output from being displayed + # when using magics with decodator @output_can_be_silenced + # when the last Python token in the expression is a ';'. + if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False): + if DisplayHook.semicolon_at_end_of_expression(cell): + return None + return result def find_line_magic(self, magic_name): @@ -3199,6 +3207,7 @@ def error_before_exec(value): # Execute the user code interactivity = "none" if silent else self.ast_node_interactivity + has_raised = await self.run_ast_nodes(code_ast.body, cell_name, interactivity=interactivity, compiler=compiler, result=result) diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 55408d4af1e..e64b959322b 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -422,6 +422,7 @@ def test_time(): def test_time_no_output_with_semicolon(): ip = get_ipython() + # Test %time cases with tt.AssertPrints(" 123456"): with tt.AssertPrints("Wall time: ", suppress=False): with tt.AssertPrints("CPU times: ", suppress=False): @@ -447,6 +448,32 @@ def test_time_no_output_with_semicolon(): with tt.AssertPrints("CPU times: ", suppress=False): ip.run_cell("%time 123000+456 # ;Comment") + # Test %%time cases + with tt.AssertPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456\n\n\n") + + with tt.AssertNotPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456;\n\n\n") + + with tt.AssertPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456 # Comment\n\n\n") + + with tt.AssertNotPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456; # Comment\n\n\n") + + with tt.AssertPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456 # ;Comment\n\n\n") + def test_time_last_not_expression(): ip.run_cell("%%time\n" From 1d40f7c174aaea0115898df23257639478508251 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 11:42:55 -0800 Subject: [PATCH 0338/1752] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com> --- IPython/extensions/autoreload.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 9dc95b4f27f..690fef5fce3 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -608,11 +608,11 @@ def autoreload(self, line=""): args = magic_arguments.parse_argstring(self.autoreload, line) mode = args.mode.lower() - def p(msg): - print(msg) + p = print + logger = logging.getLogger("autoreload") def l(msg): - logging.getLogger("autoreload").info(msg) + logger.info(msg) def pl(msg): p(msg) @@ -622,11 +622,11 @@ def pl(msg): self._reloader._report = lambda msg: None elif args.print is True: if args.log is True: - self._reloader._report = lambda msg: pl(msg) + self._reloader._report = pl else: - self._reloader._report = lambda msg: p(msg) + self._reloader._report = p elif args.log is True: - self._reloader._report = lambda msg: l(msg) + self._reloader._report = l if mode == "" or mode == "now": self._reloader.check(True) From c1e9957deccc46598ec11aca88a5346e1d2a9262 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 11:48:33 -0800 Subject: [PATCH 0339/1752] Fix RST syntax --- .../whatsnew/pr/autoreload-verbosity.rst | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/source/whatsnew/pr/autoreload-verbosity.rst b/docs/source/whatsnew/pr/autoreload-verbosity.rst index 155f890915a..68ff3001819 100644 --- a/docs/source/whatsnew/pr/autoreload-verbosity.rst +++ b/docs/source/whatsnew/pr/autoreload-verbosity.rst @@ -1,21 +1,26 @@ Autoreload verbosity ==================== -We introduce more descriptive names for the `%autoreload` parameter: -- `%autoreload now` (also `%autoreload`) - perform autoreload immediately. -- `%autoreload off` (also `%autoreload 0`) - turn off autoreload. -- `%autoreload explicit` (also `%autoreload 1`) - turn on autoreload only for modules whitelisted by `%aimport` statements. -- `%autoreload all` (also `%autoreload 2`) - turn on autoreload for all modules except those blacklisted by `%aimport` statements. -- `%autoreload complete` (also `%autoreload 3`) - all the fatures of `all` but also adding new objects from the imported modules (see IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects). +We introduce more descriptive names for the ``%autoreload`` parameter: + +- ``%autoreload now`` (also ``%autoreload``) - perform autoreload immediately. +- ``%autoreload off`` (also ``%autoreload 0``) - turn off autoreload. +- ``%autoreload explicit`` (also ``%autoreload 1``) - turn on autoreload only for modules + whitelisted by ``%aimport`` statements. +- ``%autoreload all`` (also ``%autoreload 2``) - turn on autoreload for all modules except those + blacklisted by ``%aimport`` statements. +- ``%autoreload complete`` (also ``%autoreload 3``) - all the fatures of ``all`` but also adding new + objects from the imported modules (see + IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects). The original designations (e.g. "2") still work, and these new ones are case-insensitive. -Additionally, the option `--print` or `-p` can be added to the line to print the names of modules -being reloaded. Similarly, `--log` or `-l` will output the names to the logger at INFO level. Both -can be used simultaneously. +Additionally, the option ``--print`` or ``-p`` can be added to the line to print the names of +modules being reloaded. Similarly, ``--log`` or ``-l`` will output the names to the logger at INFO +level. Both can be used simultaneously. -The parsing logic for `%aimport` is now improved such that modules can be whitelisted and -blacklisted in the same line, e.g. it's now possible to call `%aimport os, -math` to include `os` -for `%autoreload explicit` and exclude `math` for modes 2 and 3. +The parsing logic for ``%aimport`` is now improved such that modules can be whitelisted and +blacklisted in the same line, e.g. it's now possible to call ``%aimport os, -math`` to include +``os`` for ``%autoreload explicit`` and exclude ``math`` for modes ``all`` and ``complete``. From 8579f42aaccbfb104f39cf2436a294f5b02e7187 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 11:55:46 -0800 Subject: [PATCH 0340/1752] Use new names in docstring. --- IPython/extensions/autoreload.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 690fef5fce3..2d46814fd82 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -653,14 +653,14 @@ def aimport(self, parameter_s="", stream=None): List modules to automatically import and not to import. %aimport foo - Import module 'foo' and mark it to be autoreloaded for %autoreload 1 + Import module 'foo' and mark it to be autoreloaded for %autoreload explicit %aimport foo, bar - Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload 1 + Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload explicit %aimport -foo, bar - Mark module 'foo' to not be autoreloaded for %autoreload 1, 2, or 3, and 'bar' - to be autoreloaded for 1. + Mark module 'foo' to not be autoreloaded for %autoreload explicit, all, or complete, and 'bar' + to be autoreloaded for mode explicit. """ modname = parameter_s if not modname: From 158e03b4ac81fd762cb362f86ae976ab8d48f7e1 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 12:01:41 -0800 Subject: [PATCH 0341/1752] `l` doesn't need to be a function --- IPython/extensions/autoreload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index 2d46814fd82..f9fae779da4 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -611,8 +611,8 @@ def autoreload(self, line=""): p = print logger = logging.getLogger("autoreload") - def l(msg): - logger.info(msg) + + l = logger.info def pl(msg): p(msg) From 3ee7def624cb9acfa35da5119fa31643952fd8dd Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 12:02:12 -0800 Subject: [PATCH 0342/1752] Fix formatting --- IPython/extensions/autoreload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index f9fae779da4..e34c8fe1052 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -608,7 +608,7 @@ def autoreload(self, line=""): args = magic_arguments.parse_argstring(self.autoreload, line) mode = args.mode.lower() - p = print + p = print logger = logging.getLogger("autoreload") From eec5731d81b5762efe85c5497ac02fd699472044 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 13 Dec 2022 13:52:44 -0800 Subject: [PATCH 0343/1752] Fully undo mode flags --- IPython/extensions/autoreload.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index e34c8fe1052..cb63a55ef6e 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -633,14 +633,16 @@ def pl(msg): elif mode == "0" or mode == "off": self._reloader.enabled = False elif mode == "1" or mode == "explicit": - self._reloader.check_all = False self._reloader.enabled = True + self._reloader.check_all = False + self._reloader.autoload_obj = False elif mode == "2" or mode == "all": - self._reloader.check_all = True self._reloader.enabled = True - elif mode == "3" or mode == "complete": self._reloader.check_all = True + self._reloader.autoload_obj = False + elif mode == "3" or mode == "complete": self._reloader.enabled = True + self._reloader.check_all = True self._reloader.autoload_obj = True else: raise ValueError(f'Unrecognized autoreload mode "{mode}".') From 661d6d7c8212dd0b6d35853bf0a1fb7d1aad545e Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Fri, 16 Dec 2022 12:26:55 +0900 Subject: [PATCH 0344/1752] Fix paste/cpaste magic --- IPython/core/interactiveshell.py | 6 +++++- IPython/terminal/magics.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 21e428b54d4..e137c922d75 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3138,8 +3138,12 @@ def error_before_exec(value): else: cell = raw_cell + # Do NOT store paste/cpaste magic history + if "get_ipython().run_line_magic(" in cell and "paste" in cell: + store_history = False + # Store raw and processed history - if store_history and raw_cell.strip(" %") != "paste": + if store_history: self.history_manager.store_inputs(self.execution_count, cell, raw_cell) if not silent: self.logger.log(cell, raw_cell) diff --git a/IPython/terminal/magics.py b/IPython/terminal/magics.py index 66d532511b3..cea53e4a248 100644 --- a/IPython/terminal/magics.py +++ b/IPython/terminal/magics.py @@ -147,7 +147,7 @@ def cpaste(self, parameter_s=''): sentinel = opts.get('s', u'--') block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet)) - self.store_or_execute(block, name, store_history=False) + self.store_or_execute(block, name, store_history=True) @line_magic def paste(self, parameter_s=''): From 3455b5738de94fb4587d507a560424188acabb7e Mon Sep 17 00:00:00 2001 From: azjps Date: Mon, 19 Dec 2022 21:59:21 +1300 Subject: [PATCH 0345/1752] Set up shell command-line tab-completion for ipython Set up shell command-line tab-completion using argcomplete and ipython/traitlets#811 argcomplete supports following setuptools console_scripts to the corresponding package's __main__.py to look for a PYTHON_ARGCOMPLETE_OK marker. --- IPython/__init__.py | 1 + IPython/__main__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/IPython/__init__.py b/IPython/__init__.py index 03b3116a98a..c224f9a8c90 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -1,3 +1,4 @@ +# PYTHON_ARGCOMPLETE_OK """ IPython: tools for interactive and parallel computing in Python. diff --git a/IPython/__main__.py b/IPython/__main__.py index d5123f33a20..8e9f989a82c 100644 --- a/IPython/__main__.py +++ b/IPython/__main__.py @@ -1,3 +1,4 @@ +# PYTHON_ARGCOMPLETE_OK # encoding: utf-8 """Terminal-based IPython entry point. """ From a7cebe885813722fe432cf05d9effe59e05d5856 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Thu, 22 Dec 2022 16:08:13 +0000 Subject: [PATCH 0346/1752] Fix warning in docs build --- IPython/core/completer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index f2853d3c48a..4edeb93426b 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -59,7 +59,8 @@ Both forward and backward completions can be deactivated by setting the -:any:`Completer.backslash_combining_completions` option to ``False``. +:std:configtrait:`Completer.backslash_combining_completions` option to +``False``. Experimental @@ -166,7 +167,7 @@ should not be suppressed to ``MatcherResult`` under ``do_not_suppress`` key. The suppression behaviour can is user-configurable via -:any:`IPCompleter.suppress_competing_matchers`. +:std:configtrait:`IPCompleter.suppress_competing_matchers`. """ @@ -972,7 +973,7 @@ class Completer(Configurable): help="""Activate greedy completion. .. deprecated:: 8.8 - Use :any:`Completer.evaluation` and :any:`Completer.auto_close_dict_keys` instead. + Use :std:configtrait:`Completer.evaluation` and :std:configtrait:`Completer.auto_close_dict_keys` instead. When enabled in IPython 8.8 or newer, changes configuration as follows: From f2b3fab00c6fdb56cd5512d457182d1dda4012ca Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 23 Dec 2022 11:16:53 +0100 Subject: [PATCH 0347/1752] Test on more recent Python versions. --- .github/workflows/mypy.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index e05678f724d..52f3e79ab9c 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: ["3.x"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f4677fb4d2..73968555c4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] deps: [test_extra] # Test all on ubuntu, test ends on macos include: @@ -27,15 +27,15 @@ jobs: python-version: "3.8" deps: test_extra - os: macos-latest - python-version: "3.10" + python-version: "3.11" deps: test_extra # Tests minimal dependencies set - os: ubuntu-latest - python-version: "3.10" + python-version: "3.11" deps: test # Tests latest development Python version - os: ubuntu-latest - python-version: "3.11-dev" + python-version: "3.12-dev" deps: test # Installing optional dependencies stuff takes ages on PyPy - os: ubuntu-latest From 6cd25549de0ef4565ef70dfa8170833966baca6d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 23 Dec 2022 11:16:53 +0100 Subject: [PATCH 0348/1752] Test on more recent Python versions. --- .github/workflows/mypy.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index e05678f724d..52f3e79ab9c 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: ["3.x"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f4677fb4d2..73968555c4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] deps: [test_extra] # Test all on ubuntu, test ends on macos include: @@ -27,15 +27,15 @@ jobs: python-version: "3.8" deps: test_extra - os: macos-latest - python-version: "3.10" + python-version: "3.11" deps: test_extra # Tests minimal dependencies set - os: ubuntu-latest - python-version: "3.10" + python-version: "3.11" deps: test # Tests latest development Python version - os: ubuntu-latest - python-version: "3.11-dev" + python-version: "3.12-dev" deps: test # Installing optional dependencies stuff takes ages on PyPy - os: ubuntu-latest From 023a594a32450ad02d771de6f83805f3bcd1662a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 23 Dec 2022 19:04:09 +0100 Subject: [PATCH 0349/1752] extend unicode for Python 3.12 --- IPython/core/completer.py | 2 +- IPython/core/tests/test_completer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 4edeb93426b..5ba8ea0fdbd 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -285,7 +285,7 @@ def cast(type_, obj): # write this). With below range we cover them all, with a density of ~67% # biggest next gap we consider only adds up about 1% density and there are 600 # gaps that would need hard coding. -_UNICODE_RANGES = [(32, 0x3134b), (0xe0001, 0xe01f0)] +_UNICODE_RANGES = [(32, 0x323B0), (0xE0001, 0xE01F0)] # Public API __all__ = ["Completer", "IPCompleter"] diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 5e8cb35bc33..7783798eb36 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -99,7 +99,7 @@ def test_unicode_range(): assert len_exp == len_test, message # fail if new unicode symbols have been added. - assert len_exp <= 138552, message + assert len_exp <= 143041, message @contextmanager From 02894af40a93c28073ab84a14f5f396204cbb823 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 23 Dec 2022 19:44:09 +0100 Subject: [PATCH 0350/1752] skip test on 3.12 --- IPython/extensions/tests/test_autoreload.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index 88637fbab9c..2c3c9db212d 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -367,7 +367,8 @@ class TestEnum(Enum): self.shell.run_code("assert func2() == 'changed'") self.shell.run_code("t = Test(); assert t.new_func() == 'changed'") self.shell.run_code("assert number == 1") - self.shell.run_code("assert TestEnum.B.value == 'added'") + if sys.version_info < (3, 12): + self.shell.run_code("assert TestEnum.B.value == 'added'") # ----------- TEST IMPORT FROM MODULE -------------------------- From ed7f35f8b721d4b4dcafea173ce724bee25704c7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 11:57:18 +0100 Subject: [PATCH 0351/1752] Fix tests for pygments > 2.14 Pygments 2.14+ have the bash lexer return some tokens as Text.Whitespace instead of Text, this update the test to support this. --- IPython/lib/tests/test_lexers.py | 52 ++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/IPython/lib/tests/test_lexers.py b/IPython/lib/tests/test_lexers.py index efa00d601ea..000b8fe6fd9 100644 --- a/IPython/lib/tests/test_lexers.py +++ b/IPython/lib/tests/test_lexers.py @@ -4,11 +4,14 @@ # Distributed under the terms of the Modified BSD License. from unittest import TestCase +from pygments import __version__ as pygments_version from pygments.token import Token from pygments.lexers import BashLexer from .. import lexers +pyg214 = tuple(int(x) for x in pygments_version.split(".")[:2]) >= (2, 14) + class TestLexers(TestCase): """Collection of lexers tests""" @@ -18,25 +21,26 @@ def setUp(self): def testIPythonLexer(self): fragment = '!echo $HOME\n' - tokens = [ + bash_tokens = [ (Token.Operator, '!'), ] - tokens.extend(self.bash_lexer.get_tokens(fragment[1:])) - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + bash_tokens.extend(self.bash_lexer.get_tokens(fragment[1:])) + ipylex_token = list(self.lexer.get_tokens(fragment)) + assert bash_tokens[:-1] == ipylex_token[:-1] - fragment_2 = '!' + fragment + fragment_2 = "!" + fragment tokens_2 = [ (Token.Operator, '!!'), - ] + tokens[1:] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + ] + bash_tokens[1:] + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment_2 = '\t %%!\n' + fragment[1:] tokens_2 = [ (Token.Text, '\t '), (Token.Operator, '%%!'), (Token.Text, '\n'), - ] + tokens[1:] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + ] + bash_tokens[1:] + assert tokens_2 == list(self.lexer.get_tokens(fragment_2)) fragment_2 = 'x = ' + fragment tokens_2 = [ @@ -44,8 +48,8 @@ def testIPythonLexer(self): (Token.Text, ' '), (Token.Operator, '='), (Token.Text, ' '), - ] + tokens - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + ] + bash_tokens + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment_2 = 'x, = ' + fragment tokens_2 = [ @@ -54,8 +58,8 @@ def testIPythonLexer(self): (Token.Text, ' '), (Token.Operator, '='), (Token.Text, ' '), - ] + tokens - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + ] + bash_tokens + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment_2 = 'x, = %sx ' + fragment[1:] tokens_2 = [ @@ -67,8 +71,10 @@ def testIPythonLexer(self): (Token.Operator, '%'), (Token.Keyword, 'sx'), (Token.Text, ' '), - ] + tokens[1:] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + ] + bash_tokens[1:] + if tokens_2[7] == (Token.Text, " ") and pyg214: # pygments 2.14+ + tokens_2[7] = (Token.Text.Whitespace, " ") + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment_2 = 'f = %R function () {}\n' tokens_2 = [ @@ -80,7 +86,7 @@ def testIPythonLexer(self): (Token.Keyword, 'R'), (Token.Text, ' function () {}\n'), ] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + assert tokens_2 == list(self.lexer.get_tokens(fragment_2)) fragment_2 = '\t%%xyz\n$foo\n' tokens_2 = [ @@ -89,7 +95,7 @@ def testIPythonLexer(self): (Token.Keyword, 'xyz'), (Token.Text, '\n$foo\n'), ] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + assert tokens_2 == list(self.lexer.get_tokens(fragment_2)) fragment_2 = '%system?\n' tokens_2 = [ @@ -98,7 +104,7 @@ def testIPythonLexer(self): (Token.Operator, '?'), (Token.Text, '\n'), ] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment_2 = 'x != y\n' tokens_2 = [ @@ -109,7 +115,7 @@ def testIPythonLexer(self): (Token.Name, 'y'), (Token.Text, '\n'), ] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment_2 = ' ?math.sin\n' tokens_2 = [ @@ -118,7 +124,7 @@ def testIPythonLexer(self): (Token.Text, 'math.sin'), (Token.Text, '\n'), ] - self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + assert tokens_2[:-1] == list(self.lexer.get_tokens(fragment_2))[:-1] fragment = ' *int*?\n' tokens = [ @@ -126,7 +132,7 @@ def testIPythonLexer(self): (Token.Operator, '?'), (Token.Text, '\n'), ] - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + assert tokens == list(self.lexer.get_tokens(fragment)) fragment = '%%writefile -a foo.py\nif a == b:\n pass' tokens = [ @@ -145,7 +151,9 @@ def testIPythonLexer(self): (Token.Keyword, 'pass'), (Token.Text, '\n'), ] - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + if tokens[10] == (Token.Text, "\n") and pyg214: # pygments 2.14+ + tokens[10] = (Token.Text.Whitespace, "\n") + assert tokens[:-1] == list(self.lexer.get_tokens(fragment))[:-1] fragment = '%%timeit\nmath.sin(0)' tokens = [ @@ -173,4 +181,4 @@ def testIPythonLexer(self): (Token.Punctuation, '>'), (Token.Text, '\n'), ] - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + assert tokens == list(self.lexer.get_tokens(fragment)) From 0520f55c8a2cb500ad2428d1c0b36adc57b11309 Mon Sep 17 00:00:00 2001 From: Nir Schulman Date: Sat, 31 Dec 2022 08:47:28 +0200 Subject: [PATCH 0352/1752] Removed the usage of minor-version entrypoints --- setupbase.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setupbase.py b/setupbase.py index 748b4dd6a8a..3be9e831133 100644 --- a/setupbase.py +++ b/setupbase.py @@ -211,19 +211,16 @@ def find_entry_points(): use, our own build_scripts_entrypt class below parses these and builds command line scripts. - Each of our entry points gets a plain name, e.g. ipython, a name - suffixed with the Python major version number, e.g. ipython3, and - a name suffixed with the Python major.minor version number, eg. ipython3.8. + Each of our entry points gets a plain name, e.g. ipython, and a name + suffixed with the Python major version number, e.g. ipython3. """ ep = [ 'ipython%s = IPython:start_ipython', ] major_suffix = str(sys.version_info[0]) - minor_suffix = ".".join([str(sys.version_info[0]), str(sys.version_info[1])]) return ( [e % "" for e in ep] + [e % major_suffix for e in ep] - + [e % minor_suffix for e in ep] ) class install_lib_symlink(Command): From 34da39cb0d7654f21d6f551864be94cc16f9ad34 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 11:06:33 +0100 Subject: [PATCH 0353/1752] Please linter --- setupbase.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setupbase.py b/setupbase.py index 3be9e831133..a867c73ecd0 100644 --- a/setupbase.py +++ b/setupbase.py @@ -218,10 +218,8 @@ def find_entry_points(): 'ipython%s = IPython:start_ipython', ] major_suffix = str(sys.version_info[0]) - return ( - [e % "" for e in ep] - + [e % major_suffix for e in ep] - ) + return [e % "" for e in ep] + [e % major_suffix for e in ep] + class install_lib_symlink(Command): user_options = [ From 8af5442ee0d7c34d0066c6651ac2d364790c6d6c Mon Sep 17 00:00:00 2001 From: Jake Herrmann Date: Thu, 8 Dec 2022 17:12:57 -0900 Subject: [PATCH 0354/1752] Fix vi mode escape delay --- IPython/terminal/shortcuts.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 7d6de8b3b04..01072cd99a1 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -68,9 +68,14 @@ def reformat_and_execute(event): reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell) event.current_buffer.validate_and_handle() + @Condition + def ebivim(): + return shell.emacs_bindings_in_vi_insert_mode + kb.add('escape', 'enter', filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode + & ebivim ))(reformat_and_execute) kb.add("c-\\")(quit) @@ -333,10 +338,6 @@ def _(event): if sys.platform == "win32": kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) - @Condition - def ebivim(): - return shell.emacs_bindings_in_vi_insert_mode - focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) From 11dd2521aee4a87d68f2244d6fee86c6e1873fcd Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 12:00:52 +0100 Subject: [PATCH 0355/1752] please formatter --- IPython/terminal/shortcuts.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py index 01072cd99a1..6ca91ec31ba 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts.py @@ -72,11 +72,11 @@ def reformat_and_execute(event): def ebivim(): return shell.emacs_bindings_in_vi_insert_mode - kb.add('escape', 'enter', filter=(has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - & ebivim - ))(reformat_and_execute) + kb.add( + "escape", + "enter", + filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim), + )(reformat_and_execute) kb.add("c-\\")(quit) From 158a084ae04db1807f56c9768ac3934f43c9d8d8 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 6 Dec 2022 12:07:48 -0800 Subject: [PATCH 0356/1752] Add `PySide6` to list of installed Qt versions --- IPython/external/qt_loaders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index 39ea298460b..87a6c3d5084 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -377,12 +377,14 @@ def load_qt(api_options): PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s PySide >= 1.0.3 installed: %s PySide2 installed: %s + PySide6 installed: %s Tried to load: %r """ % (loaded_api(), has_binding(QT_API_PYQT), has_binding(QT_API_PYQT5), has_binding(QT_API_PYSIDE), has_binding(QT_API_PYSIDE2), + has_binding(QT_API_PYSIDE6), api_options)) From 749aadd303c0dca64460df936da3efa71457760a Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 6 Dec 2022 12:07:56 -0800 Subject: [PATCH 0357/1752] Initial notes --- IPython/external/qt_for_kernel.py | 5 +++++ IPython/terminal/interactiveshell.py | 1 + IPython/terminal/pt_inputhooks/__init__.py | 5 +++++ IPython/terminal/pt_inputhooks/qt.py | 2 ++ 4 files changed, 13 insertions(+) diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index b3168f6e2e0..f883a8514e3 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -23,6 +23,9 @@ else: use what QT_API says + Note that %gui's implementation will always set a `QT_API`, see + `IPython.terminal.pt_inputhooks.get_inputhook_name_and_func` + """ # NOTE: This is no longer an external, third-party module, and should be # considered part of IPython. For compatibility however, it is being kept in @@ -99,6 +102,7 @@ def get_options(): return [loaded] mpl = sys.modules.get('matplotlib', None) + print(f'{mpl=}') # will be None of matplotlib has not yet been imported if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"): # 1.0.1 only supports PyQt4 v1 @@ -120,6 +124,7 @@ def get_options(): raise RuntimeError("Invalid Qt API %r, valid values are: %r" % (qt_api, ', '.join(_qt_apis))) else: + print(f'{qt_api=}') return [qt_api] diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index c867b553f2e..31ef868f417 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -712,6 +712,7 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): if gui and (gui not in {"inline", "webagg"}): + # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) else: self.active_eventloop = self._inputhook = None diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 69ff0ba1e6a..3095e99a74c 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -55,8 +55,13 @@ def get_inputhook_name_and_func(gui): os.environ["QT_API"] = "pyqt5" gui_mod = "qt" elif gui == "qt6": + # XXX: this locks us into pyqt6 even if pyside6 is installed. os.environ["QT_API"] = "pyqt6" gui_mod = "qt" + print(f'{gui_mod=}') + # Note: `IPython.terminal.pt_inputhooks.qt` imports `IPython.external.qt_for_kernel` and that's + # where the environment variable locks us into `pyqt6`, despite the fact that it seems `PySide6` + # is supported. mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod) return gui, mod.inputhook diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index f1e710aff54..bed1e62c388 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -1,5 +1,6 @@ import sys import os +#`qt_for_kernel` will import the "best" qt version from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper from IPython import get_ipython @@ -63,6 +64,7 @@ def inputhook(context): timer = QtCore.QTimer() timer.timeout.connect(event_loop.quit) while not context.input_is_ready(): + # NOTE: run the event loop, and after 50 ms, call `quit` to exit it. timer.start(50) # 50 ms _exec(event_loop) timer.stop() From 10af8213f0f617582630de243822f9768a31e11f Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 09:58:20 -0800 Subject: [PATCH 0358/1752] Initial notes to see call flow --- IPython/external/qt_for_kernel.py | 3 +++ IPython/terminal/interactiveshell.py | 1 + IPython/terminal/pt_inputhooks/__init__.py | 1 + IPython/terminal/pt_inputhooks/qt.py | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index f883a8514e3..86c0101d4fd 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -95,6 +95,7 @@ def matplotlib_options(mpl): mpqt) def get_options(): + print(f'`get_options` called with {os.environ.get("QT_API", None)=}') """Return a list of acceptable QT APIs, in decreasing order of preference.""" #already imported Qt somewhere. Use that loaded = loaded_api() @@ -129,5 +130,7 @@ def get_options(): api_opts = get_options() +print(f'Importing `IPython.terminal.pt_inputhooks.qt` with {api_opts=}') QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts) +print(f'Loaded Qt with {QT_API=}') enum_helper = enum_factory(QT_API, QtCore) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 31ef868f417..ef12546eee8 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -711,6 +711,7 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): + print(f'Someone called `enable_gui` with {gui=}.') if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 3095e99a74c..8ae82f1c850 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -41,6 +41,7 @@ def __str__(self): def get_inputhook_name_and_func(gui): + print(f'`get_inputhook_name_and_func` called with {gui=}') if gui in registered: return gui, registered[gui] diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index bed1e62c388..d18a339ab1e 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -57,7 +57,7 @@ def inputhook(context): QtCore.QTimer.singleShot(0, _reclaim_excepthook) event_loop = QtCore.QEventLoop(app) - + print(f'`inputhook` running Qt {QtCore.qVersion()} event loop.\r') if sys.platform == 'win32': # The QSocketNotifier method doesn't appear to work on Windows. # Use polling instead. From c5bfe33b9f00afaa042d54d1d7ddf439717ae90a Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Fri, 9 Dec 2022 11:22:41 -0800 Subject: [PATCH 0359/1752] Update error message to include `PyQt6` --- IPython/external/qt_loaders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index 87a6c3d5084..6566838d286 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -369,12 +369,13 @@ def load_qt(api_options): else: raise ImportError(""" Could not load requested Qt binding. Please ensure that - PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available, - and only one is imported per session. + PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or + PySide6is available, and only one is imported per session. Currently-imported Qt library: %r PyQt4 available (requires QtCore, QtGui, QtSvg): %s PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s + PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s PySide >= 1.0.3 installed: %s PySide2 installed: %s PySide6 installed: %s @@ -382,6 +383,7 @@ def load_qt(api_options): """ % (loaded_api(), has_binding(QT_API_PYQT), has_binding(QT_API_PYQT5), + has_binding(QT_API_PYQT6), has_binding(QT_API_PYSIDE), has_binding(QT_API_PYSIDE2), has_binding(QT_API_PYSIDE6), From 815bbd76a2c18e1869c656561884c1cc6fa55109 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 14:58:40 -0800 Subject: [PATCH 0360/1752] More debugging print statements --- IPython/terminal/interactiveshell.py | 1 + IPython/terminal/pt_inputhooks/qt.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index ef12546eee8..83c0bd783ad 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -716,6 +716,7 @@ def enable_gui(self, gui=None): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) else: + print(f'Disconnecting event loop {self._inputhook=}') self.active_eventloop = self._inputhook = None # For prompt_toolkit 3.0. We have to create an asyncio event loop with diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index d18a339ab1e..370494f3980 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -21,6 +21,9 @@ def _reclaim_excepthook(): sys.excepthook = shell.excepthook +announced = 0 + + def inputhook(context): global _appref app = QtCore.QCoreApplication.instance() @@ -57,7 +60,14 @@ def inputhook(context): QtCore.QTimer.singleShot(0, _reclaim_excepthook) event_loop = QtCore.QEventLoop(app) - print(f'`inputhook` running Qt {QtCore.qVersion()} event loop.\r') + global announced + if announced == 0: + print(f'`inputhook` running Qt {QtCore.qVersion()} event loop.\r') + announced += 1 + elif announced == 10: + announced = 0 + else: + announced += 1 if sys.platform == 'win32': # The QSocketNotifier method doesn't appear to work on Windows. # Use polling instead. From 4d707a61fa42eccdf842b8b1aa506a8036648471 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 14:59:26 -0800 Subject: [PATCH 0361/1752] Bring in Qt import logic from `ipykernel` See https://github.com/ipython/ipykernel/pull/1054 --- IPython/terminal/pt_inputhooks/__init__.py | 91 +++++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 8ae82f1c850..1d29e98646c 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -40,6 +40,82 @@ def __str__(self): ', '.join(backends + sorted(registered))) +def set_qt_api(gui): + """Sets the `QT_API` environment variable if it isn't already set.""" + # TODO: how do we do this here? + # if hasattr(kernel, "app"): + # raise RuntimeError("Kernel already running a Qt event loop.") + + # if gui != "qt" and hasattr(kernel, "last_qt_version"): + # if kernel.last_qt_version != gui: + # raise ValueError( + # "Cannot switch Qt versions for this session; " + # f"must use {kernel.last_qt_version}." + # ) + + qt_api = os.environ.get("QT_API", None) + if qt_api is not None and gui != "qt": + env2gui = { + "pyside": "qt4", + "pyqt": "qt4", + "pyside2": "qt5", + "pyqt5": "qt5", + "pyside6": "qt6", + "pyqt6": "qt6", + } + if env2gui[qt_api] != gui: + print( + f'Request for "{gui}" will be ignored because `QT_API` ' + f'environment variable is set to "{qt_api}"' + ) + else: + if gui == "qt4": + try: + import PyQt # noqa + + os.environ["QT_API"] = "pyqt" + except ImportError: + try: + import PySide # noqa + + os.environ["QT_API"] = "pyside" + except ImportError: + # Neither implementation installed; set it to something so IPython gives an error + os.environ["QT_API"] = "pyqt" + elif gui == "qt5": + try: + import PyQt5 # noqa + + os.environ["QT_API"] = "pyqt5" + except ImportError: + try: + import PySide2 # noqa + + os.environ["QT_API"] = "pyside2" + except ImportError: + os.environ["QT_API"] = "pyqt5" + elif gui == "qt6": + try: + import PyQt6 # noqa + + os.environ["QT_API"] = "pyqt6" + except ImportError: + try: + import PySide6 # noqa + + os.environ["QT_API"] = "pyside6" + except ImportError: + os.environ["QT_API"] = "pyqt6" + elif gui == "qt": + # Don't set QT_API; let IPython logic choose the version. + if "QT_API" in os.environ.keys(): + del os.environ["QT_API"] + else: + raise ValueError( + f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' + ) + + def get_inputhook_name_and_func(gui): print(f'`get_inputhook_name_and_func` called with {gui=}') if gui in registered: @@ -49,20 +125,13 @@ def get_inputhook_name_and_func(gui): raise UnknownBackend(gui) if gui in aliases: + print('gui has an alias') return get_inputhook_name_and_func(aliases[gui]) gui_mod = gui - if gui == "qt5": - os.environ["QT_API"] = "pyqt5" - gui_mod = "qt" - elif gui == "qt6": - # XXX: this locks us into pyqt6 even if pyside6 is installed. - os.environ["QT_API"] = "pyqt6" + if gui.startswith("qt"): + set_qt_api(gui) gui_mod = "qt" - print(f'{gui_mod=}') - # Note: `IPython.terminal.pt_inputhooks.qt` imports `IPython.external.qt_for_kernel` and that's - # where the environment variable locks us into `pyqt6`, despite the fact that it seems `PySide6` - # is supported. - mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod) + mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod) return gui, mod.inputhook From 5c57ffec50e1e86013a236f922ea9a53baec427e Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 16:08:38 -0800 Subject: [PATCH 0362/1752] Don't allow switching gui without detatching --- IPython/terminal/interactiveshell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 83c0bd783ad..5fd51e5ee4f 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -712,6 +712,8 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): print(f'Someone called `enable_gui` with {gui=}.') + if self._inputhook is not None and gui is not None: + raise RuntimeError("Shell already running a gui event loop.") if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) From 453a46210b67ceb58bf0498b3cba22537acc1d25 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 16:10:15 -0800 Subject: [PATCH 0363/1752] Keep track of Qt version since it can't change --- IPython/terminal/pt_inputhooks/__init__.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 1d29e98646c..9a87ee23ce5 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -40,18 +40,19 @@ def __str__(self): ', '.join(backends + sorted(registered))) +last_qt_version = None # stores which version (i.e. `gui`) was requested the first time. + def set_qt_api(gui): """Sets the `QT_API` environment variable if it isn't already set.""" - # TODO: how do we do this here? - # if hasattr(kernel, "app"): - # raise RuntimeError("Kernel already running a Qt event loop.") - # if gui != "qt" and hasattr(kernel, "last_qt_version"): - # if kernel.last_qt_version != gui: - # raise ValueError( - # "Cannot switch Qt versions for this session; " - # f"must use {kernel.last_qt_version}." - # ) + global last_qt_version + + if gui != "qt" and last_qt_version is not None: + if last_qt_version != gui: + raise ValueError( + "Cannot switch Qt versions for this session; " + f"must use {last_qt_version}." + ) qt_api = os.environ.get("QT_API", None) if qt_api is not None and gui != "qt": @@ -115,6 +116,9 @@ def set_qt_api(gui): f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' ) + # Due to the import mechanism, we can't change Qt versions once we've chosen one. So we tag the + # version so we can check for this and give an error. + last_qt_version = gui def get_inputhook_name_and_func(gui): print(f'`get_inputhook_name_and_func` called with {gui=}') From 0833adb1c1fec59950622acf60ad43e32d5a51e3 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 16:10:30 -0800 Subject: [PATCH 0364/1752] More debug printing --- IPython/external/qt_for_kernel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index 86c0101d4fd..41c1704dfec 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -100,6 +100,7 @@ def get_options(): #already imported Qt somewhere. Use that loaded = loaded_api() if loaded is not None: + print(f'`QtCore` already imported: {loaded=}') return [loaded] mpl = sys.modules.get('matplotlib', None) From 6b26292d659c967bd779be3ca439cfdb7ec5de1f Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 16:25:01 -0800 Subject: [PATCH 0365/1752] Formatting --- IPython/external/qt_for_kernel.py | 12 +++++------ IPython/external/qt_loaders.py | 24 +++++++++++++--------- IPython/terminal/interactiveshell.py | 6 +++--- IPython/terminal/pt_inputhooks/__init__.py | 8 +++++--- IPython/terminal/pt_inputhooks/qt.py | 3 +-- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index 41c1704dfec..956b08ce9ae 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -100,11 +100,11 @@ def get_options(): #already imported Qt somewhere. Use that loaded = loaded_api() if loaded is not None: - print(f'`QtCore` already imported: {loaded=}') + print(f"`QtCore` already imported: {loaded=}") return [loaded] - mpl = sys.modules.get('matplotlib', None) - print(f'{mpl=}') # will be None of matplotlib has not yet been imported + mpl = sys.modules.get("matplotlib", None) + print(f"{mpl=}") # will be None of matplotlib has not yet been imported if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"): # 1.0.1 only supports PyQt4 v1 @@ -126,12 +126,12 @@ def get_options(): raise RuntimeError("Invalid Qt API %r, valid values are: %r" % (qt_api, ', '.join(_qt_apis))) else: - print(f'{qt_api=}') + print(f"{qt_api=}") return [qt_api] api_opts = get_options() -print(f'Importing `IPython.terminal.pt_inputhooks.qt` with {api_opts=}') +print(f"Importing `IPython.terminal.pt_inputhooks.qt` with {api_opts=}") QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts) -print(f'Loaded Qt with {QT_API=}') +print(f"Loaded Qt with {QT_API=}") enum_helper = enum_factory(QT_API, QtCore) diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index 6566838d286..b3fd6a7e1e0 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -367,7 +367,8 @@ def load_qt(api_options): commit_api(api) return result else: - raise ImportError(""" + raise ImportError( + """ Could not load requested Qt binding. Please ensure that PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or PySide6is available, and only one is imported per session. @@ -380,15 +381,18 @@ def load_qt(api_options): PySide2 installed: %s PySide6 installed: %s Tried to load: %r - """ % (loaded_api(), - has_binding(QT_API_PYQT), - has_binding(QT_API_PYQT5), - has_binding(QT_API_PYQT6), - has_binding(QT_API_PYSIDE), - has_binding(QT_API_PYSIDE2), - has_binding(QT_API_PYSIDE6), - api_options)) - + """ + % ( + loaded_api(), + has_binding(QT_API_PYQT), + has_binding(QT_API_PYQT5), + has_binding(QT_API_PYQT6), + has_binding(QT_API_PYSIDE), + has_binding(QT_API_PYSIDE2), + has_binding(QT_API_PYSIDE6), + api_options, + ) + ) def enum_factory(QT_API, QtCore): """Construct an enum helper to account for PyQt5 <-> PyQt6 changes.""" diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 5fd51e5ee4f..9bc4114e933 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -460,7 +460,7 @@ def prompt(): enable_open_in_editor=self.extra_open_editor_shortcuts, color_depth=self.color_depth, tempfile_suffix=".py", - **self._extra_prompt_options() + **self._extra_prompt_options(), ) def _make_style_from_name_or_cls(self, name_or_cls): @@ -711,14 +711,14 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): - print(f'Someone called `enable_gui` with {gui=}.') + print(f"Someone called `enable_gui` with {gui=}.") if self._inputhook is not None and gui is not None: raise RuntimeError("Shell already running a gui event loop.") if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) else: - print(f'Disconnecting event loop {self._inputhook=}') + print(f"Disconnecting event loop {self._inputhook=}") self.active_eventloop = self._inputhook = None # For prompt_toolkit 3.0. We have to create an asyncio event loop with diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 9a87ee23ce5..8b714509568 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -40,7 +40,9 @@ def __str__(self): ', '.join(backends + sorted(registered))) -last_qt_version = None # stores which version (i.e. `gui`) was requested the first time. +last_qt_version = None +"""Stores which version (i.e. `gui`) was requested the first time.""" + def set_qt_api(gui): """Sets the `QT_API` environment variable if it isn't already set.""" @@ -121,7 +123,7 @@ def set_qt_api(gui): last_qt_version = gui def get_inputhook_name_and_func(gui): - print(f'`get_inputhook_name_and_func` called with {gui=}') + print(f"`get_inputhook_name_and_func` called with {gui=}") if gui in registered: return gui, registered[gui] @@ -129,7 +131,7 @@ def get_inputhook_name_and_func(gui): raise UnknownBackend(gui) if gui in aliases: - print('gui has an alias') + print("gui has an alias") return get_inputhook_name_and_func(aliases[gui]) gui_mod = gui diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index 370494f3980..d8b4536acdf 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -1,6 +1,5 @@ import sys import os -#`qt_for_kernel` will import the "best" qt version from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper from IPython import get_ipython @@ -62,7 +61,7 @@ def inputhook(context): event_loop = QtCore.QEventLoop(app) global announced if announced == 0: - print(f'`inputhook` running Qt {QtCore.qVersion()} event loop.\r') + print(f"`inputhook` running Qt {QtCore.qVersion()} event loop.") announced += 1 elif announced == 10: announced = 0 From 005205a19c17ee5715cf81639cf2533bed06176c Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 12 Dec 2022 16:29:12 -0800 Subject: [PATCH 0366/1752] More formatting --- IPython/external/qt_for_kernel.py | 2 +- IPython/external/qt_loaders.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index 956b08ce9ae..f88b16bb381 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -104,7 +104,7 @@ def get_options(): return [loaded] mpl = sys.modules.get("matplotlib", None) - print(f"{mpl=}") # will be None of matplotlib has not yet been imported + print(f"{mpl=}") # will be None of matplotlib has not yet been imported if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"): # 1.0.1 only supports PyQt4 v1 diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index b3fd6a7e1e0..827220e85be 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -394,6 +394,7 @@ def load_qt(api_options): ) ) + def enum_factory(QT_API, QtCore): """Construct an enum helper to account for PyQt5 <-> PyQt6 changes.""" From e49d78daac8d975bde23f09e51d12dace9e74e33 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 12:56:08 -0800 Subject: [PATCH 0367/1752] A note that Qt4 is effectively disabled --- IPython/terminal/pt_inputhooks/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 8b714509568..a056202ff5e 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -72,6 +72,7 @@ def set_qt_api(gui): f'environment variable is set to "{qt_api}"' ) else: + # NOTE: 'qt4' is not selectable because it's set as an alias for 'qt'; see `aliases` above. if gui == "qt4": try: import PyQt # noqa From 63d3670b8feb4645775b691eb5690f72d7516f6d Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 18:34:14 -0800 Subject: [PATCH 0368/1752] Fix typo --- IPython/external/qt_loaders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index 827220e85be..f3831dac801 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -370,8 +370,8 @@ def load_qt(api_options): raise ImportError( """ Could not load requested Qt binding. Please ensure that - PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or - PySide6is available, and only one is imported per session. + PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or + PySide6 is available, and only one is imported per session. Currently-imported Qt library: %r PyQt4 available (requires QtCore, QtGui, QtSvg): %s From 7bf21b53baa0fec30675cee6c39c27e5f17325a3 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 18:34:44 -0800 Subject: [PATCH 0369/1752] Use existing constants --- IPython/terminal/pt_inputhooks/__init__.py | 53 ++++++++++++---------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index a056202ff5e..7a041890dfa 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -40,33 +40,41 @@ def __str__(self): ', '.join(backends + sorted(registered))) -last_qt_version = None -"""Stores which version (i.e. `gui`) was requested the first time.""" - - def set_qt_api(gui): """Sets the `QT_API` environment variable if it isn't already set.""" - global last_qt_version + qt_api = os.environ.get("QT_API", None) - if gui != "qt" and last_qt_version is not None: - if last_qt_version != gui: - raise ValueError( - "Cannot switch Qt versions for this session; " - f"must use {last_qt_version}." + from IPython.external.qt_loaders import ( + QT_API_PYQT, + QT_API_PYQT5, + QT_API_PYQT6, + QT_API_PYSIDE, + QT_API_PYSIDE2, + QT_API_PYSIDE6, + QT_API_PYQTv1, + loaded_api, + ) + + loaded = loaded_api() + + qt_env2gui = { + QT_API_PYSIDE: 'qt4', + QT_API_PYQTv1: 'qt4', + QT_API_PYQT: 'qt4', + QT_API_PYSIDE2: 'qt5', + QT_API_PYQT5: 'qt5', + QT_API_PYSIDE6: 'qt6', + QT_API_PYQT6: 'qt6', + } + if loaded is not None and gui != 'qt': + if qt_env2gui[loaded] != gui: + raise ImportError( + f'Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}.' ) - qt_api = os.environ.get("QT_API", None) - if qt_api is not None and gui != "qt": - env2gui = { - "pyside": "qt4", - "pyqt": "qt4", - "pyside2": "qt5", - "pyqt5": "qt5", - "pyside6": "qt6", - "pyqt6": "qt6", - } - if env2gui[qt_api] != gui: + if qt_api is not None and gui != 'qt': + if qt_env2gui[qt_api] != gui: print( f'Request for "{gui}" will be ignored because `QT_API` ' f'environment variable is set to "{qt_api}"' @@ -119,9 +127,6 @@ def set_qt_api(gui): f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' ) - # Due to the import mechanism, we can't change Qt versions once we've chosen one. So we tag the - # version so we can check for this and give an error. - last_qt_version = gui def get_inputhook_name_and_func(gui): print(f"`get_inputhook_name_and_func` called with {gui=}") From d30f8950b9cb8ddc8cb4b626190b3b50792be38c Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 18:47:39 -0800 Subject: [PATCH 0370/1752] TST: smoke test for `get_inputhook_name_and_func` --- IPython/terminal/tests/test_pt_inputhooks.py | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 IPython/terminal/tests/test_pt_inputhooks.py diff --git a/IPython/terminal/tests/test_pt_inputhooks.py b/IPython/terminal/tests/test_pt_inputhooks.py new file mode 100644 index 00000000000..b9aa4764c0c --- /dev/null +++ b/IPython/terminal/tests/test_pt_inputhooks.py @@ -0,0 +1,48 @@ +import os +import importlib + +import pytest + +from IPython.terminal.pt_inputhooks import set_qt_api, get_inputhook_name_and_func + + +guis_avail = [] + + +def _get_qt_vers(): + """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due + to the import mechanism, we can't import multiple versions of Qt in one session.""" + for gui in ['qt', 'qt6', 'qt5', 'qt4']: + print(f'Trying {gui}') + try: + set_qt_api(gui) + importlib.import_module("IPython.terminal.pt_inputhooks.qt") + guis_avail.append(gui) + if 'QT_API' in os.environ.keys(): + del os.environ['QT_API'] + except ImportError: + pass # that version of Qt isn't available. + except RuntimeError: + pass # the version of IPython doesn't know what to do with this Qt version. + + +_get_qt_vers() + + +@pytest.mark.skipif(len(guis_avail) == 0, reason='No viable version of PyQt or PySide installed.') +def test_inputhook_qt(): + gui = guis_avail[0] + + # Choose a qt version and get the input hook function. This will import Qt... + get_inputhook_name_and_func(gui) + + # ...and now we're stuck with this version of Qt for good; can't switch. + for not_gui in ['qt6', 'qt5', 'qt4']: + if not_gui not in guis_avail: + break + + with pytest.raises(ImportError): + get_inputhook_name_and_func(not_gui) + + # A gui of 'qt' means "best available", or in this case, the last one that was used. + get_inputhook_name_and_func('qt') From bf4956cbc9e012f69ba00cf39285cfd23e70d008 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 20 Dec 2022 10:12:23 -0800 Subject: [PATCH 0371/1752] Remove debug print statements --- IPython/external/qt_for_kernel.py | 6 ------ IPython/terminal/interactiveshell.py | 2 -- IPython/terminal/pt_inputhooks/__init__.py | 2 -- IPython/terminal/pt_inputhooks/qt.py | 12 +----------- 4 files changed, 1 insertion(+), 21 deletions(-) diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index f88b16bb381..986cae3321e 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -95,16 +95,13 @@ def matplotlib_options(mpl): mpqt) def get_options(): - print(f'`get_options` called with {os.environ.get("QT_API", None)=}') """Return a list of acceptable QT APIs, in decreasing order of preference.""" #already imported Qt somewhere. Use that loaded = loaded_api() if loaded is not None: - print(f"`QtCore` already imported: {loaded=}") return [loaded] mpl = sys.modules.get("matplotlib", None) - print(f"{mpl=}") # will be None of matplotlib has not yet been imported if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"): # 1.0.1 only supports PyQt4 v1 @@ -126,12 +123,9 @@ def get_options(): raise RuntimeError("Invalid Qt API %r, valid values are: %r" % (qt_api, ', '.join(_qt_apis))) else: - print(f"{qt_api=}") return [qt_api] api_opts = get_options() -print(f"Importing `IPython.terminal.pt_inputhooks.qt` with {api_opts=}") QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts) -print(f"Loaded Qt with {QT_API=}") enum_helper = enum_factory(QT_API, QtCore) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 9bc4114e933..2179010f546 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -711,14 +711,12 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): - print(f"Someone called `enable_gui` with {gui=}.") if self._inputhook is not None and gui is not None: raise RuntimeError("Shell already running a gui event loop.") if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) else: - print(f"Disconnecting event loop {self._inputhook=}") self.active_eventloop = self._inputhook = None # For prompt_toolkit 3.0. We have to create an asyncio event loop with diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 7a041890dfa..ff13ee4fd9b 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -129,7 +129,6 @@ def set_qt_api(gui): def get_inputhook_name_and_func(gui): - print(f"`get_inputhook_name_and_func` called with {gui=}") if gui in registered: return gui, registered[gui] @@ -137,7 +136,6 @@ def get_inputhook_name_and_func(gui): raise UnknownBackend(gui) if gui in aliases: - print("gui has an alias") return get_inputhook_name_and_func(aliases[gui]) gui_mod = gui diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index d8b4536acdf..cf6d11ea6ca 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -20,9 +20,6 @@ def _reclaim_excepthook(): sys.excepthook = shell.excepthook -announced = 0 - - def inputhook(context): global _appref app = QtCore.QCoreApplication.instance() @@ -59,14 +56,7 @@ def inputhook(context): QtCore.QTimer.singleShot(0, _reclaim_excepthook) event_loop = QtCore.QEventLoop(app) - global announced - if announced == 0: - print(f"`inputhook` running Qt {QtCore.qVersion()} event loop.") - announced += 1 - elif announced == 10: - announced = 0 - else: - announced += 1 + if sys.platform == 'win32': # The QSocketNotifier method doesn't appear to work on Windows. # Use polling instead. From 18cf490405428a86180cd6faf6790f6e6b1e7219 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Tue, 20 Dec 2022 10:34:06 -0800 Subject: [PATCH 0372/1752] Formatting --- IPython/terminal/pt_inputhooks/__init__.py | 20 ++++++++++---------- IPython/terminal/tests/test_pt_inputhooks.py | 16 +++++++++------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index ff13ee4fd9b..146424c0a34 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -59,21 +59,21 @@ def set_qt_api(gui): loaded = loaded_api() qt_env2gui = { - QT_API_PYSIDE: 'qt4', - QT_API_PYQTv1: 'qt4', - QT_API_PYQT: 'qt4', - QT_API_PYSIDE2: 'qt5', - QT_API_PYQT5: 'qt5', - QT_API_PYSIDE6: 'qt6', - QT_API_PYQT6: 'qt6', + QT_API_PYSIDE: "qt4", + QT_API_PYQTv1: "qt4", + QT_API_PYQT: "qt4", + QT_API_PYSIDE2: "qt5", + QT_API_PYQT5: "qt5", + QT_API_PYSIDE6: "qt6", + QT_API_PYQT6: "qt6", } - if loaded is not None and gui != 'qt': + if loaded is not None and gui != "qt": if qt_env2gui[loaded] != gui: raise ImportError( - f'Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}.' + f"Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}." ) - if qt_api is not None and gui != 'qt': + if qt_api is not None and gui != "qt": if qt_env2gui[qt_api] != gui: print( f'Request for "{gui}" will be ignored because `QT_API` ' diff --git a/IPython/terminal/tests/test_pt_inputhooks.py b/IPython/terminal/tests/test_pt_inputhooks.py index b9aa4764c0c..775d1c7ae6f 100644 --- a/IPython/terminal/tests/test_pt_inputhooks.py +++ b/IPython/terminal/tests/test_pt_inputhooks.py @@ -12,14 +12,14 @@ def _get_qt_vers(): """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due to the import mechanism, we can't import multiple versions of Qt in one session.""" - for gui in ['qt', 'qt6', 'qt5', 'qt4']: - print(f'Trying {gui}') + for gui in ["qt", "qt6", "qt5", "qt4"]: + print(f"Trying {gui}") try: set_qt_api(gui) importlib.import_module("IPython.terminal.pt_inputhooks.qt") guis_avail.append(gui) - if 'QT_API' in os.environ.keys(): - del os.environ['QT_API'] + if "QT_API" in os.environ.keys(): + del os.environ["QT_API"] except ImportError: pass # that version of Qt isn't available. except RuntimeError: @@ -29,7 +29,9 @@ def _get_qt_vers(): _get_qt_vers() -@pytest.mark.skipif(len(guis_avail) == 0, reason='No viable version of PyQt or PySide installed.') +@pytest.mark.skipif( + len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed." +) def test_inputhook_qt(): gui = guis_avail[0] @@ -37,7 +39,7 @@ def test_inputhook_qt(): get_inputhook_name_and_func(gui) # ...and now we're stuck with this version of Qt for good; can't switch. - for not_gui in ['qt6', 'qt5', 'qt4']: + for not_gui in ["qt6", "qt5", "qt4"]: if not_gui not in guis_avail: break @@ -45,4 +47,4 @@ def test_inputhook_qt(): get_inputhook_name_and_func(not_gui) # A gui of 'qt' means "best available", or in this case, the last one that was used. - get_inputhook_name_and_func('qt') + get_inputhook_name_and_func("qt") From cc74642003f1ab22415ae0a32a4aa9548e3e398c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 14:04:45 +0100 Subject: [PATCH 0373/1752] whats new version8.8 --- docs/source/whatsnew/version8.rst | 66 ++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index d3c33704bad..e1d4574e452 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,44 @@ 8.x Series ============ +.. _version 8.8.0: + +IPython 8.8.0 +------------- + +First release of IPython in 2023 as there was no release at the end of +December. + +This is an unusually big release (relatively speaking) with more than 15 Pull +Requests merge. + +Of particular interest are: + + - :ghpull:`13852` that replace the greedy completer and improve + completion, in particular for dictionary keys. + - :ghpull:`13858` that adds ``py.typed`` to ``setup.cfg`` to make sure it is + bundled in wheels. + - :ghpull:`13869` that implements tab completions for IPython options in the + shell when using `argcomplete `. I + believe this also needs a recent version of Traitlets. + - :ghpull:`13865` makes the ``inspector`` class of `InteractiveShell` + configurable. + - :ghpull:`13880` that remove minor-version entrypoints as the minor version + entry points that would be included in the wheel would be the one of the + Python version that was used to build the ``whl`` file. + +In no particular order, the rest of the changes update the test suite to be +compatible with Pygments 2.14, various docfixes, testing on more recent python +versions and various updates. + +As usual you can find the full list of PRs on GitHub under `the 8.8 milestone +`__. + +Many thanks to @krassowski for the many PRs and @jasongrout for reviewing and +merging contributions. + +Thanks to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. .. _version 8.7.0: @@ -138,7 +176,7 @@ Here is a non exhaustive list of changes that have been implemented for IPython - Fix paste magic on wayland. :ghpull:`13671` - show maxlen in deque's repr. :ghpull:`13648` -Restore line numbers for Input +Restore line numbers for Input ------------------------------ Line number information in tracebacks from input are restored. @@ -269,7 +307,7 @@ Thanks to the `D. E. Shaw group `__ for sponsoring work on IPython and related libraries. .. _version 8.1.1: - + IPython 8.1.1 ------------- @@ -403,10 +441,10 @@ The 8.x branch started diverging from its predecessor around IPython 7.12 (January 2020). This release contains 250+ pull requests, in addition to many of the features -and backports that have made it to the 7.x branch. Please see the +and backports that have made it to the 7.x branch. Please see the `8.0 milestone `__ for the full list of pull requests. -Please feel free to send pull requests to updates those notes after release, +Please feel free to send pull requests to updates those notes after release, I have likely forgotten a few things reviewing 250+ PRs. Dependencies changes/downstream packaging @@ -421,7 +459,7 @@ looking for help to do so. - minimal Python is now 3.8 - ``nose`` is not a testing requirement anymore - ``pytest`` replaces nose. - - ``iptest``/``iptest3`` cli entrypoints do not exists anymore. + - ``iptest``/``iptest3`` cli entrypoints do not exists anymore. - minimum officially support ``numpy`` version has been bumped, but this should not have much effect on packaging. @@ -443,7 +481,7 @@ deprecation warning: - Please add **since which version** something is deprecated. As a side note, it is much easier to conditionally compare version -numbers rather than using ``try/except`` when functionality changes with a version. +numbers rather than using ``try/except`` when functionality changes with a version. I won't list all the removed features here, but modules like ``IPython.kernel``, which was just a shim module around ``ipykernel`` for the past 8 years, have been @@ -475,7 +513,7 @@ by mypy. Featured changes ---------------- -Here is a features list of changes in IPython 8.0. This is of course non-exhaustive. +Here is a features list of changes in IPython 8.0. This is of course non-exhaustive. Please note as well that many features have been added in the 7.x branch as well (and hence why you want to read the 7.x what's new notes), in particular features contributed by QuantStack (with respect to debugger protocol and Xeus @@ -523,7 +561,7 @@ The error traceback is now correctly formatted, showing the cell number in which ZeroDivisionError: division by zero -The ``stack_data`` package has been integrated, which provides smarter information in the traceback; +The ``stack_data`` package has been integrated, which provides smarter information in the traceback; in particular it will highlight the AST node where an error occurs which can help to quickly narrow down errors. For example in the following snippet:: @@ -563,7 +601,7 @@ and IPython 8.0 is capable of telling you where the index error occurs:: ----> 3 return x[0][i][0] ^^^^^^^ -The corresponding locations marked here with ``^`` will show up highlighted in +The corresponding locations marked here with ``^`` will show up highlighted in the terminal and notebooks. Finally, a colon ``::`` and line number is appended after a filename in @@ -760,7 +798,7 @@ Previously, this was not the case for the Vi-mode prompts:: This is now fixed, and Vi prompt prefixes - ``[ins]`` and ``[nav]`` - are skipped just as the normal ``In`` would be. -IPython shell can be started in the Vi mode using ``ipython --TerminalInteractiveShell.editing_mode=vi``, +IPython shell can be started in the Vi mode using ``ipython --TerminalInteractiveShell.editing_mode=vi``, You should be able to change mode dynamically with ``%config TerminalInteractiveShell.editing_mode='vi'`` Empty History Ranges @@ -787,8 +825,8 @@ when followed with :kbd:`F2`), send it to `dpaste.org `_ using Windows timing implementation: Switch to process_time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter`` -(which counted time even when the process was sleeping) to being based on ``time.process_time`` instead +Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter`` +(which counted time even when the process was sleeping) to being based on ``time.process_time`` instead (which only counts CPU time). This brings it closer to the behavior on Linux. See :ghpull:`12984`. Miscellaneous @@ -813,7 +851,7 @@ Re-added support for XDG config directories ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ XDG support through the years comes and goes. There is a tension between having -an identical location for configuration in all platforms versus having simple instructions. +an identical location for configuration in all platforms versus having simple instructions. After initial failures a couple of years ago, IPython was modified to automatically migrate XDG config files back into ``~/.ipython``. That migration code has now been removed. IPython now checks the XDG locations, so if you _manually_ move your config @@ -841,7 +879,7 @@ Removing support for older Python versions We are removing support for Python up through 3.7, allowing internal code to use the more -efficient ``pathlib`` and to make better use of type annotations. +efficient ``pathlib`` and to make better use of type annotations. .. image:: ../_images/8.0/pathlib_pathlib_everywhere.jpg :alt: "Meme image of Toy Story with Woody and Buzz, with the text 'pathlib, pathlib everywhere'" From 198bb721ef48a538133fa4d646828e6838856113 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 14:35:34 +0100 Subject: [PATCH 0374/1752] Misc release process update A few of those things failed during release time, as I was building docs on 3.10 (not 3.11) after recently upgrading. --- IPython/core/completer.py | 2 +- tools/release_helper.sh | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 5ba8ea0fdbd..f0bbb4e5619 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -256,7 +256,7 @@ JEDI_INSTALLED = False -if TYPE_CHECKING or GENERATING_DOCUMENTATION: +if TYPE_CHECKING or GENERATING_DOCUMENTATION and sys.version_info >= (3, 11): from typing import cast from typing_extensions import TypedDict, NotRequired, Protocol, TypeAlias, TypeGuard else: diff --git a/tools/release_helper.sh b/tools/release_helper.sh index d221f551e66..ebf8098195c 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -2,15 +2,6 @@ # when releasing with bash, simple source it to get asked questions. # misc check before starting - -python -c 'import keyring' -python -c 'import twine' -python -c 'import sphinx' -python -c 'import sphinx_rtd_theme' -python -c 'import pytest' -python -c 'import build' - - BLACK=$(tput setaf 1) RED=$(tput setaf 1) GREEN=$(tput setaf 2) @@ -22,6 +13,22 @@ WHITE=$(tput setaf 7) NOR=$(tput sgr0) +echo "Checking all tools are installed..." + +python -c 'import keyring' +python -c 'import twine' +python -c 'import sphinx' +python -c 'import sphinx_rtd_theme' +python -c 'import pytest' +python -c 'import build' +# those are necessary fo building the docs +echo "Checking imports for docs" +python -c 'import numpy' +python -c 'import matplotlib' + + + + echo "Will use $BLUE'$EDITOR'$NOR to edit files when necessary" echo -n "PREV_RELEASE (X.y.z) [$PREV_RELEASE]: " read input From add5877a42ba8e3960bc92eb994c15955eacf254 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 15:03:24 +0100 Subject: [PATCH 0375/1752] release 8.8.0 --- IPython/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index e2ce2eac2b4..e99b4f3493d 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -# _version_extra = "" # Uncomment this for full releases +_version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 01bc9d96e0dd50fad02d7b1e3d2e58113c617b6b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 3 Jan 2023 15:04:11 +0100 Subject: [PATCH 0376/1752] back to dev --- IPython/core/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/release.py b/IPython/core/release.py index e99b4f3493d..0416637fc7d 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,11 +16,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 8 +_version_minor = 9 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" -_version_extra = "" # Uncomment this for full releases +# _version_extra = "" # Uncomment this for full releases # Construct full version string from these. _ver = [_version_major, _version_minor, _version_patch] From 64e72a955f3ae4eb0ad823936a20364e4475e057 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 8 Jan 2023 00:17:23 +0000 Subject: [PATCH 0377/1752] Restore shortcuts in documentation, define identifiers --- .../{shortcuts.py => shortcuts/__init__.py} | 373 +++++++++--------- IPython/terminal/shortcuts/auto_match.py | 90 +++++ IPython/terminal/shortcuts/autosuggestions.py | 39 ++ docs/autogen_shortcuts.py | 252 +++++++++--- docs/source/_static/theme_overrides.css | 7 + docs/source/conf.py | 5 +- docs/source/config/shortcuts/index.rst | 29 +- 7 files changed, 525 insertions(+), 270 deletions(-) rename IPython/terminal/{shortcuts.py => shortcuts/__init__.py} (62%) create mode 100644 IPython/terminal/shortcuts/auto_match.py create mode 100644 IPython/terminal/shortcuts/autosuggestions.py create mode 100644 docs/source/_static/theme_overrides.css diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts/__init__.py similarity index 62% rename from IPython/terminal/shortcuts.py rename to IPython/terminal/shortcuts/__init__.py index 6ca91ec31ba..6bc6ec7c39f 100644 --- a/IPython/terminal/shortcuts.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -16,14 +16,29 @@ from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER -from prompt_toolkit.filters import (has_focus, has_selection, Condition, - vi_insert_mode, emacs_insert_mode, has_completions, vi_mode) -from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline +from prompt_toolkit.filters import ( + has_focus as has_focus_impl, + has_selection, + Condition, + vi_insert_mode, + emacs_insert_mode, + has_completions, + vi_mode, +) +from prompt_toolkit.key_binding.bindings.completion import ( + display_completions_like_readline, +) from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings import named_commands as nc from prompt_toolkit.key_binding.vi_state import InputMode, ViState +from prompt_toolkit.layout.layout import FocusableElement from IPython.utils.decorators import undoc +from . import auto_match as match, autosuggestions + + +__all__ = ["create_ipython_shortcuts"] + @undoc @Condition @@ -32,80 +47,84 @@ def cursor_in_leading_ws(): return (not before) or before.isspace() -# Needed for to accept autosuggestions in vi insert mode -def _apply_autosuggest(event): - """ - Apply autosuggestion if at end of line. - """ - b = event.current_buffer - d = b.document - after_cursor = d.text[d.cursor_position :] - lines = after_cursor.split("\n") - end_of_current_line = lines[0].strip() - suggestion = b.suggestion - if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): - b.insert_text(suggestion.text) - else: - nc.end_of_line(event) +def has_focus(value: FocusableElement): + """Wrapper around has_focus adding a nice `__name__` to tester function""" + tester = has_focus_impl(value).func + tester.__name__ = f"is_focused({value})" + return Condition(tester) -def create_ipython_shortcuts(shell): - """Set up the prompt_toolkit keyboard shortcuts for IPython""" + +def create_ipython_shortcuts(shell, for_all_platforms: bool = False): + """Set up the prompt_toolkit keyboard shortcuts for IPython.""" + # Warning: if possible, do NOT define handler functions in the locals + # scope of this function, instead define functions in the global + # scope, or a separate module, and include a user-friendly docstring + # describing the action. kb = KeyBindings() insert_mode = vi_insert_mode | emacs_insert_mode - if getattr(shell, 'handle_return', None): + if getattr(shell, "handle_return", None): return_handler = shell.handle_return(shell) else: return_handler = newline_or_execute_outer(shell) - kb.add('enter', filter=(has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - ))(return_handler) - - def reformat_and_execute(event): - reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell) - event.current_buffer.validate_and_handle() + kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))( + return_handler + ) @Condition def ebivim(): return shell.emacs_bindings_in_vi_insert_mode - kb.add( + @kb.add( "escape", "enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim), - )(reformat_and_execute) + ) + def reformat_and_execute(event): + """Reformat code and execute it""" + reformat_text_before_cursor( + event.current_buffer, event.current_buffer.document, shell + ) + event.current_buffer.validate_and_handle() kb.add("c-\\")(quit) - kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) - )(previous_history_or_previous_completion) + kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))( + previous_history_or_previous_completion + ) - kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) - )(next_history_or_next_completion) + kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))( + next_history_or_next_completion + ) - kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions) - )(dismiss_completion) + kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))( + dismiss_completion + ) - kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer) + kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer) - kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) + kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) - supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP')) - kb.add('c-z', filter=supports_suspend)(suspend_to_bg) + supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP")) + kb.add("c-z", filter=supports_suspend)(suspend_to_bg) # Ctrl+I == Tab - kb.add('tab', filter=(has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - & cursor_in_leading_ws - ))(indent_buffer) - kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode) - )(newline_autoindent_outer(shell.input_transformer_manager)) + kb.add( + "tab", + filter=( + has_focus(DEFAULT_BUFFER) + & ~has_selection + & insert_mode + & cursor_in_leading_ws + ), + )(indent_buffer) + kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))( + newline_autoindent_outer(shell.input_transformer_manager) + ) - kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) + kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) @Condition def auto_match(): @@ -146,6 +165,8 @@ def _preceding_text(): before_cursor = app.current_buffer.document.current_line_before_cursor return bool(m.match(before_cursor)) + _preceding_text.__name__ = f"preceding_text({pattern!r})" + condition = Condition(_preceding_text) _preceding_text_cache[pattern] = condition return condition @@ -161,6 +182,8 @@ def _following_text(): app = get_app() return bool(m.match(app.current_buffer.document.current_line_after_cursor)) + _following_text.__name__ = f"following_text({pattern!r})" + condition = Condition(_following_text) _following_text_cache[pattern] = condition return condition @@ -178,151 +201,110 @@ def not_inside_unclosed_string(): return not ('"' in s or "'" in s) # auto match - @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) - def _(event): - event.current_buffer.insert_text("()") - event.current_buffer.cursor_left() - - @kb.add("[", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) - def _(event): - event.current_buffer.insert_text("[]") - event.current_buffer.cursor_left() - - @kb.add("{", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) - def _(event): - event.current_buffer.insert_text("{}") - event.current_buffer.cursor_left() + auto_match_parens = {"(": match.parenthesis, "[": match.brackets, "{": match.braces} + for key, cmd in auto_match_parens.items(): + kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))( + cmd + ) - @kb.add( + kb.add( '"', filter=focused_insert & auto_match & not_inside_unclosed_string & preceding_text(lambda line: all_quotes_paired('"', line)) & following_text(r"[,)}\]]|$"), - ) - def _(event): - event.current_buffer.insert_text('""') - event.current_buffer.cursor_left() + )(match.double_quote) - @kb.add( + kb.add( "'", filter=focused_insert & auto_match & not_inside_unclosed_string & preceding_text(lambda line: all_quotes_paired("'", line)) & following_text(r"[,)}\]]|$"), - ) - def _(event): - event.current_buffer.insert_text("''") - event.current_buffer.cursor_left() + )(match.single_quote) - @kb.add( + kb.add( '"', filter=focused_insert & auto_match & not_inside_unclosed_string & preceding_text(r'^.*""$'), - ) - def _(event): - event.current_buffer.insert_text('""""') - event.current_buffer.cursor_left(3) + )(match.docstring_double_quotes) - @kb.add( + kb.add( "'", filter=focused_insert & auto_match & not_inside_unclosed_string & preceding_text(r"^.*''$"), - ) - def _(event): - event.current_buffer.insert_text("''''") - event.current_buffer.cursor_left(3) + )(match.docstring_single_quotes) # raw string - @kb.add( - "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") - ) - def _(event): - matches = re.match( - r".*(r|R)[\"'](-*)", - event.current_buffer.document.current_line_before_cursor, - ) - dashes = matches.group(2) or "" - event.current_buffer.insert_text("()" + dashes) - event.current_buffer.cursor_left(len(dashes) + 1) + auto_match_parens_raw_string = { + "(": match.raw_string_parenthesis, + "[": match.raw_string_bracket, + "{": match.raw_string_braces, + } + for key, cmd in auto_match_parens_raw_string.items(): + kb.add( + key, + filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"), + )(cmd) - @kb.add( - "[", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + # just move cursor + kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))( + match.skip_over ) - def _(event): - matches = re.match( - r".*(r|R)[\"'](-*)", - event.current_buffer.document.current_line_before_cursor, - ) - dashes = matches.group(2) or "" - event.current_buffer.insert_text("[]" + dashes) - event.current_buffer.cursor_left(len(dashes) + 1) - - @kb.add( - "{", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") + kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))( + match.skip_over + ) + kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))( + match.skip_over + ) + kb.add('"', filter=focused_insert & auto_match & following_text('^"'))( + match.skip_over + ) + kb.add("'", filter=focused_insert & auto_match & following_text("^'"))( + match.skip_over ) - def _(event): - matches = re.match( - r".*(r|R)[\"'](-*)", - event.current_buffer.document.current_line_before_cursor, - ) - dashes = matches.group(2) or "" - event.current_buffer.insert_text("{}" + dashes) - event.current_buffer.cursor_left(len(dashes) + 1) - - # just move cursor - @kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)")) - @kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]")) - @kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}")) - @kb.add('"', filter=focused_insert & auto_match & following_text('^"')) - @kb.add("'", filter=focused_insert & auto_match & following_text("^'")) - def _(event): - event.current_buffer.cursor_right() - @kb.add( + kb.add( "backspace", filter=focused_insert & preceding_text(r".*\($") & auto_match & following_text(r"^\)"), - ) - @kb.add( + )(match.delete_pair) + kb.add( "backspace", filter=focused_insert & preceding_text(r".*\[$") & auto_match & following_text(r"^\]"), - ) - @kb.add( + )(match.delete_pair) + kb.add( "backspace", filter=focused_insert & preceding_text(r".*\{$") & auto_match & following_text(r"^\}"), - ) - @kb.add( + )(match.delete_pair) + kb.add( "backspace", filter=focused_insert & preceding_text('.*"$') & auto_match & following_text('^"'), - ) - @kb.add( + )(match.delete_pair) + kb.add( "backspace", filter=focused_insert & preceding_text(r".*'$") & auto_match & following_text(r"^'"), - ) - def _(event): - event.current_buffer.delete() - event.current_buffer.delete_before_cursor() + )(match.delete_pair) if shell.display_completions == "readlinelike": kb.add( @@ -335,37 +317,22 @@ def _(event): ), )(display_completions_like_readline) - if sys.platform == "win32": + if sys.platform == "win32" or for_all_platforms: kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode - @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) - def _(event): - _apply_autosuggest(event) - - @kb.add("c-e", filter=focused_insert_vi & ebivim) - def _(event): - _apply_autosuggest(event) - - @kb.add("c-f", filter=focused_insert_vi) - def _(event): - b = event.current_buffer - suggestion = b.suggestion - if suggestion: - b.insert_text(suggestion.text) - else: - nc.forward_char(event) - - @kb.add("escape", "f", filter=focused_insert_vi & ebivim) - def _(event): - b = event.current_buffer - suggestion = b.suggestion - if suggestion: - t = re.split(r"(\S+\s+)", suggestion.text) - b.insert_text(next((x for x in t if x), "")) - else: - nc.forward_word(event) + # autosuggestions + kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))( + autosuggestions.accept_in_vi_insert_mode + ) + kb.add("c-e", filter=focused_insert_vi & ebivim)( + autosuggestions.accept_in_vi_insert_mode + ) + kb.add("c-f", filter=focused_insert_vi)(autosuggestions.accept) + kb.add("escape", "f", filter=focused_insert_vi & ebivim)( + autosuggestions.accept_word + ) # Simple Control keybindings key_cmd_dict = { @@ -423,7 +390,7 @@ def set_input_mode(self, mode): def reformat_text_before_cursor(buffer, document, shell): - text = buffer.delete_before_cursor(len(document.text[:document.cursor_position])) + text = buffer.delete_before_cursor(len(document.text[: document.cursor_position])) try: formatted_text = shell.reformat_handler(text) buffer.insert_text(formatted_text) @@ -432,7 +399,6 @@ def reformat_text_before_cursor(buffer, document, shell): def newline_or_execute_outer(shell): - def newline_or_execute(event): """When the user presses return, insert a newline or execute the code.""" b = event.current_buffer @@ -451,34 +417,38 @@ def newline_or_execute(event): if d.line_count == 1: check_text = d.text else: - check_text = d.text[:d.cursor_position] + check_text = d.text[: d.cursor_position] status, indent = shell.check_complete(check_text) - + # if all we have after the cursor is whitespace: reformat current text # before cursor - after_cursor = d.text[d.cursor_position:] + after_cursor = d.text[d.cursor_position :] reformatted = False if not after_cursor.strip(): reformat_text_before_cursor(b, d, shell) reformatted = True - if not (d.on_last_line or - d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() - ): + if not ( + d.on_last_line + or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() + ): if shell.autoindent: - b.insert_text('\n' + indent) + b.insert_text("\n" + indent) else: - b.insert_text('\n') + b.insert_text("\n") return - if (status != 'incomplete') and b.accept_handler: + if (status != "incomplete") and b.accept_handler: if not reformatted: reformat_text_before_cursor(b, d, shell) b.validate_and_handle() else: if shell.autoindent: - b.insert_text('\n' + indent) + b.insert_text("\n" + indent) else: - b.insert_text('\n') + b.insert_text("\n") + + newline_or_execute.__qualname__ = "newline_or_execute" + return newline_or_execute @@ -501,12 +471,14 @@ def next_history_or_next_completion(event): def dismiss_completion(event): + """Dismiss completion""" b = event.current_buffer if b.complete_state: b.cancel_completion() def reset_buffer(event): + """Reset buffer""" b = event.current_buffer if b.complete_state: b.cancel_completion() @@ -515,16 +487,22 @@ def reset_buffer(event): def reset_search_buffer(event): + """Reset search buffer""" if event.current_buffer.document.text: event.current_buffer.reset() else: event.app.layout.focus(DEFAULT_BUFFER) + def suspend_to_bg(event): + """Suspend to background""" event.app.suspend_to_background() + def quit(event): """ + Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise. + On platforms that support SIGQUIT, send SIGQUIT to the current process. On other platforms, just exit the process with a message. """ @@ -534,8 +512,11 @@ def quit(event): else: sys.exit("Quit") + def indent_buffer(event): - event.current_buffer.insert_text(' ' * 4) + """Indent buffer""" + event.current_buffer.insert_text(" " * 4) + @undoc def newline_with_copy_margin(event): @@ -547,9 +528,12 @@ def newline_with_copy_margin(event): Preserve margin and cursor position when using Control-O to insert a newline in EMACS mode """ - warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " - "see `newline_autoindent_outer(shell)(event)` for a replacement.", - DeprecationWarning, stacklevel=2) + warnings.warn( + "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " + "see `newline_autoindent_outer(shell)(event)` for a replacement.", + DeprecationWarning, + stacklevel=2, + ) b = event.current_buffer cursor_start_pos = b.document.cursor_position_col @@ -560,6 +544,7 @@ def newline_with_copy_margin(event): pos_diff = cursor_start_pos - cursor_end_pos b.cursor_right(count=pos_diff) + def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: """ Return a function suitable for inserting a indented newline after the cursor. @@ -571,28 +556,33 @@ def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: """ def newline_autoindent(event): - """insert a newline after the cursor indented appropriately.""" + """Insert a newline after the cursor indented appropriately.""" b = event.current_buffer d = b.document if b.complete_state: b.cancel_completion() - text = d.text[:d.cursor_position] + '\n' + text = d.text[: d.cursor_position] + "\n" _, indent = inputsplitter.check_complete(text) - b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False) + b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False) + + newline_autoindent.__qualname__ = "newline_autoindent" return newline_autoindent def open_input_in_editor(event): + """Open code from input in external editor""" event.app.current_buffer.open_in_editor() -if sys.platform == 'win32': +if sys.platform == "win32": from IPython.core.error import TryNext - from IPython.lib.clipboard import (ClipboardEmpty, - win32_clipboard_get, - tkinter_clipboard_get) + from IPython.lib.clipboard import ( + ClipboardEmpty, + win32_clipboard_get, + tkinter_clipboard_get, + ) @undoc def win_paste(event): @@ -606,3 +596,10 @@ def win_paste(event): except ClipboardEmpty: return event.current_buffer.insert_text(text.replace("\t", " " * 4)) + +else: + + @undoc + def win_paste(event): + """Stub used when auto-generating shortcuts for documentation""" + pass diff --git a/IPython/terminal/shortcuts/auto_match.py b/IPython/terminal/shortcuts/auto_match.py new file mode 100644 index 00000000000..0976bb20336 --- /dev/null +++ b/IPython/terminal/shortcuts/auto_match.py @@ -0,0 +1,90 @@ +import re +from prompt_toolkit.key_binding import KeyPressEvent + + +def parenthesis(event: KeyPressEvent): + """Auto-close parenthesis""" + event.current_buffer.insert_text("()") + event.current_buffer.cursor_left() + + +def brackets(event: KeyPressEvent): + """Auto-close brackets""" + event.current_buffer.insert_text("[]") + event.current_buffer.cursor_left() + + +def braces(event: KeyPressEvent): + """Auto-close braces""" + event.current_buffer.insert_text("{}") + event.current_buffer.cursor_left() + + +def double_quote(event: KeyPressEvent): + """Auto-close double quotes""" + event.current_buffer.insert_text('""') + event.current_buffer.cursor_left() + + +def single_quote(event: KeyPressEvent): + """Auto-close single quotes""" + event.current_buffer.insert_text("''") + event.current_buffer.cursor_left() + + +def docstring_double_quotes(event: KeyPressEvent): + """Auto-close docstring (double quotes)""" + event.current_buffer.insert_text('""""') + event.current_buffer.cursor_left(3) + + +def docstring_single_quotes(event: KeyPressEvent): + """Auto-close docstring (single quotes)""" + event.current_buffer.insert_text("''''") + event.current_buffer.cursor_left(3) + + +def raw_string_parenthesis(event: KeyPressEvent): + """Auto-close parenthesis in raw strings""" + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) or "" + event.current_buffer.insert_text("()" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + +def raw_string_bracket(event: KeyPressEvent): + """Auto-close bracker in raw strings""" + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) or "" + event.current_buffer.insert_text("[]" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + +def raw_string_braces(event: KeyPressEvent): + """Auto-close braces in raw strings""" + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) or "" + event.current_buffer.insert_text("{}" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + +def skip_over(event: KeyPressEvent): + """Skip over automatically added parenthesis. + + (rather than adding another parenthesis)""" + event.current_buffer.cursor_right() + + +def delete_pair(event: KeyPressEvent): + """Delete auto-closed parenthesis""" + event.current_buffer.delete() + event.current_buffer.delete_before_cursor() diff --git a/IPython/terminal/shortcuts/autosuggestions.py b/IPython/terminal/shortcuts/autosuggestions.py new file mode 100644 index 00000000000..fe1e8d07b7c --- /dev/null +++ b/IPython/terminal/shortcuts/autosuggestions.py @@ -0,0 +1,39 @@ +import re +from prompt_toolkit.key_binding import KeyPressEvent +from prompt_toolkit.key_binding.bindings import named_commands as nc + + +# Needed for to accept autosuggestions in vi insert mode +def accept_in_vi_insert_mode(event: KeyPressEvent): + """Apply autosuggestion if at end of line.""" + b = event.current_buffer + d = b.document + after_cursor = d.text[d.cursor_position :] + lines = after_cursor.split("\n") + end_of_current_line = lines[0].strip() + suggestion = b.suggestion + if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): + b.insert_text(suggestion.text) + else: + nc.end_of_line(event) + + +def accept(event): + """Accept suggestion""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion: + b.insert_text(suggestion.text) + else: + nc.forward_char(event) + + +def accept_word(event): + """Fill partial suggestion by word""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + b.insert_text(next((x for x in t if x), "")) + else: + nc.forward_word(event) diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index db7fe8d4917..b5886ffa576 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -1,45 +1,98 @@ +from dataclasses import dataclass +from inspect import getsource from pathlib import Path +from typing import cast, Callable, List, Union +from html import escape as html_escape +import re + +from prompt_toolkit.keys import KEY_ALIASES +from prompt_toolkit.key_binding import KeyBindingsBase +from prompt_toolkit.filters import Filter, Condition +from prompt_toolkit.shortcuts import PromptSession from IPython.terminal.shortcuts import create_ipython_shortcuts -def name(c): - s = c.__class__.__name__ - if s == '_Invert': - return '(Not: %s)' % name(c.filter) - if s in log_filters.keys(): - return '(%s: %s)' % (log_filters[s], ', '.join(name(x) for x in c.filters)) - return log_filters[s] if s in log_filters.keys() else s +@dataclass +class Shortcut: + #: a sequence of keys (each element on the list corresponds to pressing one or more keys) + keys_sequence: list[str] + filter: str -def sentencize(s): - """Extract first sentence - """ - s = s.replace('\n', ' ').strip().split('.') - s = s[0] if len(s) else s - try: - return " ".join(s.split()) - except AttributeError: - return s +@dataclass +class Handler: + description: str + identifier: str -def most_common(lst, n=3): - """Most common elements occurring more then `n` times - """ - from collections import Counter - c = Counter(lst) - return [k for (k, v) in c.items() if k and v > n] +@dataclass +class Binding: + handler: Handler + shortcut: Shortcut -def multi_filter_str(flt): - """Yield readable conditional filter - """ - assert hasattr(flt, 'filters'), 'Conditional filter required' - yield name(flt) +class _NestedFilter(Filter): + """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`.""" + + filters: List[Filter] + + +class _Invert(Filter): + """Protocol reflecting non-public prompt_toolkit's `_Invert`.""" + + filter: Filter + + +conjunctions_labels = {"_AndList": "and", "_OrList": "or"} +ATOMIC_CLASSES = {"Never", "Always", "Condition"} + + +def format_filter( + filter_: Union[Filter, _NestedFilter, Condition, _Invert], + is_top_level=True, + skip=None, +) -> str: + """Create easily readable description of the filter.""" + s = filter_.__class__.__name__ + if s == "Condition": + func = cast(Condition, filter_).func + name = func.__name__ + if name == "": + source = getsource(func) + return source.split("=")[0].strip() + return func.__name__ + elif s == "_Invert": + operand = cast(_Invert, filter_).filter + if operand.__class__.__name__ in ATOMIC_CLASSES: + return f"not {format_filter(operand, is_top_level=False)}" + return f"not ({format_filter(operand, is_top_level=False)})" + elif s in conjunctions_labels: + filters = cast(_NestedFilter, filter_).filters + conjunction = conjunctions_labels[s] + glue = f" {conjunction} " + result = glue.join(format_filter(x, is_top_level=False) for x in filters) + if len(filters) > 1 and not is_top_level: + result = f"({result})" + return result + elif s in ["Never", "Always"]: + return s.lower() + else: + raise ValueError(f"Unknown filter type: {filter_}") + + +def sentencize(s) -> str: + """Extract first sentence""" + s = re.split(r"\.\W", s.replace("\n", " ").strip()) + s = s[0] if len(s) else "" + if not s.endswith("."): + s += "." + try: + return " ".join(s.split()) + except AttributeError: + return s -log_filters = {'_AndList': 'And', '_OrList': 'Or'} -log_invert = {'_Invert'} class _DummyTerminal: """Used as a buffer to get prompt_toolkit bindings @@ -50,47 +103,118 @@ class _DummyTerminal: editing_mode = "emacs" -ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings - -dummy_docs = [] # ignore bindings without proper documentation - -common_docs = most_common([kb.handler.__doc__ for kb in ipy_bindings]) -if common_docs: - dummy_docs.extend(common_docs) +def create_identifier(handler: Callable): + parts = handler.__module__.split(".") + name = handler.__name__ + package = parts[0] + if len(parts) > 1: + final_module = parts[-1] + return f"{package}:{final_module}.{name}" + else: + return f"{package}:{name}" + + +def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]: + """Collect bindings to a simple format that does not depend on prompt-toolkit internals""" + bindings: List[Binding] = [] + + for kb in prompt_bindings.bindings: + bindings.append( + Binding( + handler=Handler( + description=kb.handler.__doc__ or "", + identifier=create_identifier(kb.handler), + ), + shortcut=Shortcut( + keys_sequence=[ + str(k.value) if hasattr(k, "value") else k for k in kb.keys + ], + filter=format_filter(kb.filter, skip={"has_focus_filter"}), + ), + ) + ) + return bindings + + +INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}} + + +def format_prompt_keys(keys: str, add_alternatives=True) -> str: + """Format prompt toolkit key with modifier into an RST representation.""" + + def to_rst(key): + escaped = key.replace("\\", "\\\\") + return f":kbd:`{escaped}`" + + keys_to_press: list[str] + + prefixes = { + "c-s-": [to_rst("ctrl"), to_rst("shift")], + "s-c-": [to_rst("ctrl"), to_rst("shift")], + "c-": [to_rst("ctrl")], + "s-": [to_rst("shift")], + } + + for prefix, modifiers in prefixes.items(): + if keys.startswith(prefix): + remainder = keys[len(prefix) :] + keys_to_press = [*modifiers, to_rst(remainder)] + break + else: + keys_to_press = [to_rst(keys)] -dummy_docs = list(set(dummy_docs)) + result = " + ".join(keys_to_press) -single_filter = {} -multi_filter = {} -for kb in ipy_bindings: - doc = kb.handler.__doc__ - if not doc or doc in dummy_docs: - continue + if keys in INDISTINGUISHABLE_KEYS and add_alternatives: + alternative = INDISTINGUISHABLE_KEYS[keys] - shortcut = ' '.join([k if isinstance(k, str) else k.name for k in kb.keys]) - shortcut += shortcut.endswith('\\') and '\\' or '' - if hasattr(kb.filter, 'filters'): - flt = ' '.join(multi_filter_str(kb.filter)) - multi_filter[(shortcut, flt)] = sentencize(doc) - else: - single_filter[(shortcut, name(kb.filter))] = sentencize(doc) + result = ( + result + + " (or " + + format_prompt_keys(alternative, add_alternatives=False) + + ")" + ) + return result if __name__ == '__main__': here = Path(__file__).parent dest = here / "source" / "config" / "shortcuts" - def sort_key(item): - k, v = item - shortcut, flt = k - return (str(shortcut), str(flt)) - - for filters, output_filename in [ - (single_filter, "single_filtered"), - (multi_filter, "multi_filtered"), - ]: - with (dest / "{}.csv".format(output_filename)).open( - "w", encoding="utf-8" - ) as csv: - for (shortcut, flt), v in sorted(filters.items(), key=sort_key): - csv.write(":kbd:`{}`\t{}\t{}\n".format(shortcut, flt, v)) + ipy_bindings = create_ipython_shortcuts(_DummyTerminal(), for_all_platforms=True) + + session = PromptSession(key_bindings=ipy_bindings) + prompt_bindings = session.app.key_bindings + + assert prompt_bindings + # Ensure that we collected the default shortcuts + assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings) + + bindings = bindings_from_prompt_toolkit(prompt_bindings) + + def sort_key(binding: Binding): + return binding.handler.identifier, binding.shortcut.filter + + filters = [] + with (dest / "table.tsv").open("w", encoding="utf-8") as csv: + for binding in sorted(bindings, key=sort_key): + sequence = ", ".join( + [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence] + ) + if binding.shortcut.filter == "always": + condition_label = "-" + else: + # we cannot fit all the columns as the filters got too complex over time + condition_label = "ⓘ" + + csv.write( + "\t".join( + [ + sequence, + sentencize(binding.handler.description) + + f" :raw-html:`
` `{binding.handler.identifier}`", + f':raw-html:`{condition_label}`', + ] + ) + + "\n" + ) diff --git a/docs/source/_static/theme_overrides.css b/docs/source/_static/theme_overrides.css new file mode 100644 index 00000000000..156db8c24b0 --- /dev/null +++ b/docs/source/_static/theme_overrides.css @@ -0,0 +1,7 @@ +/* + Needed to revert problematic lack of wrapping in sphinx_rtd_theme, see: + https://github.com/readthedocs/sphinx_rtd_theme/issues/117 +*/ +.wy-table-responsive table.shortcuts td, .wy-table-responsive table.shortcuts th { + white-space: normal!important; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index d04d4637ba7..868c0d0e346 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -211,7 +211,6 @@ def filter(self, record): # given in html_static_path. # html_style = 'default.css' - # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None @@ -327,6 +326,10 @@ def filter(self, record): modindex_common_prefix = ['IPython.'] +def setup(app): + app.add_css_file("theme_overrides.css") + + # Cleanup # ------- # delete release info to avoid pickling errors from sphinx diff --git a/docs/source/config/shortcuts/index.rst b/docs/source/config/shortcuts/index.rst index 4103d92a7bd..e361ec26c5d 100755 --- a/docs/source/config/shortcuts/index.rst +++ b/docs/source/config/shortcuts/index.rst @@ -4,28 +4,23 @@ IPython shortcuts Available shortcuts in an IPython terminal. -.. warning:: +.. note:: - This list is automatically generated, and may not hold all available - shortcuts. In particular, it may depend on the version of ``prompt_toolkit`` - installed during the generation of this page. + This list is automatically generated. Key bindings defined in ``prompt_toolkit`` may differ + between installations depending on the ``prompt_toolkit`` version. -Single Filtered shortcuts -========================= - -.. csv-table:: - :header: Shortcut,Filter,Description - :widths: 30, 30, 100 - :delim: tab - :file: single_filtered.csv +* Comma-separated keys, e.g. :kbd:`Esc`, :kbd:`f`, indicate a sequence which can be activated by pressing the listed keys in succession. +* Plus-separated keys, e.g. :kbd:`Esc` + :kbd:`f` indicate a combination which requires pressing all keys simultaneously. +* Hover over the ⓘ icon in the filter column to see when the shortcut is active.g +.. role:: raw-html(raw) + :format: html -Multi Filtered shortcuts -======================== .. csv-table:: - :header: Shortcut,Filter,Description - :widths: 30, 30, 100 + :header: Shortcut,Description and identifier,Filter :delim: tab - :file: multi_filtered.csv + :class: shortcuts + :file: table.tsv + :widths: 20 75 5 From f6cb59f82a748f6225e8f9460722c8169a00d665 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 8 Jan 2023 14:45:38 +0000 Subject: [PATCH 0378/1752] Fix mypy job, fix issues detected by mypy --- .github/workflows/mypy.yml | 2 ++ IPython/terminal/shortcuts/__init__.py | 12 ++++++------ IPython/terminal/shortcuts/auto_match.py | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 52f3e79ab9c..c7fa22c7210 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -29,11 +29,13 @@ jobs: pip install mypy pyflakes flake8 - name: Lint with mypy run: | + set -e mypy -p IPython.terminal mypy -p IPython.core.magics mypy -p IPython.core.guarded_eval mypy -p IPython.core.completer - name: Lint with pyflakes run: | + set -e flake8 IPython/core/magics/script.py flake8 IPython/core/magics/packaging.py diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 6bc6ec7c39f..68eaf65ff60 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -11,7 +11,7 @@ import sys import re import os -from typing import Callable +from typing import Callable, Dict, Union from prompt_toolkit.application.current import get_app @@ -143,10 +143,10 @@ def all_quotes_paired(quote, buf): return paired focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER) - _preceding_text_cache = {} - _following_text_cache = {} + _preceding_text_cache: Dict[Union[str, Callable], Condition] = {} + _following_text_cache: Dict[Union[str, Callable], Condition] = {} - def preceding_text(pattern): + def preceding_text(pattern: Union[str, Callable]): if pattern in _preceding_text_cache: return _preceding_text_cache[pattern] @@ -383,8 +383,8 @@ def set_input_mode(self, mode): self._input_mode = mode if shell.editing_mode == "vi" and shell.modal_cursor: - ViState._input_mode = InputMode.INSERT - ViState.input_mode = property(get_input_mode, set_input_mode) + ViState._input_mode = InputMode.INSERT # type: ignore + ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore return kb diff --git a/IPython/terminal/shortcuts/auto_match.py b/IPython/terminal/shortcuts/auto_match.py index 0976bb20336..bb0ca8b3169 100644 --- a/IPython/terminal/shortcuts/auto_match.py +++ b/IPython/terminal/shortcuts/auto_match.py @@ -50,7 +50,7 @@ def raw_string_parenthesis(event: KeyPressEvent): r".*(r|R)[\"'](-*)", event.current_buffer.document.current_line_before_cursor, ) - dashes = matches.group(2) or "" + dashes = matches.group(2) if matches else "" event.current_buffer.insert_text("()" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) @@ -61,7 +61,7 @@ def raw_string_bracket(event: KeyPressEvent): r".*(r|R)[\"'](-*)", event.current_buffer.document.current_line_before_cursor, ) - dashes = matches.group(2) or "" + dashes = matches.group(2) if matches else "" event.current_buffer.insert_text("[]" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) @@ -72,7 +72,7 @@ def raw_string_braces(event: KeyPressEvent): r".*(r|R)[\"'](-*)", event.current_buffer.document.current_line_before_cursor, ) - dashes = matches.group(2) or "" + dashes = matches.group(2) if matches else "" event.current_buffer.insert_text("{}" + dashes) event.current_buffer.cursor_left(len(dashes) + 1) From 1e51d378075a54559c257d91ff2e516bfced6cbc Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 8 Jan 2023 17:58:04 +0000 Subject: [PATCH 0379/1752] Implement token-by-token autosuggestions --- IPython/terminal/shortcuts/__init__.py | 1 + IPython/terminal/shortcuts/autosuggestions.py | 52 +++++++++- IPython/terminal/tests/test_shortcuts.py | 94 +++++++++++++++++++ IPython/tests/test_shortcuts.py | 40 -------- 4 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 IPython/terminal/tests/test_shortcuts.py delete mode 100644 IPython/tests/test_shortcuts.py diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 68eaf65ff60..f25a8c1a869 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -333,6 +333,7 @@ def not_inside_unclosed_string(): kb.add("escape", "f", filter=focused_insert_vi & ebivim)( autosuggestions.accept_word ) + kb.add("c-right", filter=has_focus(DEFAULT_BUFFER))(autosuggestions.accept_token) # Simple Control keybindings key_cmd_dict = { diff --git a/IPython/terminal/shortcuts/autosuggestions.py b/IPython/terminal/shortcuts/autosuggestions.py index fe1e8d07b7c..158d988acae 100644 --- a/IPython/terminal/shortcuts/autosuggestions.py +++ b/IPython/terminal/shortcuts/autosuggestions.py @@ -1,7 +1,13 @@ import re +import tokenize +from io import StringIO +from typing import List, Optional + from prompt_toolkit.key_binding import KeyPressEvent from prompt_toolkit.key_binding.bindings import named_commands as nc +from IPython.utils.tokenutil import generate_tokens + # Needed for to accept autosuggestions in vi insert mode def accept_in_vi_insert_mode(event: KeyPressEvent): @@ -18,7 +24,7 @@ def accept_in_vi_insert_mode(event: KeyPressEvent): nc.end_of_line(event) -def accept(event): +def accept(event: KeyPressEvent): """Accept suggestion""" b = event.current_buffer suggestion = b.suggestion @@ -28,7 +34,7 @@ def accept(event): nc.forward_char(event) -def accept_word(event): +def accept_word(event: KeyPressEvent): """Fill partial suggestion by word""" b = event.current_buffer suggestion = b.suggestion @@ -37,3 +43,45 @@ def accept_word(event): b.insert_text(next((x for x in t if x), "")) else: nc.forward_word(event) + + +def accept_token(event: KeyPressEvent): + """Fill partial suggestion by token""" + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + prefix = b.text + text = prefix + suggestion.text + + tokens: List[Optional[str]] = [None, None, None] + substings = [""] + i = 0 + + for token in generate_tokens(StringIO(text).readline): + if token.type == tokenize.NEWLINE: + index = len(text) + else: + index = text.index(token[1], len(substings[-1])) + substings.append(text[:index]) + tokenized_so_far = substings[-1] + if tokenized_so_far.startswith(prefix): + if i == 0 and len(tokenized_so_far) > len(prefix): + tokens[0] = tokenized_so_far[len(prefix) :] + substings.append(tokenized_so_far) + i += 1 + tokens[i] = token[1] + if i == 2: + break + i += 1 + + if tokens[0]: + to_insert: str + insert_text = substings[-2] + if tokens[1] and len(tokens[1]) == 1: + insert_text = substings[-1] + to_insert = insert_text[len(prefix) :] + b.insert_text(to_insert) + return + + nc.forward_word(event) diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py new file mode 100644 index 00000000000..92242f75d38 --- /dev/null +++ b/IPython/terminal/tests/test_shortcuts.py @@ -0,0 +1,94 @@ +import pytest +from IPython.terminal.shortcuts.autosuggestions import ( + accept_in_vi_insert_mode, + accept_token, +) + +from unittest.mock import patch, Mock + + +def make_event(text, cursor, suggestion): + event = Mock() + event.current_buffer = Mock() + event.current_buffer.suggestion = Mock() + event.current_buffer.text = text + event.current_buffer.cursor_position = cursor + event.current_buffer.suggestion.text = suggestion + event.current_buffer.document = Mock() + event.current_buffer.document.get_end_of_line_position = Mock(return_value=0) + event.current_buffer.document.text = text + event.current_buffer.document.cursor_position = cursor + return event + + +@pytest.mark.parametrize( + "text, cursor, suggestion, called", + [ + ("123456", 6, "123456789", True), + ("123456", 3, "123456789", False), + ("123456 \n789", 6, "123456789", True), + ], +) +def test_autosuggest_at_EOL(text, cursor, suggestion, called): + """ + test that autosuggest is only applied at end of line. + """ + + event = make_event(text, cursor, suggestion) + event.current_buffer.insert_text = Mock() + accept_in_vi_insert_mode(event) + if called: + event.current_buffer.insert_text.assert_called() + else: + event.current_buffer.insert_text.assert_not_called() + # event.current_buffer.document.get_end_of_line_position.assert_called() + + +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "def "), + ("d", "ef out(tag: str, n=50):", "ef "), + ("de ", "f out(tag: str, n=50):", "f "), + ("def", " out(tag: str, n=50):", " "), + ("def ", "out(tag: str, n=50):", "out("), + ("def o", "ut(tag: str, n=50):", "ut("), + ("def ou", "t(tag: str, n=50):", "t("), + ("def out", "(tag: str, n=50):", "("), + ("def out(", "tag: str, n=50):", "tag: "), + ("def out(t", "ag: str, n=50):", "ag: "), + ("def out(ta", "g: str, n=50):", "g: "), + ("def out(tag", ": str, n=50):", ": "), + ("def out(tag:", " str, n=50):", " "), + ("def out(tag: ", "str, n=50):", "str, "), + ("def out(tag: s", "tr, n=50):", "tr, "), + ("def out(tag: st", "r, n=50):", "r, "), + ("def out(tag: str", ", n=50):", ", n"), + ("def out(tag: str,", " n=50):", " n"), + ("def out(tag: str, ", "n=50):", "n="), + ("def out(tag: str, n", "=50):", "="), + ("def out(tag: str, n=", "50):", "50)"), + ("def out(tag: str, n=5", "0):", "0)"), + ("def out(tag: str, n=50", "):", "):"), + ("def out(tag: str, n=50)", ":", ":"), + ], +) +def test_autosuggest_token(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + event.current_buffer.insert_text = Mock() + accept_token(event) + assert event.current_buffer.insert_text.called + assert event.current_buffer.insert_text.call_args[0] == (expected,) + + +def test_autosuggest_token_empty(): + full = "def out(tag: str, n=50):" + event = make_event(full, len(full), "") + event.current_buffer.insert_text = Mock() + + with patch( + "prompt_toolkit.key_binding.bindings.named_commands.forward_word" + ) as forward_word: + accept_token(event) + assert not event.current_buffer.insert_text.called + assert forward_word.called diff --git a/IPython/tests/test_shortcuts.py b/IPython/tests/test_shortcuts.py deleted file mode 100644 index 42edb92ba58..00000000000 --- a/IPython/tests/test_shortcuts.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest -from IPython.terminal.shortcuts import _apply_autosuggest - -from unittest.mock import Mock - - -def make_event(text, cursor, suggestion): - event = Mock() - event.current_buffer = Mock() - event.current_buffer.suggestion = Mock() - event.current_buffer.cursor_position = cursor - event.current_buffer.suggestion.text = suggestion - event.current_buffer.document = Mock() - event.current_buffer.document.get_end_of_line_position = Mock(return_value=0) - event.current_buffer.document.text = text - event.current_buffer.document.cursor_position = cursor - return event - - -@pytest.mark.parametrize( - "text, cursor, suggestion, called", - [ - ("123456", 6, "123456789", True), - ("123456", 3, "123456789", False), - ("123456 \n789", 6, "123456789", True), - ], -) -def test_autosuggest_at_EOL(text, cursor, suggestion, called): - """ - test that autosuggest is only applied at end of line. - """ - - event = make_event(text, cursor, suggestion) - event.current_buffer.insert_text = Mock() - _apply_autosuggest(event) - if called: - event.current_buffer.insert_text.assert_called() - else: - event.current_buffer.insert_text.assert_not_called() - # event.current_buffer.document.get_end_of_line_position.assert_called() From 4fe9e715821706fb964ffa14e2cc296a6a5ae3d5 Mon Sep 17 00:00:00 2001 From: Maor Kleinberger Date: Sat, 7 Jan 2023 20:34:35 +0200 Subject: [PATCH 0380/1752] Use check_same_thread=False by default for history sqlite db --- IPython/core/history.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IPython/core/history.py b/IPython/core/history.py index 1a89060e92e..897997be140 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -177,6 +177,10 @@ class HistoryAccessor(HistoryAccessorBase): """ ).tag(config=True) + @default("connection_options") + def _default_connection_options(self): + return dict(check_same_thread=False) + # The SQLite database db = Any() @observe('db') From 2a5f51a098e49ab34b45a00070aa65f672b1c9c6 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 8 Jan 2023 22:21:58 +0000 Subject: [PATCH 0381/1752] Implement traversal of autosuggestions and by-character fill --- IPython/terminal/interactiveshell.py | 20 +- IPython/terminal/shortcuts/__init__.py | 37 ++- IPython/terminal/shortcuts/auto_suggest.py | 255 ++++++++++++++++++ IPython/terminal/shortcuts/autosuggestions.py | 87 ------ IPython/terminal/tests/test_shortcuts.py | 2 +- docs/autogen_shortcuts.py | 1 + 6 files changed, 303 insertions(+), 99 deletions(-) create mode 100644 IPython/terminal/shortcuts/auto_suggest.py delete mode 100644 IPython/terminal/shortcuts/autosuggestions.py diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index c867b553f2e..0abff28db90 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -4,6 +4,7 @@ import os import sys from warnings import warn +from typing import Union as UnionType from IPython.core.async_helpers import get_asyncio_loop from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC @@ -49,6 +50,7 @@ from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook from .ptutils import IPythonPTCompleter, IPythonPTLexer from .shortcuts import create_ipython_shortcuts +from .shortcuts.auto_suggest import NavigableAutoSuggestFromHistory PTK3 = ptk_version.startswith('3.') @@ -183,7 +185,7 @@ class TerminalInteractiveShell(InteractiveShell): 'menus, decrease for short and wide.' ).tag(config=True) - pt_app = None + pt_app: UnionType[PromptSession, None] = None debugger_history = None debugger_history_file = Unicode( @@ -376,18 +378,25 @@ def _displayhook_class_default(self): ).tag(config=True) autosuggestions_provider = Unicode( - "AutoSuggestFromHistory", + "NavigableAutoSuggestFromHistory", help="Specifies from which source automatic suggestions are provided. " - "Can be set to `'AutoSuggestFromHistory`' or `None` to disable" - "automatic suggestions. Default is `'AutoSuggestFromHistory`'.", + "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and " + ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, " + " or ``None`` to disable automatic suggestions. " + "Default is `'NavigableAutoSuggestFromHistory`'.", allow_none=True, ).tag(config=True) def _set_autosuggestions(self, provider): + # disconnect old handler + if self.auto_suggest and isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory): + self.auto_suggest.disconnect() if provider is None: self.auto_suggest = None elif provider == "AutoSuggestFromHistory": self.auto_suggest = AutoSuggestFromHistory() + elif provider == "NavigableAutoSuggestFromHistory": + self.auto_suggest = NavigableAutoSuggestFromHistory() else: raise ValueError("No valid provider.") if self.pt_app: @@ -462,6 +471,8 @@ def prompt(): tempfile_suffix=".py", **self._extra_prompt_options() ) + if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory): + self.auto_suggest.connect(self.pt_app) def _make_style_from_name_or_cls(self, name_or_cls): """ @@ -649,6 +660,7 @@ def init_alias(self): def __init__(self, *args, **kwargs): super(TerminalInteractiveShell, self).__init__(*args, **kwargs) + self.auto_suggest: UnionType[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] = None self._set_autosuggestions(self.autosuggestions_provider) self.init_prompt_toolkit_cli() self.init_term_title() diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index f25a8c1a869..3bb39066ec1 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -34,12 +34,24 @@ from prompt_toolkit.layout.layout import FocusableElement from IPython.utils.decorators import undoc -from . import auto_match as match, autosuggestions +from . import auto_match as match, auto_suggest __all__ = ["create_ipython_shortcuts"] +try: + # only added in 3.0.30 + from prompt_toolkit.filters import has_suggestion +except ImportError: + + @undoc + @Condition + def has_suggestion(): + buffer = get_app().current_buffer + return buffer.suggestion is not None and buffer.suggestion.text != "" + + @undoc @Condition def cursor_in_leading_ws(): @@ -324,16 +336,27 @@ def not_inside_unclosed_string(): # autosuggestions kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))( - autosuggestions.accept_in_vi_insert_mode + auto_suggest.accept_in_vi_insert_mode ) kb.add("c-e", filter=focused_insert_vi & ebivim)( - autosuggestions.accept_in_vi_insert_mode + auto_suggest.accept_in_vi_insert_mode + ) + kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept) + kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word) + kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_token + ) + from functools import partial + + kb.add("up", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.swap_autosuggestion_up(shell.auto_suggest) + ) + kb.add("down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.swap_autosuggestion_down(shell.auto_suggest) ) - kb.add("c-f", filter=focused_insert_vi)(autosuggestions.accept) - kb.add("escape", "f", filter=focused_insert_vi & ebivim)( - autosuggestions.accept_word + kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_character ) - kb.add("c-right", filter=has_focus(DEFAULT_BUFFER))(autosuggestions.accept_token) # Simple Control keybindings key_cmd_dict = { diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py new file mode 100644 index 00000000000..0e8533f6572 --- /dev/null +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -0,0 +1,255 @@ +import re +import tokenize +from io import StringIO +from typing import Callable, List, Optional, Union + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.key_binding import KeyPressEvent +from prompt_toolkit.key_binding.bindings import named_commands as nc +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion +from prompt_toolkit.document import Document +from prompt_toolkit.history import History +from prompt_toolkit.shortcuts import PromptSession + +from IPython.utils.tokenutil import generate_tokens + + +def _get_query(document: Document): + return document.text.rsplit("\n", 1)[-1] + + +class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): + """ """ + + def __init__( + self, + ): + self.skip_lines = 0 + self._connected_apps = [] + + def reset_history_position(self, _: Buffer): + self.skip_lines = 0 + + def disconnect(self): + for pt_app in self._connected_apps: + text_insert_event = pt_app.default_buffer.on_text_insert + text_insert_event.remove_handler(self.reset_history_position) + + def connect(self, pt_app: PromptSession): + self._connected_apps.append(pt_app) + pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position) + + def get_suggestion( + self, buffer: Buffer, document: Document + ) -> Optional[Suggestion]: + text = _get_query(document) + + if text.strip(): + for suggestion, _ in self._find_next_match( + text, self.skip_lines, buffer.history + ): + return Suggestion(suggestion) + + return None + + def _find_match( + self, text: str, skip_lines: float, history: History, previous: bool + ): + line_number = -1 + + for string in reversed(list(history.get_strings())): + for line in reversed(string.splitlines()): + line_number += 1 + if not previous and line_number < skip_lines: + continue + # do not return empty suggestions as these + # close the auto-suggestion overlay (and are useless) + if line.startswith(text) and len(line) > len(text): + yield line[len(text) :], line_number + if previous and line_number >= skip_lines: + return + + def _find_next_match(self, text: str, skip_lines: float, history: History): + return self._find_match(text, skip_lines, history, previous=False) + + def _find_previous_match(self, text: str, skip_lines: float, history: History): + return reversed( + list(self._find_match(text, skip_lines, history, previous=True)) + ) + + def up(self, query: str, other_than: str, history: History): + for suggestion, line_number in self._find_next_match( + query, self.skip_lines, history + ): + # if user has history ['very.a', 'very', 'very.b'] and typed 'very' + # we want to switch from 'very.b' to 'very.a' because a) if they + # suggestion equals current text, prompt-toolit aborts suggesting + # b) user likely would not be interested in 'very' anyways (they + # already typed it). + if query + suggestion != other_than: + self.skip_lines = line_number + break + else: + # no matches found, cycle back to beginning + self.skip_lines = 0 + + def down(self, query: str, other_than: str, history: History): + for suggestion, line_number in self._find_previous_match( + query, self.skip_lines, history + ): + if query + suggestion != other_than: + self.skip_lines = line_number + break + else: + # no matches found, cycle to end + for suggestion, line_number in self._find_previous_match( + query, float("Inf"), history + ): + if query + suggestion != other_than: + self.skip_lines = line_number + break + + +# Needed for to accept autosuggestions in vi insert mode +def accept_in_vi_insert_mode(event: KeyPressEvent): + """Apply autosuggestion if at end of line.""" + b = event.current_buffer + d = b.document + after_cursor = d.text[d.cursor_position :] + lines = after_cursor.split("\n") + end_of_current_line = lines[0].strip() + suggestion = b.suggestion + if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): + b.insert_text(suggestion.text) + else: + nc.end_of_line(event) + + +def accept(event: KeyPressEvent): + """Accept autosuggestion""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion: + b.insert_text(suggestion.text) + else: + nc.forward_char(event) + + +def accept_word(event: KeyPressEvent): + """Fill partial autosuggestion by word""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + b.insert_text(next((x for x in t if x), "")) + else: + nc.forward_word(event) + + +def accept_character(event: KeyPressEvent): + """Fill partial autosuggestion by character""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion and suggestion.text: + b.insert_text(suggestion.text[0]) + + +def accept_token(event: KeyPressEvent): + """Fill partial autosuggestion by token""" + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + prefix = _get_query(b.document) + text = prefix + suggestion.text + + tokens: List[Optional[str]] = [None, None, None] + substrings = [""] + i = 0 + + for token in generate_tokens(StringIO(text).readline): + if token.type == tokenize.NEWLINE: + index = len(text) + else: + index = text.index(token[1], len(substrings[-1])) + substrings.append(text[:index]) + tokenized_so_far = substrings[-1] + if tokenized_so_far.startswith(prefix): + if i == 0 and len(tokenized_so_far) > len(prefix): + tokens[0] = tokenized_so_far[len(prefix) :] + substrings.append(tokenized_so_far) + i += 1 + tokens[i] = token[1] + if i == 2: + break + i += 1 + + if tokens[0]: + to_insert: str + insert_text = substrings[-2] + if tokens[1] and len(tokens[1]) == 1: + insert_text = substrings[-1] + to_insert = insert_text[len(prefix) :] + b.insert_text(to_insert) + return + + nc.forward_word(event) + + +Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] + + +def _swap_autosuggestion( + buffer: Buffer, + provider: NavigableAutoSuggestFromHistory, + direction_method: Callable, +): + """ + We skip most recent history entry (in either direction) if it equals the + current autosuggestion because if user cycles when auto-suggestion is shown + they most likely want something else than what was suggested (othewrise + they would have accepted the suggestion). + """ + suggestion = buffer.suggestion + if not suggestion: + return + + query = _get_query(buffer.document) + current = query + suggestion.text + + direction_method(query=query, other_than=current, history=buffer.history) + + new_suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = new_suggestion + + +def swap_autosuggestion_up(provider: Provider): + def swap_autosuggestion_up(event: KeyPressEvent): + """Get next autosuggestion from history.""" + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + + return _swap_autosuggestion( + buffer=event.current_buffer, provider=provider, direction_method=provider.up + ) + + swap_autosuggestion_up.__name__ = "swap_autosuggestion_up" + return swap_autosuggestion_up + + +def swap_autosuggestion_down( + provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] +): + def swap_autosuggestion_down(event: KeyPressEvent): + """Get previous autosuggestion from history.""" + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + + return _swap_autosuggestion( + buffer=event.current_buffer, + provider=provider, + direction_method=provider.down, + ) + + swap_autosuggestion_down.__name__ = "swap_autosuggestion_down" + return swap_autosuggestion_down diff --git a/IPython/terminal/shortcuts/autosuggestions.py b/IPython/terminal/shortcuts/autosuggestions.py deleted file mode 100644 index 158d988acae..00000000000 --- a/IPython/terminal/shortcuts/autosuggestions.py +++ /dev/null @@ -1,87 +0,0 @@ -import re -import tokenize -from io import StringIO -from typing import List, Optional - -from prompt_toolkit.key_binding import KeyPressEvent -from prompt_toolkit.key_binding.bindings import named_commands as nc - -from IPython.utils.tokenutil import generate_tokens - - -# Needed for to accept autosuggestions in vi insert mode -def accept_in_vi_insert_mode(event: KeyPressEvent): - """Apply autosuggestion if at end of line.""" - b = event.current_buffer - d = b.document - after_cursor = d.text[d.cursor_position :] - lines = after_cursor.split("\n") - end_of_current_line = lines[0].strip() - suggestion = b.suggestion - if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): - b.insert_text(suggestion.text) - else: - nc.end_of_line(event) - - -def accept(event: KeyPressEvent): - """Accept suggestion""" - b = event.current_buffer - suggestion = b.suggestion - if suggestion: - b.insert_text(suggestion.text) - else: - nc.forward_char(event) - - -def accept_word(event: KeyPressEvent): - """Fill partial suggestion by word""" - b = event.current_buffer - suggestion = b.suggestion - if suggestion: - t = re.split(r"(\S+\s+)", suggestion.text) - b.insert_text(next((x for x in t if x), "")) - else: - nc.forward_word(event) - - -def accept_token(event: KeyPressEvent): - """Fill partial suggestion by token""" - b = event.current_buffer - suggestion = b.suggestion - - if suggestion: - prefix = b.text - text = prefix + suggestion.text - - tokens: List[Optional[str]] = [None, None, None] - substings = [""] - i = 0 - - for token in generate_tokens(StringIO(text).readline): - if token.type == tokenize.NEWLINE: - index = len(text) - else: - index = text.index(token[1], len(substings[-1])) - substings.append(text[:index]) - tokenized_so_far = substings[-1] - if tokenized_so_far.startswith(prefix): - if i == 0 and len(tokenized_so_far) > len(prefix): - tokens[0] = tokenized_so_far[len(prefix) :] - substings.append(tokenized_so_far) - i += 1 - tokens[i] = token[1] - if i == 2: - break - i += 1 - - if tokens[0]: - to_insert: str - insert_text = substings[-2] - if tokens[1] and len(tokens[1]) == 1: - insert_text = substings[-1] - to_insert = insert_text[len(prefix) :] - b.insert_text(to_insert) - return - - nc.forward_word(event) diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py index 92242f75d38..39cc6e24819 100644 --- a/IPython/terminal/tests/test_shortcuts.py +++ b/IPython/terminal/tests/test_shortcuts.py @@ -1,5 +1,5 @@ import pytest -from IPython.terminal.shortcuts.autosuggestions import ( +from IPython.terminal.shortcuts.auto_suggest import ( accept_in_vi_insert_mode, accept_token, ) diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index b5886ffa576..f8fd17bb51f 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -101,6 +101,7 @@ class _DummyTerminal: input_transformer_manager = None display_completions = None editing_mode = "emacs" + auto_suggest = None def create_identifier(handler: Callable): From 43d6a9b11a1aa2843edaf0d3ca1c3a4c4f699182 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 8 Jan 2023 22:49:19 +0000 Subject: [PATCH 0382/1752] Accepting suggestions with cursor in place and resume on backspace --- IPython/terminal/shortcuts/__init__.py | 6 +++ IPython/terminal/shortcuts/auto_suggest.py | 49 +++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 3bb39066ec1..84d599f237a 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -357,6 +357,12 @@ def not_inside_unclosed_string(): kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_character ) + kb.add("left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_and_keep_cursor + ) + kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.backspace_and_resume_hint + ) # Simple Control keybindings key_cmd_dict = { diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 0e8533f6572..8124f5c4b5d 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -37,6 +37,8 @@ def disconnect(self): def connect(self, pt_app: PromptSession): self._connected_apps.append(pt_app) + # note: `on_text_changed` could be used for a bit different behaviour + # on character deletion (i.e. reseting history position on backspace) pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position) def get_suggestion( @@ -113,35 +115,35 @@ def down(self, query: str, other_than: str, history: History): # Needed for to accept autosuggestions in vi insert mode def accept_in_vi_insert_mode(event: KeyPressEvent): """Apply autosuggestion if at end of line.""" - b = event.current_buffer - d = b.document + buffer = event.current_buffer + d = buffer.document after_cursor = d.text[d.cursor_position :] lines = after_cursor.split("\n") end_of_current_line = lines[0].strip() - suggestion = b.suggestion + suggestion = buffer.suggestion if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): - b.insert_text(suggestion.text) + buffer.insert_text(suggestion.text) else: nc.end_of_line(event) def accept(event: KeyPressEvent): """Accept autosuggestion""" - b = event.current_buffer - suggestion = b.suggestion + buffer = event.current_buffer + suggestion = buffer.suggestion if suggestion: - b.insert_text(suggestion.text) + buffer.insert_text(suggestion.text) else: nc.forward_char(event) def accept_word(event: KeyPressEvent): """Fill partial autosuggestion by word""" - b = event.current_buffer - suggestion = b.suggestion + buffer = event.current_buffer + suggestion = buffer.suggestion if suggestion: t = re.split(r"(\S+\s+)", suggestion.text) - b.insert_text(next((x for x in t if x), "")) + buffer.insert_text(next((x for x in t if x), "")) else: nc.forward_word(event) @@ -154,6 +156,33 @@ def accept_character(event: KeyPressEvent): b.insert_text(suggestion.text[0]) +def accept_and_keep_cursor(event: KeyPressEvent): + """Accept autosuggestion and keep cursor in place""" + buffer = event.current_buffer + old_position = buffer.cursor_position + suggestion = buffer.suggestion + if suggestion: + buffer.insert_text(suggestion.text) + buffer.cursor_position = old_position + else: + nc.backward_char(event) + + +def backspace_and_resume_hint(event: KeyPressEvent): + """Resume autosuggestions after deleting last character""" + current_buffer = event.current_buffer + + def resume_hinting(buffer: Buffer): + if buffer.auto_suggest: + suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document) + if suggestion: + buffer.suggestion = suggestion + current_buffer.on_text_changed.remove_handler(resume_hinting) + + current_buffer.on_text_changed.add_handler(resume_hinting) + nc.backward_delete_char(event) + + def accept_token(event: KeyPressEvent): """Fill partial autosuggestion by token""" b = event.current_buffer From dfb5353105b3d33647bb943df32ffbad13f98978 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 8 Jan 2023 22:56:22 +0000 Subject: [PATCH 0383/1752] Accept with cursor in place with ctrl + down, move left after accepting --- IPython/terminal/shortcuts/__init__.py | 3 +++ IPython/terminal/shortcuts/auto_suggest.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 84d599f237a..c4af1378df8 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -358,6 +358,9 @@ def not_inside_unclosed_string(): auto_suggest.accept_character ) kb.add("left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_and_move_cursor_left + ) + kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_and_keep_cursor ) kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 8124f5c4b5d..19ecb865378 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -164,8 +164,12 @@ def accept_and_keep_cursor(event: KeyPressEvent): if suggestion: buffer.insert_text(suggestion.text) buffer.cursor_position = old_position - else: - nc.backward_char(event) + + +def accept_and_move_cursor_left(event: KeyPressEvent): + """Accept autosuggestion and move cursor left""" + accept_and_keep_cursor(event) + nc.backward_char(event) def backspace_and_resume_hint(event: KeyPressEvent): From 039c83ae7ba2e4dfa737692bd90c3b03c534c2c7 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 9 Jan 2023 00:33:09 +0000 Subject: [PATCH 0384/1752] Lint and add more tests --- IPython/terminal/interactiveshell.py | 8 +- IPython/terminal/shortcuts/auto_suggest.py | 3 +- IPython/terminal/tests/test_shortcuts.py | 139 +++++++++++++++++++++ 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 0abff28db90..be81c51a120 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -389,7 +389,9 @@ def _displayhook_class_default(self): def _set_autosuggestions(self, provider): # disconnect old handler - if self.auto_suggest and isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory): + if self.auto_suggest and isinstance( + self.auto_suggest, NavigableAutoSuggestFromHistory + ): self.auto_suggest.disconnect() if provider is None: self.auto_suggest = None @@ -660,7 +662,9 @@ def init_alias(self): def __init__(self, *args, **kwargs): super(TerminalInteractiveShell, self).__init__(*args, **kwargs) - self.auto_suggest: UnionType[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] = None + self.auto_suggest: UnionType[ + AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None + ] = None self._set_autosuggestions(self.autosuggestions_provider) self.init_prompt_toolkit_cli() self.init_term_title() diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 19ecb865378..988853f25b3 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -58,7 +58,6 @@ def _find_match( self, text: str, skip_lines: float, history: History, previous: bool ): line_number = -1 - for string in reversed(list(history.get_strings())): for line in reversed(string.splitlines()): line_number += 1 @@ -167,7 +166,7 @@ def accept_and_keep_cursor(event: KeyPressEvent): def accept_and_move_cursor_left(event: KeyPressEvent): - """Accept autosuggestion and move cursor left""" + """Accept autosuggestion and move cursor left in place""" accept_and_keep_cursor(event) nc.backward_char(event) diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py index 39cc6e24819..da8e841eddf 100644 --- a/IPython/terminal/tests/test_shortcuts.py +++ b/IPython/terminal/tests/test_shortcuts.py @@ -2,8 +2,18 @@ from IPython.terminal.shortcuts.auto_suggest import ( accept_in_vi_insert_mode, accept_token, + accept_character, + accept_word, + accept_and_keep_cursor, + NavigableAutoSuggestFromHistory, + swap_autosuggestion_up, + swap_autosuggestion_down, ) +from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.shortcuts import PromptSession +from prompt_toolkit.buffer import Buffer + from unittest.mock import patch, Mock @@ -81,6 +91,59 @@ def test_autosuggest_token(text, suggestion, expected): assert event.current_buffer.insert_text.call_args[0] == (expected,) +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "d"), + ("d", "ef out(tag: str, n=50):", "e"), + ("de ", "f out(tag: str, n=50):", "f"), + ("def", " out(tag: str, n=50):", " "), + ], +) +def test_accept_character(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + event.current_buffer.insert_text = Mock() + accept_character(event) + assert event.current_buffer.insert_text.called + assert event.current_buffer.insert_text.call_args[0] == (expected,) + + +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "def "), + ("d", "ef out(tag: str, n=50):", "ef "), + ("de", "f out(tag: str, n=50):", "f "), + ("def", " out(tag: str, n=50):", " "), + # (this is why we also have accept_token) + ("def ", "out(tag: str, n=50):", "out(tag: "), + ], +) +def test_accept_word(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + event.current_buffer.insert_text = Mock() + accept_word(event) + assert event.current_buffer.insert_text.called + assert event.current_buffer.insert_text.call_args[0] == (expected,) + + +@pytest.mark.parametrize( + "text, suggestion, expected, cursor", + [ + ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0), + ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4), + ], +) +def test_accept_and_keep_cursor(text, suggestion, expected, cursor): + event = make_event(text, cursor, suggestion) + buffer = event.current_buffer + buffer.insert_text = Mock() + accept_and_keep_cursor(event) + assert buffer.insert_text.called + assert buffer.insert_text.call_args[0] == (expected,) + assert buffer.cursor_position == cursor + + def test_autosuggest_token_empty(): full = "def out(tag: str, n=50):" event = make_event(full, len(full), "") @@ -92,3 +155,79 @@ def test_autosuggest_token_empty(): accept_token(event) assert not event.current_buffer.insert_text.called assert forward_word.called + + +async def test_navigable_provider(): + provider = NavigableAutoSuggestFromHistory() + history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"]) + buffer = Buffer(history=history) + + async for _ in history.load(): + pass + + buffer.cursor_position = 5 + buffer.text = "very" + + up = swap_autosuggestion_up(provider) + down = swap_autosuggestion_down(provider) + + event = Mock() + event.current_buffer = buffer + + def get_suggestion(): + suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + return suggestion + + assert get_suggestion().text == "_c" + + # should go up + up(event) + assert get_suggestion().text == "_b" + + # should skip over 'very' which is identical to buffer content + up(event) + assert get_suggestion().text == "_a" + + # should cycle back to beginning + up(event) + assert get_suggestion().text == "_c" + + # should cycle back through end boundary + down(event) + assert get_suggestion().text == "_a" + + down(event) + assert get_suggestion().text == "_b" + + down(event) + assert get_suggestion().text == "_c" + + down(event) + assert get_suggestion().text == "_a" + + +def test_navigable_provider_connection(): + provider = NavigableAutoSuggestFromHistory() + provider.skip_lines = 1 + + session_1 = PromptSession() + provider.connect(session_1) + + assert provider.skip_lines == 1 + session_1.default_buffer.on_text_insert.fire() + assert provider.skip_lines == 0 + + session_2 = PromptSession() + provider.connect(session_2) + provider.skip_lines = 2 + + assert provider.skip_lines == 2 + session_2.default_buffer.on_text_insert.fire() + assert provider.skip_lines == 0 + + provider.skip_lines = 3 + provider.disconnect() + session_1.default_buffer.on_text_insert.fire() + session_2.default_buffer.on_text_insert.fire() + assert provider.skip_lines == 3 From c97747aab90214f1c9b682aebe3f384a136caee0 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 9 Jan 2023 00:43:53 +0000 Subject: [PATCH 0385/1752] Mock session to avoid Windows issues on CI. Instantiating a real session on Windows CI leads to exception: ``` prompt_toolkit.output.win32.NoConsoleScreenBufferError: No Windows console found. Are you running cmd.exe? ``` with the following traceback: ``` IPython\terminal\tests\test_shortcuts.py:214: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ C:\hostedtoolcache\windows\PyPy\3.8.15\x86\lib\site-packages\prompt_toolkit\shortcuts\prompt.py:476: in __init__ self.app = self._create_application(editing_mode, erase_when_done) C:\hostedtoolcache\windows\PyPy\3.8.15\x86\lib\site-packages\prompt_toolkit\shortcuts\prompt.py:765: in _create_application output=self._output, C:\hostedtoolcache\windows\PyPy\3.8.15\x86\lib\site-packages\prompt_toolkit\application\application.py:282: in __init__ self.output = output or session.output C:\hostedtoolcache\windows\PyPy\3.8.15\x86\lib\site-packages\prompt_toolkit\application\current.py:71: in output self._output = create_output() C:\hostedtoolcache\windows\PyPy\3.8.15\x86\lib\site-packages\prompt_toolkit\output\defaults.py:85: in create_output return Win32Output(stdout, default_color_depth=color_depth_from_env) C:\hostedtoolcache\windows\PyPy\3.8.15\x86\lib\site-packages\prompt_toolkit\output\win32.py:114: in __init__ info = self.get_win32_screen_buffer_info() ``` --- IPython/terminal/tests/test_shortcuts.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py index da8e841eddf..21434b74d18 100644 --- a/IPython/terminal/tests/test_shortcuts.py +++ b/IPython/terminal/tests/test_shortcuts.py @@ -11,7 +11,6 @@ ) from prompt_toolkit.history import InMemoryHistory -from prompt_toolkit.shortcuts import PromptSession from prompt_toolkit.buffer import Buffer from unittest.mock import patch, Mock @@ -207,18 +206,24 @@ def get_suggestion(): assert get_suggestion().text == "_a" +def create_session_mock(): + session = Mock() + session.default_buffer = Buffer() + return session + + def test_navigable_provider_connection(): provider = NavigableAutoSuggestFromHistory() provider.skip_lines = 1 - session_1 = PromptSession() + session_1 = create_session_mock() provider.connect(session_1) assert provider.skip_lines == 1 session_1.default_buffer.on_text_insert.fire() assert provider.skip_lines == 0 - session_2 = PromptSession() + session_2 = create_session_mock() provider.connect(session_2) provider.skip_lines = 2 From dda97ea16fcecb56af542268031d6c02afb1d17a Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 9 Jan 2023 00:59:31 +0000 Subject: [PATCH 0386/1752] Add two final tests to increase coverage --- IPython/terminal/tests/test_shortcuts.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py index 21434b74d18..9f158434487 100644 --- a/IPython/terminal/tests/test_shortcuts.py +++ b/IPython/terminal/tests/test_shortcuts.py @@ -1,5 +1,6 @@ import pytest from IPython.terminal.shortcuts.auto_suggest import ( + accept, accept_in_vi_insert_mode, accept_token, accept_character, @@ -12,6 +13,7 @@ from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.buffer import Buffer +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from unittest.mock import patch, Mock @@ -30,6 +32,22 @@ def make_event(text, cursor, suggestion): return event +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"), + ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"), + ], +) +def test_accept(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + buffer = event.current_buffer + buffer.insert_text = Mock() + accept(event) + assert buffer.insert_text.called + assert buffer.insert_text.call_args[0] == (expected,) + + @pytest.mark.parametrize( "text, cursor, suggestion, called", [ @@ -156,6 +174,17 @@ def test_autosuggest_token_empty(): assert forward_word.called +def test_other_providers(): + """Ensure that swapping autosuggestions does not break with other providers""" + provider = AutoSuggestFromHistory() + up = swap_autosuggestion_up(provider) + down = swap_autosuggestion_down(provider) + event = Mock() + event.current_buffer = Buffer() + assert up(event) is None + assert down(event) is None + + async def test_navigable_provider(): provider = NavigableAutoSuggestFromHistory() history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"]) From 2e0a730903561765a90957e5ce652f74790890ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:29:12 +0000 Subject: [PATCH 0387/1752] Fix typos in a comment --- IPython/terminal/shortcuts/auto_suggest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 988853f25b3..8c699d8a23d 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -83,8 +83,8 @@ def up(self, query: str, other_than: str, history: History): query, self.skip_lines, history ): # if user has history ['very.a', 'very', 'very.b'] and typed 'very' - # we want to switch from 'very.b' to 'very.a' because a) if they - # suggestion equals current text, prompt-toolit aborts suggesting + # we want to switch from 'very.b' to 'very.a' because a) if the + # suggestion equals current text, prompt-toolkit aborts suggesting # b) user likely would not be interested in 'very' anyways (they # already typed it). if query + suggestion != other_than: From b332fd1ab5682acdc984aab07c7cdb99ab5b4c3c Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 9 Jan 2023 14:03:28 -0800 Subject: [PATCH 0388/1752] Remove direct support for Qt4 See https://github.com/ipython/ipykernel/pull/1071 Note that it's still there if coming from `matplotlib` --- IPython/core/magics/basic.py | 6 ++++-- IPython/external/qt_for_kernel.py | 7 ------- IPython/external/qt_loaders.py | 5 +---- IPython/lib/guisupport.py | 22 ++++++++++---------- IPython/terminal/pt_inputhooks/__init__.py | 19 ++--------------- IPython/terminal/tests/test_pt_inputhooks.py | 4 ++-- docs/source/config/eventloops.rst | 2 +- docs/source/interactive/reference.rst | 19 ++++++----------- 8 files changed, 27 insertions(+), 57 deletions(-) diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index 7dfa84ce2d5..0699994d3fc 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -493,8 +493,10 @@ def gui(self, parameter_s=''): are supported: wxPython, PyQt4, PyGTK, Tk and Cocoa (OSX):: %gui wx # enable wxPython event loop integration - %gui qt4|qt # enable PyQt4 event loop integration - %gui qt5 # enable PyQt5 event loop integration + %gui qt # enable PyQt/PySide event loop integration + # with the latest version available. + %gui qt6 # enable PyQt6/PySide6 event loop integration + %gui qt5 # enable PyQt5/PySide2 event loop integration %gui gtk # enable PyGTK event loop integration %gui gtk3 # enable Gtk3 event loop integration %gui gtk4 # enable Gtk4 event loop integration diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index 986cae3321e..11e88625d1d 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -45,7 +45,6 @@ QT_API_PYQT5, QT_API_PYSIDE2, # QT4 - QT_API_PYQTv1, QT_API_PYQT, QT_API_PYSIDE, # default @@ -59,10 +58,6 @@ # QT5 QT_API_PYQT5, QT_API_PYSIDE2, - # QT4 - QT_API_PYQTv1, - QT_API_PYQT, - QT_API_PYSIDE, # default QT_API_PYQT_DEFAULT, ) @@ -116,8 +111,6 @@ def get_options(): QT_API_PYSIDE6, QT_API_PYQT5, QT_API_PYSIDE2, - QT_API_PYQT, - QT_API_PYSIDE, ] elif qt_api not in _qt_apis: raise RuntimeError("Invalid Qt API %r, valid values are: %r" % diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index f3831dac801..c900c8f30b4 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -24,6 +24,7 @@ QT_API_PYSIDE2 = 'pyside2' # Qt4 +# NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side. QT_API_PYQT = "pyqt" # Force version 2 QT_API_PYQTv1 = "pyqtv1" # Force version 2 QT_API_PYSIDE = "pyside" @@ -374,20 +375,16 @@ def load_qt(api_options): PySide6 is available, and only one is imported per session. Currently-imported Qt library: %r - PyQt4 available (requires QtCore, QtGui, QtSvg): %s PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s - PySide >= 1.0.3 installed: %s PySide2 installed: %s PySide6 installed: %s Tried to load: %r """ % ( loaded_api(), - has_binding(QT_API_PYQT), has_binding(QT_API_PYQT5), has_binding(QT_API_PYQT6), - has_binding(QT_API_PYSIDE), has_binding(QT_API_PYSIDE2), has_binding(QT_API_PYSIDE6), api_options, diff --git a/IPython/lib/guisupport.py b/IPython/lib/guisupport.py index cfd325e9da8..9089fa8bcdd 100644 --- a/IPython/lib/guisupport.py +++ b/IPython/lib/guisupport.py @@ -106,11 +106,11 @@ def start_event_loop_wx(app=None): app._in_event_loop = True #----------------------------------------------------------------------------- -# qt4 +# Qt #----------------------------------------------------------------------------- -def get_app_qt4(*args, **kwargs): - """Create a new qt4 app or return an existing one.""" +def get_app_qt(*args, **kwargs): + """Create a new Qt app or return an existing one.""" from IPython.external.qt_for_kernel import QtGui app = QtGui.QApplication.instance() if app is None: @@ -119,8 +119,8 @@ def get_app_qt4(*args, **kwargs): app = QtGui.QApplication(*args, **kwargs) return app -def is_event_loop_running_qt4(app=None): - """Is the qt4 event loop running.""" +def is_event_loop_running_qt(app=None): + """Is the qt event loop running.""" # New way: check attribute on shell instance ip = get_ipython() if ip is not None: @@ -128,18 +128,18 @@ def is_event_loop_running_qt4(app=None): # Old way: check attribute on QApplication singleton if app is None: - app = get_app_qt4(['']) + app = get_app_qt(['']) if hasattr(app, '_in_event_loop'): return app._in_event_loop else: - # Does qt4 provide a other way to detect this? + # Does qt provide a other way to detect this? return False -def start_event_loop_qt4(app=None): - """Start the qt4 event loop in a consistent manner.""" +def start_event_loop_qt(app=None): + """Start the qt event loop in a consistent manner.""" if app is None: - app = get_app_qt4(['']) - if not is_event_loop_running_qt4(app): + app = get_app_qt(['']) + if not is_event_loop_running_qt(app): app._in_event_loop = True app.exec_() app._in_event_loop = False diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 146424c0a34..0e6bacc0a57 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -8,7 +8,6 @@ backends = [ "qt", - "qt4", "qt5", "qt6", "gtk", @@ -80,21 +79,7 @@ def set_qt_api(gui): f'environment variable is set to "{qt_api}"' ) else: - # NOTE: 'qt4' is not selectable because it's set as an alias for 'qt'; see `aliases` above. - if gui == "qt4": - try: - import PyQt # noqa - - os.environ["QT_API"] = "pyqt" - except ImportError: - try: - import PySide # noqa - - os.environ["QT_API"] = "pyside" - except ImportError: - # Neither implementation installed; set it to something so IPython gives an error - os.environ["QT_API"] = "pyqt" - elif gui == "qt5": + if gui == "qt5": try: import PyQt5 # noqa @@ -124,7 +109,7 @@ def set_qt_api(gui): del os.environ["QT_API"] else: raise ValueError( - f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' + f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".' ) diff --git a/IPython/terminal/tests/test_pt_inputhooks.py b/IPython/terminal/tests/test_pt_inputhooks.py index 775d1c7ae6f..bb4baaaadfe 100644 --- a/IPython/terminal/tests/test_pt_inputhooks.py +++ b/IPython/terminal/tests/test_pt_inputhooks.py @@ -12,7 +12,7 @@ def _get_qt_vers(): """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due to the import mechanism, we can't import multiple versions of Qt in one session.""" - for gui in ["qt", "qt6", "qt5", "qt4"]: + for gui in ["qt", "qt6", "qt5"]: print(f"Trying {gui}") try: set_qt_api(gui) @@ -39,7 +39,7 @@ def test_inputhook_qt(): get_inputhook_name_and_func(gui) # ...and now we're stuck with this version of Qt for good; can't switch. - for not_gui in ["qt6", "qt5", "qt4"]: + for not_gui in ["qt6", "qt5"]: if not_gui not in guis_avail: break diff --git a/docs/source/config/eventloops.rst b/docs/source/config/eventloops.rst index dd527a61ec2..6bf349f2df6 100644 --- a/docs/source/config/eventloops.rst +++ b/docs/source/config/eventloops.rst @@ -7,7 +7,7 @@ loop, so you can use both a GUI and an interactive prompt together. IPython supports a number of common GUI toolkits, but from IPython 3.0, it is possible to integrate other event loops without modifying IPython itself. -Supported event loops include ``qt4``, ``qt5``, ``gtk2``, ``gtk3``, ``gtk4``, +Supported event loops include ``qt5``, ``qt6``, ``gtk2``, ``gtk3``, ``gtk4``, ``wx``, ``osx`` and ``tk``. Make sure the event loop you specify matches the GUI toolkit used by your own code. diff --git a/docs/source/interactive/reference.rst b/docs/source/interactive/reference.rst index 8eed5348fd3..1df77dcfbfc 100644 --- a/docs/source/interactive/reference.rst +++ b/docs/source/interactive/reference.rst @@ -44,7 +44,7 @@ the command-line by passing the full class name and a corresponding value; type <...snip...> --matplotlib= (InteractiveShellApp.matplotlib) Default: None - Choices: ['auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'nbagg', 'notebook', 'osx', 'qt', 'qt4', 'qt5', 'tk', 'wx'] + Choices: ['auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'nbagg', 'notebook', 'osx', 'qt', 'qt5', 'qt6', 'tk', 'wx'] Configure matplotlib for interactive use with the default matplotlib backend. <...snip...> @@ -892,7 +892,7 @@ GUI event loop support ====================== IPython has excellent support for working interactively with Graphical User -Interface (GUI) toolkits, such as wxPython, PyQt4/PySide, PyGTK and Tk. This is +Interface (GUI) toolkits, such as wxPython, PyQt/PySide, PyGTK and Tk. This is implemented by running the toolkit's event loop while IPython is waiting for input. @@ -902,7 +902,7 @@ For users, enabling GUI event loop integration is simple. You simple use the %gui [GUINAME] With no arguments, ``%gui`` removes all GUI support. Valid ``GUINAME`` -arguments include ``wx``, ``qt``, ``qt5``, ``gtk``, ``gtk3`` ``gtk4``, and +arguments include ``wx``, ``qt``, ``qt5``, ``qt6``, ``gtk``, ``gtk3`` ``gtk4``, and ``tk``. Thus, to use wxPython interactively and create a running :class:`wx.App` @@ -936,16 +936,9 @@ PyQt and PySide .. attempt at explanation of the complete mess that is Qt support When you use ``--gui=qt`` or ``--matplotlib=qt``, IPython can work with either -PyQt4 or PySide. There are three options for configuration here, because -PyQt4 has two APIs for QString and QVariant: v1, which is the default on -Python 2, and the more natural v2, which is the only API supported by PySide. -v2 is also the default for PyQt4 on Python 3. IPython's code for the QtConsole -uses v2, but you can still use any interface in your code, since the -Qt frontend is in a different process. - -The default will be to import PyQt4 without configuration of the APIs, thus -matching what most applications would expect. It will fall back to PySide if -PyQt4 is unavailable. +PyQt or PySide. ``qt`` implies "use the latest version available", and it favors +PyQt over PySide. To request a specific version, use ``qt5`` or ``qt6``. Note that +Qt4 is not supported with the ``--gui`` switch (and has not been for some time now). If specified, IPython will respect the environment variable ``QT_API`` used by ETS. ETS 4.0 also works with both PyQt4 and PySide, but it requires From 964e296f396a412cf67abb54467c654a538ef611 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 9 Jan 2023 14:08:51 -0800 Subject: [PATCH 0389/1752] Put "4" back in; others depend on it (e.g. `ipykernel`) --- IPython/lib/guisupport.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IPython/lib/guisupport.py b/IPython/lib/guisupport.py index 9089fa8bcdd..c08cea38d21 100644 --- a/IPython/lib/guisupport.py +++ b/IPython/lib/guisupport.py @@ -109,7 +109,7 @@ def start_event_loop_wx(app=None): # Qt #----------------------------------------------------------------------------- -def get_app_qt(*args, **kwargs): +def get_app_qt4(*args, **kwargs): """Create a new Qt app or return an existing one.""" from IPython.external.qt_for_kernel import QtGui app = QtGui.QApplication.instance() @@ -119,7 +119,7 @@ def get_app_qt(*args, **kwargs): app = QtGui.QApplication(*args, **kwargs) return app -def is_event_loop_running_qt(app=None): +def is_event_loop_running_qt4(app=None): """Is the qt event loop running.""" # New way: check attribute on shell instance ip = get_ipython() @@ -128,18 +128,18 @@ def is_event_loop_running_qt(app=None): # Old way: check attribute on QApplication singleton if app is None: - app = get_app_qt(['']) + app = get_app_qt4(['']) if hasattr(app, '_in_event_loop'): return app._in_event_loop else: # Does qt provide a other way to detect this? return False -def start_event_loop_qt(app=None): +def start_event_loop_qt4(app=None): """Start the qt event loop in a consistent manner.""" if app is None: app = get_app_qt(['']) - if not is_event_loop_running_qt(app): + if not is_event_loop_running_qt4(app): app._in_event_loop = True app.exec_() app._in_event_loop = False From 7a2ad1fb696a0ff85f2240ffe3ba00049479a6f6 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 9 Jan 2023 14:12:22 -0800 Subject: [PATCH 0390/1752] Formatting --- IPython/lib/guisupport.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/lib/guisupport.py b/IPython/lib/guisupport.py index c08cea38d21..4d532d0f4d5 100644 --- a/IPython/lib/guisupport.py +++ b/IPython/lib/guisupport.py @@ -115,7 +115,7 @@ def get_app_qt4(*args, **kwargs): app = QtGui.QApplication.instance() if app is None: if not args: - args = ([''],) + args = ([""],) app = QtGui.QApplication(*args, **kwargs) return app @@ -128,7 +128,7 @@ def is_event_loop_running_qt4(app=None): # Old way: check attribute on QApplication singleton if app is None: - app = get_app_qt4(['']) + app = get_app_qt4([""]) if hasattr(app, '_in_event_loop'): return app._in_event_loop else: @@ -138,7 +138,7 @@ def is_event_loop_running_qt4(app=None): def start_event_loop_qt4(app=None): """Start the qt event loop in a consistent manner.""" if app is None: - app = get_app_qt(['']) + app = get_app_qt4([""]) if not is_event_loop_running_qt4(app): app._in_event_loop = True app.exec_() From 91607ebc27d9e9cf9fc333e0f2fe3895a04c48e5 Mon Sep 17 00:00:00 2001 From: Maor Kleinberger Date: Tue, 10 Jan 2023 01:29:21 +0200 Subject: [PATCH 0391/1752] Use prompt_toolkit.application.create_app_session for debugger prompt Running the debugger prompt in the default prompt_toolkit session causes issues when more than one prompt_toolkit app is running simultaneously. The errors look exactly like those mentioned in #12192. This commit solves this by using the dedicated API from prompt_toolkit. --- IPython/terminal/debugger.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index 1859da20410..e47f5f43c8f 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -10,6 +10,7 @@ from pathlib import Path from pygments.token import Token +from prompt_toolkit.application import create_app_session from prompt_toolkit.shortcuts.prompt import PromptSession from prompt_toolkit.enums import EditingMode from prompt_toolkit.formatted_text import PygmentsTokens @@ -96,6 +97,17 @@ def gen_comp(self, text): self.pt_loop = asyncio.new_event_loop() self.pt_app = PromptSession(**options) + def _prompt(self): + """ + In case another prompt_toolkit apps have to run in parallel to this one (e.g. in madbg), + create_app_session must be used to prevent mixing up between them. According to the prompt_toolkit docs: + + If you need multiple applications running at the same time, you have to create a separate + `AppSession` using a `with create_app_session():` block. + """ + with create_app_session(): + return self.pt_app.prompt() + def cmdloop(self, intro=None): """Repeatedly issue a prompt, accept input, parse an initial prefix off the received input, and dispatch to action methods, passing them @@ -129,9 +141,7 @@ def cmdloop(self, intro=None): # Run the prompt in a different thread. if not _use_simple_prompt: try: - line = self.thread_executor.submit( - self.pt_app.prompt - ).result() + line = self.thread_executor.submit(self._prompt).result() except EOFError: line = "EOF" else: From 73fd29a13a83b077f3828d88daabdff23a8e74a8 Mon Sep 17 00:00:00 2001 From: Garland Zhang Date: Wed, 11 Jan 2023 16:31:59 -0800 Subject: [PATCH 0392/1752] Update script.py Handle non utf-8 characters during decoding for %%bash --- IPython/core/magics/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/magics/script.py b/IPython/core/magics/script.py index 9fd2fc6c0dd..e0615c0ca85 100644 --- a/IPython/core/magics/script.py +++ b/IPython/core/magics/script.py @@ -210,7 +210,7 @@ def in_thread(coro): async def _handle_stream(stream, stream_arg, file_object): while True: - line = (await stream.readline()).decode("utf8") + line = (await stream.readline()).decode("utf8", errors="replace") if not line: break if stream_arg: From 462382b3e30a006427d71c7e046b6e6164bda0a2 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 16 Jan 2023 17:16:00 +0100 Subject: [PATCH 0393/1752] MAINT: Remove usage of traitlets. Now this is mostly validate at typechecheck time, instead of runtime. We don't use any validation logic so I'm unsure it is really necessary. --- IPython/core/application.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IPython/core/application.py b/IPython/core/application.py index 26c061661ac..2aa0f104399 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -123,9 +123,8 @@ def load_subconfig(self, fname, path=None, profile=None): return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path) class BaseIPythonApplication(Application): - - name = u'ipython' - description = Unicode(u'IPython: an enhanced interactive Python shell.') + name = "ipython" + description = "IPython: an enhanced interactive Python shell." version = Unicode(release.version) aliases = base_aliases From 46c503d07bb4def76e824c54f4712a5fa4e7538d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 16 Jan 2023 17:15:26 +0100 Subject: [PATCH 0394/1752] MISC docs, cleanup and typing (in progress). --- IPython/terminal/ipapp.py | 4 ++-- IPython/terminal/shortcuts/__init__.py | 4 ++-- IPython/terminal/shortcuts/auto_suggest.py | 27 +++++++++++++++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index df4648b8914..6280bce3b20 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -156,7 +156,7 @@ def make_report(self,traceback): flags.update(frontend_flags) aliases = dict(base_aliases) -aliases.update(shell_aliases) +aliases.update(shell_aliases) # type: ignore[arg-type] #----------------------------------------------------------------------------- # Main classes and functions @@ -180,7 +180,7 @@ def start(self): class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): name = u'ipython' description = usage.cl_usage - crash_handler_class = IPAppCrashHandler + crash_handler_class = IPAppCrashHandler # typing: ignore[assignment] examples = _examples flags = flags diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index c4af1378df8..bc0c95c6f0b 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -163,11 +163,11 @@ def preceding_text(pattern: Union[str, Callable]): return _preceding_text_cache[pattern] if callable(pattern): - def _preceding_text(): app = get_app() before_cursor = app.current_buffer.document.current_line_before_cursor - return bool(pattern(before_cursor)) + # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603 + return bool(pattern(before_cursor)) # type: ignore[operator] else: m = re.compile(pattern) diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 8c699d8a23d..f623f8e1acf 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -1,7 +1,7 @@ import re import tokenize from io import StringIO -from typing import Callable, List, Optional, Union +from typing import Callable, List, Optional, Union, Generator, Tuple from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding import KeyPressEvent @@ -19,7 +19,11 @@ def _get_query(document: Document): class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): - """ """ + """ + A subclass of AutoSuggestFromHistory that allow navigation to next/previous + suggestion from history. To do so it remembers the current position, but it + state need to carefully be cleared on the right events. + """ def __init__( self, @@ -56,7 +60,24 @@ def get_suggestion( def _find_match( self, text: str, skip_lines: float, history: History, previous: bool - ): + ) -> Generator[Tuple[str, float], None, None]: + """ + text: str + + skip_lines: float + float is used as the base value is +inf + + Yields + ------ + Tuple with: + str: + current suggestion. + float: + will actually yield only ints, which is passed back via skip_lines, + which may be a +inf (float) + + + """ line_number = -1 for string in reversed(list(history.get_strings())): for line in reversed(string.splitlines()): From 577c3b1225aae1bf9d2a71f64c7cbb848fbbfc54 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 17 Jan 2023 09:04:55 +0100 Subject: [PATCH 0395/1752] Misc review cleanup. Uniformize import, and put minimal prompt toolkit to 3.30 --- IPython/terminal/shortcuts/__init__.py | 56 +++++++++++----------- IPython/terminal/shortcuts/auto_suggest.py | 2 +- setup.cfg | 2 +- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index bc0c95c6f0b..a8b07026d31 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -6,52 +6,38 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -import warnings +import os +import re import signal import sys -import re -import os +import warnings from typing import Callable, Dict, Union - from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER +from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions +from prompt_toolkit.filters import has_focus as has_focus_impl from prompt_toolkit.filters import ( - has_focus as has_focus_impl, has_selection, - Condition, + has_suggestion, vi_insert_mode, - emacs_insert_mode, - has_completions, vi_mode, ) +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.bindings import named_commands as nc from prompt_toolkit.key_binding.bindings.completion import ( display_completions_like_readline, ) -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.bindings import named_commands as nc from prompt_toolkit.key_binding.vi_state import InputMode, ViState from prompt_toolkit.layout.layout import FocusableElement +from IPython.terminal.shortcuts import auto_match as match +from IPython.terminal.shortcuts import auto_suggest from IPython.utils.decorators import undoc -from . import auto_match as match, auto_suggest - __all__ = ["create_ipython_shortcuts"] -try: - # only added in 3.0.30 - from prompt_toolkit.filters import has_suggestion -except ImportError: - - @undoc - @Condition - def has_suggestion(): - buffer = get_app().current_buffer - return buffer.suggestion is not None and buffer.suggestion.text != "" - - @undoc @Condition def cursor_in_leading_ws(): @@ -66,8 +52,24 @@ def has_focus(value: FocusableElement): return Condition(tester) -def create_ipython_shortcuts(shell, for_all_platforms: bool = False): - """Set up the prompt_toolkit keyboard shortcuts for IPython.""" +def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings: + """Set up the prompt_toolkit keyboard shortcuts for IPython. + + Parameters + ---------- + shell: InteractiveShell + The current IPython shell Instance + for_all_platforms: bool (default false) + This parameter is mostly used in generating the documentation + to create the shortcut binding for all the platforms, and export + them. + + Returns + ------- + KeyBindings + the keybinding instance for prompt toolkit. + + """ # Warning: if possible, do NOT define handler functions in the locals # scope of this function, instead define functions in the global # scope, or a separate module, and include a user-friendly docstring @@ -613,8 +615,8 @@ def open_input_in_editor(event): from IPython.core.error import TryNext from IPython.lib.clipboard import ( ClipboardEmpty, - win32_clipboard_get, tkinter_clipboard_get, + win32_clipboard_get, ) @undoc diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index f623f8e1acf..f7fb47e8cb5 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -260,7 +260,7 @@ def _swap_autosuggestion( """ We skip most recent history entry (in either direction) if it equals the current autosuggestion because if user cycles when auto-suggestion is shown - they most likely want something else than what was suggested (othewrise + they most likely want something else than what was suggested (otherwise they would have accepted the suggestion). """ suggestion = buffer.suggestion diff --git a/setup.cfg b/setup.cfg index de327aba545..d196214d19a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ install_requires = matplotlib-inline pexpect>4.3; sys_platform != "win32" pickleshare - prompt_toolkit>=3.0.11,<3.1.0 + prompt_toolkit>=3.0.30,<3.1.0 pygments>=2.4.0 stack_data traitlets>=5 From ad41919ea90c23c024a8c0e58c2224d21815de80 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 17 Jan 2023 09:10:49 +0100 Subject: [PATCH 0396/1752] add docs --- IPython/terminal/shortcuts/auto_match.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/IPython/terminal/shortcuts/auto_match.py b/IPython/terminal/shortcuts/auto_match.py index bb0ca8b3169..c29f97a268c 100644 --- a/IPython/terminal/shortcuts/auto_match.py +++ b/IPython/terminal/shortcuts/auto_match.py @@ -1,3 +1,9 @@ +""" +Utilities function for keybinding with prompt toolkit. + +This will be bound to specific key press and filter modes, +like whether we are in edit mode, and whether the completer is open. +""" import re from prompt_toolkit.key_binding import KeyPressEvent From 3c529d3cbc6fa3eb736544860ad984c0198bcb35 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 17 Jan 2023 09:21:51 +0100 Subject: [PATCH 0397/1752] misc typing --- IPython/terminal/interactiveshell.py | 10 +++++---- IPython/terminal/shortcuts/auto_suggest.py | 26 +++++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index be81c51a120..7213809e5fd 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -144,6 +144,10 @@ class PtkHistoryAdapter(History): """ + auto_suggest: UnionType[ + AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None + ] + def __init__(self, shell): super().__init__() self.shell = shell @@ -660,11 +664,9 @@ def init_alias(self): self.alias_manager.soft_define_alias(cmd, cmd) - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super(TerminalInteractiveShell, self).__init__(*args, **kwargs) - self.auto_suggest: UnionType[ - AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None - ] = None + self.auto_suggest = None self._set_autosuggestions(self.autosuggestions_provider) self.init_prompt_toolkit_cli() self.init_term_title() diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index f7fb47e8cb5..a8e1d7bfeac 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -1,7 +1,7 @@ import re import tokenize from io import StringIO -from typing import Callable, List, Optional, Union, Generator, Tuple +from typing import Callable, List, Optional, Union, Generator, Tuple, Sequence from prompt_toolkit.buffer import Buffer from prompt_toolkit.key_binding import KeyPressEvent @@ -62,10 +62,18 @@ def _find_match( self, text: str, skip_lines: float, history: History, previous: bool ) -> Generator[Tuple[str, float], None, None]: """ - text: str - - skip_lines: float - float is used as the base value is +inf + text : str + Text content to find a match for, the user cursor is most of the + time at the end of this text. + skip_lines : float + number of items to skip in the search, this is used to indicate how + far in the list the user has navigated by pressing up or down. + The float type is used as the base value is +inf + history : History + prompt_toolkit History instance to fetch previous entries from. + previous : bool + Direction of the search, whether we are looking previous match + (True), or next match (False). Yields ------ @@ -91,7 +99,9 @@ def _find_match( if previous and line_number >= skip_lines: return - def _find_next_match(self, text: str, skip_lines: float, history: History): + def _find_next_match( + self, text: str, skip_lines: float, history: History + ) -> Generator[Tuple[str, float], None, None]: return self._find_match(text, skip_lines, history, previous=False) def _find_previous_match(self, text: str, skip_lines: float, history: History): @@ -99,7 +109,7 @@ def _find_previous_match(self, text: str, skip_lines: float, history: History): list(self._find_match(text, skip_lines, history, previous=True)) ) - def up(self, query: str, other_than: str, history: History): + def up(self, query: str, other_than: str, history: History) -> None: for suggestion, line_number in self._find_next_match( query, self.skip_lines, history ): @@ -115,7 +125,7 @@ def up(self, query: str, other_than: str, history: History): # no matches found, cycle back to beginning self.skip_lines = 0 - def down(self, query: str, other_than: str, history: History): + def down(self, query: str, other_than: str, history: History) -> None: for suggestion, line_number in self._find_previous_match( query, self.skip_lines, history ): From 3e2655952703c7426f10b9e9d86fde2a8cd0391e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 17 Jan 2023 09:36:11 +0100 Subject: [PATCH 0398/1752] Reformat and move some mapping to global location. --- IPython/terminal/shortcuts/__init__.py | 23 +++++++++-------------- IPython/terminal/shortcuts/auto_match.py | 8 ++++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index a8b07026d31..d287368272a 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -165,6 +165,7 @@ def preceding_text(pattern: Union[str, Callable]): return _preceding_text_cache[pattern] if callable(pattern): + def _preceding_text(): app = get_app() before_cursor = app.current_buffer.document.current_line_before_cursor @@ -215,12 +216,18 @@ def not_inside_unclosed_string(): return not ('"' in s or "'" in s) # auto match - auto_match_parens = {"(": match.parenthesis, "[": match.brackets, "{": match.braces} - for key, cmd in auto_match_parens.items(): + for key, cmd in match.auto_match_parens.items(): kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))( cmd ) + # raw string + for key, cmd in match.auto_match_parens_raw_string.items(): + kb.add( + key, + filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"), + )(cmd) + kb.add( '"', filter=focused_insert @@ -255,18 +262,6 @@ def not_inside_unclosed_string(): & preceding_text(r"^.*''$"), )(match.docstring_single_quotes) - # raw string - auto_match_parens_raw_string = { - "(": match.raw_string_parenthesis, - "[": match.raw_string_bracket, - "{": match.raw_string_braces, - } - for key, cmd in auto_match_parens_raw_string.items(): - kb.add( - key, - filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"), - )(cmd) - # just move cursor kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))( match.skip_over diff --git a/IPython/terminal/shortcuts/auto_match.py b/IPython/terminal/shortcuts/auto_match.py index c29f97a268c..46cb1bd8754 100644 --- a/IPython/terminal/shortcuts/auto_match.py +++ b/IPython/terminal/shortcuts/auto_match.py @@ -94,3 +94,11 @@ def delete_pair(event: KeyPressEvent): """Delete auto-closed parenthesis""" event.current_buffer.delete() event.current_buffer.delete_before_cursor() + + +auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces} +auto_match_parens_raw_string = { + "(": raw_string_parenthesis, + "[": raw_string_bracket, + "{": raw_string_braces, +} From 8d598b073c080738b9f6e69e45e2ffd97a24a92e Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 19 Jan 2023 12:06:58 -0800 Subject: [PATCH 0399/1752] Make error a warning. An exception here causes issues because `matplotlib.get_backend()` actually sets a backend if none was set. --- IPython/terminal/interactiveshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 2179010f546..a997639ba9c 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -712,7 +712,7 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): if self._inputhook is not None and gui is not None: - raise RuntimeError("Shell already running a gui event loop.") + warn(f"Shell was already running a gui event loop for {self.active_eventloop}; switching to {gui}.") if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) From bbfab79dc5bb7002c7b40191b7d2976fa4f4e7b2 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 19 Jan 2023 12:16:23 -0800 Subject: [PATCH 0400/1752] Formatting --- IPython/terminal/interactiveshell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index a997639ba9c..176806b8f30 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -712,7 +712,9 @@ def inputhook(self, context): active_eventloop = None def enable_gui(self, gui=None): if self._inputhook is not None and gui is not None: - warn(f"Shell was already running a gui event loop for {self.active_eventloop}; switching to {gui}.") + warn( + f"Shell was already running a gui event loop for {self.active_eventloop}; switching to {gui}." + ) if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) From e0fc2386083aed55b44d4c50661fb53bd4170a3f Mon Sep 17 00:00:00 2001 From: nfgf Date: Sat, 21 Jan 2023 13:03:39 -0500 Subject: [PATCH 0401/1752] Documentation update --- IPython/core/magic.py | 2 +- docs/source/config/custommagics.rst | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 95653dc7893..4f9e4e548f7 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -280,7 +280,7 @@ def no_var_expand(magic_func): def output_can_be_silenced(magic_func): """Mark a magic function so its output may be silenced. - The output is silenced if the Python expression used as a parameter of + The output is silenced if the Python code used as a parameter of the magic ends in a semicolon, not counting a Python comment that can follow it. """ diff --git a/docs/source/config/custommagics.rst b/docs/source/config/custommagics.rst index 99d4068773c..0a37b858a4c 100644 --- a/docs/source/config/custommagics.rst +++ b/docs/source/config/custommagics.rst @@ -139,13 +139,26 @@ Accessing user namespace and local scope ======================================== When creating line magics, you may need to access surrounding scope to get user -variables (e.g when called inside functions). IPython provide the +variables (e.g when called inside functions). IPython provides the ``@needs_local_scope`` decorator that can be imported from ``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope`` can also be applied to cell magics even if cell magics cannot appear at local scope context. +Silencing the magic output +========================== + +Sometimes it may be useful to define a magic that can be silenced the same way +that non-magic expressions can, i.e., by appending a semicolon at the end of the Python +code to be executed. That can be achieved by decorating the magic function with +the decorator ``@output_can_be_silenced`` that can be imported from +``IPython.core.magics``. When this decorator is used, IPython will parse the Python +code used by the magic and, if the last token is a ``;``, the output created by the +magic will not show up on the screen. If you want to see an example of this decorator +in action, take a look on the ``time`` magic defined in +``IPython.core.magics.execution.py``. + Complete Example ================ From fc700ddd1756a4302707abacf62198d1c3809738 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 22 Jan 2023 13:14:48 +0000 Subject: [PATCH 0402/1752] Discard auto-suggestion on `Esc` --- IPython/terminal/shortcuts/__init__.py | 5 +++-- IPython/terminal/shortcuts/auto_suggest.py | 6 ++++++ IPython/terminal/tests/test_shortcuts.py | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index d287368272a..ad4dc39d870 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -343,8 +343,9 @@ def not_inside_unclosed_string(): kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_token ) - from functools import partial - + kb.add("escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER), eager=True)( + auto_suggest.discard + ) kb.add("up", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.swap_autosuggestion_up(shell.auto_suggest) ) diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index a8e1d7bfeac..733a46d416f 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -167,6 +167,12 @@ def accept(event: KeyPressEvent): nc.forward_char(event) +def discard(event: KeyPressEvent): + """Discard autosuggestion""" + buffer = event.current_buffer + buffer.suggestion = None + + def accept_word(event: KeyPressEvent): """Fill partial autosuggestion by word""" buffer = event.current_buffer diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py index 9f158434487..a43a4ba2605 100644 --- a/IPython/terminal/tests/test_shortcuts.py +++ b/IPython/terminal/tests/test_shortcuts.py @@ -6,6 +6,7 @@ accept_character, accept_word, accept_and_keep_cursor, + discard, NavigableAutoSuggestFromHistory, swap_autosuggestion_up, swap_autosuggestion_down, @@ -48,6 +49,22 @@ def test_accept(text, suggestion, expected): assert buffer.insert_text.call_args[0] == (expected,) +@pytest.mark.parametrize( + "text, suggestion", + [ + ("", "def out(tag: str, n=50):"), + ("def ", "out(tag: str, n=50):"), + ], +) +def test_discard(text, suggestion): + event = make_event(text, len(text), suggestion) + buffer = event.current_buffer + buffer.insert_text = Mock() + discard(event) + assert not buffer.insert_text.called + assert buffer.suggestion is None + + @pytest.mark.parametrize( "text, cursor, suggestion, called", [ From 9c028ecc7fcd9ec8ba89cc60f4b49096dffa86ca Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 22 Jan 2023 20:01:51 +0000 Subject: [PATCH 0403/1752] Autosuggest: only navigate on edges of doc, show in multi-line doc --- IPython/terminal/interactiveshell.py | 51 ++++++++++++------ IPython/terminal/shortcuts/__init__.py | 44 +++++++++++++--- IPython/terminal/shortcuts/auto_suggest.py | 60 ++++++++++++++++++++-- IPython/terminal/tests/test_shortcuts.py | 42 +++++++++++++-- 4 files changed, 166 insertions(+), 31 deletions(-) diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 7213809e5fd..c61024c124d 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -50,7 +50,10 @@ from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook from .ptutils import IPythonPTCompleter, IPythonPTLexer from .shortcuts import create_ipython_shortcuts -from .shortcuts.auto_suggest import NavigableAutoSuggestFromHistory +from .shortcuts.auto_suggest import ( + NavigableAutoSuggestFromHistory, + AppendAutoSuggestionInAnyLine, +) PTK3 = ptk_version.startswith('3.') @@ -577,23 +580,39 @@ def get_message(): get_message = get_message() options = { - 'complete_in_thread': False, - 'lexer':IPythonPTLexer(), - 'reserve_space_for_menu':self.space_for_menu, - 'message': get_message, - 'prompt_continuation': ( - lambda width, lineno, is_soft_wrap: - PygmentsTokens(self.prompts.continuation_prompt_tokens(width))), - 'multiline': True, - 'complete_style': self.pt_complete_style, - + "complete_in_thread": False, + "lexer": IPythonPTLexer(), + "reserve_space_for_menu": self.space_for_menu, + "message": get_message, + "prompt_continuation": ( + lambda width, lineno, is_soft_wrap: PygmentsTokens( + self.prompts.continuation_prompt_tokens(width) + ) + ), + "multiline": True, + "complete_style": self.pt_complete_style, + "input_processors": [ # Highlight matching brackets, but only when this setting is # enabled, and only when the DEFAULT_BUFFER has the focus. - 'input_processors': [ConditionalProcessor( - processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & - Condition(lambda: self.highlight_matching_brackets))], - } + ConditionalProcessor( + processor=HighlightMatchingBracketProcessor(chars="[](){}"), + filter=HasFocus(DEFAULT_BUFFER) + & ~IsDone() + & Condition(lambda: self.highlight_matching_brackets), + ), + # Show auto-suggestion in lines other than the last line. + ConditionalProcessor( + processor=AppendAutoSuggestionInAnyLine(), + filter=HasFocus(DEFAULT_BUFFER) + & ~IsDone() + & Condition( + lambda: isinstance( + self.auto_suggest, NavigableAutoSuggestFromHistory + ) + ), + ), + ], + } if not PTK3: options['inputhook'] = self.inputhook diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index ad4dc39d870..fc870f0c01c 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -52,6 +52,18 @@ def has_focus(value: FocusableElement): return Condition(tester) +@Condition +def has_line_below() -> bool: + document = get_app().current_buffer.document + return document.cursor_position_row < len(document.lines) - 1 + + +@Condition +def has_line_above() -> bool: + document = get_app().current_buffer.document + return document.cursor_position_row != 0 + + def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings: """Set up the prompt_toolkit keyboard shortcuts for IPython. @@ -332,6 +344,12 @@ def not_inside_unclosed_string(): focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode # autosuggestions + @Condition + def navigable_suggestions(): + return isinstance( + shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory + ) + kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))( auto_suggest.accept_in_vi_insert_mode ) @@ -343,19 +361,33 @@ def not_inside_unclosed_string(): kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_token ) - kb.add("escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER), eager=True)( + kb.add("escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.discard ) - kb.add("up", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( - auto_suggest.swap_autosuggestion_up(shell.auto_suggest) + kb.add( + "up", + filter=navigable_suggestions + & ~has_line_above + & has_suggestion + & has_focus(DEFAULT_BUFFER), + )(auto_suggest.swap_autosuggestion_up(shell.auto_suggest)) + kb.add( + "down", + filter=navigable_suggestions + & ~has_line_below + & has_suggestion + & has_focus(DEFAULT_BUFFER), + )(auto_suggest.swap_autosuggestion_down(shell.auto_suggest)) + kb.add("up", filter=navigable_suggestions & has_focus(DEFAULT_BUFFER))( + auto_suggest.up_and_update_hint ) - kb.add("down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( - auto_suggest.swap_autosuggestion_down(shell.auto_suggest) + kb.add("down", filter=navigable_suggestions & has_focus(DEFAULT_BUFFER))( + auto_suggest.down_and_update_hint ) kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_character ) - kb.add("left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + kb.add("c-left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_and_move_cursor_left ) kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 733a46d416f..7898a5514d6 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -10,12 +10,43 @@ from prompt_toolkit.document import Document from prompt_toolkit.history import History from prompt_toolkit.shortcuts import PromptSession +from prompt_toolkit.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) from IPython.utils.tokenutil import generate_tokens def _get_query(document: Document): - return document.text.rsplit("\n", 1)[-1] + return document.lines[document.cursor_position_row] + + +class AppendAutoSuggestionInAnyLine(Processor): + """ + Append the auto suggestion to lines other than the last (appending to the + last line is natively supported by the prompt toolkit). + """ + + def __init__(self, style: str = "class:auto-suggestion") -> None: + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + is_last_line = ti.lineno == ti.document.line_count - 1 + is_active_line = ti.lineno == ti.document.cursor_position_row + + if not is_last_line and is_active_line: + buffer = ti.buffer_control.buffer + + if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line: + suggestion = buffer.suggestion.text + else: + suggestion = "" + + return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) + else: + return Transformation(fragments=ti.fragments) class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): @@ -208,21 +239,40 @@ def accept_and_move_cursor_left(event: KeyPressEvent): nc.backward_char(event) +def _update_hint(buffer: Buffer): + if buffer.auto_suggest: + suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + + def backspace_and_resume_hint(event: KeyPressEvent): """Resume autosuggestions after deleting last character""" current_buffer = event.current_buffer def resume_hinting(buffer: Buffer): - if buffer.auto_suggest: - suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document) - if suggestion: - buffer.suggestion = suggestion + _update_hint(buffer) current_buffer.on_text_changed.remove_handler(resume_hinting) current_buffer.on_text_changed.add_handler(resume_hinting) nc.backward_delete_char(event) +def up_and_update_hint(event: KeyPressEvent): + """Go up and update hint""" + current_buffer = event.current_buffer + + current_buffer.auto_up(count=event.arg) + _update_hint(current_buffer) + + +def down_and_update_hint(event: KeyPressEvent): + """Go down and update hint""" + current_buffer = event.current_buffer + + current_buffer.auto_down(count=event.arg) + _update_hint(current_buffer) + + def accept_token(event: KeyPressEvent): """Fill partial autosuggestion by token""" b = event.current_buffer diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py index a43a4ba2605..309205d4f54 100644 --- a/IPython/terminal/tests/test_shortcuts.py +++ b/IPython/terminal/tests/test_shortcuts.py @@ -14,6 +14,7 @@ from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.buffer import Buffer +from prompt_toolkit.document import Document from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from unittest.mock import patch, Mock @@ -26,10 +27,7 @@ def make_event(text, cursor, suggestion): event.current_buffer.text = text event.current_buffer.cursor_position = cursor event.current_buffer.suggestion.text = suggestion - event.current_buffer.document = Mock() - event.current_buffer.document.get_end_of_line_position = Mock(return_value=0) - event.current_buffer.document.text = text - event.current_buffer.document.cursor_position = cursor + event.current_buffer.document = Document(text=text, cursor_position=cursor) return event @@ -252,6 +250,42 @@ def get_suggestion(): assert get_suggestion().text == "_a" +async def test_navigable_provider_multiline_entries(): + provider = NavigableAutoSuggestFromHistory() + history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"]) + buffer = Buffer(history=history) + + async for _ in history.load(): + pass + + buffer.cursor_position = 5 + buffer.text = "very" + up = swap_autosuggestion_up(provider) + down = swap_autosuggestion_down(provider) + + event = Mock() + event.current_buffer = buffer + + def get_suggestion(): + suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + return suggestion + + assert get_suggestion().text == "_c" + + up(event) + assert get_suggestion().text == "_b" + + up(event) + assert get_suggestion().text == "_a" + + down(event) + assert get_suggestion().text == "_b" + + down(event) + assert get_suggestion().text == "_c" + + def create_session_mock(): session = Mock() session.default_buffer = Buffer() From 11011b059a2bbd95006ce3a57e8d1cdc1e481eea Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sun, 22 Jan 2023 20:29:33 +0000 Subject: [PATCH 0404/1752] Add missing line_below/line_above conditions --- IPython/terminal/shortcuts/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index fc870f0c01c..6d68c17824d 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -52,12 +52,14 @@ def has_focus(value: FocusableElement): return Condition(tester) +@undoc @Condition def has_line_below() -> bool: document = get_app().current_buffer.document return document.cursor_position_row < len(document.lines) - 1 +@undoc @Condition def has_line_above() -> bool: document = get_app().current_buffer.document @@ -378,12 +380,13 @@ def navigable_suggestions(): & has_suggestion & has_focus(DEFAULT_BUFFER), )(auto_suggest.swap_autosuggestion_down(shell.auto_suggest)) - kb.add("up", filter=navigable_suggestions & has_focus(DEFAULT_BUFFER))( - auto_suggest.up_and_update_hint - ) - kb.add("down", filter=navigable_suggestions & has_focus(DEFAULT_BUFFER))( - auto_suggest.down_and_update_hint - ) + kb.add( + "up", filter=has_line_above & navigable_suggestions & has_focus(DEFAULT_BUFFER) + )(auto_suggest.up_and_update_hint) + kb.add( + "down", + filter=has_line_below & navigable_suggestions & has_focus(DEFAULT_BUFFER), + )(auto_suggest.down_and_update_hint) kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( auto_suggest.accept_character ) From b811ef130cbf93100b9e64a26cce28659b663669 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 23 Jan 2023 09:45:11 +0100 Subject: [PATCH 0405/1752] DOC: add some what's new with respect to #13888 --- docs/source/_images/autosuggest.gif | Bin 0 -> 137790 bytes docs/source/whatsnew/version8.rst | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/source/_images/autosuggest.gif diff --git a/docs/source/_images/autosuggest.gif b/docs/source/_images/autosuggest.gif new file mode 100644 index 0000000000000000000000000000000000000000..ee105489432284124c225cd13cdf6787e39250b9 GIT binary patch literal 137790 zcmV)5K*_&HNk%w1VIKrN0`~v_0E7YnhX4U<5CV7x16URVVi5y_00#pG2TB_VZ3PYt z4G=IU5ItBCLunC2cM(N?5lD&=N)r)Q3=v%j5q1C)1`QHMg%U`I6EQ&(Hck^eW)nw> z6+LzrWdRut8W|)Y86-3rG-w$^h#5`_95;C#HWeQgARiniA1!1bJB1+!7$glLBp5Oz zN(3aAP9&UQB%)^}en2INPbHdACRi~hnq?-SXD2X!C>u;DIVUJV6DUqEDGVYh4<{); z2Pu7EDTHDwW<@IrA}bXjDQPurtXJk%lFj*(s?3#xjg_6Wn6TEIK`ES7KAqV5o}Hea&heo{DW#>Qs;R51O)0Cbtgu@& zu;==*va+(xilw1985^!&HDwzs*sxL`K9c~`uhhrYbLz_6;pX*k2g!^6qS#q<2e z$H&La%*oBo%+1ZsdPL8qf6vd)(bLn_*4Nj+tk>Dr+T7aP_WRzOYTnVt-r(Tik5k~` z+vS^G<-U;R-PGmz{O9NA=%!}r?C!3 zPC#fxHvj+tA^!_bMO0HmK~P09E-(WD0000X`2+ zoJq5$&6_xL>fFh*r_Y~2g9;r=w5ZXeNRujE%CxD|r%fOuNY$>^LVf7tMxUk{Fh!ZPz2)FU9 z#gHRQo=my2<(`h=Uf#^Pv**vCJ!gI#y0q!js8g$!I(oJ1*RW&Do-ML9?c2C>>)wsp zHt*lSg9{(t);ICv$dfBy9@IGV=g^}|pU%-a_3PNPYlm)qyZ7(l!@K|PF220^^XL(i zPtU%+`}bSd!=F#TzWwHz@9W>szkf{m{sk!DfCMs@Ux5fFs9=HsF6iKc5SG_qgcMe2 zA#)R6sNsej-j-p9AciR7XCIDe;)y68R$_`Qw&j5M-#V2wOop#=^gkg>#R zPn04|9c18Wq+u~yq=XGHGzkL@5NW}rlP>(|Q3)LkV88$gL|}rCNLi7B10WD41rZQH zfPoYhIYGey2!PpF8s5-D$dPysX4@4&OzGqcIdHV33`GP{L=HwgR7Igh6ahn(9<>m_ zq?7`1f|W*HkwTgctZx9f8a~ zmpMXX&Y_&FAOOO;wtTD#QW2g+p5F-zyMGY=Ee320? z5G()>KN?Yn%|uy2LIXV}9EB7|T+nYpNGpA>M@=s+)2gjn(0`feb(NEMoLT8h7mWS|4)}vPae$bOjeHixGwxDv$s1twA%7yER3UdM&3@F`u08 z(@ZbEujBp#%w|Pokc(5-1Vki7ANlBV&mv@Sz6-BwOVsw;sGH?n6+p~AP!~ZoTZ6bX z^l&&sbsN-0$wiU@#L7^30XWSzchUL|5II3GrOXGQbrfEdFoBi;1iMkT%idZ5Wxi|n1NCKuYn0fmeik6pRIH#tqBlMASuX(}7-I7%g@D2N>Ja;Igg#_31yImq6Y}_kujastaJ1tZ z!KlN#rVxl-gkv7&=!P)vbh<0>p&arEg%DE5NUFrmbx|+^8em|97XZOoFo;1AaaS^g zkYQv>0G<~%892)g1Z7Q{!4MvzyycBiYes<87y+;VfwZ7{%p1U^kl@N#O^SR*REQLA zut5+c3VsmiR2lNszr9&?jNm(|JmINJR0Lr+KD@bA=kEljAvVe?i93mg2h=L)#!4G|W;~o4U$0Bg` zgjn!{ANat=H|h}&Le}X{SHOce@)3%}HS(xTSzYS_MUY7fP6}pl7NQ=KJB8eBWnd6q zL1tDnrY-4Freug0Mj*aPnNI>EkN`wuNksAizywP`0;LA9z3R1#mjEb$ECsMVuQ3E^ z4ipIdF2H~lY%@&`2muMw4P_gaP0R9@g&I@oL1oM<1!lrOq--bZ34B&wd zuW$ngZW1A5C97Kvicr0pwXd1$+5ptGymlEuu9Kpx7rh$=ix$kSCWx*qshHLgEWoG* zxq>Emr3rheu@#5VM=PRWHhtug3~s;$KekaMDpX7!uE1Cnx`2ouX>6tPIK^0yO5vt_ zY^elU0vcX0t|%-4l|e9LL8^+7D->a6XK+Fpj8;mAElLJCcgVZ7n`We%?vd8 zA#D0_5zA%dAtrgr1mFSYj3DIYC0Uk19KDXaSd445hq4Rg3$lZ=6g z2|{AKop=mq*a3XGx-}U$q{a?oD=i0tt07v-N`)hUk-Y%q@&-Tvy%r|>8Y@~27WT0H zQY}3_h2>36mT5_o+9?Ar!YC{-nJlP652}DBG8%^d%jXL2W^XzBu!-5Yf`yh)v?fqBi`6pcc$v=IYY{89>-6yX+8*p8yN-+jz$_0P&RzWX&1K@(vdQpISS!Mw_r#P&_0XDRH5)pg%_i{@HcmgN{!IygklYe_>R=!t%z{fBVkp;NG zYM&$!n==p0=X|ZuX3;l&piq6+=MD0(3-6E*vao&Kw}qH-Bff%R17UW!Qhw4#Rh>jH zp@eNYAZG$0cN;N(RZvD4tIuHRLKn4P+d4$J-hj%cEClCra5UQtvn+GuX zqKMKYWxN0PDUS679FQgqfGbp%5G;fPLx3rsHw8GrKOE0pddP?GHflm>OAJGYxac)Y@Oub>Ifv#D&1ZzC#0T%t3}4W6$u|%NCUwzw zbqEn`k#L2sz=m9SjWUsJWjB6fn097pG6bOoV$d?$wh-KA5HA*E5&?%lCx-f_5~h2Uzm+hz)^hK0+==c8>y) zkC^|1FaYR0omMdN7zOl5F79X$KAc*Gjb5lyVAyQjXqM*pjR%p2X8;CZ@B?o7137?o@FzGY^HnY|cn`sj z6-8tR0bT)+0DbjtD^QbhSc~d25YLoYvW0kzc##|EP9cy%A22z{WLz0kW`Y$zm?;8f zwoU?cTqLjoPxCK{)mpM;f|vPQnQ0|Va6n}u5S~X6p_l=WqFoHP%;YC12IVpnKU7q5L2O&mul>l&(l*s>J zET?3ZSJz*`AP7A$2LeW5WH5aEfM9jNCRN}Eb+7~)Lk3%Um7x%p{wbD@;VaA1EYA`x z)3O9+V19{0hUXUrY$pZ3!!i=;H^b9acbQ776cK#Modr=ixwK36ND%qR5b#8S010`J zxdaCnLIV+s7`QYp3IV}&Fa88VwmEX`15L3h5HUKVhIm>kNK+f|Z@w~mLQta3WKaHc zS_njDIwV;01cMHtX=G%W`1o#;rctznD;_#m6vd%+XI>2BhX}C+)j*yDVU@x{27JI$ z_z(^-B~t_81~$bHfbIU3gNK@p|U9pvJ%0vNa$oO3uY7HZO#AYvJC-WFiR36 zv$Gzy1eoJ%Mq{%U0kUSMb|6V^^x}s_;k=r#BsJ5ZyF0v|+q*82 zjzmkm$}76YTf58qyt3Q8&^x`<3lh>>z1W+*7ID4W+r8e)5ZwP;5XgHF*CM{&cTp)0&{M!~Yf4w1wN(FCs$Q!`~jT*<`AOT}o+6IT2g5c6~tV=)SL5ngN% zQ_u-9l?V>Am0g)qY23#`vBsVeHe*vZXS0;V@>9a3Yxn&KD|6M)Pa zo6|X;6FT`YI+zg23Ngph^a@POhEE(Z?>EV)JQJ0i5v}{W4xuY3MVD6)2?I7PZm6i0JZM|YG*d(=mN6i9;< z1%-4-iKJs}P!4GNDTj11seH~gvC0==ySVERZ`=$7!E{b{G;dH1{9q2#pbl603AhqA zH)RggKn>w=4s;*|hlHlhhf*ZrQ6ME!BxOfGD?|r&2k|TQ zkPU9#+JvgoLvh9{+l*6ZaH3obDk{pdKoAt854ar#f*r>NVT|!WByHr|XH2FOF?2tfDh6z3?Lrj#Bd7p{SM|#zLii9%|Hfh@D4le$rV9_MmQ`< z7~oAf;L=BZ%bayoXoX%$5JF%!?d@cJFb~j3;Yhv_aLp2RTzy2C4{~JN+V@FRP7w5s z5Vd9xi69Bq-4IFb)J_f6A~uYmQ;f%$jITB_wEc|PEsfL|GbPPy9W{;coaA6DiZl~TFcRk_buDcXAc;{w41)m;k=(c8Oy=dw-_3Cz-YNxzR`zqYRH_zMxk zrw`%a+XP`Y_`u5ua|LMd(*oh>^dbl9U=Hsv4vg+(Fm4b^xiLs+o@IcZ>dBt9{GMSf zpY&;;_<7X(nMf9N2=y=yba0$zN{q7}?+;PyN+7`RYw!I_!2hf75rK3t1<4kK2ljl= z-=GeLz3e6~+y=4OYjhIdjqL)Vrk~WNI0dI-Jg0Sfr+K=ki0o#53aA2c1IVDzm?#PF zaOCoy@(Xdn5bVlZHNh_*!GQk*^AJGR6jWde*O1Hk&)E)@|D57}R0Yc2lPuMtb1&CsMO=u@as zrB0<<)#_EOS+#EE+SThwDj{<^j$X_0VGN;KV8 zY8G7iLX|1FL^1zX*s^IyvCUPAoO1UpEu2z`Upzs#Q7>oS-1&3p(WO%jJ!|^)imkCb zRIQw$_NiHV?fXPlJbAKKUNI%Zun8`Gw*+seU*GQ$=#u-a^Fi49@R#_$-b>=b08A7hmWWffbD6E)?z_Vx$R>EoLnqca9 z?1@!&`No}jy5WV8$Lg9Rm2lp{ryF8iIglYNR?(%M4=2$joNG?m?FtrMgi%JM0@=kI ze5Bdsopb6~f{k)ci=sP&R6OXx2q&x%A}RWKhaWoYt zG_yMzZN&dkM;;HFLd$SG>_(WMq9F68hNR%5OxO1F^ixnn6?N3A`lC+@C18MIh9M-v z0u_FmIf#l^@VTX~H)1kp8g<^0=aa|QqSG8|s0rsBIy5Z@(}Kj4EXBJl;=>z$^!aAa zqK-;~T716wrXHexx?);>@X;n5aN}|aHhiv$W=?b7{ndzZskIhcqYzX|rfcbpi_CcF zXb{bU)WlU+UVR1jAt~NShM98OO=eNQ(2yBXww^i#Gb`{7l^rRU2S%fi8%yID{iQ z41)jZ9!!=XP@iD>vVyTN3w|daK*(tCiZ*y7@YyJgc*C2UN_@!REs){54l>-pg~TYf zrDu^Pu7Cp>e1K{12Ew}zoR5-LV`vHS>`8Q0!Fpt^`|iB=W{?b+xK8#7RvI6Z z(}7J-Gg^bT?plbi`3d`>_JTH>jNLYmv2(t4$6If{X^+Bovv)HTZ8*sr1H#`c; z`H{z!wg8h$C<7g;V#HgK_WpbD!xx_{rMIi12pMv4N+Ou{!Q||z!J#9;y&NY38S+qS zP40r2(5q(zO^rd&G82L%!H;ox#3}77kKcYm1_h0S4D{;=ZG=EDhD2@=BC?#N%8~z# zG8{00b%_E4uTY74Y=aI$Xj^xVmpXNct$mE2o+euYLMp&J|rOn zC9*?=yfArjabQ6ph(I#@rX0T#f?Rr25CUP1hYXQgJ+d)AFosc#V zD_nbMrH@*yaXqp)$gAL}Kh`0Haq+-gUf`HUh^UEHwyGm43fDKd{bUKP86+?cg29Do z@Pr*C(>4a#y@I@|aD%Kxk6c)efWgplQ4r)D3wcK!jz?$)xnvG!V;kp8=ZKnd&I5h2 zBcdFPIkTxvI&l*sK|=^Xk>XE>7DS!R;9mw+nGhxFl8L-@Cp=H&or1uG9)fs?5##{H zW~Nl7D`n|to_Vxk{)&EI#AcUNm_e$5LW;5hOkyHnr{uZCiL#TUQFt42 z_$ilqCPb2zpyWID*^7M4^P#aiszE-993EoOpf<(nO?6t)NM7_HU6m+Av8vU!uCtb_ zGzh@@vc!|Lld0Z7j{o?9qJ;>dZq{U2ALbFvmL^uQi)E};UTU-{FsA<)xe?kMFLyh$ z9i*pLt7al3(MF0KZ&tOc#jFhJ$aXHFsdW$vK1%CV)ip$tfe?iyj8I8VmWYkOXeDf! zxU5QHP_(R^;}tNAR@thRxUMzrE1Ap6(;iW|21x^OeVbQ~s+O;U_$6ZPsmd)bBth?x zRb$P2Ui7BdjAHT1`O*hSpQ2BF3UTjz6H;IJLgZ48a0jv^Dv5f4gQyDGEN_@lIbOU1 zYh_T(7gBKzBPpc1#WWy4#8I`72C;$JeJ66Rz!(*VHMUW3FoX}JK`VNYK?l4rXN7pc z*=h77!_1+K2Q%SH88E;DUPy@}+|cP(xWyTMv4t@VL|Ycb$3FkVW|9p;<4-_!m&{Tv zJTIuQ9wFqw&uxeu>8M^Xhgr-keXLcO=CmICObJbML7EYA<~FOD&2FBEdh`(vI3A>K z_@Iy!0*8jO$*@yzmV-LX@s0#4<_dzsnsU~5xhmwrBf1kr59Z)Gv+1?Nf@qjGn3A?H zY=UZXS@fJ5-K<^^_8^rc1EnpE=~ffY8j0Y+Z>T1O=*`+f7<}{b8SQ;5Wzx;3X!OH|?%N42W!s!y|#3m+aYibU~1 zt$BRI<8kS9xwO%*7)HV3!^-%^4L+=zC>$v(CnvJ$Dp8r!{F)r&meeCoCze|~*|_4? z&*!w!Vm6iFZm1bQJoJrnXuLs)W1Dy-@ec38TkdnGyME+d6)gZrKnlrwfELpCy(a|l zARz)S+ISlpVWS)#lEx|4$MCw4^5fB6%qdg?D9Am~MJ5Gnj9LOCkZl;;1irFyu7NUliRK^)9^H}L=^iC?@q z8V|_VV_vG)Mo^VP4=9OR|MmxoK9d=m5Gny*_(e(^>8%_EGH4+ay9iRdgz+Xa6vx8I zQ-~4ket-PuA3Ap@8oLYPm)fWzijW$70HB5Nvjaql_K*SqG>8((vbH%0E0PN-D1!!B zG+?SI1pJ5xbO_u@BPl?@_P`yV5y84>!2jc+B@n=$0V&}r2nckdZ(@iIoQM!qK=dnv z1IaSm=t11u!1lNzEFv6=nva8Mt!5}b{aZpNY{JXnKlHi+gL65{m;ykUlELX9C#(!9 zxH)|&hATh_D=>v{iNP&ALo`f7sDMJoq5}U@xP}PHs{C?`ZJCK^2nVyXhCx$9uSkle zXbOWELw=x#H{nA=JVZo<2sT`-CWr+lk%myHLCbK1REQ9nkOt!OzeKExagm4N@To}P zh6-`PO)N!IY{Eq}FCn}PBiIeOXvI_X3Mps<_-cbPFvD2PMO~!3RNO^hY>rj@MPVF9 zFzZEPJVsWYg{JhL;0-qXa75Q_hg=@X@3ia88OkNimN7)ZIJ3%e+w{7A@yAW6vk zh==3~b?OFYfCfBlwy21##(+RSY&($LNuJ!lGcv`M#151sFmK9% zksirCAQ6(`OG|@Dk|k-9Cy5f@)06E(5G~;nFBz)SIg}CGEWb>?h}af+pal+@6p)F6 z8?cdg=)6%Z2n^8>4)G8WvC99XL<$9=Dci(N-Bdp4847KZzJj=v9Q&C~G0C~CPV3Z) zy2Olsv$u$Nl~@@qLTVU<>6LjAn_(%IV@Z}xDW-E;Olip&e9;zefthfDx~@Z)l#?yP za~F7#7hvHykJ%VM&#IdeTwY73^;=t zd7PSf%&1^V$tbbPBO0oo8^WJib%=X>vTBeF9?CMII*t7qz!4b~naMJ8 z^`>aT8VeahElP<)@S;+^SA3O-Q`Jt2SfkT&qxFa*%Q~evqE&a?qdq#(TIk4yC?!JL z%;-ujMyeHBqSOC%k-bS;R(U6`(RxZ*s+@xL)}?xtd8k-4$OKx* z1!71AciTH{Eih^+}r#y-%1o0=O3OjgWD;vAmWW^_qy(-D*C$7S(PQ9+w zq`zw|psdweg=z@BN~(*XmO-gXgAF(mHJdB=0>&7qz||(gEr=&Dyk-c)gAgf_atM<` zsi@uDsGVBrh^bGQDO4k?vO256r7xf&1EC5NxYVq%eJi9g+mLfAx;4UMovNyWTevFQ zxy>qi3R(ZK8rjnwU1=yQv)ZW#U86REh{JWF$cU6DFX~n zpxfD&;O#kML56@hY$Ljo}^jTC+?vg zBSrr#rmm$jAptrN6TU0RTVviWQEZZoT!mu}j@$xVh%I}qE;CaZ9^@RQVH#XBG?TML z*0(uR5ezsR@Hf}?XPWvD+mNw0}gozg$6NU zCI++&Av@H+Jqf+#o2#W z>A32KVd&X}lZR7aAu5Pzn3Tz}Ne%+x2bj!=hnZZ1U^^&vJGk>ybe?K-ROi3byQ{`a z!NY34ECyCH*xa>oP zytS4nN5BSfk%?<)-V)JlbeRdy2AzW&lyj(5`4b0kL2q%B;-7z4JD201QTkAP{^g!rMT?#LjL1KF8f2A1SP+Sf&gs z#6r7)ICDhdFQnW+B*S(6ZwasJ^c6EVgu{1`!_26|Zn(odY)EJHLymm#K`g`xKk;Y` z@b78FM~p<^<_b!*MCZ%Ifb7IiEQl%y#oa`49(P6+7a#kk3|Qn17?f&goW)wa#U5Ys zW&Cj_Kd&Hvaw*qEC!caFzj7=mp9;@%F7I+LUydsOaxousGIxkDFLVDiPjfLpb2V>s zHI=^!~mti~4b3X6$d);%YxXZxvb3!llQv`IW*iLvhbVhG));D~c=XQ?g zX>WFH&-UvG^TF$CtwxA$-|BDIc5*Lw(s*@qPj_{9re|MwcYpWALU(wdcY0?Id9QbT z&v(tZcYW`7f7f#M`gegJ_;_n~fSxjRZuV-}+|q`L6(a zp(lEUaDt;x0123Y1Q37~pn2waf(@X0u&_yTa14cvjHl*$yeA{C_X@Bd`iL-s7BB#i zSI$;s`?i03!>5XKC~yM7M|%IHUx37q0xduR5wHLb$bcR| zi7H3|3&4QEu!0R}fF3vsDD)to1c+c{$V4e<@F2p33KueL=3WB`{#j7&6fbg#bEt1}Gq;pbwrp z0empx#DD>zI~N!+^03JU2oE?|*wDZPnI%+2JOEK}N(u`gmK1DafdkqN8#JtlfcmGH4z1>RH{)k@Id%vLk6b^@lLIJH7i83XgU9=Wh?kfC4R-qG?bF1*S=2(uN>mX z?;a+CFGrIuZTd9o)T&prZteOt?ATFSx|D4(rVSV_no4Xjv_OZXJ%1uG0HCJ?3daYm z@G@dV0-XXzNTB`!;?}NGmQ1+;Ap_Ycf3IFSask8kf>)#%WRl_n2qSM9@Br)J#FZOj zmq4W-MamZ@sR)D11E@g-d$VvRb&%ZVrdYVPey6Mlu=Z9 z<&zS{S5UROq0<0%_4uO@ zULZx(5JNc8!f^-~#H5oGfmP-~Risd6cwkv@L791KfkOx(pb!FJLWoJGnNOhEUteFn z_d%w68uY4}2FXfmM7#F-Yp?}vQ4KwWJS2r2?`$KQK}!gcC#C7CyDq!!y8AA?2U!}g zLPiu9fB%241J^P%|yglEWZ$Uox z#KWda?G$i9tP;%7TNY^0E7iS58%22s-*-@(4`9%P6jcy$0mQ@J0IRVbtNC$QV~reG z$#6|wwbhAn^>x@|w=BdK$k?;Uw|&f$jiNsremLTZE57)NLG#=%049)SbOA_@>ghsG zyE?0x>}7h;UmBob6%S-dVIF!0LB*GtQs98Y31p<5rhm#>ka9s_0;(*mo=}8qlzR(qsyweQh%{%`*^wCRSk>e@NSmWo?(rBYX*K?nc_t`g;V*mgkz#ip4QD7Va zJsf0{z)e1lyp+cywAFq21;78q=>u{N7!Sl0g{EX>b{;E;6-3|w%+c;i`Jt2jBGNws zsSbbzVFD`fQHzIQ0Tud?MWGIrsOgcggeE*83L#a!li)@-?n6imUGTz##BhcNso_LW zrIQ3qAUeb8k#GP|fF2rvX-yj#MG|O%47^YZQ+Qz&ZU7&rFaa(n*v@#aHar1>?pPae z01Nu!p8Xjlfm^#m7y6<^CKkkrHe|>WvRFaBsOJ#%7zZ63q>yd>?Pgc#!#h5M2|cE; zkcK=YA`@wn6`CX_GpR{J97z-S(NHFnjHD$YDG@Jp;9ooeQWRPs9G(auS)oMY@17ta zTs>d|V)+jdzSBgCc?th4u3U_O%9b%x=FW=;dq7nn;FyD4AqB1kCZ43AN(ivhl_GnH zEzg3MT<-1$$gohLB=HWyZKgB)*as#QDb8_{vz+FX*NaA@1t7d&2MbweJKxDpcPa#) z^qhzhbdUh!3?KmoC_#Fn;DqD=008|gK@Y;Cxr%531#me_2+Z`Btx2nGPN@P4NC3>K zgg`-Dgp(H=5CXVxfQ|{70tvX{05%q+p%3LiL?wbzjaq=C3R2P(A|sAE-0UDP%Zx&Z zpbK*}qj2XuDpHfGRKqb+XGMaDgN(q-(mZt`^Gis7B8VDv^$8!%7ziPo@CvWCfgu51 zDq7R3*0n~escipE%>ivA1w42|AH}!=Labm4evpF@SA8pB11s3Uvc|21eMwwBas?8E z0H$bZY-3@1HHciH5arMZFfyfAe$c}bQ#~wbLo3?RcEqrxB}rmCV#|{u>~@57$QAe? z4@<-<65R0YYfr1&-SU>SrtPiHq|k=!wV@1u1uk-vt6X#bcDXyNs&k_&-RT;#xzxQb zcC)M9?Ow#X-2EY1| zNZ>jK39FB`ecW^OQ&VG}+S*%-@9 z=+}+}!@r^>g*o<;PQ(f_1gG2?U%T7$nFB580jF8UDr)lpgOn5_{-;#r13(Xi0OtY} z1tF8J^q@1XY5W%Ykcf`LlmXTkB<#~)E9eW8I2D0tIfY4eUiGG9Eo=35+K``)f{hCx z1*0hi(&w^QsX0~YrjUA+I6m&IldbIJ*1C|lW>Ua})TALu;n6GDPI$af0v!>}P6g36 zxAp(E>~WKuyy<+&n!&d{@kJrs2yu6b+Pod|?dZ`TAj_j8?FCFnK~&|#(!LMmZvech z+y+0mZ_k|)9Kuiuxsf3zAO6PwINXJ79tsZ!z$p_LLD~a|Km$*jyp1|Pk&XSuqvHIlphM{a4^sW(Rx2&eGk-eN z(OUC|*i)YItfz#res!QrN5uaG6d9Z_0S=dM00oG^wVj~P6q9(^P8h+H%U)t+N4@TL zH{sMz*yiv+*Q^EUd*2Ikpupd~@P{;yk*weoD0-n9?bFX{);(qtO|Gjy8 z5B%X1AG^XYKJt@~+~X_1`Obg5=NlE_tv|o|*8dsvNiyS}U%&g_cUSgDqGY;%zx?K3 zQTRs!@AI?2{Vhtr_T4Z4`Kt*2^uIs;>1F@?^S}R;+Q0t=pa8~*@U34#7+|}tTlXm7 z05+fldPMIrTqQ|d#93U!QQ!k+paxP;^fl7YRT9tb+$A+!2yUPWo*+XAU)5>dJt4#j zCd3P(pbW|&0M4Kd-XH{aAP(*z51L2~_8<@j;X?eN5Eh{j5}^?$;SM695;p%K4eFp1 zMxhjP1QSl76$)S#vW6FYm^oE}7JvX=LBtnu0v*uM9C(6JU7;F+9~26W#tDE{HAIMU zSU+JHVz3CvAPn4mL~1R>6T|{$DM}OILLjc8A+jNDEKd65P_LapZk>n}5CKey0NIdV zMD!t6y#ik?!huxQWo6bOhGM#8VQMU5L}*z8I9UQjnWH$1M_}SXOhG5`RU+hxCt{N* z=AsIINvR--yvV*Mt(3AzN6ciD`11L}i-~b6o%@suf6J*q_Jb(t=RDtZF z3p!LlCe$Tbg-ANU*B}HNltZ^v1|O6IJk&u#{GmPwL^-Y_PM!uSR>bz*gpLRWC3mRhklHO#{h_cP|#Q24_7D%cPRfRSym7jiP3x1iC9@k z8ot|xbOn;eKS&PkmZXHu$3c$kr=^vR#VmK_laLKq67tmbTibOz5tZ+`PgaxkPjzL(9uK-GGhQkPXHd=yR$~?O;vYIEXQJDc#f!k+vz18Uf$< z4d7&`;BZrv{wYdKX-PniD_Dg(aTtUkn}osXfa5Bt~<&N1qP z)DJ`8k7e%91pN=|?9u>9SsW#387UA0^@jrit3||Bmn0-cX3z#XL@uLKLJ#D43vc(6iEgdeo_QLLX<=)&}0$hgv``N!IVc0EJ&%1NRd?4K1)hHfI|kT zNa}4wWMoBd07rTxPU#d+EyPc`B|-=lQJMcI*oLm4ngrn~L{xp&Xq2wtjp~-5>S(ZA z6m->BrQ=;LY!6T?k>sJ#(ja~YRpw#-Bn)gRbP21?~1P2niud|#9~3#V>y;& zO@t?6mS*i$XQ6KLZf`=2t~oVtMXVNUL4a#fuWU8r0ooRBaWDI(;PTp?a1j?VwlDp5 z@B22M>eg@jdawTW?}G8K{{}FD0k8lUaDNf70VlA1A+Q2B@Ov?^14l4=L9hf@aClL$ z1!u5!VX#5GcrEI(@O^?~hUhc#V z2iejck&q!T3nALniM!%PWalT^bsSsS)}+qNn3-@$UYm9j0v*dSwsta`D!@iMLYat%N6yy?gS z*c-m>vA*%!zyX{xCk4T|@hwks1Xi57rt8F!tIwHhFatowZCuBxTF8xD$(2vbmFTCO zoS3;>w?#8MJH#~8u+PEs46pwz%F;852<1=?opB6B(IK7E@dS_j^HM-_JHxRGs%_aK z^x0BhLNoL|cb##3osx(h*%{Q?rQO=4u-n0%Gq)W<7xWuj@`~);--Yar0N!g2UdVzp zJBu_OKQT+kbPVURIAxwp-}Fhibc)zCPWSYI(X>wo^>XR7P!~1!0ku&l^=T2cQa5$H zFtt-hHM>ByR9E$+P_3)~faZb7X(d zFH?47Uv>^Iwg+l+HlzP*xq5bIS2Jdhb|nwC&B^myvn=Wb0cVkaCLu?^w zjbRzy&>4a*bt9j3Cj=6_q3bHd9Oh#J*x^dcMjjpvNR|do0)rqDqI`e1^@TS=WTQ4V z;(E(QBu-)_o`zU$VkcUb_1gFI;rBu4cSE3}Dzajuc%w?lA}!jYWnnFXC!c~F1cO@@ zFqRTo7{UBT_>aD>GBV>rJYzKWcZS2>hQH?zMZ`9;_=!k3N{AylGQ=S213J=pi>n@5 z)3!s%?L3y6J&OMwQtZ7|TpZ!L=G%DV4&6ZGE(sni2`<4Mf@>g12yP*G<22B?1b26L zm*CJqf+st&@2#r;E6=mHlNvims2{DELZ(E* zj0q~#H8_+;)t0G;BV>19X%@=WLwB966{6x}(U{>4H{=phKu2b*1tqdd=J4;w%EP-V zsQIc7-ap599bTfVv8!raz1E~Z(0pPj6KZ2(Ef*|dkz+oE@2Hf@{X>DOBZHAW zvl{!ON=ZmF@p~=oF3K;dI&Bqup*h;$E1uT!i!~%iFJ-CUfoX+yYs`c@0WcpLq>{VlTCbL zFC$ns1-w62^-6v#kz!j$yy(z3I+ZmH_oWG1Qqf!9oJu!+9%p+W*S+8SvVCEpi@7H= zlvg(AL3I-^&a2`%tl>6_=wR2FBVS%kX7ss?2=d?~yX;2bW^$EKbKV&fUXxE9EN`Y0s1M07|1~nJ=8TKfm7sB=0(>rR-kZ zOKQ~qI{Nueq3*LLfPo8z=dkD5A?iILnry|n(ctt8d`x>HEFU6ZYzgprz_I$epqube z+e_h|mv-Wz1Q?PX$E=+=#Kf~)ooKRTL1@?{jg0-$;k2klDu$q<@khRkk+ldu4TZC) z3k!;ezr*-Fs14lE}D(2qgA4caop56SDdvPA#PJG>Z!ZX99<0 zve^raMRnfMWV6U%IhWbKBL)ZS-*)bJ5hCYx%B{vyg#0L+UNUIXBsVV(c469NJ0I2R)#*z`e6qpiz z6upA4qCW#U%aeb}qKD!V(mK`x7^BD-iOBew=iXz?k~NEx#b-+?jwNyEH-7=3^rPWU z3su_joQ}KT&QTkDg!p+Lt=X4>!fL+;V_>mju1luGldW(iuG?^F&v3j9xZ}mXLeljD zFlb=CsQ3~@edy%Iu)ZK10To`JKyM4Ia^}LBerzt9gMR!)erYH=9>6>h9fv)ym^}*? zhzd|t$ql6))ZzgwtjRJ zwAJ)UVnE%>dsWery7t(wPOz3}98ax`!u>Kn9cQ@f_m3*sWt8Rcy`kvIykq2JOTMt>3(K7JayR$*~%k znkzgWL9q+hHBJAPa(#*TmUcO=^F70<8~h`SI@It-&W8u^UTzp|#*>`L7gYO&$)*|m z0kO_h2c<BjCintdXu;y#4_nwbyv3iB635Q;pFBq~1^XI==g5_}Uan7xJO*W~>J z&BaU=Ce7>wqxb@JPJ+7koP|?A`!dK*=R?qv?b9mHS&xluuRNufWPFcJuYEu`n(A}F zC>uQw0Hu;C!RW+g1OP6{u_H_(6n@=M z`9J4*xRD03O#C?EQb`tMFns_AjfMvup2eBtXyvgv?iM5O^P5XtwOH!hQxx-u4Z~Z)CrE_*u zWKccNom~bbP4)|2Llls;r)}*b{Zv#m5LLD}Rt-q}gI8aYEhf-6b)NZ!=;|!vfxw$z zq_|U~()@|r?<|%@%M=V(wJ?Lp-YC28tJoiCrmd?K3oY)esrqPVeuI}tk(X0`5Ys83 zB`;gxe2dgdU)3pj^0rJp`9P=4x9OuUd4=I(d0u|IZcPArWohSIgR$3o4eLb}Z*&h| zmu2WRcavAkdpH>Hzt&IvyjbnCSRr(FOV@|(UK2#ly>jadawVXsdmPxbKsz7b=Afug zu%WU-8&DiGrD*7;=BlPjH=GX8vxQ?E*(r$&&!$i`l~t|U>#n_CW<6?5F)3sa$0c56 z{MB5uc;uYiZrIaFlG@wE!UZ)_UZ8s(S2RR#7^S+W0Au2k431 zpFRkn>=ZwD@*XoX{ZzNqxvzUtGt*&u-L0>86n5gjugv~?iL&Q+(?Q6^ni;a&r4vbh z8u$k3VTMWv>T}_92?l>LZ{SAs5vSBj(o0z2vbFY8F4YDKbXpMpXc?fR;PaP}up}*M z8D!Jr^VaUPq-1LudYV$_Y$ahuUD7=KY>Cg#t<#Fmta;=mg{x(Vgf-Ji)2M$o=YQr#wGp zr4Q&YAioc$LD;15vr7|2!9sjLr!2^Uatc?Z?eC4Lmflae69=4zP$48SU&8vZ0Ya%r zL;lz7`jRrB!P;Wbgq6l_{mGkRtty=~5mtyvxxIJC#utjO@Rmy4d>@lL3mqf#JJVmAd zLWRj!dSt+?M#Rs2igRs>1`=5~QRSoLVS4&|rpUH2xWJg6%d45V&^+8IQ*2upaHTN` zz@*>nTYJ<^n11;}?e%R3w=~-=BGuL@gwz__&O4~#>^hA;tDRoxDUhoBdAI-#^3SIi zPa_>_s6ui^8mrV`N_xYgWQoApRnAoN8JbF4Pf315=R1Td4F8`UHjJs&8 z=@R19_g^S^LdIa6hhtkSwZze5(j~LDn3?)vCg%9K|0C_0J>B!yRAxj>a4FoOZt;<8 zr{f{4=T{B--Jiui-VLU~gUR{6Wjfd$I-y(ZJi7F={-;P@d+d zIl^G7!ud)>aDDn3wdzSPj1Y}j|BnU{Zk;V~mWY)2&zuS|HQzg;Sz;zWD|$^5u33A~ zEQ!FZBfBQ4c%6~REU64}?~O87^MSu$;A8HcQoT##5_F z-J!|TI!irJ$K9(*GcjwXI7>56Y&5C~nM#1HY0_*E({^dn{?Md8oTaTIraRZ9x=x_W zo~1)-g3+|-V-x6cwLpX<3?-P9N$zfd}6j$Xpo;Few_h zi1`(X%`M3ONCryVV8@tqhWDaN1ZF5sFukm|9|ut62a%c%5$g0&um)j=^#dg&QMU8( zqUPA+NjQ?UI8qZiGUqs=UA4yP35pXrE9N+BNVpobxT=2XVoGzikZ=!ZahKI|&2Mu& zPN~Pben(ic+9%qN29oA5fWs|7Iw&lLQ`ZEN7>HVH&=xV-)>9-JDKC~bFK!Yq;XE%X zDIcXaAKe!5Pk_dFAhE0zIp;k8Q&Ir|ZGmS=0zBGetPK2Y4FU>Dg31m2FSR#@^YMQ6 zK84H+nUVfus&-xo%s>zoPW3ghsBjZ!b(5G?3rNC_qa%qDH7^oBFG2_qO=v(3ZJ_^2 zPnjqsR+1!EF)vm_D&C+i-sA>yb>-X!P$Z&IGK zd$_W`&Z{2yVNP%Ne z9I;tL;79O-3y1KL5ugWZF6&H1lboJeu^wTXQ&q#RY zbW{s$u?Yaw>w;Xc04qHDI5toi%W z>_-szAy9G%2NHc*rqhdex<%b+VJNU*c&!7b-y-0bQjg!FtS8rikg2Jf8xRPYQF