Skip to content

Commit 2186613

Browse files
committed
Keep the pygments plugin machinery off the startup path
Resolving a theme's base style went through `pygments.styles`, whose module body imports `pygments.plugin` and with it `importlib.metadata`, `email` and `zipfile` -- about 10ms and 36 modules on every `ipython` start, all of it so that third party *style plugins* can be found by name. No theme IPython ships needs that. Both call sites only ever want the base style's `styles` mapping, so add `PyColorize._pygments_base_styles`, which for the handful of builtin styles IPython's own themes are based on executes the single `pygments/styles/*.py` module that defines them and reads the mapping out of it. Anything else -- an unknown name, or a style that has moved within pygments -- still goes through `pygments.styles.get_style_by_name`, plugins included, so this is a shortcut and never the source of truth. The style module is deliberately not registered in `sys.modules`: it is pure data, and registering a submodule of a package that is not itself imported breaks later `import pygments.styles.<mod>` statements.
1 parent dd41c52 commit 2186613

2 files changed

Lines changed: 78 additions & 17 deletions

File tree

IPython/terminal/interactiveshell.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
display_formatter_default_active_types,
1212
terminal_default_mime_renderers,
1313
)
14-
from IPython.utils.PyColorize import theme_table
14+
from IPython.utils.PyColorize import _pygments_base_styles, theme_table
1515
from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
1616
from IPython.utils.process import abbrev_cwd
1717
from traitlets import (
@@ -44,7 +44,7 @@
4444
from prompt_toolkit.patch_stdout import patch_stdout
4545
from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
4646
from prompt_toolkit.styles import DynamicStyle, merge_styles
47-
from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
47+
from prompt_toolkit.styles.pygments import style_from_pygments_dict
4848
from pygments.style import Style
4949

5050
from .magics import TerminalMagics
@@ -841,24 +841,19 @@ def _make_style_from_name_or_cls(self, name_or_cls):
841841
theme = theme_table.get(legacy, None)
842842
assert theme is not None, legacy
843843

844-
# `pygments.styles` drags in the pygments plugin machinery
845-
# (importlib.metadata -> zipfile -> shutil); it is only needed to
846-
# resolve a theme's base style, which happens once, here.
847-
from pygments.styles import get_style_by_name
848-
849844
if legacy == "nocolor":
850845
style_overrides = {}
851-
style_cls = _NoStyle
846+
base_styles = _NoStyle.styles
852847
else:
853848
style_overrides = {**theme.extra_style, **self.highlighting_style_overrides}
854849
if theme.base is not None:
855-
style_cls = get_style_by_name(theme.base)
850+
base_styles = _pygments_base_styles(theme.base)
856851
else:
857-
style_cls = _NoStyle
852+
base_styles = _NoStyle.styles
858853

859854
style = merge_styles(
860855
[
861-
style_from_pygments_cls(style_cls),
856+
style_from_pygments_dict(base_styles),
862857
style_from_pygments_dict(style_overrides),
863858
]
864859
)

IPython/utils/PyColorize.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,77 @@
2727
__all__ = ["Parser", "Theme"]
2828

2929

30+
# Which ``pygments/styles/*.py`` module (and class in it) defines each of the
31+
# builtin pygments styles the themes IPython ships use as a `Theme.base`.
32+
#
33+
# This is only a shortcut, never the source of truth: any name missing from
34+
# here, and any entry that no longer resolves, falls back to pygments' own
35+
# `get_style_by_name`, plugins and all. See `_pygments_base_styles`.
36+
_BUILTIN_PYGMENTS_STYLES: dict[str, tuple[str, str]] = {
37+
"default": ("default", "DefaultStyle"),
38+
"gruvbox-dark": ("gruvbox", "GruvboxDarkStyle"),
39+
"monokai": ("monokai", "MonokaiStyle"),
40+
"pastie": ("pastie", "PastieStyle"),
41+
}
42+
43+
44+
def _exec_pygments_style_module(module: str, class_name: str) -> Any | None:
45+
"""Read one style's ``styles`` mapping out of ``pygments/styles/<module>.py``.
46+
47+
Returns None if pygments is not laid out as expected, leaving it to the
48+
caller to fall back to `pygments.styles.get_style_by_name`.
49+
50+
The module is executed in isolation and deliberately *not* registered in
51+
`sys.modules`: registering a submodule of a package that is not itself
52+
imported breaks later ``import pygments.styles.<module>`` statements, and
53+
keeping `pygments.styles` unimported is the entire point. Style modules
54+
are pure data, so executing one twice is harmless, and nothing but the
55+
``styles`` dict escapes this function.
56+
"""
57+
from importlib.machinery import PathFinder
58+
from importlib.util import module_from_spec
59+
60+
package = PathFinder.find_spec("pygments.styles", list(pygments.__path__))
61+
if package is None or package.submodule_search_locations is None:
62+
return None
63+
spec = PathFinder.find_spec(
64+
f"pygments.styles.{module}", list(package.submodule_search_locations)
65+
)
66+
if spec is None or spec.loader is None:
67+
return None
68+
style_module = module_from_spec(spec)
69+
try:
70+
spec.loader.exec_module(style_module)
71+
return getattr(style_module, class_name).styles
72+
except Exception:
73+
return None
74+
75+
76+
def _pygments_base_styles(name: str) -> Any:
77+
"""Return the token -> style-string mapping of a pygments style, by name.
78+
79+
Equivalent to ``pygments.styles.get_style_by_name(name).styles``, which is
80+
all IPython ever wants from a base style, but able to answer for the
81+
handful of builtin styles IPython's own themes are based on without
82+
importing `pygments.styles`.
83+
84+
Importing that package -- which importing any of its submodules does too --
85+
runs `pygments.plugin`, and with it `importlib.metadata` and `email`:
86+
roughly 10ms whose only purpose is to make third party *style plugins*
87+
findable by name. No theme IPython ships needs that, and this is on the
88+
startup path.
89+
"""
90+
target = _BUILTIN_PYGMENTS_STYLES.get(name)
91+
if target is not None:
92+
styles = _exec_pygments_style_module(*target)
93+
if styles is not None:
94+
return styles
95+
96+
from pygments.styles import get_style_by_name
97+
98+
return get_style_by_name(name).styles
99+
100+
30101
class Symbols(TypedDict):
31102
top_line: str
32103
arrow_body: str
@@ -63,12 +134,7 @@ def __init__(
63134
@cache
64135
def as_pygments_style(self) -> type[Style]:
65136
if self.base is not None:
66-
# `pygments.styles` pulls in the pygments plugin machinery, and
67-
# with it `importlib.metadata`, `zipfile` and `shutil`; keep that
68-
# off the import path of everything that merely wants a Theme.
69-
from pygments.styles import get_style_by_name
70-
71-
base_styles = get_style_by_name(self.base).styles
137+
base_styles = _pygments_base_styles(self.base)
72138
else:
73139
base_styles = {}
74140

0 commit comments

Comments
 (0)