|
1 |
| -from __future__importannotations |
2 | 1 | importio
|
3 | 2 | importos
|
4 | 3 | importsys
|
5 | 4 |
|
| 5 | +fromcollections.abcimportCallable,Iterator,Mapping |
| 6 | +fromdataclassesimportdataclass,field,Field |
| 7 | + |
6 | 8 | COLORIZE=True
|
7 | 9 |
|
| 10 | + |
8 | 11 | # types
|
9 | 12 | 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 |
26 | 15 |
|
27 | 16 |
|
28 | 17 | classANSIColors:
|
@@ -86,6 +75,186 @@ class ANSIColors:
|
86 | 75 | setattr(NoColors,attr,"")
|
87 | 76 |
|
88 | 77 |
|
| 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 | + |
89 | 258 | defget_colors(
|
90 | 259 | colorize:bool=False,*,file:IO[str]|IO[bytes]|None=None
|
91 | 260 | )->ANSIColors:
|
@@ -138,26 +307,40 @@ def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
|
138 | 307 | returnhasattr(file,"isatty")andfile.isatty()
|
139 | 308 |
|
140 | 309 |
|
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 |
143 | 339 |
|
144 |
| -ift: |
145 |
| -theme=t |
146 |
| -return |
| 340 | +ifnotisinstance(t,Theme): |
| 341 | +raiseValueError(f"Expected Theme object, found{t}") |
147 | 342 |
|
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 |
161 | 344 |
|
162 | 345 |
|
163 |
| -set_theme() |
| 346 | +set_theme(default_theme) |