Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf610bbd

Browse files
ambvhugovk
andauthored
gh-133346: Make theming support in _colorize extensible (GH-133347)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent9cc77aa commitf610bbd

20 files changed

+581
-367
lines changed

‎Doc/whatsnew/3.14.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1466,7 +1466,7 @@ pdb
14661466
* Source code displayed in:mod:`pdb` will be syntax-highlighted. This feature
14671467
can be controlled using the same methods as PyREPL, in addition to the newly
14681468
added ``colorize`` argument of:class:`pdb.Pdb`.
1469-
(Contributed by Tian Gao in:gh:`133355`.)
1469+
(Contributed by Tian Gaoand Łukasz Langain:gh:`133355`.)
14701470

14711471

14721472
pickle

‎Lib/_colorize.py

Lines changed: 219 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
1-
from __future__importannotations
21
importio
32
importos
43
importsys
54

5+
fromcollections.abcimportCallable,Iterator,Mapping
6+
fromdataclassesimportdataclass,field,Field
7+
68
COLORIZE=True
79

10+
811
# types
912
ifFalse:
10-
fromtypingimportIO,Literal
11-
12-
typeColorTag=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
2615

2716

2817
classANSIColors:
@@ -86,6 +75,186 @@ class ANSIColors:
8675
setattr(NoColors,attr,"")
8776

8877

78+
#
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+
89258
defget_colors(
90259
colorize: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:
138307
returnhasattr(file,"isatty")andfile.isatty()
139308

140309

141-
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
143339

144-
ift:
145-
theme=t
146-
return
340+
ifnotisinstance(t,Theme):
341+
raiseValueError(f"Expected Theme object, found{t}")
147342

148-
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
161344

162345

163-
set_theme()
346+
set_theme(default_theme)

‎Lib/_pyrepl/reader.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
fromdataclassesimportdataclass,field,fields
2929

3030
from .importcommands,console,input
31-
from .utilsimportwlen,unbracket,disp_str,gen_colors
31+
from .utilsimportwlen,unbracket,disp_str,gen_colors,THEME
3232
from .traceimporttrace
3333

3434

@@ -491,11 +491,8 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
491491
prompt=self.ps1
492492

493493
ifself.can_colorize:
494-
prompt= (
495-
f"{_colorize.theme["PROMPT"]}"
496-
f"{prompt}"
497-
f"{_colorize.theme["RESET"]}"
498-
)
494+
t=THEME()
495+
prompt=f"{t.prompt}{prompt}{t.reset}"
499496
returnprompt
500497

501498
defpush_input_trans(self,itrans:input.KeymapTranslator)->None:

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp