Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions IPython/core/kitty.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

from base64 import b64encode, b64decode
from collections.abc import Iterator
import logging
import os
import sys
import warnings

#: Module logger ``IPython.core.kitty``
#: We don't have a global logger so let's use a module leveled one.
_log = logging.getLogger(__name__)

#: Set ``IPYTHON_KITTY_GRAPHICS`` to ``1``/``true`` or ``0``/``false`` to state
#: outright whether the terminal speaks the kitty graphics protocol. Unset (or
#: empty) autodetects. Forcing it also skips the detection itself, which walks
Expand Down Expand Up @@ -71,7 +76,13 @@ def _proc_ancestor_names() -> Iterator[str]:

def _psutil_ancestor_names() -> Iterator[str]:
"""Yield ancestor process names, nearest first, using psutil."""
import psutil
try:
import psutil
except ImportError as exc_import:
_log.error(
"Cannot import `psutil`(%s). Image rendering is disabled.", exc_import
)
return

try:
process = psutil.Process()
Expand All @@ -97,7 +108,7 @@ def _ancestor_process_names() -> Iterator[str]:
Everywhere else -- macOS, or a Linux without ``/proc`` -- fall back to
psutil, which IPython depends on anyway.
"""
if sys.platform == "linux" and os.path.isdir("/proc/self"):
if sys.platform in ("linux", "android") and os.path.isdir("/proc/self"):
yield from _proc_ancestor_names()
else:
yield from _psutil_ancestor_names()
Expand All @@ -108,7 +119,13 @@ def _supports_kitty_graphics() -> bool:
if forced is not None:
return forced

if sys.platform not in ("darwin", "linux"):
# Platforms including "android" can actually run shells like kitty and
# render images. See also
# https://github.com/ipython/ipython/issues/15422 .
# Thus, instead of a whitelist, let's build a blacklist.
# Make the linter happy. A tuple includes platform names should be placed
# here.
if sys.platform == "win32":
return False

isatty = getattr(sys.stdout, "isatty", None)
Expand All @@ -130,9 +147,6 @@ def _supports_kitty_graphics() -> bool:
return any(name in supported_terminals for name in _ancestor_process_names())


supports_kitty_graphics = _supports_kitty_graphics()


def png_to_kitty_ansi(png: bytes) -> str:
if not png.startswith(b"\x89PNG\r\n\x1a\n"):
raise ValueError
Expand Down Expand Up @@ -162,9 +176,11 @@ def kitty_png_render(png: bytes | str, _md_dict: object) -> None:

display_formatter_default_active_types = [
"text/plain",
*(["image/png"] if supports_kitty_graphics else []),
]

terminal_default_mime_renderers = {
"image/png": kitty_png_render,
}

if _supports_kitty_graphics():
display_formatter_default_active_types.append("image/png")
terminal_default_mime_renderers["image/png"] = kitty_png_render
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"matplotlib-inline>=0.1.6",
'pexpect>4.6; sys_platform != "win32" and sys_platform != "emscripten"',
"prompt_toolkit>=3.0.41,<3.1.0",
'psutil>=7; sys_platform != "emscripten" and sys_platform != "cygwin"',
'psutil>=7; sys_platform != "emscripten" and sys_platform != "cygwin" and sys_platform != "android"',
"pygments>=2.14.0", # wheel
"stack_data>=0.6.0",
"traitlets>=5.13.0",
Expand Down
44 changes: 44 additions & 0 deletions tests/test_kitty.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,47 @@ def isatty(self):
env=env,
)
assert result.returncode == 0, result.stderr


def test_ancestor_process_names_on_android_reads_proc(monkeypatch):
"""android joins linux on the /proc walk, out of psutil's reach."""
from IPython.core import kitty

monkeypatch.setattr(sys, "platform", "android")
monkeypatch.setattr(os.path, "isdir", lambda path: path == "/proc/self")
monkeypatch.setattr(os, "getppid", lambda: 42)

def no_psutil():
raise AssertionError("android with /proc must not fall back to psutil")

monkeypatch.setattr(kitty, "_psutil_ancestor_names", no_psutil)
monkeypatch.setattr(
kitty,
"_read_proc_stat",
_fake_proc({42: ("bash", 7), 7: ("kitty", 1), 1: ("init", 0)}),
)
assert list(kitty._ancestor_process_names()) == ["bash", "kitty", "init"]


@pytest.mark.parametrize(
"platform, expected",
[
("win32", False),
("android", True),
("freebsd", True),
],
)
def test_supports_kitty_graphics_platform_gate(monkeypatch, platform, expected):
"""Only platforms known not to speak the protocol are ruled out.

Detection used to whitelist darwin/linux, which locked out Termux and
friends (ipython/ipython#15422); it is a win32 blacklist now.
"""
from IPython.core import kitty

monkeypatch.delenv("IPYTHON_KITTY_GRAPHICS", raising=False)
monkeypatch.setattr(sys, "platform", platform)
monkeypatch.setattr(sys, "stdout", StdoutTTY())
monkeypatch.setattr(kitty, "_ancestor_process_names", lambda: iter(["kitty"]))

assert kitty._supports_kitty_graphics() is expected
Loading