From 32c393b6babeb82dab97aa4f2ac73edc358b4ebf 2022-04-05 12:44:04 From: Matthias Bussonnier Date: 2022-04-05 12:44:04 Subject: [PATCH] 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 --- diff --git a/IPython/core/inputtransformer.py b/IPython/core/inputtransformer.py index f668f46..77f69f3 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 3a56007..a8f676f 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -325,7 +325,7 @@ ESC_PAREN = '/' # Call first argument with rest of line as arguments 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 @@ class HelpEnd(TokenTransformBase): 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 4de97b8..bfc936d 100644 --- a/IPython/core/tests/test_inputtransformer.py +++ b/IPython/core/tests/test_inputtransformer.py @@ -59,108 +59,93 @@ syntax = \ ('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 abc6303..0613dc0 100644 --- a/IPython/core/tests/test_inputtransformer2.py +++ b/IPython/core/tests/test_inputtransformer2.py @@ -14,45 +14,65 @@ import pytest 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 test_side_effects_I(): 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 test_side_effects_II(): 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 b557eb1..24fed4b 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.