diff --git a/IPython/__init__.py b/IPython/__init__.py index a42caad7438..c2c800aa2c9 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -80,6 +80,11 @@ def embed_kernel(module=None, local_ns=None, **kwargs): and/or you want to load full IPython configuration, you probably want `IPython.start_kernel()` instead. + This is a deprecated alias for `ipykernel.embed.embed_kernel()`, + to be removed in the future. + You should import directly from `ipykernel.embed`; this wrapper + fails anyway if you don't have `ipykernel` package installed. + Parameters ---------- module : types.ModuleType, optional diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 4f3405e2f28..811422f2c8e 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3152,14 +3152,17 @@ def _run_cell( try: result = runner(coro) except BaseException as e: - info = ExecutionInfo( - raw_cell, store_history, silent, shell_futures, cell_id - ) - result = ExecutionResult(info) - result.error_in_exec = e - self.showtraceback(running_compiled_code=True) - finally: - return result + try: + info = ExecutionInfo( + raw_cell, store_history, silent, shell_futures, cell_id + ) + result = ExecutionResult(info) + result.error_in_exec = e + self.showtraceback(running_compiled_code=True) + except: + pass + + return result def should_run_async( self, raw_cell: str, *, transformed_cell=None, preprocessing_exc_tuple=None diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index a90cf14e46e..faa2ff8d44b 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -160,10 +160,11 @@ def visit_For(self, node): class Timer(timeit.Timer): """Timer class that explicitly uses self.inner - + which is an undocumented implementation detail of CPython, not shared by PyPy. """ + # Timer.timeit copied from CPython 3.4.2 def timeit(self, number=timeit.default_number): """Time 'number' executions of the main statement. @@ -201,7 +202,6 @@ def __init__(self, shell): @no_var_expand @line_cell_magic def prun(self, parameter_s='', cell=None): - """Run a statement through the python code profiler. **Usage, in line mode**:: @@ -1000,7 +1000,7 @@ def _run_with_debugger( # Stop iteration is raised on quit command pass - except: + except Exception: etype, value, tb = sys.exc_info() # Skip three frames in the traceback: the %run one, # one inside bdb.py, and the command-line typed by the @@ -1142,7 +1142,7 @@ def timeit(self, line='', cell=None, local_ns=None): ) if stmt == "" and cell is None: return - + timefunc = timeit.default_timer number = int(getattr(opts, "n", 0)) default_repeat = 7 if timeit.default_repeat < 7 else timeit.default_repeat @@ -1262,7 +1262,7 @@ def timeit(self, line='', cell=None, local_ns=None): @needs_local_scope @line_cell_magic @output_can_be_silenced - def time(self,line='', cell=None, local_ns=None): + def time(self, line="", cell=None, local_ns=None): """Time execution of a Python statement or expression. The CPU and wall clock times are printed, and the value of the @@ -1277,13 +1277,19 @@ def time(self,line='', cell=None, local_ns=None): - In cell mode, you can time the cell body (a directly following statement raises an error). - This function provides very basic timing functionality. Use the timeit + This function provides very basic timing functionality. Use the timeit magic for more control over the measurement. .. versionchanged:: 7.3 User variables are no longer expanded, the magic line is always left unmodified. + .. versionchanged:: 8.3 + The time magic now correctly propagates system-exiting exceptions + (such as ``KeyboardInterrupt`` invoked when interrupting execution) + rather than just printing out the exception traceback. + The non-system-exception will still be caught as before. + Examples -------- :: @@ -1324,10 +1330,10 @@ def time(self,line='', cell=None, local_ns=None): Compiler : 0.78 s """ # fail immediately if the given expression can't be compiled - + if line and cell: raise UsageError("Can't use statement directly after '%%time'!") - + if cell: expr = self.shell.transform_cell(cell) else: @@ -1338,7 +1344,7 @@ def time(self,line='', cell=None, local_ns=None): t0 = clock() expr_ast = self.shell.compile.ast_parse(expr) - tp = clock()-t0 + tp = clock() - t0 # Apply AST transformations expr_ast = self.shell.transform_ast(expr_ast) @@ -1346,8 +1352,8 @@ def time(self,line='', cell=None, local_ns=None): # Minimum time above which compilation time will be reported tc_min = 0.1 - expr_val=None - if len(expr_ast.body)==1 and isinstance(expr_ast.body[0], ast.Expr): + expr_val = None + if len(expr_ast.body) == 1 and isinstance(expr_ast.body[0], ast.Expr): mode = 'eval' source = '' expr_ast = ast.Expression(expr_ast.body[0].value) @@ -1356,25 +1362,25 @@ def time(self,line='', cell=None, local_ns=None): source = '' # multi-line %%time case if len(expr_ast.body) > 1 and isinstance(expr_ast.body[-1], ast.Expr): - expr_val= expr_ast.body[-1] + expr_val = expr_ast.body[-1] expr_ast = expr_ast.body[:-1] expr_ast = Module(expr_ast, []) expr_val = ast.Expression(expr_val.value) t0 = clock() code = self.shell.compile(expr_ast, source, mode) - tc = clock()-t0 + tc = clock() - t0 # skew measurement as little as possible glob = self.shell.user_ns wtime = time.time # time execution wall_st = wtime() - if mode=='eval': + if mode == "eval": st = clock2() try: out = eval(code, glob, local_ns) - except: + except Exception: self.shell.showtraceback() return end = clock2() @@ -1382,12 +1388,12 @@ def time(self,line='', cell=None, local_ns=None): st = clock2() try: exec(code, glob, local_ns) - out=None + out = None # multi-line %%time case if expr_val is not None: code_2 = self.shell.compile(expr_val, source, 'eval') out = eval(code_2, glob, local_ns) - except: + except Exception: self.shell.showtraceback() return end = clock2() @@ -1630,14 +1636,15 @@ def parse_breakpoint(text, current_file): return current_file, int(text) else: return text[:colon], int(text[colon+1:]) - + + def _format_time(timespan, precision=3): """Formats the timespan in a human readable form""" if timespan >= 60.0: # we have more than a minute, format that in a human readable form # Idea from http://snipplr.com/view/5713/ - parts = [("d", 60*60*24),("h", 60*60),("min", 60), ("s", 1)] + parts = [("d", 60 * 60 * 24), ("h", 60 * 60), ("min", 60), ("s", 1)] time = [] leftover = timespan for suffix, length in parts: @@ -1649,7 +1656,6 @@ def _format_time(timespan, precision=3): break return " ".join(time) - # Unfortunately characters outside of range(128) can cause problems in # certain terminals. # See bug: https://bugs.launchpad.net/ipython/+bug/348466 @@ -1663,7 +1669,7 @@ def _format_time(timespan, precision=3): except: pass scaling = [1, 1e3, 1e6, 1e9] - + if timespan > 0.0: order = min(-int(math.floor(math.log10(timespan)) // 3), 3) else: diff --git a/IPython/core/release.py b/IPython/core/release.py index fc8430499cb..d2b7efcaac4 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,7 +16,7 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 9 -_version_minor = 1 +_version_minor = 2 _version_patch = 0 _version_extra = ".dev" # _version_extra = "b2" diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index df637ccd3cb..fa6bf5d75bd 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -174,7 +174,7 @@ class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): # This is the instance of the LLM provider from jupyter-ai to which we forward the request # to generate inline completions. _llm_provider: Any | None - _llm_prefixer: callable = lambda self, x: "wrong" + _llm_prefixer: Callable = lambda self, x: "wrong" def __init__(self): super().__init__() @@ -325,7 +325,6 @@ async def _trigger_llm(self, buffer) -> None: """ # we likely want to store the current cursor position, and cancel if the cursor has moved. try: - import jupyter_ai_magics import jupyter_ai.completions.models as jai_models except ModuleNotFoundError: jai_models = None @@ -333,9 +332,7 @@ async def _trigger_llm(self, buffer) -> None: warnings.warn("No LLM provider found, cannot trigger LLM completions") return if jai_models is None: - warnings.warn( - "LLM Completion requires `jupyter_ai_magics` and `jupyter_ai` to be installed" - ) + warnings.warn("LLM Completion requires `jupyter_ai` to be installed") self._cancel_running_llm_task() @@ -365,7 +362,7 @@ async def _trigger_llm_core(self, buffer: Buffer): Unlike with JupyterAi, as we do not have multiple cell, the cell id is always set to `None`. - We set the prefix to the current cell content, but could also inset the + We set the prefix to the current cell content, but could also insert the rest of the history or even just the non-fail history. In the same way, we do not have cell id. @@ -378,21 +375,27 @@ async def _trigger_llm_core(self, buffer: Buffer): providers. """ try: - import jupyter_ai_magics import jupyter_ai.completions.models as jai_models except ModuleNotFoundError: jai_models = None + if not jai_models: + raise ValueError("jupyter-ai is not installed") + + if not self._llm_provider: + raise ValueError("No LLM provider found, cannot trigger LLM completions") + hm = buffer.history.shell.history_manager prefix = self._llm_prefixer(hm) get_ipython().log.debug("prefix: %s", prefix) self._request_number += 1 request_number = self._request_number + request = jai_models.InlineCompletionRequest( number=request_number, - prefix=prefix + buffer.document.text, - suffix="", + prefix=prefix + buffer.document.text_before_cursor, + suffix=buffer.document.text_after_cursor, mime="text/x-python", stream=True, path=None, @@ -438,7 +441,7 @@ async def llm_autosuggestion(event: KeyPressEvent): doc = event.current_buffer.document lines_to_insert = max(0, _MIN_LINES - doc.line_count + doc.cursor_position_row) for _ in range(lines_to_insert): - event.current_buffer.insert_text("\n", move_cursor=False) + event.current_buffer.insert_text("\n", move_cursor=False, fire_event=False) await provider._trigger_llm(event.current_buffer) diff --git a/docs/source/interactive/reference.rst b/docs/source/interactive/reference.rst index b0629d1c08f..4fb00142cc6 100644 --- a/docs/source/interactive/reference.rst +++ b/docs/source/interactive/reference.rst @@ -700,9 +700,14 @@ your Python programs for this to work (detailed examples follow later):: embed() # this call anywhere in your program will start IPython -You can also embed an IPython *kernel*, for use with qtconsole, etc. via -``IPython.embed_kernel()``. This should work the same way, but you can -connect an external frontend (``ipython qtconsole`` or ``ipython console``), +You can also embed an IPython *kernel*, for use with qtconsole, etc. via:: + + from ipykernel.embed import embed_kernel + + embed_kernel() + +This should work the same way, but you can connect an external frontend +(``ipython qtconsole`` or ``ipython console``), rather than interacting with it in the terminal. You can run embedded instances even in code which is itself being run at diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index d0962055732..99d6376f290 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,54 @@ 8.x Series ============ +.. _version 8.36: + +IPython 8.36 +============ + +This is a small release with minor changes in the context passed to the LLM completion +provider and a fix for interruption of execution magics: + +- :ghpull:`14890` Fixed interruption of ``%%time`` and ``%%debug`` magics +- :ghpull:`14877` Removed spurious empty lines from ``prefix`` passed to LLM, and separated part after cursor into the ``suffix`` + +.. _version 8.35: + +IPython 8.35 +============ + +This small early April release includes a few backports of bug fixes for tab and LLM completions: + +- :ghpull:`14838` Fixed tab-completion of global variables in lines with a dot when jedi is off +- :ghpull:`14846` Fixed LLM request number always being set to zero and removed spurious logging +- :ghpull:`14851` Passes current input history to LLMs + + +.. _version 8.34: + +IPython 8.34 +============ + +This tiny beginning of March release included two bug fixes: + +- :ghpull:`14823` Fixed right arrow incorrectly accepting invisible auto-suggestions +- :ghpull:`14828` Fixed Qt backend crash + +along with a backport of improved documentation and configurability of LLM completions. + +.. _version 8.33: + +IPython 8.33 +============ + +This small end of February release included a few backports of bug fixes and minor enhancements: + +- :ghpull:`14717` Fixed auto-suggestion on Prompt Toolkit < 3.0.49 +- :ghpull:`14738` Fixed Python 3.13 compatibility of ``local_ns`` +- :ghpull:`14700` Improved Qt object management and performance +- :ghpull:`14790` Better documentation and configurability of LLM completions + + .. _version 8.32: IPython 8.32 diff --git a/docs/source/whatsnew/version9.rst b/docs/source/whatsnew/version9.rst index 93eb60b378d..5f12cbfa948 100644 --- a/docs/source/whatsnew/version9.rst +++ b/docs/source/whatsnew/version9.rst @@ -2,6 +2,19 @@ 9.x Series ============ +.. _version92: + +IPython 9.2 +=========== + +This is a small release with minor changes in the context passed to the LLM completion +provider along few other bug fixes and documentation improvements: + +- :ghpull:`14890` Fixed interruption of ``%%time`` and ``%%debug`` magics +- :ghpull:`14877` Removed spurious empty lines from ``prefix`` passed to LLM, and separated part after cursor into the ``suffix`` +- :ghpull:`14876` Fixed syntax warning in Python 3.14 (remove return from finally block) +- :ghpull:`14887` Documented the recommendation to use ``ipykernel.embed.embed_kernel()`` over ``ipython.embed``. + .. _version91: IPython 9.1 diff --git a/tests/fake_llm.py b/tests/fake_llm.py index cac5882598c..df3428e20b9 100644 --- a/tests/fake_llm.py +++ b/tests/fake_llm.py @@ -42,7 +42,7 @@ async def stream_inline_completions(self, request): assert request.number > 0 token = f"t{request.number}s0" - last_line = request.prefix.rstrip("\n").splitlines()[-1] + last_line = request.prefix.splitlines()[-1] if not FIBONACCI.startswith(last_line): return diff --git a/tests/test_magic.py b/tests/test_magic.py index 2cb99056819..14dfef5dcc0 100644 --- a/tests/test_magic.py +++ b/tests/test_magic.py @@ -16,8 +16,6 @@ from threading import Thread from subprocess import CalledProcessError from textwrap import dedent -from time import sleep -from threading import Thread from unittest import TestCase, mock import pytest @@ -854,6 +852,16 @@ def test_timeit_invalid_return(): _ip.run_line_magic("timeit", "return") +def test_timeit_raise_on_interrupt(): + ip = get_ipython() + + with pytest.raises(KeyboardInterrupt): + thread = Thread(target=_interrupt_after_1s) + thread.start() + ip.run_cell_magic("timeit", "", "from time import sleep; sleep(2)") + thread.join() + + @dec.skipif(execution.profile is None) def test_prun_special_syntax(): "Test %%prun with IPython special syntax" @@ -1680,6 +1688,16 @@ def test_timeit_arguments(): _ip.run_line_magic("timeit", "-n1 -r1 a=('#')") +def test_time_raise_on_interrupt(): + ip = get_ipython() + + with pytest.raises(KeyboardInterrupt): + thread = Thread(target=_interrupt_after_1s) + thread.start() + ip.run_cell_magic("time", "", "from time import sleep; sleep(2)") + thread.join() + + MINIMAL_LAZY_MAGIC = """ from IPython.core.magic import ( Magics, diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py index 40d1e3e1d17..54296ac171f 100644 --- a/tests/test_shortcuts.py +++ b/tests/test_shortcuts.py @@ -1,4 +1,5 @@ import pytest +from IPython.terminal.interactiveshell import PtkHistoryAdapter from IPython.terminal.shortcuts.auto_suggest import ( accept, accept_or_jump_to_end, @@ -39,7 +40,7 @@ def make_event(text, cursor, suggestion): try: from .fake_llm import FIBONACCI except ImportError: - FIBONACCI = None + FIBONACCI = "" @dec.skip_without("jupyter_ai") @@ -49,9 +50,13 @@ async def test_llm_autosuggestion(): ip = get_ipython() ip.auto_suggest = provider ip.llm_provider_class = "tests.fake_llm.FibonacciCompletionProvider" + ip.history_manager.get_range = Mock(return_value=[]) text = "def fib" - event = make_event(text, len(text), "") - event.current_buffer.history.shell.history_manager.get_range = Mock(return_value=[]) + event = Mock() + event.current_buffer = Buffer( + history=PtkHistoryAdapter(ip), + ) + event.current_buffer.insert_text(text, move_cursor=True) await llm_autosuggestion(event) assert event.current_buffer.suggestion.text == FIBONACCI[len(text) :] diff --git a/tools/release_helper.sh b/tools/release_helper.sh index 5b625b240e8..47171397be7 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -72,8 +72,8 @@ maybe_edit(){ fi echo - if [ $value = 'e' ] ; then - $=EDITOR $1 + if [ "$value" = 'e' ] ; then + $EDITOR $1 fi }