From 2705b3db4954741a4c141d61b1ed210ca2d93d3a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 18 Apr 2023 14:23:56 +0200 Subject: [PATCH 01/24] Backport PR #14022: Fix failing docs build. --- IPython/lib/lexers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/IPython/lib/lexers.py b/IPython/lib/lexers.py index 0c9b6e1bc7e..42d5b7a87c5 100644 --- a/IPython/lib/lexers.py +++ b/IPython/lib/lexers.py @@ -194,6 +194,13 @@ class IPythonTracebackLexer(DelegatingLexer): aliases = ['ipythontb'] def __init__(self, **options): + """ + A subclass of `DelegatingLexer` which delegates to the appropriate to either IPyLexer, + IPythonPartialTracebackLexer. + """ + # note we need a __init__ doc, as otherwise it inherits the doc from the super class + # which will fail the documentation build as it references section of the pygments docs that + # do not exists when building IPython's docs. self.python3 = get_bool_opt(options, 'python3', False) if self.python3: self.aliases = ['ipython3tb'] @@ -503,6 +510,13 @@ class IPyLexer(Lexer): aliases = ['ipy'] def __init__(self, **options): + """ + Create a new IPyLexer instance which dispatch to either an + IPythonCOnsoleLexer (if In prompts are present) or and IPythonLexer (if + In prompts are not present). + """ + # init docstring is necessary for docs not to fail to build do to parent + # docs referenceing a section in pygments docs. self.python3 = get_bool_opt(options, 'python3', False) if self.python3: self.aliases = ['ipy3'] From 9744560981fd38e2e9086103370a811088b0b031 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 13 Apr 2023 09:06:37 +0200 Subject: [PATCH 02/24] Backport PR #14014: Fix %page not working: OInfo not subscriptable error. --- IPython/core/magics/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index 0699994d3fc..814dec72e29 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -296,11 +296,11 @@ def page(self, parameter_s=''): oname = args and args or '_' info = self.shell._ofind(oname) - if info['found']: + if info.found: if raw: - txt = str(info["obj"]) + txt = str(info.obj) else: - txt = pformat(info["obj"]) + txt = pformat(info.obj) page.page(txt) else: print('Object `%s` not found' % oname) From 323888513c6ece2b6d29a76110da8ad0037b2e2c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 11 Apr 2023 12:02:20 +0200 Subject: [PATCH 03/24] Backport PR #14010: try to fix tbcode --- IPython/core/ultratb.py | 173 ++++++++++++++++++++++++++++++---------- pyproject.toml | 2 +- 2 files changed, 131 insertions(+), 44 deletions(-) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index c0a801c3e16..6ae64d7877a 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -89,20 +89,22 @@ #***************************************************************************** +import functools import inspect import linecache import pydoc import sys import time import traceback +import types from types import TracebackType -from typing import Tuple, List, Any, Optional +from typing import Any, List, Optional, Tuple import stack_data -from stack_data import FrameInfo as SDFrameInfo from pygments.formatters.terminal256 import Terminal256Formatter from pygments.styles import get_style_by_name +import IPython.utils.colorable as colorable # IPython's own modules from IPython import get_ipython from IPython.core import debugger @@ -113,8 +115,6 @@ from IPython.utils import py3compat from IPython.utils.terminal import get_terminal_size -import IPython.utils.colorable as colorable - # Globals # amount of space to put line numbers before verbose tracebacks INDENT_SIZE = 8 @@ -135,6 +135,54 @@ # (SyntaxErrors have to be treated specially because they have no traceback) +@functools.lru_cache() +def count_lines_in_py_file(filename: str) -> int: + """ + Given a filename, returns the number of lines in the file + if it ends with the extension ".py". Otherwise, returns 0. + """ + if not filename.endswith(".py"): + return 0 + else: + try: + with open(filename, "r") as file: + s = sum(1 for line in file) + except UnicodeError: + return 0 + return s + + """ + Given a frame object, returns the total number of lines in the file + if the filename ends with the extension ".py". Otherwise, returns 0. + """ + + +def get_line_number_of_frame(frame: types.FrameType) -> int: + """ + Given a frame object, returns the total number of lines in the file + containing the frame's code object, or the number of lines in the + frame's source code if the file is not available. + + Parameters + ---------- + frame : FrameType + The frame object whose line number is to be determined. + + Returns + ------- + int + The total number of lines in the file containing the frame's + code object, or the number of lines in the frame's source code + if the file is not available. + """ + filename = frame.f_code.co_filename + if filename is None: + print("No file....") + lines, first = inspect.getsourcelines(frame) + return first + len(lines) + return count_lines_in_py_file(filename) + + def _format_traceback_lines(lines, Colors, has_colors: bool, lvals): """ Format tracebacks lines with pointing arrow, leading numbers... @@ -194,8 +242,8 @@ def _simple_format_traceback_lines(lnum, index, lines, Colors, lvals, _line_form """ numbers_width = INDENT_SIZE - 1 res = [] - for i, line in enumerate(lines, lnum - index): + # assert isinstance(line, str) line = py3compat.cast_unicode(line) new_line, err = _line_format(line, "str") @@ -396,7 +444,7 @@ def structured_traceback( evalue: Optional[BaseException], etb: Optional[TracebackType] = None, tb_offset: Optional[int] = None, - context=5, + number_of_lines_of_context: int = 5, ): """Return a list of traceback frames. @@ -497,7 +545,7 @@ def structured_traceback( exception = self.get_parts_of_chained_exception(evalue) - if exception and not id(exception[1]) in chained_exc_ids: + if exception and (id(exception[1]) not in chained_exc_ids): chained_exception_message = ( self.prepare_chained_exception_message(evalue.__cause__)[0] if evalue is not None @@ -509,8 +557,12 @@ def structured_traceback( chained_exceptions_tb_offset = 0 out_list = ( self.structured_traceback( - etype, evalue, (etb, chained_exc_ids), - chained_exceptions_tb_offset, context) + etype, + evalue, + (etb, chained_exc_ids), # type: ignore + chained_exceptions_tb_offset, + context, + ) + chained_exception_message + out_list) @@ -673,27 +725,41 @@ class FrameInfo: """ description: Optional[str] - filename: str - lineno: int + filename: Optional[str] + lineno: Tuple[int] + # number of context lines to use + context: Optional[int] @classmethod def _from_stack_data_FrameInfo(cls, frame_info): return cls( getattr(frame_info, "description", None), - getattr(frame_info, "filename", None), - getattr(frame_info, "lineno", None), + getattr(frame_info, "filename", None), # type: ignore[arg-type] + getattr(frame_info, "lineno", None), # type: ignore[arg-type] getattr(frame_info, "frame", None), getattr(frame_info, "code", None), sd=frame_info, + context=None, ) - def __init__(self, description, filename, lineno, frame, code, sd=None): + def __init__( + self, + description: Optional[str], + filename: str, + lineno: Tuple[int], + frame, + code, + *, + sd=None, + context=None, + ): self.description = description self.filename = filename self.lineno = lineno self.frame = frame self.code = code self._sd = sd + self.context = context # self.lines = [] if sd is None: @@ -848,7 +914,6 @@ def format_record(self, frame_info: FrameInfo): result = f'{link}{", " if call else ""}{call}\n' if frame_info._sd is None: - assert False # fast fallback if file is too long tpl_link = "%s%%s%s" % (Colors.filenameEm, ColorsNormal) link = tpl_link % util_path.compress_user(frame_info.filename) @@ -858,13 +923,25 @@ def format_record(self, frame_info: FrameInfo): ).format2 first_line = frame_info.code.co_firstlineno current_line = frame_info.lineno[0] + raw_lines = frame_info.raw_lines + index = current_line - first_line + + if index >= frame_info.context: + start = max(index - frame_info.context, 0) + stop = index + frame_info.context + index = frame_info.context + else: + start = 0 + stop = index + frame_info.context + raw_lines = raw_lines[start:stop] + return "%s%s" % ( level, "".join( _simple_format_traceback_lines( current_line, - current_line - first_line, - frame_info.raw_lines, + index, + raw_lines, Colors, lvals, _line_format, @@ -942,13 +1019,13 @@ def format_exception_as_a_whole( # some locals orig_etype = etype try: - etype = etype.__name__ + etype = etype.__name__ # type: ignore except AttributeError: pass 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) + head = self.prepare_header(str(etype), self.long_header) records = ( self.get_records(etb, number_of_lines_of_context, tb_offset) if etb else [] ) @@ -1018,23 +1095,34 @@ def get_records( tbs = [] while cf is not None: try: - source_file = inspect.getsourcefile(etb.tb_frame) - lines, first = inspect.getsourcelines(etb.tb_frame) + mod = inspect.getmodule(cf.tb_frame) + if mod is not None: + mod_name = mod.__name__ + root_name, *_ = mod_name.split(".") + if root_name == "IPython": + cf = cf.tb_next + continue + max_len = get_line_number_of_frame(cf.tb_frame) + except OSError: - max_len = float("-inf") - break - max_len = max(max_len, first + len(lines)) + max_len = 0 + max_len = max(max_len, max_len) tbs.append(cf) - cf = cf.tb_next + cf = getattr(cf, "tb_next", None) if max_len > FAST_THRESHOLD: FIs = [] for tb in tbs: - frame = tb.tb_frame + frame = tb.tb_frame # type: ignore lineno = (frame.f_lineno,) code = frame.f_code filename = code.co_filename - FIs.append(FrameInfo("Raw frame", filename, lineno, frame, code)) + # TODO: Here we need to use before/after/ + FIs.append( + FrameInfo( + "Raw frame", filename, lineno, frame, code, context=context + ) + ) return FIs res = list(stack_data.FrameInfo.stack_data(etb, options=options))[tb_offset:] res = [FrameInfo._from_stack_data_FrameInfo(r) for r in res] @@ -1044,7 +1132,7 @@ def structured_traceback( self, etype: type, evalue: Optional[BaseException], - etb: Optional[TracebackType], + etb: Optional[TracebackType] = None, tb_offset: Optional[int] = None, number_of_lines_of_context: int = 5, ): @@ -1115,8 +1203,8 @@ def debugger(self, force: bool = False): with display_trap: self.pdb.reset() # Find the right frame so we don't pop up inside ipython itself - if hasattr(self, 'tb') and self.tb is not None: - etb = self.tb + if hasattr(self, "tb") and self.tb is not None: # type: ignore[has-type] + etb = self.tb # type: ignore[has-type] else: etb = self.tb = sys.last_traceback while self.tb is not None and self.tb.tb_next is not None: @@ -1291,24 +1379,23 @@ def __call__(self, etype=None, evalue=None, etb=None, def structured_traceback( self, - etype=None, - value=None, - tb=None, - tb_offset=None, - number_of_lines_of_context=5, + etype: type, + evalue: Optional[BaseException], + etb: Optional[TracebackType] = None, + tb_offset: Optional[int] = None, + number_of_lines_of_context: int = 5, ): - etype: type - value: BaseException # tb: TracebackType or tupleof tb types ? if etype is None: - etype, value, tb = sys.exc_info() - if isinstance(tb, tuple): + etype, evalue, etb = sys.exc_info() + if isinstance(etb, tuple): # tb is a tuple if this is a chained exception. - self.tb = tb[0] + self.tb = etb[0] else: - self.tb = tb + self.tb = etb return FormattedTB.structured_traceback( - self, etype, value, tb, tb_offset, number_of_lines_of_context) + self, etype, evalue, etb, tb_offset, number_of_lines_of_context + ) #--------------------------------------------------------------------------- @@ -1366,7 +1453,7 @@ def text_repr(value): """Hopefully pretty robust repr equivalent.""" # this is pretty horrible but should always return *something* try: - return pydoc.text.repr(value) + return pydoc.text.repr(value) # type: ignore[call-arg] except KeyboardInterrupt: raise except: diff --git a/pyproject.toml b/pyproject.toml index 60b027e1f55..8d4e4a6d7fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ exclude = [ #'IPython/core/interactiveshell.py', 'IPython/core/magic.py', 'IPython/core/profileapp.py', - 'IPython/core/ultratb.py', + # 'IPython/core/ultratb.py', 'IPython/lib/deepreload.py', 'IPython/lib/pretty.py', 'IPython/sphinxext/ipython_directive.py', From e5d97fbbebedb31d3be99ae510733ceba7dbce1c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 11 Apr 2023 11:10:39 +0200 Subject: [PATCH 04/24] Backport PR #14004: Don't do truthy check on object; use `is not None` --- IPython/core/oinspect.py | 2 +- IPython/core/tests/test_oinspect.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 83aada8f8b4..0b8b2b820b9 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -835,7 +835,7 @@ def info(self, obj, oname="", info=None, detail_level=0) -> Dict[str, Any]: att_name = oname.split(".")[-1] parents_docs = None prelude = "" - if info and info.parent and hasattr(info.parent, HOOK_NAME): + if info and info.parent is not None and hasattr(info.parent, HOOK_NAME): parents_docs_dict = getattr(info.parent, HOOK_NAME) parents_docs = parents_docs_dict.get(att_name, None) out = dict( diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index d37a405efbd..fd325c5c388 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -370,6 +370,23 @@ def cleanup_user_ns(**kwargs): del ip.user_ns[k] +def test_pinfo_bool_raise(): + """ + Test that bool method is not called on parent. + """ + + class RaiseBool: + attr = None + + def __bool__(self): + raise ValueError("pinfo should not access this method") + + raise_bool = RaiseBool() + + with cleanup_user_ns(raise_bool=raise_bool): + ip._inspect("pinfo", "raise_bool.attr", detail_level=0) + + def test_pinfo_getindex(): def dummy(): """ From d1bf6900a95859407293b21cbd91ccf27596ce61 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 10:04:58 +0200 Subject: [PATCH 05/24] Backport PR #14027: Resume hinting on right arrow press by default --- IPython/terminal/shortcuts/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 8189326b4bc..3a39ac3e73b 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -277,10 +277,7 @@ def create_identifier(handler: Callable): Binding( auto_suggest.resume_hinting, ["right"], - # For now this binding is inactive (the filter includes `never`). - # TODO: remove `never` if we reach a consensus in #13991 - # TODO: use `emacs_like_insert_mode` once #13991 is in - "never & default_buffer_focused & ((vi_insert_mode & ebivim) | emacs_insert_mode)", + "default_buffer_focused & emacs_like_insert_mode", ), ] From b083def2ef5b4b8769e7c201013732a16aaf434b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 10:05:45 +0200 Subject: [PATCH 06/24] Backport PR #14026: Switch default shortcuts for cycling auto-suggestions --- IPython/terminal/shortcuts/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 8189326b4bc..4f9040a7563 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -229,7 +229,7 @@ def create_identifier(handler: Callable): ), Binding( auto_suggest.swap_autosuggestion_up, - ["up"], + ["c-up"], "navigable_suggestions" " & ~has_line_above" " & has_suggestion" @@ -237,7 +237,7 @@ def create_identifier(handler: Callable): ), Binding( auto_suggest.swap_autosuggestion_down, - ["down"], + ["c-down"], "navigable_suggestions" " & ~has_line_below" " & has_suggestion" @@ -245,12 +245,12 @@ def create_identifier(handler: Callable): ), Binding( auto_suggest.up_and_update_hint, - ["up"], + ["c-up"], "has_line_above & navigable_suggestions & default_buffer_focused", ), Binding( auto_suggest.down_and_update_hint, - ["down"], + ["c-down"], "has_line_below & navigable_suggestions & default_buffer_focused", ), Binding( @@ -265,8 +265,8 @@ def create_identifier(handler: Callable): ), Binding( auto_suggest.accept_and_keep_cursor, - ["c-down"], - "has_suggestion & default_buffer_focused & emacs_like_insert_mode", + ["escape", "down"], + "has_suggestion & default_buffer_focused & emacs_insert_mode", ), Binding( auto_suggest.backspace_and_resume_hint, From 12e7d13cbba4580a4ac0e8a72fd80b05576d5723 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 10:06:33 +0200 Subject: [PATCH 07/24] Backport PR #14020: chore: move pytest configuration to `pyproject.toml` --- MANIFEST.in | 2 -- pyproject.toml | 51 +++++++++++++++++++++++++++++++++++++++++++++++++- pytest.ini | 40 --------------------------------------- 3 files changed, 50 insertions(+), 43 deletions(-) delete mode 100644 pytest.ini diff --git a/MANIFEST.in b/MANIFEST.in index 970adeef334..721eded1b7b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,9 +3,7 @@ include COPYING.rst include LICENSE include setupbase.py include MANIFEST.in -include pytest.ini include py.typed -include mypy.ini include .mailmap include .flake8 include .pre-commit-config.yaml diff --git a/pyproject.toml b/pyproject.toml index 8d4e4a6d7fe..f8eac8af6df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = ["setuptools >= 51.0.0"] build-backend = "setuptools.build_meta" + [tool.mypy] python_version = 3.8 ignore_missing_imports = true @@ -28,5 +29,53 @@ exclude = [ 'IPython/utils/path.py', 'IPython/utils/timing.py', 'IPython/utils/text.py' - ] +] +[tool.pytest.ini_options] +addopts = [ + "--durations=10", + "-pIPython.testing.plugin.pytest_ipdoctest", + "--ipdoctest-modules", + "--ignore=docs", + "--ignore=examples", + "--ignore=htmlcov", + "--ignore=ipython_kernel", + "--ignore=ipython_parallel", + "--ignore=results", + "--ignore=tmp", + "--ignore=tools", + "--ignore=traitlets", + "--ignore=IPython/core/tests/daft_extension", + "--ignore=IPython/sphinxext", + "--ignore=IPython/terminal/pt_inputhooks", + "--ignore=IPython/__main__.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/utils/_process_cli.py", + "--ignore=IPython/utils/_process_posix.py", + "--ignore=IPython/utils/_process_win32.py", + "--ignore=IPython/utils/_process_win32_controller.py", + "--ignore=IPython/utils/daemonize.py", + "--ignore=IPython/utils/eventful.py", + "--ignore=IPython/kernel", + "--ignore=IPython/consoleapp.py", + "--ignore=IPython/core/inputsplitter.py", + "--ignore=IPython/lib/kernel.py", + "--ignore=IPython/utils/jsonutil.py", + "--ignore=IPython/utils/localinterfaces.py", + "--ignore=IPython/utils/log.py", + "--ignore=IPython/utils/signatures.py", + "--ignore=IPython/utils/traitlets.py", + "--ignore=IPython/utils/version.py" +] +doctest_optionflags = [ + "NORMALIZE_WHITESPACE", + "ELLIPSIS" +] +ipdoctest_optionflags = [ + "NORMALIZE_WHITESPACE", + "ELLIPSIS" +] +asyncio_mode = "strict" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 5cc977692b8..00000000000 --- a/pytest.ini +++ /dev/null @@ -1,40 +0,0 @@ -[pytest] -addopts = --durations=10 - -p IPython.testing.plugin.pytest_ipdoctest --ipdoctest-modules - --ignore=docs - --ignore=examples - --ignore=htmlcov - --ignore=ipython_kernel - --ignore=ipython_parallel - --ignore=results - --ignore=tmp - --ignore=tools - --ignore=traitlets - --ignore=IPython/core/tests/daft_extension - --ignore=IPython/sphinxext - --ignore=IPython/terminal/pt_inputhooks - --ignore=IPython/__main__.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/utils/_process_cli.py - --ignore=IPython/utils/_process_posix.py - --ignore=IPython/utils/_process_win32.py - --ignore=IPython/utils/_process_win32_controller.py - --ignore=IPython/utils/daemonize.py - --ignore=IPython/utils/eventful.py - - --ignore=IPython/kernel - --ignore=IPython/consoleapp.py - --ignore=IPython/core/inputsplitter.py - --ignore=IPython/lib/kernel.py - --ignore=IPython/utils/jsonutil.py - --ignore=IPython/utils/localinterfaces.py - --ignore=IPython/utils/log.py - --ignore=IPython/utils/signatures.py - --ignore=IPython/utils/traitlets.py - --ignore=IPython/utils/version.py -doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS -ipdoctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS -asyncio_mode = strict From 531489067d61c80720181f0923f0c740e0cd2502 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 10:07:30 +0200 Subject: [PATCH 08/24] Backport PR #14011: Raise an ImportError if docstrings should be sphinxified, but docrepr is't available --- IPython/core/interactiveshell.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index e4a1a4caa88..7392de7c022 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1781,9 +1781,12 @@ def _inspect(self, meth, oname, namespaces=None, **kw): This function is meant to be called by pdef, pdoc & friends. """ info: OInfo = self._object_find(oname, namespaces) - docformat = ( - sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None - ) + if self.sphinxify_docstring: + if sphinxify is None: + raise ImportError("Module ``docrepr`` required but missing") + docformat = sphinxify(self.object_inspect(oname)) + else: + docformat = None if info.found or hasattr(info.parent, oinspect.HOOK_NAME): pmethod = getattr(self.inspector, meth) # TODO: only apply format_screen to the plain/text repr of the mime From 4524687e1b062ea1550a4c5433a5dfae13d0d6bc Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 10:50:49 +0200 Subject: [PATCH 09/24] Backport PR #14033: MAINT: reintroduce a OInfo get method for backward compatibility. --- IPython/core/oinspect.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 0b8b2b820b9..ef6a0d02d7f 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -69,6 +69,23 @@ class OInfo: parent: Any obj: Any + def get(self, field): + """Get a field from the object for backward compatibility with before 8.12 + + see https://github.com/h5py/h5py/issues/2253 + """ + # We need to deprecate this at some point, but the warning will show in completion. + # Let's comment this for now and uncomment end of 2023 ish + # warnings.warn( + # f"OInfo dataclass with fields access since IPython 8.12 please use OInfo.{field} instead." + # "OInfo used to be a dict but a dataclass provide static fields verification with mypy." + # "This warning and backward compatibility `get()` method were added in 8.13.", + # DeprecationWarning, + # stacklevel=2, + # ) + return getattr(self, field) + + def pylight(code): return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True)) From df8e61dc0f794f01fc97fca2f612f7ff82d8c9b4 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 11:25:43 +0200 Subject: [PATCH 10/24] Backport PR #14047: What's new 8.12.1 --- docs/source/whatsnew/version8.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 244eb71fac8..c2046223f60 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,31 @@ 8.x Series ============ +.. _version 8.12.1: + +IPython 8.12.1 +-------------- + +This is the twin release of IPython 8.13 that contain only critical UI and bug +fixes. The next minor version of IPython has dropped support for Python 3.8 – as +per Nep 29 and this IPython 8.12.x will now only receive bugfixes. + + + - :ghpull:`14004` Fix a bug introduced in IPython 8.12 that crash when + inspecting some docstrings. + - :ghpull:`14010` Fix fast traceback code that was not working in some case. + - :ghpull:`14014` Fix ``%page`` magic broken in some case. + - :ghpull:`14026`, :ghpull:`14027` Tweak default shortcut with respect to + autosuggestions. + - :ghpull:`14033` add back the ability to use ``.get()`` on OInfo object for + backward compatibility with h5py (this will be re-deprecated later, and h5py + will also get a fix). + +As usual you can find the full list of PRs on GitHub under `the 8.12.1 milestone +`__. + +Thanks to the D.E. Shaw group for the request and sponsoring the work. + .. _version 8.12.0: IPython 8.12 From f4df70bea0818fc6569bc6dc7d99f62017a0682a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 11:51:05 +0200 Subject: [PATCH 11/24] release 8.12.1 --- 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 d9c699b4f7c..0d4e826067f 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -17,7 +17,7 @@ # version _version_major = 8 _version_minor = 12 -_version_patch = 0 +_version_patch = 1 _version_extra = ".dev" # _version_extra = "rc1" _version_extra = "" # Uncomment this for full releases From a09873dcf70d5055d1b0d5c282b309c4a76955a2 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 11:52:00 +0200 Subject: [PATCH 12/24] 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 0d4e826067f..2cc354303c5 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -17,10 +17,10 @@ # version _version_major = 8 _version_minor = 12 -_version_patch = 1 +_version_patch = 2 _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 4049037323b6ee014508d42e367c848221ac497a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 28 Apr 2023 10:02:12 +0200 Subject: [PATCH 13/24] Backport PR #14029: Allow safe access to the `__getattribute__` method of modules --- IPython/core/guarded_eval.py | 2 ++ IPython/core/tests/test_guarded_eval.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index 3c95213734a..1eca14db11d 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -637,6 +637,7 @@ def _list_methods(cls, source=None): dict_keys: Type[collections.abc.KeysView] = type({}.keys()) method_descriptor: Any = type(list.copy) +module = type(builtins) NUMERICS = {int, float, complex} @@ -686,6 +687,7 @@ def _list_methods(cls, source=None): *NUMERICS, dict_keys, method_descriptor, + module, } diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 905cf3ab8e3..6fb321abe12 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -568,3 +568,15 @@ class B(NamedTuple): pass assert A.__getitem__ == B.__getitem__ + + +@dec.skip_without("numpy") +def test_module_access(): + import numpy + + context = limited(numpy=numpy) + assert guarded_eval("numpy.linalg.norm", context) == numpy.linalg.norm + + context = minimal(numpy=numpy) + with pytest.raises(GuardRejection): + guarded_eval("np.linalg.norm", context) From c363cee1a37cf494b8944e1794f8c3e5fde7ff8f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 4 May 2023 10:46:54 +0200 Subject: [PATCH 14/24] Backport PR #14052: Only enable "resume hinting" with right arrow if at the EOL --- IPython/terminal/shortcuts/__init__.py | 4 +++- IPython/terminal/shortcuts/filters.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index f8241e0234f..2f7effbe78c 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -277,7 +277,9 @@ def create_identifier(handler: Callable): Binding( auto_suggest.resume_hinting, ["right"], - "default_buffer_focused & emacs_like_insert_mode", + "is_cursor_at_the_end_of_line" + " & default_buffer_focused" + " & emacs_like_insert_mode", ), ] diff --git a/IPython/terminal/shortcuts/filters.py b/IPython/terminal/shortcuts/filters.py index 5a28069799f..5a582afedcc 100644 --- a/IPython/terminal/shortcuts/filters.py +++ b/IPython/terminal/shortcuts/filters.py @@ -52,6 +52,13 @@ def has_line_below() -> bool: return document.cursor_position_row < len(document.lines) - 1 +@undoc +@Condition +def is_cursor_at_the_end_of_line() -> bool: + document = get_app().current_buffer.document + return document.is_cursor_at_the_end_of_line + + @undoc @Condition def has_line_above() -> bool: @@ -179,6 +186,7 @@ def is_windows_os(): "never": Never(), "has_line_below": has_line_below, "has_line_above": has_line_above, + "is_cursor_at_the_end_of_line": is_cursor_at_the_end_of_line, "has_selection": has_selection, "has_suggestion": has_suggestion, "vi_mode": vi_mode, From 64354badfd75dff0b6975dc88eb29d931df871fd Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 4 May 2023 14:36:08 +0200 Subject: [PATCH 15/24] release 8.12.2 --- 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 2cc354303c5..149f553b82a 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 2 _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 502d7657a84d268b324319980da4178a40fad17f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 4 May 2023 14:36:31 +0200 Subject: [PATCH 16/24] 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 149f553b82a..91a9cd43dca 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -17,10 +17,10 @@ # version _version_major = 8 _version_minor = 12 -_version_patch = 2 +_version_patch = 3 _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 e7861a10e1505500d6cfda08d68314ba30875f2f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 2 Jun 2023 10:35:50 +0200 Subject: [PATCH 17/24] Backport PR #14080: Add pass-through filter for shortcuts --- IPython/terminal/shortcuts/__init__.py | 3 +- IPython/terminal/shortcuts/auto_suggest.py | 7 ++++- IPython/terminal/shortcuts/filters.py | 34 +++++++++++++++++++++- docs/autogen_shortcuts.py | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 2f7effbe78c..12890f4ab6e 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -279,7 +279,8 @@ def create_identifier(handler: Callable): ["right"], "is_cursor_at_the_end_of_line" " & default_buffer_focused" - " & emacs_like_insert_mode", + " & emacs_like_insert_mode" + " & pass_through", ), ] diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 9b03370e9e3..65f91577ce9 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -20,6 +20,8 @@ from IPython.core.getipython import get_ipython from IPython.utils.tokenutil import generate_tokens +from .filters import pass_through + def _get_query(document: Document): return document.lines[document.cursor_position_row] @@ -267,7 +269,10 @@ def backspace_and_resume_hint(event: KeyPressEvent): def resume_hinting(event: KeyPressEvent): """Resume autosuggestions""" - return _update_hint(event.current_buffer) + pass_through.reply(event) + # Order matters: if update happened first and event reply second, the + # suggestion would be auto-accepted if both actions are bound to same key. + _update_hint(event.current_buffer) def up_and_update_hint(event: KeyPressEvent): diff --git a/IPython/terminal/shortcuts/filters.py b/IPython/terminal/shortcuts/filters.py index 5a582afedcc..7c9d6a9c41d 100644 --- a/IPython/terminal/shortcuts/filters.py +++ b/IPython/terminal/shortcuts/filters.py @@ -13,7 +13,8 @@ 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.key_binding import KeyPressEvent +from prompt_toolkit.filters import Condition, Filter, emacs_insert_mode, has_completions from prompt_toolkit.filters import has_focus as has_focus_impl from prompt_toolkit.filters import ( Always, @@ -175,6 +176,36 @@ def is_windows_os(): return sys.platform == "win32" +class PassThrough(Filter): + """A filter allowing to implement pass-through behaviour of keybindings. + + Prompt toolkit key processor dispatches only one event per binding match, + which means that adding a new shortcut will suppress the old shortcut + if the keybindings are the same (unless one is filtered out). + + To stop a shortcut binding from suppressing other shortcuts: + - add the `pass_through` filter to list of filter, and + - call `pass_through.reply(event)` in the shortcut handler. + """ + + def __init__(self): + self._is_replying = False + + def reply(self, event: KeyPressEvent): + self._is_replying = True + try: + event.key_processor.reset() + event.key_processor.feed_multiple(event.key_sequence) + event.key_processor.process_keys() + finally: + self._is_replying = False + + def __call__(self): + return not self._is_replying + + +pass_through = PassThrough() + # these one is callable and re-used multiple times hence needs to be # only defined once beforhand so that transforming back to human-readable # names works well in the documentation. @@ -248,6 +279,7 @@ def is_windows_os(): "followed_by_single_quote": following_text("^'"), "navigable_suggestions": navigable_suggestions, "cursor_in_leading_ws": cursor_in_leading_ws, + "pass_through": pass_through, } diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index 387c105f6f2..23b47111665 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -88,6 +88,8 @@ def format_filter( return result elif s in ["Never", "Always"]: return s.lower() + elif s == "PassThrough": + return "pass_through" else: raise ValueError(f"Unknown filter type: {filter_}") From 7a33e1b9c268dae5e48ddd5a0ac4fd0be9609925 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 19 Sep 2023 11:15:36 +0200 Subject: [PATCH 18/24] Backport PR #14169: Set instance on singleton configurable when created via constructor. --- IPython/terminal/embed.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/IPython/terminal/embed.py b/IPython/terminal/embed.py index ce5ee01ff18..59fa6106776 100644 --- a/IPython/terminal/embed.py +++ b/IPython/terminal/embed.py @@ -158,6 +158,12 @@ def __init__(self, **kw): assert ( "user_global_ns" not in kw ), "Key word argument `user_global_ns` has been replaced by `user_module` since IPython 4.0." + # temporary fix for https://github.com/ipython/ipython/issues/14164 + cls = type(self) + if cls._instance is None: + for subclass in cls._walk_mro(): + subclass._instance = self + cls._instance = self clid = kw.pop('_init_location_id', None) if not clid: From 286fc427ce16f1349b042f7d0c22a079fb02222c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 1 Sep 2023 13:52:38 +0200 Subject: [PATCH 19/24] Backport PR #14148: fix RTD --- .readthedocs.yaml | 16 ++++++++++++++++ MANIFEST.in | 1 + 2 files changed, 17 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000000..643b40b70fc --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/source/conf.py + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 721eded1b7b..7d96f8c24f1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,7 @@ exclude tools exclude CONTRIBUTING.md exclude .editorconfig exclude SECURITY.md +exclude .readthedocs.yaml graft scripts From 5e09c85423f5d551501c3c61ea1e28169e23b5d5 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 1 Sep 2023 13:37:15 +0200 Subject: [PATCH 20/24] fix RTD --- .readthedocs.yaml | 16 ++++++++++++++++ MANIFEST.in | 1 + 2 files changed, 17 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000000..643b40b70fc --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/source/conf.py + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 721eded1b7b..7d96f8c24f1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,7 @@ exclude tools exclude CONTRIBUTING.md exclude .editorconfig exclude SECURITY.md +exclude .readthedocs.yaml graft scripts From 181c7f725debfa7200e863edf3aba26da3dcb36f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Sep 2023 02:00:35 -0700 Subject: [PATCH 21/24] Backport PR #14177: pin sphinx --- docs/requirements.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index add92ee1e12..15be4264537 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,12 @@ -e .[doc] +sphinx>7 +setuptools +sphinx_rtd_theme>=1.2.0 +numpy +exceptiongroup +testpath +matplotlib +docrepr +prompt_toolkit +ipykernel +stack_data From 298b0a8772e8fe963bc3c1bb4e22205cf75b1a9d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Sep 2023 01:25:02 -0700 Subject: [PATCH 22/24] Backport PR #14174: Whats new 8.12.3 --- docs/source/whatsnew/version8.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index c2046223f60..1c72164160b 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,24 @@ 8.x Series ============ +.. _version 8.12.3: + +IPython 8.12.3 +-------------- + +Tiny release of 8.12.3 that backport a small number of fixes for users still +using Python 3.8. + + - :ghpull:`14080` add passthrough filter shortcuts + - :ghpull:`14169` Fix `InteractiveShellEmbed` + +.. _version 8.12.2: + +IPython 8.12.2 +-------------- + +See 8.13 Release notes. + .. _version 8.12.1: IPython 8.12.1 From 4e1d3e5577cbec75ebb665b1cc70ee7839042857 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Sep 2023 11:11:39 +0200 Subject: [PATCH 23/24] release 8.12.3 --- 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 91a9cd43dca..23d1993bbde 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,7 +20,7 @@ _version_patch = 3 _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 9b465903726f1629f656e946bd32b4eed445309d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Sep 2023 11:12:03 +0200 Subject: [PATCH 24/24] 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 23d1993bbde..ceb44cfbef4 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -17,10 +17,10 @@ # version _version_major = 8 _version_minor = 12 -_version_patch = 3 +_version_patch = 4 _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]