diff --git a/rich/jupyter.py b/rich/jupyter.py index 24135a9f7..bae8016c9 100644 --- a/rich/jupyter.py +++ b/rich/jupyter.py @@ -83,6 +83,12 @@ def escape(text: str) -> str: def display(segments: Iterable[Segment], text: str) -> None: """Render segments to Jupyter.""" + segments = list(segments) + if not segments and not text: + # display() always prints a newline, so if there is no content then + # we don't want a single newline appearing. + # See https://github.com/Textualize/rich/issues/3274 + return None html = _render_segments(segments) jupyter_renderable = JupyterRenderable(html, text) try: diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index 7c327e622..067f2d8ac 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -30,3 +30,22 @@ def test_jupyter_lines_env(): console = Console( width=40, _environ={"JUPYTER_LINES": "broken"}, force_jupyter=True ) + + +def test_jupyter_capture(monkeypatch): + # If inside a capture, ipython's display shouldn't be called, + # or we would get spurious newlines. + # See https://github.com/Textualize/rich/issues/3274 + called = False + + def mock_display(*args, **kwargs): + nonlocal called + called = True + + monkeypatch.setattr("IPython.display.display", mock_display) + console = Console(force_jupyter=True) + with console.capture(): + console.print("foo") + assert not called + console.print("foo") + assert called