1515if TYPE_CHECKING : # pragma: no cover
1616 from types import TracebackType
1717
18+ import re
19+ from pathlib import Path
20+ from typing import TYPE_CHECKING
21+
22+ from rich .console import Console
23+ from rich .console import Group
24+ from rich .console import RenderableType
1825from rich .logging import RichHandler
26+ from rich .padding import Padding
27+ from rich .text import Text
28+
29+ CONSOLE_WIDTH = 1000
30+
31+
32+ class IconRichHandler (RichHandler ):
33+ _icons = {
34+ logging .CRITICAL : '💀' ,
35+ logging .ERROR : '❌' ,
36+ logging .WARNING : '⚠️' ,
37+ logging .DEBUG : '💀' ,
38+ logging .INFO : 'ℹ️' ,
39+ }
40+
41+ @staticmethod
42+ def in_warp () -> bool :
43+ return os .getenv ('TERM_PROGRAM' ) == 'WarpTerminal'
44+
45+ def get_level_text (self , record : logging .LogRecord ) -> Text :
46+ icon = self ._icons .get (record .levelno , record .levelname )
47+ if self .in_warp () and icon in ['⚠️' , '⚙️' , 'ℹ️' ]:
48+ icon = icon + ' ' # add space to avoid rendering issues in Warp
49+ return Text (icon )
50+
51+ def render_message (self , record : logging .LogRecord , message : str ) -> Text :
52+ icon = self ._icons .get (record .levelno , record .levelname )
53+ record = logging .makeLogRecord (record .__dict__ )
54+ record .levelname = icon
55+ return super ().render_message (record , message )
1956
2057
2158class Logger :
@@ -51,12 +88,59 @@ class Reaction(Enum):
5188 _configured = False
5289 _mode : 'Logger.Mode' = Mode .VERBOSE
5390 _reaction : 'Logger.Reaction' = Reaction .RAISE # TODO: not default?
91+ _console = Console (width = CONSOLE_WIDTH , force_jupyter = False )
92+
93+ @classmethod
94+ def print2 (cls , * objects , ** kwargs ):
95+ """Print objects to the console with left padding.
96+
97+ - Renderables (Rich types like Text, Table, Panel, etc.) are
98+ kept as-is.
99+ - Non-renderables (ints, floats, Path, etc.) are converted to
100+ str().
101+ """
102+ safe_objects = []
103+ for obj in objects :
104+ if isinstance (obj , (str , RenderableType )):
105+ safe_objects .append (obj )
106+ elif isinstance (obj , Path ):
107+ # Rich can render Path objects, but str() ensures
108+ # consistency
109+ safe_objects .append (str (obj ))
110+ else :
111+ safe_objects .append (str (obj ))
112+
113+ # Join with spaces, like print()
114+ padded = Padding (* safe_objects , (0 , 0 , 0 , 3 ))
115+ cls ._console .print (padded , ** kwargs )
116+
117+ @classmethod
118+ def print (cls , * objects , ** kwargs ):
119+ """Print objects to the console with left padding."""
120+ safe_objects = []
121+ for obj in objects :
122+ if isinstance (obj , RenderableType ):
123+ safe_objects .append (obj )
124+ elif isinstance (obj , Path ):
125+ safe_objects .append (str (obj ))
126+ else :
127+ safe_objects .append (str (obj ))
128+
129+ # If multiple objects, join with spaces
130+ renderable = (
131+ ' ' .join (str (o ) for o in safe_objects )
132+ if all (isinstance (o , str ) for o in safe_objects )
133+ else Group (* safe_objects )
134+ )
135+
136+ padded = Padding (renderable , (0 , 0 , 0 , 3 ))
137+ cls ._console .print (padded , ** kwargs )
54138
55139 # ---------------- environment detection ----------------
56140 @staticmethod
57141 def _in_jupyter () -> bool : # pragma: no cover - heuristic
58142 try :
59- from IPython import get_ipython # type: ignore[import-not-found]
143+ from IPython import get_ipython
60144
61145 return get_ipython () is not None
62146 except Exception : # noqa: BLE001
@@ -138,7 +222,7 @@ def configure(
138222 # locals_max_string=0, # no local string previews
139223 )
140224 console = Console (
141- width = 120 ,
225+ width = CONSOLE_WIDTH ,
142226 # color_system="truecolor",
143227 force_jupyter = False ,
144228 # force_terminal=False,
@@ -149,8 +233,8 @@ def configure(
149233 handler = RichHandler (
150234 rich_tracebacks = rich_tracebacks ,
151235 markup = True ,
152- show_time = False ,
153- show_path = False ,
236+ show_time = False , # show_time=(mode == cls.Mode.VERBOSE),
237+ show_path = False , # show_path=(mode == cls.Mode.VERBOSE),
154238 tracebacks_show_locals = False ,
155239 tracebacks_suppress = ['easydiffraction' ],
156240 tracebacks_max_frames = 10 ,
@@ -250,6 +334,52 @@ def _lazy_config(cls) -> None:
250334 if not cls ._configured : # pragma: no cover - trivial
251335 cls .configure ()
252336
337+ # ---------------- text formatting helpers ----------------
338+ @staticmethod
339+ def _chapter (title : str ) -> str :
340+ """Formats a chapter header with bold magenta text, uppercase,
341+ and padding.
342+ """
343+ width = 80
344+ symbol = '─'
345+ full_title = f' { title .upper ()} '
346+ pad_len = (width - len (full_title )) // 2
347+ padding = symbol * pad_len
348+ line = f'[bold magenta]{ padding } { full_title } { padding } [/bold magenta]'
349+ if len (line ) < width :
350+ line += symbol
351+ formatted = f'{ line } '
352+ from easydiffraction .utils .env import is_notebook as in_jupyter
353+
354+ if not in_jupyter ():
355+ formatted = f'\n { formatted } '
356+ return formatted
357+
358+ @staticmethod
359+ def _section (title : str ) -> str :
360+ """Formats a section header with bold green text."""
361+ full_title = f'{ title .upper ()} '
362+ line = '━' * len (full_title )
363+ formatted = f'[bold green]{ full_title } \n { line } [/bold green]'
364+
365+ # Avoid injecting extra newlines; callers can separate sections
366+ return formatted
367+
368+ @staticmethod
369+ def _paragraph (message : str ) -> Text :
370+ parts = re .split (r"('.*?')" , message )
371+ text = Text ()
372+ for part in parts :
373+ if part .startswith ("'" ) and part .endswith ("'" ):
374+ text .append (part )
375+ else :
376+ text .append (part , style = 'bold blue' )
377+ formatted = f'{ text .markup } '
378+
379+ # Paragraphs should not force an extra leading newline; rely on
380+ # caller
381+ return formatted
382+
253383 # ---------------- core routing ----------------
254384 @classmethod
255385 def handle (
@@ -278,33 +408,50 @@ def handle(
278408
279409 # ---------------- convenience API ----------------
280410 @classmethod
281- def debug (cls , message : str ) -> None :
282- cls .handle (message , level = cls .Level .DEBUG , exc_type = None )
411+ def debug (cls , * messages : str ) -> None :
412+ for message in messages :
413+ cls .handle (message , level = cls .Level .DEBUG , exc_type = None )
283414
284415 @classmethod
285- def info (cls , message : str ) -> None :
286- cls .handle (message , level = cls .Level .INFO , exc_type = None )
416+ def info (cls , * messages : str ) -> None :
417+ for message in messages :
418+ cls .handle (message , level = cls .Level .INFO , exc_type = None )
287419
288420 @classmethod
289- def warning (cls , message : str , exc_type : type [BaseException ] | None = None ) -> None :
290- cls .handle (message , level = cls .Level .WARNING , exc_type = exc_type )
421+ def warning (cls , * messages : str , exc_type : type [BaseException ] | None = None ) -> None :
422+ for message in messages :
423+ cls .handle (message , level = cls .Level .WARNING , exc_type = exc_type )
291424
292425 @classmethod
293- def error (cls , message : str , exc_type : type [BaseException ] = AttributeError ) -> None :
294- if cls ._reaction is cls .Reaction .RAISE :
295- cls .handle (message , level = cls .Level .ERROR , exc_type = exc_type )
296- elif cls ._reaction is cls .Reaction .WARN :
297- cls .handle (message , level = cls .Level .WARNING , exc_type = UserWarning )
426+ def error (cls , * messages : str , exc_type : type [BaseException ] = AttributeError ) -> None :
427+ for message in messages :
428+ if cls ._reaction is cls .Reaction .RAISE :
429+ cls .handle (message , level = cls .Level .ERROR , exc_type = exc_type )
430+ elif cls ._reaction is cls .Reaction .WARN :
431+ cls .handle (message , level = cls .Level .WARNING , exc_type = UserWarning )
298432
299433 @classmethod
300- def critical (cls , message : str , exc_type : type [BaseException ] = RuntimeError ) -> None :
301- cls .handle (message , level = cls .Level .CRITICAL , exc_type = exc_type )
434+ def critical (cls , * messages : str , exc_type : type [BaseException ] = RuntimeError ) -> None :
435+ for message in messages :
436+ cls .handle (message , level = cls .Level .CRITICAL , exc_type = exc_type )
302437
303438 @classmethod
304439 def exception (cls , message : str ) -> None :
305440 """Log current exception from inside ``except`` block."""
306441 cls ._lazy_config ()
307442 cls ._logger .error (message , exc_info = True )
308443
444+ @classmethod
445+ def paragraph (cls , message : str ) -> None :
446+ cls .print (cls ._paragraph (message ))
447+
448+ @classmethod
449+ def section (cls , message : str ) -> None :
450+ cls .print (cls ._section (message ))
451+
452+ @classmethod
453+ def chapter (cls , message : str ) -> None :
454+ cls .info (cls ._chapter (message ))
455+
309456
310457log = Logger # ergonomic alias
0 commit comments