gh-133346: Make theming support in _colorize extensible (GH-133347) · python/cpython@f610bbd

GitHub

@@ -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)