@@ -1,28 +1,17 @@
1-from __future__ importannotations
21importio
32importos
43importsys
545+fromcollections.abcimportCallable, Iterator, Mapping
6+fromdataclassesimportdataclass, field, Field
7+68COLORIZE=True
7910+811# types
912ifFalse:
10-fromtypingimportIO, Literal
11-12- type ColorTag=Literal[
13-"PROMPT",
14-"KEYWORD",
15-"BUILTIN",
16-"COMMENT",
17-"STRING",
18-"NUMBER",
19-"OP",
20-"DEFINITION",
21-"SOFT_KEYWORD",
22-"RESET",
23- ]
24-25-theme: dict[ColorTag, str]
13+fromtypingimportIO, Self, ClassVar
14+_theme: Theme
261527162817classANSIColors:
@@ -86,6 +75,186 @@ class ANSIColors:
8675setattr(NoColors, attr, "")
8776887778+#
79+# Experimental theming support (see gh-133346)
80+#
81+82+# - Create a theme by copying an existing `Theme` with one or more sections
83+# replaced, using `default_theme.copy_with()`;
84+# - create a theme section by copying an existing `ThemeSection` with one or
85+# more colors replaced, using for example `default_theme.syntax.copy_with()`;
86+# - create a theme from scratch by instantiating a `Theme` data class with
87+# the required sections (which are also dataclass instances).
88+#
89+# Then call `_colorize.set_theme(your_theme)` to set it.
90+#
91+# Put your theme configuration in $PYTHONSTARTUP for the interactive shell,
92+# or sitecustomize.py in your virtual environment or Python installation for
93+# other uses. Your applications can call `_colorize.set_theme()` too.
94+#
95+# Note that thanks to the dataclasses providing default values for all fields,
96+# creating a new theme or theme section from scratch is possible without
97+# specifying all keys.
98+#
99+# For example, here's a theme that makes punctuation and operators less prominent:
100+#
101+# try:
102+# from _colorize import set_theme, default_theme, Syntax, ANSIColors
103+# except ImportError:
104+# pass
105+# else:
106+# theme_with_dim_operators = default_theme.copy_with(
107+# syntax=Syntax(op=ANSIColors.INTENSE_BLACK),
108+# )
109+# set_theme(theme_with_dim_operators)
110+# del set_theme, default_theme, Syntax, ANSIColors, theme_with_dim_operators
111+#
112+# Guarding the import ensures that your .pythonstartup file will still work in
113+# Python 3.13 and older. Deleting the variables ensures they don't remain in your
114+# interactive shell's global scope.
115+116+classThemeSection(Mapping[str, str]):
117+"""A mixin/base class for theme sections.
118+119+ It enables dictionary access to a section, as well as implements convenience
120+ methods.
121+ """
122+123+# The two types below are just that: types to inform the type checker that the
124+# mixin will work in context of those fields existing
125+__dataclass_fields__: ClassVar[dict[str, Field[str]]]
126+_name_to_value: Callable[[str], str]
127+128+def__post_init__(self) ->None:
129+name_to_value= {}
130+forcolor_nameinself.__dataclass_fields__:
131+name_to_value[color_name] =getattr(self, color_name)
132+super().__setattr__('_name_to_value', name_to_value.__getitem__)
133+134+defcopy_with(self, **kwargs: str) ->Self:
135+color_state: dict[str, str] = {}
136+forcolor_nameinself.__dataclass_fields__:
137+color_state[color_name] =getattr(self, color_name)
138+color_state.update(kwargs)
139+returntype(self)(**color_state)
140+141+@classmethod
142+defno_colors(cls) ->Self:
143+color_state: dict[str, str] = {}
144+forcolor_nameincls.__dataclass_fields__:
145+color_state[color_name] =""
146+returncls(**color_state)
147+148+def__getitem__(self, key: str) ->str:
149+returnself._name_to_value(key)
150+151+def__len__(self) ->int:
152+returnlen(self.__dataclass_fields__)
153+154+def__iter__(self) ->Iterator[str]:
155+returniter(self.__dataclass_fields__)
156+157+158+@dataclass(frozen=True)
159+classArgparse(ThemeSection):
160+usage: str=ANSIColors.BOLD_BLUE
161+prog: str=ANSIColors.BOLD_MAGENTA
162+prog_extra: str=ANSIColors.MAGENTA
163+heading: str=ANSIColors.BOLD_BLUE
164+summary_long_option: str=ANSIColors.CYAN
165+summary_short_option: str=ANSIColors.GREEN
166+summary_label: str=ANSIColors.YELLOW
167+summary_action: str=ANSIColors.GREEN
168+long_option: str=ANSIColors.BOLD_CYAN
169+short_option: str=ANSIColors.BOLD_GREEN
170+label: str=ANSIColors.BOLD_YELLOW
171+action: str=ANSIColors.BOLD_GREEN
172+reset: str=ANSIColors.RESET
173+174+175+@dataclass(frozen=True)
176+classSyntax(ThemeSection):
177+prompt: str=ANSIColors.BOLD_MAGENTA
178+keyword: str=ANSIColors.BOLD_BLUE
179+builtin: str=ANSIColors.CYAN
180+comment: str=ANSIColors.RED
181+string: str=ANSIColors.GREEN
182+number: str=ANSIColors.YELLOW
183+op: str=ANSIColors.RESET
184+definition: str=ANSIColors.BOLD
185+soft_keyword: str=ANSIColors.BOLD_BLUE
186+reset: str=ANSIColors.RESET
187+188+189+@dataclass(frozen=True)
190+classTraceback(ThemeSection):
191+type: str=ANSIColors.BOLD_MAGENTA
192+message: str=ANSIColors.MAGENTA
193+filename: str=ANSIColors.MAGENTA
194+line_no: str=ANSIColors.MAGENTA
195+frame: str=ANSIColors.MAGENTA
196+error_highlight: str=ANSIColors.BOLD_RED
197+error_range: str=ANSIColors.RED
198+reset: str=ANSIColors.RESET
199+200+201+@dataclass(frozen=True)
202+classUnittest(ThemeSection):
203+passed: str=ANSIColors.GREEN
204+warn: str=ANSIColors.YELLOW
205+fail: str=ANSIColors.RED
206+fail_info: str=ANSIColors.BOLD_RED
207+reset: str=ANSIColors.RESET
208+209+210+@dataclass(frozen=True)
211+classTheme:
212+"""A suite of themes for all sections of Python.
213+214+ When adding a new one, remember to also modify `copy_with` and `no_colors`
215+ below.
216+ """
217+argparse: Argparse=field(default_factory=Argparse)
218+syntax: Syntax=field(default_factory=Syntax)
219+traceback: Traceback=field(default_factory=Traceback)
220+unittest: Unittest=field(default_factory=Unittest)
221+222+defcopy_with(
223+self,
224+*,
225+argparse: Argparse|None=None,
226+syntax: Syntax|None=None,
227+traceback: Traceback|None=None,
228+unittest: Unittest|None=None,
229+ ) ->Self:
230+"""Return a new Theme based on this instance with some sections replaced.
231+232+ Themes are immutable to protect against accidental modifications that
233+ could lead to invalid terminal states.
234+ """
235+returntype(self)(
236+argparse=argparseorself.argparse,
237+syntax=syntaxorself.syntax,
238+traceback=tracebackorself.traceback,
239+unittest=unittestorself.unittest,
240+ )
241+242+@classmethod
243+defno_colors(cls) ->Self:
244+"""Return a new Theme where colors in all sections are empty strings.
245+246+ This allows writing user code as if colors are always used. The color
247+ fields will be ANSI color code strings when colorization is desired
248+ and possible, and empty strings otherwise.
249+ """
250+returncls(
251+argparse=Argparse.no_colors(),
252+syntax=Syntax.no_colors(),
253+traceback=Traceback.no_colors(),
254+unittest=Unittest.no_colors(),
255+ )
256+257+89258defget_colors(
90259colorize: bool=False, *, file: IO[str] |IO[bytes] |None=None
91260) ->ANSIColors:
@@ -138,26 +307,40 @@ def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
138307returnhasattr(file, "isatty") andfile.isatty()
139308140309141-defset_theme(t: dict[ColorTag, str] |None=None) ->None:
142-globaltheme
310+default_theme=Theme()
311+theme_no_color=default_theme.no_colors()
312+313+314+defget_theme(
315+*,
316+tty_file: IO[str] |IO[bytes] |None=None,
317+force_color: bool=False,
318+force_no_color: bool=False,
319+) ->Theme:
320+"""Returns the currently set theme, potentially in a zero-color variant.
321+322+ In cases where colorizing is not possible (see `can_colorize`), the returned
323+ theme contains all empty strings in all color definitions.
324+ See `Theme.no_colors()` for more information.
325+326+ It is recommended not to cache the result of this function for extended
327+ periods of time because the user might influence theme selection by
328+ the interactive shell, a debugger, or application-specific code. The
329+ environment (including environment variable state and console configuration
330+ on Windows) can also change in the course of the application life cycle.
331+ """
332+ifforce_coloror (notforce_no_colorandcan_colorize(file=tty_file)):
333+return_theme
334+returntheme_no_color
335+336+337+defset_theme(t: Theme) ->None:
338+global_theme
143339144-ift:
145-theme=t
146-return
340+ifnotisinstance(t, Theme):
341+raiseValueError(f"Expected Theme object, found {t}")
147342148-colors=get_colors()
149-theme= {
150-"PROMPT": colors.BOLD_MAGENTA,
151-"KEYWORD": colors.BOLD_BLUE,
152-"BUILTIN": colors.CYAN,
153-"COMMENT": colors.RED,
154-"STRING": colors.GREEN,
155-"NUMBER": colors.YELLOW,
156-"OP": colors.RESET,
157-"DEFINITION": colors.BOLD,
158-"SOFT_KEYWORD": colors.BOLD_BLUE,
159-"RESET": colors.RESET,
160- }
343+_theme=t
161344162345163-set_theme()
346+set_theme(default_theme)