Skip to content

Commit 443859d

Browse files
committed
Allow forcing kitty graphics support with an environment variable
Whether the terminal speaks the kitty graphics protocol is guessed by walking up the process tree looking for a known terminal name. The guess can be wrong -- inside tmux, in a container, or for an emulator that is not on the list -- and it is not free: it imports psutil and inspects the process tree on every startup that has a tty. `IPYTHON_KITTY_GRAPHICS=1` or `=0` now states the answer outright, and short-circuits before any of that work. On a tty that takes `import IPython.terminal.ipapp` from 494 to 488 modules, with psutil no longer imported at all. Accepted values are `1`/`true` and `0`/`false`, case-insensitive; unset or empty keeps autodetecting. An unrecognised value warns and falls back to autodetection rather than being treated as false, so that a typo cannot silently disable graphics.
1 parent c41dc87 commit 443859d

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

IPython/core/kitty.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
11
# Implements https://sw.kovidgoyal.net/kitty/graphics-protocol/
22

33
from base64 import b64encode, b64decode
4+
import os
45
import sys
6+
import warnings
7+
8+
#: Set ``IPYTHON_KITTY_GRAPHICS`` to ``1``/``true`` or ``0``/``false`` to state
9+
#: outright whether the terminal speaks the kitty graphics protocol. Unset (or
10+
#: empty) autodetects. Forcing it also skips the detection itself, which walks
11+
#: the process tree and is the reason IPython imports psutil at startup.
12+
_FORCE_ENVVAR = "IPYTHON_KITTY_GRAPHICS"
13+
14+
15+
def _forced_kitty_graphics() -> bool | None:
16+
"""Whether the user has stated support explicitly; None to autodetect."""
17+
value = os.environ.get(_FORCE_ENVVAR)
18+
if value is None or value == "":
19+
return None
20+
if value.lower() in {"1", "true"}:
21+
return True
22+
if value.lower() in {"0", "false"}:
23+
return False
24+
warnings.warn(
25+
f"Ignoring {_FORCE_ENVVAR}={value!r}: expected one of"
26+
" '0', '1', 'false', 'true' or '' (autodetect).",
27+
UserWarning,
28+
stacklevel=2,
29+
)
30+
return None
31+
532

633
def _supports_kitty_graphics() -> bool:
34+
forced = _forced_kitty_graphics()
35+
if forced is not None:
36+
return forced
37+
738
import platform
839

940
if platform.system() not in ("Darwin", "Linux"):
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Forcing kitty graphics support on or off
2+
----------------------------------------
3+
4+
IPython decides whether the terminal understands the `kitty graphics protocol
5+
<https://sw.kovidgoyal.net/kitty/graphics-protocol/>`__ by walking up the
6+
process tree looking for a known terminal emulator. That guess can be wrong --
7+
for instance inside ``tmux``, a container, or an emulator not on the list -- and
8+
it is not free: it imports ``psutil`` and inspects the process tree on every
9+
startup that has a tty.
10+
11+
The ``IPYTHON_KITTY_GRAPHICS`` environment variable now states the answer
12+
outright and skips the detection entirely::
13+
14+
IPYTHON_KITTY_GRAPHICS=1 ipython # my terminal does support it
15+
IPYTHON_KITTY_GRAPHICS=0 ipython # it does not; do not even look
16+
17+
Accepted values are ``1``/``true`` and ``0``/``false``, case-insensitive.
18+
Leaving it unset, or setting it to the empty string, keeps the existing
19+
autodetection. Any other value is ignored with a warning, so a typo cannot
20+
silently turn graphics off.

tests/test_kitty.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import subprocess
22
import sys
3+
4+
import pytest
35
from pathlib import Path
46

57

@@ -82,3 +84,73 @@ def name(self):
8284
monkeypatch.setattr(psutil, "Process", lambda *args, **kwargs: DeniedProcess())
8385

8486
assert kitty._supports_kitty_graphics() is False
87+
88+
89+
def test_kitty_graphics_forced_on(monkeypatch):
90+
"""IPYTHON_KITTY_GRAPHICS=1 states support without probing anything."""
91+
import psutil
92+
93+
from IPython.core import kitty
94+
95+
monkeypatch.setenv("IPYTHON_KITTY_GRAPHICS", "1")
96+
97+
def fail(*args, **kwargs):
98+
raise AssertionError("detection must be skipped when forced")
99+
100+
monkeypatch.setattr(psutil, "Process", fail)
101+
monkeypatch.setattr(sys, "stdout", DummyStdout()) # not even a tty
102+
103+
assert kitty._supports_kitty_graphics() is True
104+
105+
106+
def test_kitty_graphics_forced_off(monkeypatch):
107+
"""IPYTHON_KITTY_GRAPHICS=0 wins over a terminal that does support it."""
108+
import psutil
109+
110+
from IPython.core import kitty
111+
112+
monkeypatch.setenv("IPYTHON_KITTY_GRAPHICS", "0")
113+
114+
def fail(*args, **kwargs):
115+
raise AssertionError("detection must be skipped when forced")
116+
117+
monkeypatch.setattr(psutil, "Process", fail)
118+
monkeypatch.setattr(sys, "stdout", StdoutTTY())
119+
120+
assert kitty._supports_kitty_graphics() is False
121+
122+
123+
@pytest.mark.parametrize("value", ["1", "true", "TRUE"])
124+
def test_kitty_graphics_force_true_spellings(monkeypatch, value):
125+
from IPython.core import kitty
126+
127+
monkeypatch.setenv("IPYTHON_KITTY_GRAPHICS", value)
128+
assert kitty._forced_kitty_graphics() is True
129+
130+
131+
@pytest.mark.parametrize("value", ["0", "false", "FALSE"])
132+
def test_kitty_graphics_force_false_spellings(monkeypatch, value):
133+
from IPython.core import kitty
134+
135+
monkeypatch.setenv("IPYTHON_KITTY_GRAPHICS", value)
136+
assert kitty._forced_kitty_graphics() is False
137+
138+
139+
@pytest.mark.parametrize("value", ["", None])
140+
def test_kitty_graphics_unset_autodetects(monkeypatch, value):
141+
from IPython.core import kitty
142+
143+
if value is None:
144+
monkeypatch.delenv("IPYTHON_KITTY_GRAPHICS", raising=False)
145+
else:
146+
monkeypatch.setenv("IPYTHON_KITTY_GRAPHICS", value)
147+
assert kitty._forced_kitty_graphics() is None
148+
149+
150+
def test_kitty_graphics_bad_value_warns_and_autodetects(monkeypatch):
151+
"""A typo must not silently disable (or enable) graphics."""
152+
from IPython.core import kitty
153+
154+
monkeypatch.setenv("IPYTHON_KITTY_GRAPHICS", "yes-please")
155+
with pytest.warns(UserWarning, match="IPYTHON_KITTY_GRAPHICS"):
156+
assert kitty._forced_kitty_graphics() is None

0 commit comments

Comments
 (0)