From c31fed6f3227cccad94ab318e42058a4a677680f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 24 Sep 2021 17:33:52 -0700 Subject: [PATCH 01/49] 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 234b4871465..ba490fecc78 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -20,11 +20,11 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 7 -_version_minor = 28 +_version_minor = 29 _version_patch = 0 _version_extra = '.dev' # _version_extra = 'b1' -_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 0456c001bf780b07076452aee9c50aed47b1235c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 30 Sep 2021 13:56:48 -0700 Subject: [PATCH 02/49] Backport PR #13162: print_figure return base64 str instead of bytes --- IPython/core/pylabtools.py | 54 ++++++++++++++++++++------- IPython/core/tests/test_display.py | 6 ++- IPython/core/tests/test_pylabtools.py | 14 +++++-- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 8e3aade62d0..21190ed1723 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -5,6 +5,8 @@ # Distributed under the terms of the Modified BSD License. from io import BytesIO +from binascii import b2a_base64 +from functools import partial import warnings from IPython.core.display import _pngxy @@ -99,7 +101,7 @@ def figsize(sizex, sizey): matplotlib.rcParams['figure.figsize'] = [sizex, sizey] -def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs): +def print_figure(fig, fmt="png", bbox_inches="tight", base64=False, **kwargs): """Print a figure to an image, and return the resulting file data Returned data will be bytes unless ``fmt='svg'``, @@ -107,6 +109,12 @@ def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs): Any keyword args are passed to fig.canvas.print_figure, such as ``quality`` or ``bbox_inches``. + + If `base64` is True, return base64-encoded str instead of raw bytes + for binary-encoded image formats + + .. versionadded: 7.29 + base64 argument """ # When there's an empty figure, we shouldn't return anything, otherwise we # get big blank areas in the qt console. @@ -138,19 +146,31 @@ def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs): data = bytes_io.getvalue() if fmt == 'svg': data = data.decode('utf-8') + elif base64: + data = b2a_base64(data).decode("ascii") return data -def retina_figure(fig, **kwargs): - """format a figure as a pixel-doubled (retina) PNG""" - pngdata = print_figure(fig, fmt='retina', **kwargs) +def retina_figure(fig, base64=False, **kwargs): + """format a figure as a pixel-doubled (retina) PNG + + If `base64` is True, return base64-encoded str instead of raw bytes + for binary-encoded image formats + + .. versionadded: 7.29 + base64 argument + """ + pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs) # Make sure that retina_figure acts just like print_figure and returns # None when the figure is empty. if pngdata is None: return w, h = _pngxy(pngdata) metadata = {"width": w//2, "height":h//2} + if base64: + pngdata = b2a_base64(pngdata).decode("ascii") return pngdata, metadata + # We need a little factory function here to create the closure where # safe_execfile can live. def mpl_runner(safe_execfile): @@ -249,16 +269,22 @@ def select_figure_formats(shell, formats, **kwargs): gs = "%s" % ','.join([repr(f) for f in supported]) raise ValueError("supported formats are: %s not %s" % (gs, bs)) - if 'png' in formats: - png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs)) - if 'retina' in formats or 'png2x' in formats: - png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs)) - if 'jpg' in formats or 'jpeg' in formats: - jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', **kwargs)) - if 'svg' in formats: - svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg', **kwargs)) - if 'pdf' in formats: - pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf', **kwargs)) + if "png" in formats: + png_formatter.for_type( + Figure, partial(print_figure, fmt="png", base64=True, **kwargs) + ) + if "retina" in formats or "png2x" in formats: + png_formatter.for_type(Figure, partial(retina_figure, base64=True, **kwargs)) + if "jpg" in formats or "jpeg" in formats: + jpg_formatter.for_type( + Figure, partial(print_figure, fmt="jpg", base64=True, **kwargs) + ) + if "svg" in formats: + svg_formatter.for_type(Figure, partial(print_figure, fmt="svg", **kwargs)) + if "pdf" in formats: + pdf_formatter.for_type( + Figure, partial(print_figure, fmt="pdf", base64=True, **kwargs) + ) #----------------------------------------------------------------------------- # Code for initializing matplotlib and importing pylab diff --git a/IPython/core/tests/test_display.py b/IPython/core/tests/test_display.py index 9f5fc7ecbf5..7ba11324c1b 100644 --- a/IPython/core/tests/test_display.py +++ b/IPython/core/tests/test_display.py @@ -187,10 +187,12 @@ def test_set_matplotlib_formats_kwargs(): display.set_matplotlib_formats('png', **kwargs) formatter = ip.display_formatter.formatters['image/png'] f = formatter.lookup_by_type(Figure) - cell = f.__closure__[0].cell_contents + formatter_kwargs = f.keywords expected = kwargs + expected["base64"] = True + expected["fmt"] = "png" expected.update(cfg.print_figure_kwargs) - nt.assert_equal(cell, expected) + nt.assert_equal(formatter_kwargs, expected) def test_display_available(): """ diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index f2adb3b1793..027d6342e1c 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -5,7 +5,8 @@ # Distributed under the terms of the Modified BSD License. -from io import UnsupportedOperation, BytesIO +from binascii import a2b_base64 +from io import BytesIO import matplotlib matplotlib.use('Agg') @@ -104,8 +105,11 @@ def test_select_figure_formats_kwargs(): pt.select_figure_formats(ip, 'png', **kwargs) formatter = ip.display_formatter.formatters['image/png'] f = formatter.lookup_by_type(Figure) - cell = f.__closure__[0].cell_contents - nt.assert_equal(cell, kwargs) + cell = f.keywords + expected = kwargs + expected["base64"] = True + expected["fmt"] = "png" + assert cell == expected # check that the formatter doesn't raise fig = plt.figure() @@ -114,7 +118,9 @@ def test_select_figure_formats_kwargs(): plt.draw() formatter.enabled = True png = formatter(fig) - assert png.startswith(_PNG) + assert isinstance(png, str) + png_bytes = a2b_base64(png) + assert png_bytes.startswith(_PNG) def test_select_figure_formats_set(): ip = get_ipython() From e3bca870f94a74a22d396871f66b575b4cc55e64 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 30 Sep 2021 13:57:02 -0700 Subject: [PATCH 03/49] Backport PR #13142: Pdbskip #13136 Merge pull request #13142 from Carreau/pdbskip Pdbskip #13136 --- IPython/core/debugger.py | 134 ++++++++++++++++++++++++++-- IPython/core/tests/test_debugger.py | 112 +++++++++++++++++++++++ IPython/terminal/debugger.py | 4 +- docs/source/whatsnew/version7.rst | 3 +- 4 files changed, 243 insertions(+), 10 deletions(-) diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index 02d01add393..69cc55f914f 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -2,6 +2,71 @@ """ Pdb debugger class. + +This is an extension to PDB which adds a number of new features. +Note that there is also the `IPython.terminal.debugger` class which provides UI +improvements. + +We also strongly recommend to use this via the `ipdb` package, which provides +extra configuration options. + +Among other things, this subclass of PDB: + - supports many IPython magics like pdef/psource + - hide frames in tracebacks based on `__tracebackhide__` + - allows to skip frames based on `__debuggerskip__` + +The skipping and hiding frames are configurable via the `skip_predicates` +command. + +By default, frames from readonly files will be hidden, frames containing +``__tracebackhide__=True`` will be hidden. + +Frames containing ``__debuggerskip__`` will be stepped over, frames who's parent +frames value of ``__debuggerskip__`` is ``True`` will be skipped. + + >>> def helper_1(): + ... print("don't step in me") + ... + ... + ... def helper_2(): + ... print("in me neither") + ... + +One can define a decorator that wraps a function between the two helpers: + + >>> def pdb_skipped_decorator(function): + ... + ... + ... def wrapped_fn(*args, **kwargs): + ... __debuggerskip__ = True + ... helper_1() + ... __debuggerskip__ = False + ... result = function(*args, **kwargs) + ... __debuggerskip__ = True + ... helper_2() + ... return result + ... + ... return wrapped_fn + +When decorating a function, ipdb will directly step into ``bar()`` by +default: + + >>> @foo_decorator + ... def bar(x, y): + ... return x * y + + +You can toggle the behavior with + + ipdb> skip_predicates debuggerskip false + +or configure it in your ``.pdbrc`` + + + +Licencse +-------- + Modified from the standard pdb.Pdb class to avoid including readline, so that the command line completion of other programs which include this isn't damaged. @@ -9,11 +74,16 @@ In the future, this class will be expanded with improvements over the standard pdb. -The code in this file is mainly lifted out of cmd.py in Python 2.2, with minor -changes. Licensing should therefore be under the standard Python terms. For -details on the PSF (Python Software Foundation) standard license, see: +The original code in this file is mainly lifted out of cmd.py in Python 2.2, +with minor changes. Licensing should therefore be under the standard Python +terms. For details on the PSF (Python Software Foundation) standard license, +see: https://docs.python.org/2/license.html + + +All the changes since then are under the same license as IPython. + """ #***************************************************************************** @@ -51,6 +121,9 @@ # it does so with some limitations. The rest of this support is implemented in # the Tracer constructor. +DEBUGGERSKIP = "__debuggerskip__" + + def make_arrow(pad): """generate the leading arrow in front of traceback or debugger""" if pad >= 2: @@ -206,7 +279,12 @@ class Pdb(OldPdb): """ - default_predicates = {"tbhide": True, "readonly": False, "ipython_internal": True} + default_predicates = { + "tbhide": True, + "readonly": False, + "ipython_internal": True, + "debuggerskip": True, + } def __init__(self, color_scheme=None, completekey=None, stdin=None, stdout=None, context=5, **kwargs): @@ -305,6 +383,7 @@ def __init__(self, color_scheme=None, completekey=None, # list of predicates we use to skip frames self._predicates = self.default_predicates + # def set_colors(self, scheme): """Shorthand access to the color table scheme selector method.""" self.color_scheme_table.set_active_scheme(scheme) @@ -804,10 +883,53 @@ def do_where(self, arg): do_w = do_where + def break_anywhere(self, frame): + """ + + _stop_in_decorator_internals is overly restrictive, as we may still want + to trace function calls, so we need to also update break_anywhere so + that is we don't `stop_here`, because of debugger skip, we may still + stop at any point inside the function + + """ + if self._predicates["debuggerskip"]: + if DEBUGGERSKIP in frame.f_code.co_varnames: + return True + if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): + return True + return super().break_anywhere(frame) + + @skip_doctest + def _is_in_decorator_internal_and_should_skip(self, frame): + """ + Utility to tell us whether we are in a decorator internal and should stop. + + + + """ + + # if we are disabled don't skip + if not self._predicates["debuggerskip"]: + return False + + # if frame is tagged, skip by default. + if DEBUGGERSKIP in frame.f_code.co_varnames: + return True + + # if parent frame value set to True skip as well. + if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): + return True + + return False + def stop_here(self, frame): """Check if pdb should stop here""" if not super().stop_here(frame): return False + + if self._is_in_decorator_internal_and_should_skip(frame) is True: + return False + hidden = False if self.skip_hidden: hidden = self._hidden_predicate(frame) @@ -929,10 +1051,10 @@ def do_context(self, context): class InterruptiblePdb(Pdb): """Version of debugger where KeyboardInterrupt exits the debugger altogether.""" - def cmdloop(self): + def cmdloop(self, intro=None): """Wrap cmdloop() such that KeyboardInterrupt stops the debugger.""" try: - return OldPdb.cmdloop(self) + return OldPdb.cmdloop(self, intro=intro) except KeyboardInterrupt: self.stop_here = lambda frame: False self.do_quit("") diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 7c94592df6d..08a2473e1ac 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -326,6 +326,118 @@ def g(): child.close() +skip_decorators_blocks = ( + """ + def helper_1(): + pass # should not stop here + """, + """ + def helper_2(): + pass # should not stop here + """, + """ + def pdb_skipped_decorator(function): + def wrapped_fn(*args, **kwargs): + __debuggerskip__ = True + helper_1() + __debuggerskip__ = False + result = function(*args, **kwargs) + __debuggerskip__ = True + helper_2() + return result + return wrapped_fn + """, + """ + @pdb_skipped_decorator + def bar(x, y): + return x * y + """, + """import IPython.terminal.debugger as ipdb""", + """ + def f(): + ipdb.set_trace() + bar(3, 4) + """, + """ + f() + """, +) + + +def _decorator_skip_setup(): + import pexpect + + env = os.environ.copy() + env["IPY_TEST_SIMPLE_PROMPT"] = "1" + + child = pexpect.spawn( + sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env + ) + child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE + + child.expect("IPython") + child.expect("\n") + + dedented_blocks = [dedent(b).strip() for b in skip_decorators_blocks] + in_prompt_number = 1 + for cblock in dedented_blocks: + child.expect_exact(f"In [{in_prompt_number}]:") + in_prompt_number += 1 + for line in cblock.splitlines(): + child.sendline(line) + child.expect_exact(line) + child.sendline("") + return child + + +@skip_win32 +def test_decorator_skip(): + """test that decorator frames can be skipped.""" + + child = _decorator_skip_setup() + + child.expect_exact("3 bar(3, 4)") + child.expect("ipdb>") + + child.expect("ipdb>") + child.sendline("step") + child.expect_exact("step") + + child.expect_exact("1 @pdb_skipped_decorator") + + child.sendline("s") + child.expect_exact("return x * y") + + child.close() + + +@skip_win32 +def test_decorator_skip_disabled(): + """test that decorator frame skipping can be disabled""" + + child = _decorator_skip_setup() + + child.expect_exact("3 bar(3, 4)") + + for input_, expected in [ + ("skip_predicates debuggerskip False", ""), + ("skip_predicates", "debuggerskip : False"), + ("step", "---> 2 def wrapped_fn"), + ("step", "----> 3 __debuggerskip__"), + ("step", "----> 4 helper_1()"), + ("step", "---> 1 def helper_1():"), + ("next", "----> 2 pass"), + ("next", "--Return--"), + ("next", "----> 5 __debuggerskip__ = False"), + ]: + child.expect("ipdb>") + child.sendline(input_) + child.expect_exact(input_) + child.expect_exact(expected) + + child.close() + + @skip_win32 def test_where_erase_value(): """Test that `where` does not access f_locals and erase values.""" diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index 9ec246697d4..66ffc1970b5 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -71,7 +71,7 @@ def gen_comp(self, text): enable_history_search=True, mouse_support=self.shell.mouse_support, complete_style=self.shell.pt_complete_style, - style=self.shell.style, + style=getattr(self.shell, "style", None), color_depth=self.shell.color_depth, ) @@ -96,7 +96,6 @@ def cmdloop(self, intro=None): # prompt itself in a different thread (we can't start an event loop # within an event loop). This new thread won't have any event loop # running, and here we run our prompt-loop. - self.preloop() try: @@ -131,7 +130,6 @@ def in_thread(): if keyboard_interrupt: raise KeyboardInterrupt - line = self.precmd(line) stop = self.onecmd(line) stop = self.postcmd(stop, line) diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 84fccd20c53..990e0eb7021 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -2,6 +2,7 @@ 7.x Series ============ + .. _version 7.28: IPython 7.28 @@ -543,7 +544,7 @@ Change of API and exposed objects automatically detected using `frappuccino `_ (still in beta): -The following items are new and mostly related to understanding ``__tracebackbide__``:: +The following items are new and mostly related to understanding ``__tracebackhide__``:: + IPython.core.debugger.Pdb.do_down(self, arg) + IPython.core.debugger.Pdb.do_skip_hidden(self, arg) From 0f23fbe5f802b8eeadb500ccdb24f6d3e910cef3 Mon Sep 17 00:00:00 2001 From: Jakub Date: Sat, 2 Oct 2021 20:43:27 +0200 Subject: [PATCH 04/49] Manually backport whole init_virtualenv --- IPython/core/interactiveshell.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 4ae21e4b3ca..07a74033aa0 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -898,9 +898,7 @@ def init_virtualenv(self): virtualenv was built, and it ignores the --no-site-packages option. A warning will appear suggesting the user installs IPython in the virtualenv, but for many cases, it probably works well enough. - Adapted from code snippets online. - http://blog.ufsoft.org/2009/1/29/ipython-and-virtualenv """ if 'VIRTUAL_ENV' not in os.environ: @@ -931,17 +929,27 @@ def init_virtualenv(self): # Our exe is inside or has access to the virtualenv, don't need to do anything. return - warn("Attempting to work in a virtualenv. If you encounter problems, please " - "install IPython inside the virtualenv.") if sys.platform == "win32": - virtual_env = Path(os.environ["VIRTUAL_ENV"]).joinpath( - "Lib", "site-packages" - ) + virtual_env = str(Path(os.environ["VIRTUAL_ENV"], "Lib", "site-packages")) else: - virtual_env = Path(os.environ["VIRTUAL_ENV"]).joinpath( - "lib", "python{}.{}".format(*sys.version_info[:2]), "site-packages" + virtual_env_path = Path( + os.environ["VIRTUAL_ENV"], "lib", "python{}.{}", "site-packages" ) - + p_ver = sys.version_info[:2] + + # Predict version from py[thon]-x.x in the $VIRTUAL_ENV + re_m = re.search(r"\bpy(?:thon)?([23])\.(\d+)\b", os.environ["VIRTUAL_ENV"]) + if re_m: + predicted_path = Path(str(virtual_env_path).format(*re_m.groups())) + if predicted_path.exists(): + 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." + ) import site sys.path.insert(0, virtual_env) site.addsitedir(virtual_env) From 9e216e6f21afb8fff2d3aa298edb3b507659e440 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 4 Oct 2021 20:31:33 -0700 Subject: [PATCH 05/49] Backport PR #13174: Fix cpaste documentation --- IPython/terminal/magics.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/magics.py b/IPython/terminal/magics.py index 3c7e82b45e5..42231c3f803 100644 --- a/IPython/terminal/magics.py +++ b/IPython/terminal/magics.py @@ -109,7 +109,7 @@ def cpaste(self, parameter_s=''): Just press enter and type -- (and press enter again) and the block will be what was just pasted. - IPython statements (magics, shell escapes) are not supported (yet). + Shell escapes are not supported (yet). See also -------- @@ -122,9 +122,19 @@ def cpaste(self, parameter_s=''): In [8]: %cpaste Pasting code; enter '--' alone on the line to stop. :>>> a = ["world!", "Hello"] - :>>> print " ".join(sorted(a)) + :>>> print(" ".join(sorted(a))) :-- Hello world! + + :: + In [8]: %cpaste + Pasting code; enter '--' alone on the line to stop. + :>>> %alias_magic t timeit + :>>> %t -n1 pass + :-- + Created `%t` as an alias for `%timeit`. + Created `%%t` as an alias for `%%timeit`. + 354 ns ± 224 ns per loop (mean ± std. dev. of 7 runs, 1 loop each) """ opts, name = self.parse_options(parameter_s, 'rqs:', mode='string') if 'r' in opts: From a0bff31692c3bd09a3be4a134e092b1577dc600a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 6 Oct 2021 15:13:21 -0700 Subject: [PATCH 06/49] Backport PR #13172: use _exec wrapper for qt win32 inputhook --- IPython/terminal/pt_inputhooks/qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index ef2a1f55bbf..b999f5aa173 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -64,7 +64,7 @@ def inputhook(context): timer.timeout.connect(event_loop.quit) while not context.input_is_ready(): timer.start(50) # 50 ms - event_loop.exec_() + _exec(event_loop) timer.stop() else: # On POSIX platforms, we can use a file descriptor to quit the event From ac35a5b3ad19735edf714c4f4faf23b42fd899f6 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 7 Oct 2021 09:23:26 -0700 Subject: [PATCH 07/49] Backport PR #13175: Expand and Fix PDB skip. --- IPython/core/debugger.py | 23 +++++++-- IPython/core/tests/test_debugger.py | 78 ++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index 69cc55f914f..6d2333631e3 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -24,8 +24,12 @@ Frames containing ``__debuggerskip__`` will be stepped over, frames who's parent frames value of ``__debuggerskip__`` is ``True`` will be skipped. - >>> def helper_1(): + >>> def helpers_helper(): + ... pass + ... + ... def helper_1(): ... print("don't step in me") + ... helpers_helpers() # will be stepped over unless breakpoint set. ... ... ... def helper_2(): @@ -44,6 +48,7 @@ ... result = function(*args, **kwargs) ... __debuggerskip__ = True ... helper_2() + ... # setting __debuggerskip__ to False again is not necessary ... return result ... ... return wrapped_fn @@ -892,12 +897,16 @@ def break_anywhere(self, frame): stop at any point inside the function """ + + sup = super().break_anywhere(frame) + if sup: + return sup if self._predicates["debuggerskip"]: if DEBUGGERSKIP in frame.f_code.co_varnames: return True if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): return True - return super().break_anywhere(frame) + return False @skip_doctest def _is_in_decorator_internal_and_should_skip(self, frame): @@ -916,9 +925,13 @@ def _is_in_decorator_internal_and_should_skip(self, frame): if DEBUGGERSKIP in frame.f_code.co_varnames: return True - # if parent frame value set to True skip as well. - if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): - return True + # if one of the parent frame value set to True skip as well. + + cframe = frame + while getattr(cframe, "f_back", None): + cframe = cframe.f_back + if self._get_frame_locals(cframe).get(DEBUGGERSKIP): + return True return False diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 08a2473e1ac..eb3b5f353b5 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -12,6 +12,7 @@ import sys import time import warnings + from subprocess import PIPE, CalledProcessError, check_output from tempfile import NamedTemporaryFile from textwrap import dedent @@ -327,15 +328,31 @@ def g(): skip_decorators_blocks = ( + """ + def helpers_helper(): + pass # should not stop here except breakpoint + """, """ def helper_1(): - pass # should not stop here + helpers_helper() # should not stop here """, """ def helper_2(): pass # should not stop here """, """ + def pdb_skipped_decorator2(function): + def wrapped_fn(*args, **kwargs): + __debuggerskip__ = True + helper_2() + __debuggerskip__ = False + result = function(*args, **kwargs) + __debuggerskip__ = True + helper_2() + return result + return wrapped_fn + """, + """ def pdb_skipped_decorator(function): def wrapped_fn(*args, **kwargs): __debuggerskip__ = True @@ -349,6 +366,7 @@ def wrapped_fn(*args, **kwargs): """, """ @pdb_skipped_decorator + @pdb_skipped_decorator2 def bar(x, y): return x * y """, @@ -426,7 +444,7 @@ def test_decorator_skip_disabled(): ("step", "----> 3 __debuggerskip__"), ("step", "----> 4 helper_1()"), ("step", "---> 1 def helper_1():"), - ("next", "----> 2 pass"), + ("next", "----> 2 helpers_helper()"), ("next", "--Return--"), ("next", "----> 5 __debuggerskip__ = False"), ]: @@ -438,6 +456,62 @@ def test_decorator_skip_disabled(): child.close() +@skip_win32 +def test_decorator_skip_with_breakpoint(): + """test that decorator frame skipping can be disabled""" + + import pexpect + + env = os.environ.copy() + env["IPY_TEST_SIMPLE_PROMPT"] = "1" + + child = pexpect.spawn( + sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env + ) + child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE + + child.expect("IPython") + child.expect("\n") + + ### we need a filename, so we need to exec the full block with a filename + with NamedTemporaryFile(suffix=".py", dir=".", delete=True) as tf: + + name = tf.name[:-3].split("/")[-1] + tf.write("\n".join([dedent(x) for x in skip_decorators_blocks[:-1]]).encode()) + tf.flush() + codeblock = f"from {name} import f" + + dedented_blocks = [ + codeblock, + "f()", + ] + + in_prompt_number = 1 + for cblock in dedented_blocks: + child.expect_exact(f"In [{in_prompt_number}]:") + in_prompt_number += 1 + for line in cblock.splitlines(): + child.sendline(line) + child.expect_exact(line) + child.sendline("") + + # as the filename does not exists, we'll rely on the filename prompt + child.expect_exact("47 bar(3, 4)") + + for input_, expected in [ + (f"b {name}.py:3", ""), + ("step", "1---> 3 pass # should not stop here except"), + ("step", "---> 38 @pdb_skipped_decorator"), + ("continue", ""), + ]: + child.expect("ipdb>") + child.sendline(input_) + child.expect_exact(input_) + child.expect_exact(expected) + + child.close() + + @skip_win32 def test_where_erase_value(): """Test that `where` does not access f_locals and erase values.""" From 3829163766fe3579c7129c46b511482e9194dcef Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 12 Oct 2021 09:19:35 -0700 Subject: [PATCH 08/49] Backport PR #13182: ci: Simplify CI maintenance by combining Ubuntu and macOS workflows --- .github/workflows/test-osx.yml | 23 ----------------------- .github/workflows/test.yml | 20 +++++++++++++++++--- 2 files changed, 17 insertions(+), 26 deletions(-) delete mode 100644 .github/workflows/test-osx.yml diff --git a/.github/workflows/test-osx.yml b/.github/workflows/test-osx.yml deleted file mode 100644 index c9afbe0a0ea..00000000000 --- a/.github/workflows/test-osx.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Run tests on OSX - -on: [push, pull_request] - -jobs: - test: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.7 - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install and update Python dependencies - run: | - python -m pip install --upgrade pip setuptools wheel - python -m pip install --upgrade -e file://$PWD#egg=ipython[test] - python -m pip install --upgrade --upgrade-strategy eager trio curio - python -m pip install --upgrade pytest pytest-trio 'matplotlib!=3.2.0' - python -m pip install --upgrade anyio - - name: pytest - run: pytest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9bbae48cf7..2788e8640a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,13 +1,27 @@ name: Run tests -on: [push, pull_request] +on: + push: + pull_request: + # Run weekly on Monday at 1:23 UTC + schedule: + - cron: '23 1 * * 1' + workflow_dispatch: + jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest] + python-version: ["3.7", "3.8", "3.9"] + # Test all on ubuntu, test ends on macos + include: + - os: macos-latest + python-version: "3.7" + - os: macos-latest + python-version: "3.9" steps: - uses: actions/checkout@v2 From 6eebe4d70a6049d45861c4c8b13aac22163fe65d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 12 Oct 2021 09:23:50 -0700 Subject: [PATCH 09/49] Backport PR #13179: Fix qtagg eventloop --- IPython/core/pylabtools.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 21190ed1723..eda368cf49c 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -41,8 +41,6 @@ # most part it's just a reverse of the above dict, but we also need to add a # few others that map to the same GUI manually: backend2gui = dict(zip(backends.values(), backends.keys())) -# Our tests expect backend2gui to just return 'qt' -backend2gui['Qt4Agg'] = 'qt' # In the reverse mapping, there are a few extra valid matplotlib backends that # map to the same GUI support backend2gui["GTK"] = backend2gui["GTKCairo"] = "gtk" @@ -50,6 +48,13 @@ backend2gui["GTK4Cairo"] = "gtk4" backend2gui["WX"] = "wx" backend2gui["CocoaAgg"] = "osx" +# There needs to be a hysteresis here as the new QtAgg Matplotlib backend +# supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5, +# and Qt6. +backend2gui["QtAgg"] = "qt" +backend2gui["Qt4Agg"] = "qt" +backend2gui["Qt5Agg"] = "qt" + # And some backends that don't need GUI integration del backend2gui["nbAgg"] del backend2gui["agg"] @@ -57,6 +62,7 @@ del backend2gui["pdf"] del backend2gui["ps"] del backend2gui["module://matplotlib_inline.backend_inline"] +del backend2gui["module://ipympl.backend_nbagg"] #----------------------------------------------------------------------------- # Matplotlib utilities From 0ffac9cee465527ab3f2d1cf1e2fee6ff2bb11db Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 17 Oct 2021 12:44:35 -0700 Subject: [PATCH 10/49] Backport PR #13193: Use codecov Github action v2 --- .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 2788e8640a8..2ac1b746a84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,4 +47,4 @@ jobs: run: | pytest - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 From 67b23ac2b5867824c74486e25769898a0ce74080 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 18 Oct 2021 09:39:07 -0700 Subject: [PATCH 11/49] Backport PR #13187: Fix wrong inspect.signature call --- 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 238929cc6ff..b5c54e424e7 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -1496,7 +1496,7 @@ def _default_arguments(self, obj): inspect.Parameter.POSITIONAL_OR_KEYWORD) try: - sig = inspect.signature(call_obj) + sig = inspect.signature(obj) ret.extend(k for k, v in sig.parameters.items() if v.kind in _keeps) except ValueError: From 7e83a129f6bd123e2aab516875c874e2f59f5cd1 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 18 Oct 2021 09:39:37 -0700 Subject: [PATCH 12/49] Backport PR #13198: match log message for profile by name and path --- IPython/core/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/application.py b/IPython/core/application.py index 93639d88e2c..81888981a78 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -374,7 +374,7 @@ def init_profile_dir(self): self.log.fatal("Profile %r not found."%self.profile) self.exit(1) else: - self.log.debug("Using existing profile dir: %r"%p.location) + self.log.debug(f"Using existing profile dir: {p.location!r}") else: location = self.config.ProfileDir.location # location is fully specified @@ -394,7 +394,7 @@ def init_profile_dir(self): self.log.fatal("Profile directory %r not found."%location) self.exit(1) else: - self.log.info("Using existing profile dir: %r"%location) + self.log.debug(f"Using existing profile dir: {p.location!r}") # if profile_dir is specified explicitly, set profile name dir_name = os.path.basename(p.location) if dir_name.startswith('profile_'): From 10ad1d13071e8fbb857e5231fbefe7867cbae865 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 18 Oct 2021 13:21:49 -0700 Subject: [PATCH 13/49] Backport PR #13181: create ipython_dir if not exists --- IPython/core/tests/test_paths.py | 4 ++++ IPython/paths.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/IPython/core/tests/test_paths.py b/IPython/core/tests/test_paths.py index ab1c4132a8e..2182cb7cb0c 100644 --- a/IPython/core/tests/test_paths.py +++ b/IPython/core/tests/test_paths.py @@ -160,6 +160,10 @@ def test_get_ipython_dir_7(): @skip_win32 def test_get_ipython_dir_8(): """test_get_ipython_dir_8, test / home directory""" + if not os.access("/", os.W_OK): + # test only when HOME directory actually writable + return + with patch.object(paths, '_writable_dir', lambda path: bool(path)), \ patch.object(paths, 'get_xdg_dir', return_value=None), \ modified_env({ diff --git a/IPython/paths.py b/IPython/paths.py index bbe3d5c7cad..e7f8aee3070 100644 --- a/IPython/paths.py +++ b/IPython/paths.py @@ -66,6 +66,8 @@ def get_ipython_dir() -> str: warn("IPython parent '{0}' is not a writable location," " using a temp directory.".format(parent)) ipdir = tempfile.mkdtemp() + else: + os.makedirs(ipdir) assert isinstance(ipdir, str), "all path manipulation should be str(unicode), but are not." return ipdir From 31a25382c3496b6756b111b19d096d560084a752 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 27 Oct 2021 11:09:18 -0700 Subject: [PATCH 14/49] Backport PR #13218: Set InterruptiblePdb as the default debugger. --- IPython/core/interactiveshell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 4ae21e4b3ca..71c5cf1e521 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -43,7 +43,7 @@ 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 Pdb +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 @@ -1823,7 +1823,7 @@ def init_history(self): # Things related to exception handling and tracebacks (not debugging) #------------------------------------------------------------------------- - debugger_cls = Pdb + debugger_cls = InterruptiblePdb def init_traceback_handlers(self, custom_exceptions): # Syntax error handler. From 98bdc42df3e769c457e5e3b111094e5d06ddcbf6 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 28 Oct 2021 20:57:56 -0700 Subject: [PATCH 15/49] Backport PR #13225: Fix quick reference documentation for 'exec' --- IPython/core/usage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/core/usage.py b/IPython/core/usage.py index 37024c44567..53219bceb25 100644 --- a/IPython/core/usage.py +++ b/IPython/core/usage.py @@ -305,7 +305,7 @@ _i, _ii, _iii : Previous, next previous, next next previous input _i4, _ih[2:5] : Input history line 4, lines 2-4 -exec _i81 : Execute input history line #81 again +exec(_i81) : Execute input history line #81 again %rep 81 : Edit input history line #81 _, __, ___ : previous, next previous, next next previous output _dh : Directory history From 99764cd73382ca58d873f95b1213b023787b92a2 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Oct 2021 15:40:36 -0700 Subject: [PATCH 16/49] Backport PR #13231: What's New 7.29 --- docs/source/whatsnew/version7.rst | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 990e0eb7021..02e1dc43d3a 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -2,6 +2,47 @@ 7.x Series ============ +.. _version 7.29: + +IPython 7.29 +============ + + +IPython 7.29 brings a couple of new functionalities to IPython and a number of bugfixes. +It is one of the largest recent release, relatively speaking, with close to 15 Pull Requests. + + +- fix an issue where base64 was returned instead of bytes when showing figures :ghpull:`13162` +- fix compatibility with PyQt6, PySide 6 :ghpull:`13172`. This may be of + interest if you are running on Apple Silicon as only qt6.2+ is natively + compatible. +- fix matplotlib qtagg eventloop :ghpull:`13179` +- Multiple docs fixes, typos, ... etc. +- Debugger will now exit by default on SigInt :ghpull:`13218`, this will be + useful in notebook/lab if you forgot to exit the debugger. "Interrupt Kernel" + will now exist the debugger. + +It give Pdb the ability to skip code in decorators. If functions contain a +special value names ``__debuggerskip__ = True|False``, the function will not be +stepped into, and Pdb will step into lower frames only if the value is set to +``False``. The exact behavior is still likely to have corner cases and will be +refined in subsequent releases. Feedback welcome. See the debugger module +documentation for more info. Thanks to the `D. E. Shaw +group `__ for funding this feature. + +The main branch of IPython is receiving a number of changes as we received a +`NumFOCUS SDG `__ +($4800), to help us finish replacing ``nose`` by ``pytest``, and make IPython +future proof with an 8.0 release. + + +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.28: From 357745bb1195757eac09b0daa6288f7914aae36c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Oct 2021 16:46:22 -0700 Subject: [PATCH 17/49] Backport PR #13233: Fix link and try to fix list. --- docs/source/whatsnew/version7.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 02e1dc43d3a..42e12e52543 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -12,15 +12,15 @@ IPython 7.29 brings a couple of new functionalities to IPython and a number of b It is one of the largest recent release, relatively speaking, with close to 15 Pull Requests. -- fix an issue where base64 was returned instead of bytes when showing figures :ghpull:`13162` -- fix compatibility with PyQt6, PySide 6 :ghpull:`13172`. This may be of - interest if you are running on Apple Silicon as only qt6.2+ is natively - compatible. -- fix matplotlib qtagg eventloop :ghpull:`13179` -- Multiple docs fixes, typos, ... etc. -- Debugger will now exit by default on SigInt :ghpull:`13218`, this will be - useful in notebook/lab if you forgot to exit the debugger. "Interrupt Kernel" - will now exist the debugger. + - fix an issue where base64 was returned instead of bytes when showing figures :ghpull:`13162` + - fix compatibility with PyQt6, PySide 6 :ghpull:`13172`. This may be of + interest if you are running on Apple Silicon as only qt6.2+ is natively + compatible. + - fix matplotlib qtagg eventloop :ghpull:`13179` + - Multiple docs fixes, typos, ... etc. + - Debugger will now exit by default on SigInt :ghpull:`13218`, this will be + useful in notebook/lab if you forgot to exit the debugger. "Interrupt Kernel" + will now exist the debugger. It give Pdb the ability to skip code in decorators. If functions contain a special value names ``__debuggerskip__ = True|False``, the function will not be @@ -38,7 +38,7 @@ future proof with an 8.0 release. 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. From 8775ab7dbea9aa284024af1242e8fe620d644b13 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 29 Oct 2021 17:16:59 -0700 Subject: [PATCH 18/49] Backport PR #13235: Update what's of of older IPython (forgotten) --- .../enable-to-add-extra-attrs-to-iframe.rst | 38 ------------- .../whatsnew/pr/pastebin-expiry-days.rst | 7 --- docs/source/whatsnew/version7.rst | 53 +++++++++++++++++++ 3 files changed, 53 insertions(+), 45 deletions(-) delete mode 100644 docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst delete mode 100644 docs/source/whatsnew/pr/pastebin-expiry-days.rst diff --git a/docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst b/docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst deleted file mode 100644 index 1954bc439b8..00000000000 --- a/docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst +++ /dev/null @@ -1,38 +0,0 @@ -``YouTubeVideo`` autoplay and the ability to add extra attributes to ``IFrame`` -=============================================================================== - -You can add any extra attributes to the ``