11from __future__ import annotations
22
33import contextlib
4+ import dataclasses
5+ import enum
46import functools
57import logging
68import os
7- import re
9+ import platform
810import sys
9- from typing import Any , NoReturn
11+ from collections .abc import Mapping
12+ from typing import TYPE_CHECKING , Any , NoReturn
13+
14+ if TYPE_CHECKING :
15+ from collections .abc import Iterator
16+
17+ from ._compat .typing import Literal , Self
18+
19+ StrMapping = Mapping [str , "Style" ]
20+ else :
21+ StrMapping = Mapping
22+
23+ from . import __version__
1024
1125__all__ = [
1226 "ScikitBuildLogger" ,
1630 "rich_warning" ,
1731 "rich_error" ,
1832 "LEVEL_VALUE" ,
33+ "Style" ,
1934]
2035
2136
@@ -36,6 +51,15 @@ def __dir__() -> list[str]:
3651}
3752
3853
54+ class PlatformHelper :
55+ def __getattr__ (self , name : str ) -> Any :
56+ result = getattr (platform , name )
57+ return result () if callable (result ) else result
58+
59+ def __repr__ (self ) -> str :
60+ return repr (platform )
61+
62+
3963class FStringMessage :
4064 "This class captures a formatted string message and only produces it on demand."
4165
@@ -95,74 +119,249 @@ def addHandler(self, handler: logging.Handler) -> None: # noqa: N802
95119logger = ScikitBuildLogger (raw_logger )
96120
97121
98- ANY_ESCAPE = re .compile (r"\[([\w\s/]+)\]" )
99-
100-
101- _COLORS = {
102- "red" : "\33 [91m" ,
103- "green" : "\33 [92m" ,
104- "yellow" : "\33 [93m" ,
105- "blue" : "\33 [94m" ,
106- "magenta" : "\33 [95m" ,
107- "cyan" : "\33 [96m" ,
108- "bold" : "\33 [1m" ,
109- "/red" : "\33 [0m" ,
110- "/green" : "\33 [0m" ,
111- "/blue" : "\33 [0m" ,
112- "/yellow" : "\33 [0m" ,
113- "/magenta" : "\33 [0m" ,
114- "/cyan" : "\33 [0m" ,
115- "/bold" : "\33 [22m" ,
116- "reset" : "\33 [0m" ,
117- }
118- _NO_COLORS = {color : "" for color in _COLORS }
119-
120-
121- def colors () -> dict [str , str ]:
122+ def colors () -> bool :
122123 if "NO_COLOR" in os .environ :
123- return _NO_COLORS
124+ return False
124125 # Pip reroutes sys.stdout, so FORCE_COLOR is required there
125126 if os .environ .get ("FORCE_COLOR" , "" ):
126- return _COLORS
127+ return True
127128 # Avoid ValueError: I/O operation on closed file
128129 with contextlib .suppress (ValueError ):
129130 # Assume sys.stderr is similar to sys.stdout
130131 isatty = sys .stdout .isatty ()
131132 if isatty and not sys .platform .startswith ("win" ):
132- return _COLORS
133- return _NO_COLORS
133+ return True
134+ return False
134135
135136
136- def _sub_rich (m : re .Match [str ]) -> str :
137- """
138- Replace rich-like tags, but only if they are defined in colors.
139- """
140- color_dict = colors ()
141- try :
142- return "" .join (color_dict [x ] for x in m .group (1 ).split ())
143- except KeyError :
144- return m .group (0 )
137+ class Colors (enum .Enum ):
138+ black = 0
139+ red = 1
140+ green = 2
141+ yellow = 3
142+ blue = 4
143+ magenta = 5
144+ cyan = 6
145+ white = 7
146+ default = 9
147+
148+
149+ class Styles (enum .Enum ):
150+ bold = 1
151+ italic = 3
152+ underline = 4
153+ reverse = 7
154+ reset = 0
155+ normal = 22
145156
146157
147- def _process_rich (msg : object ) -> str :
148- return ANY_ESCAPE .sub (
149- _sub_rich ,
150- str (msg ),
158+ @dataclasses .dataclass (frozen = True )
159+ class Style (StrMapping ):
160+ color : bool = dataclasses .field (default_factory = colors )
161+ styles : tuple [int , ...] = dataclasses .field (default_factory = tuple )
162+ current : int = 0
163+
164+ def __str__ (self ) -> str :
165+ styles = ";" .join (str (x ) for x in self .styles )
166+ return f"\33 [{ styles } m" if styles and self .color else ""
167+
168+ @property
169+ def fg (self ) -> Self :
170+ return dataclasses .replace (self , current = 30 )
171+
172+ @property
173+ def bg (self ) -> Self :
174+ return dataclasses .replace (self , current = 40 )
175+
176+ @property
177+ def bold (self ) -> Self :
178+ return dataclasses .replace (self , styles = (* self .styles , Styles .bold .value ))
179+
180+ @property
181+ def italic (self ) -> Self :
182+ return dataclasses .replace (self , styles = (* self .styles , Styles .italic .value ))
183+
184+ @property
185+ def underline (self ) -> Self :
186+ return dataclasses .replace (self , styles = (* self .styles , Styles .underline .value ))
187+
188+ @property
189+ def reverse (self ) -> Self :
190+ return dataclasses .replace (self , styles = (* self .styles , Styles .reverse .value ))
191+
192+ @property
193+ def reset (self ) -> Self :
194+ return dataclasses .replace (self , styles = (Styles .reset .value ,), current = 0 )
195+
196+ @property
197+ def normal (self ) -> Self :
198+ return dataclasses .replace (self , styles = (* self .styles , Styles .normal .value ))
199+
200+ @property
201+ def black (self ) -> Self :
202+ return dataclasses .replace (
203+ self , styles = (* self .styles , Colors .black .value + (self .current or 30 ))
204+ )
205+
206+ @property
207+ def red (self ) -> Self :
208+ return dataclasses .replace (
209+ self , styles = (* self .styles , Colors .red .value + (self .current or 30 ))
210+ )
211+
212+ @property
213+ def green (self ) -> Self :
214+ return dataclasses .replace (
215+ self , styles = (* self .styles , Colors .green .value + (self .current or 30 ))
216+ )
217+
218+ @property
219+ def yellow (self ) -> Self :
220+ return dataclasses .replace (
221+ self , styles = (* self .styles , Colors .yellow .value + (self .current or 30 ))
222+ )
223+
224+ @property
225+ def blue (self ) -> Self :
226+ return dataclasses .replace (
227+ self , styles = (* self .styles , Colors .blue .value + (self .current or 30 ))
228+ )
229+
230+ @property
231+ def magenta (self ) -> Self :
232+ return dataclasses .replace (
233+ self , styles = (* self .styles , Colors .magenta .value + (self .current or 30 ))
234+ )
235+
236+ @property
237+ def cyan (self ) -> Self :
238+ return dataclasses .replace (
239+ self , styles = (* self .styles , Colors .cyan .value + (self .current or 30 ))
240+ )
241+
242+ @property
243+ def white (self ) -> Self :
244+ return dataclasses .replace (
245+ self , styles = (* self .styles , Colors .white .value + (self .current or 30 ))
246+ )
247+
248+ @property
249+ def default (self ) -> Self :
250+ return dataclasses .replace (
251+ self , styles = (* self .styles , Colors .default .value + (self .current or 30 ))
252+ )
253+
254+ _keys = (
255+ "bold" ,
256+ "italic" ,
257+ "underline" ,
258+ "reverse" ,
259+ "reset" ,
260+ "normal" ,
261+ "black" ,
262+ "red" ,
263+ "green" ,
264+ "yellow" ,
265+ "blue" ,
266+ "magenta" ,
267+ "cyan" ,
268+ "white" ,
269+ "default" ,
151270 )
152271
272+ def __len__ (self ) -> int :
273+ return len (self ._keys )
153274
154- def rich_print (* args : object , ** kwargs : object ) -> None :
155- args_2 = tuple (_process_rich (arg ) for arg in args )
156- if args != args_2 :
157- args_2 = (* args_2 [:- 1 ], args_2 [- 1 ] + colors ()["reset" ])
158- print (* args_2 , ** kwargs , flush = True ) # type: ignore[call-overload] # noqa: T201
275+ def __getitem__ (self , name : str ) -> Self :
276+ return getattr (self , name ) # type: ignore[no-any-return]
159277
278+ def __iter__ (self ) -> Iterator [str ]:
279+ return iter (self ._keys )
160280
161- @functools .lru_cache (maxsize = None )
162- def rich_warning (* args : object , ** kwargs : object ) -> None :
163- rich_print ("[red][yellow]WARNING:[/bold]" , * args , ** kwargs )
164281
282+ _style = Style ()
283+ _nostyle = Style (color = False )
165284
166- def rich_error (* args : object , ** kwargs : object ) -> NoReturn :
167- rich_print ("[red][bold]ERROR:[/bold]" , * args , ** kwargs )
285+
286+ def rich_print (
287+ * args : object ,
288+ file : object = None ,
289+ sep : str = " " ,
290+ end : str = "\n " ,
291+ color : Literal [
292+ "" , "black" , "red" , "green" , "yellow" , "blue" , "magenta" , "cyan" , "white"
293+ ] = "" ,
294+ ** kwargs : object ,
295+ ) -> None :
296+ """
297+ Print a message with style and useful common includes provided via formatting.
298+
299+ This function will process every argument with the following formatting:
300+
301+ - ``{__version__}``: The version of scikit-build-core.
302+ - ``{platform}``: The platform module.
303+ - ``{sys}``: The sys module.
304+ - Colors and styles.
305+
306+ Any keyword arguments will be passed directly to the `str.format` method
307+ unless they conflict with the above. ``print`` arguments work as normal, and
308+ the output will be flushed.
309+
310+ Each argument will clear the style afterwards if a style is applied. The
311+ ``color=`` argument will set a default color to apply to every argument, and
312+ is available to arguments as ``{color}``.
313+ """
314+ if color :
315+ kwargs ["color" ] = _style [color ]
316+
317+ args_1 = tuple (str (arg ) for arg in args )
318+ args_1_gen = (
319+ arg .format (
320+ __version__ = __version__ ,
321+ platform = PlatformHelper (),
322+ sys = sys ,
323+ ** _nostyle ,
324+ ** kwargs ,
325+ )
326+ for arg in args_1
327+ )
328+ args_2_gen = (
329+ arg .format (
330+ __version__ = __version__ ,
331+ platform = PlatformHelper (),
332+ sys = sys ,
333+ ** _style ,
334+ ** kwargs ,
335+ )
336+ for arg in args_1
337+ )
338+ if color :
339+ args_2 = (f"{ _style [color ]} { new } { _style .reset } " for new in args_2_gen )
340+ else :
341+ args_2 = (
342+ new if new == orig else f"{ new } { _style .reset } "
343+ for new , orig in zip (args_2_gen , args_1_gen )
344+ )
345+ print (* args_2 , flush = True , sep = sep , end = end , file = file ) # type: ignore[call-overload]
346+
347+
348+ @functools .lru_cache (maxsize = None )
349+ def rich_warning (
350+ * args : str ,
351+ color : Literal [
352+ "" , "black" , "red" , "green" , "yellow" , "blue" , "magenta" , "cyan" , "white"
353+ ] = "yellow" ,
354+ ** kwargs : object ,
355+ ) -> None :
356+ rich_print ("{bold.yellow}WARNING:" , * args , color = color , ** kwargs ) # type: ignore[arg-type]
357+
358+
359+ def rich_error (
360+ * args : str ,
361+ color : Literal [
362+ "" , "black" , "red" , "green" , "yellow" , "blue" , "magenta" , "cyan" , "white"
363+ ] = "red" ,
364+ ** kwargs : object ,
365+ ) -> NoReturn :
366+ rich_print ("{bold.red}ERROR:" , * args , color = color , ** kwargs ) # type: ignore[arg-type]
168367 raise SystemExit (7 )
0 commit comments