11# logstyles/formatter.py
2+ import threading
23
34from .utils import hex_to_ansi , reset_code
45
@@ -7,84 +8,162 @@ def escape_angle_brackets(text):
78 return text .replace ('<' , '\\ <' ).replace ('>' , '\\ >' )
89
910def create_formatter (theme , base_format , delimiter = None , override_included_parts = None ):
10- """
11- Creates a formatter function based on the given theme and base format.
12-
13- Parameters:
14- theme (dict): The theme configuration.
15- base_format (dict): The base format configuration.
16- delimiter (str, optional): Custom delimiter. Defaults to base_format's delimiter.
17- override_included_parts (list, optional): Parts to include, overriding base_format's parts_order.
18-
19- Returns:
20- function: A formatter function for loguru.
21- """
2211 timestamp_format = theme .get ('timestamp_format' , '%Y-%m-%d %H:%M:%S' )
2312 styles = theme ['styles' ]
2413 delimiter = delimiter or base_format ['delimiter' ]
2514 parts_order = base_format ['parts_order' ]
2615
27- # Determine included parts based on parts_order
28- # Each part in parts_order corresponds to a specific part in the log
29- # e.g., 'time_part' corresponds to 'time'
3016 included_parts = [
3117 part .replace ('_part' , '' ) for part in parts_order
3218 ]
3319
34- # If override_included_parts is provided, use it instead
3520 if override_included_parts is not None :
3621 included_parts = override_included_parts
3722
23+ # Default and maximum widths for fields
24+ field_widths_config = {
25+ 'time' : {'default' : len (timestamp_format ), 'max' : len (timestamp_format )}, # Fixed timestamp length
26+ 'level' : {'default' : 8 , 'max' : 8 }, # Fixed level name length
27+ 'module' : {'default' : 20 , 'max' : 30 },
28+ 'function' : {'default' : 20 , 'max' : 30 },
29+ 'line' : {'default' : 3 , 'max' : 6 },
30+ 'thread_name' : {'default' : 15 , 'max' : 25 },
31+ 'process_name' : {'default' : 15 , 'max' : 25 },
32+ # Add other fields as needed
33+ }
34+
35+ # Initialize current widths with default values for included parts
36+ current_field_widths = {}
37+ for part_key in included_parts :
38+ config = field_widths_config .get (part_key )
39+ if config :
40+ current_field_widths [part_key ] = config ['default' ]
41+ else :
42+ current_field_widths [part_key ] = None # For fields like 'message' without width settings
43+
44+ # Lock for thread safety
45+ field_widths_lock = threading .Lock ()
46+
3847 def formatter (record ):
48+ nonlocal current_field_widths
49+
3950 # Apply timestamp format
4051 time_str = record ['time' ].strftime (timestamp_format )
4152 reset = reset_code ()
4253 level_name = record ['level' ].name
4354 level_styles = styles .get (level_name , {})
44- parts_list = []
4555
46- # Prepare parts based on parts_order
56+ fields = {}
57+
58+ # Prepare field values
4759 for part in parts_order :
4860 part_key = part .replace ('_part' , '' )
4961 if part_key == 'time' and 'time' in included_parts :
50- time_color = hex_to_ansi (theme .get ('time_color' , '#FFFFFF' ))
51- parts_list .append (f"{ time_color } { time_str } { reset } " )
62+ fields ['time' ] = time_str
5263 elif part_key == 'level' and 'level' in included_parts :
53- level_fg = level_styles .get ('level_fg' , '#FFFFFF' )
54- level_bg = level_styles .get ('level_bg' )
55- level_color = hex_to_ansi (level_fg , level_bg )
56- parts_list .append (f"{ level_color } { level_name :<8} { reset } " )
64+ fields ['level' ] = level_name
5765 elif part_key == 'module' and 'module' in included_parts :
58- module_color = hex_to_ansi (theme .get ('module_color' , '#FFFFFF' ))
5966 module_name = escape_angle_brackets (record ['module' ])
60- parts_list . append ( f" { module_color } { module_name } { reset } " )
67+ fields [ 'module' ] = module_name
6168 elif part_key == 'function' and 'function' in included_parts :
62- function_color = hex_to_ansi (theme .get ('function_color' , '#FFFFFF' ))
6369 function_name = escape_angle_brackets (record ['function' ])
64- parts_list . append ( f" { function_color } { function_name } { reset } " )
70+ fields [ 'function' ] = function_name
6571 elif part_key == 'line' and 'line' in included_parts :
66- line_color = hex_to_ansi ( theme . get ( 'line_color' , '#FFFFFF' ) )
67- parts_list . append ( f" { line_color } { record ['line' ]} { reset } " )
72+ line_str = str ( record [ 'line' ] )
73+ fields ['line' ] = line_str
6874 elif part_key == 'thread_name' and 'thread_name' in included_parts :
69- thread_color = hex_to_ansi (theme .get ('thread_color' , '#FFFFFF' ))
7075 thread_name = escape_angle_brackets (record ['thread' ].name )
71- parts_list . append ( f" { thread_color } { thread_name } { reset } " )
76+ fields [ ' thread_name' ] = thread_name
7277 elif part_key == 'process_name' and 'process_name' in included_parts :
73- process_color = hex_to_ansi (theme .get ('process_color' , '#FFFFFF' ))
7478 process_name = escape_angle_brackets (record ['process' ].name )
75- parts_list . append ( f" { process_color } { process_name } { reset } " )
79+ fields [ ' process_name' ] = process_name
7680 elif part_key == 'message' and 'message' in included_parts :
77- msg_fg = level_styles .get ('message_fg' , '#FFFFFF' )
78- msg_bg = level_styles .get ('message_bg' )
79- msg_color = hex_to_ansi (msg_fg , msg_bg )
8081 message = escape_angle_brackets (record ['message' ])
81- parts_list .append (f"{ msg_color } { message } { reset } " )
82- else :
83- # Part is not included or not applicable
84- pass
82+ fields ['message' ] = message
83+
84+ # Update current field widths up to maximums
85+ with field_widths_lock :
86+ for field , value in fields .items ():
87+ config = field_widths_config .get (field )
88+ if config is None :
89+ continue # Skip fields without width settings (e.g., 'message')
90+
91+ max_width = config ['max' ]
92+ current_width = current_field_widths .get (field , 0 )
93+ value_length = len (value )
94+
95+ if current_width < max_width and value_length > current_width :
96+ if value_length <= max_width :
97+ # Update current width permanently
98+ current_field_widths [field ] = value_length
99+ else :
100+ # Exceeds max, do not update current width
101+ pass
102+
103+ # Prepare parts with appropriate widths
104+ parts_list = []
105+
106+ for part in parts_order :
107+ part_key = part .replace ('_part' , '' )
108+ if part_key in fields :
109+ value = fields [part_key ]
110+ config = field_widths_config .get (part_key )
111+ current_width = current_field_widths .get (part_key )
112+ max_width = config ['max' ] if config else None
113+
114+ # Determine the width for this field
115+ if config and len (value ) > max_width :
116+ # Value exceeds max width, use full length for this line
117+ width = len (value )
118+ else :
119+ width = current_width
120+
121+ # Pad the value to the width if width is specified
122+ if width is not None :
123+ if part_key == 'line' :
124+ # Right-justify line numbers
125+ padded_value = value .rjust (width )
126+ else :
127+ # Left-justify other fields
128+ padded_value = value .ljust (width )
129+ else :
130+ # For fields without width settings (e.g., 'message')
131+ padded_value = value
132+
133+ # Apply color
134+ if part_key == 'time' :
135+ time_color = hex_to_ansi (theme .get ('time_color' , '#FFFFFF' ))
136+ colored_value = f"{ time_color } { padded_value } { reset } "
137+ elif part_key == 'level' :
138+ level_fg = level_styles .get ('level_fg' , '#FFFFFF' )
139+ level_bg = level_styles .get ('level_bg' )
140+ level_color = hex_to_ansi (level_fg , level_bg )
141+ colored_value = f"{ level_color } { padded_value } { reset } "
142+ elif part_key == 'module' :
143+ module_color = hex_to_ansi (theme .get ('module_color' , '#FFFFFF' ))
144+ colored_value = f"{ module_color } { padded_value } { reset } "
145+ elif part_key == 'function' :
146+ function_color = hex_to_ansi (theme .get ('function_color' , '#FFFFFF' ))
147+ colored_value = f"{ function_color } { padded_value } { reset } "
148+ elif part_key == 'line' :
149+ line_color = hex_to_ansi (theme .get ('line_color' , '#FFFFFF' ))
150+ colored_value = f"{ line_color } { padded_value } { reset } "
151+ elif part_key == 'thread_name' :
152+ thread_color = hex_to_ansi (theme .get ('thread_color' , '#FFFFFF' ))
153+ colored_value = f"{ thread_color } { padded_value } { reset } "
154+ elif part_key == 'process_name' :
155+ process_color = hex_to_ansi (theme .get ('process_color' , '#FFFFFF' ))
156+ colored_value = f"{ process_color } { padded_value } { reset } "
157+ elif part_key == 'message' :
158+ msg_fg = level_styles .get ('message_fg' , '#FFFFFF' )
159+ msg_bg = level_styles .get ('message_bg' )
160+ msg_color = hex_to_ansi (msg_fg , msg_bg )
161+ colored_value = f"{ msg_color } { padded_value } { reset } "
162+ else :
163+ colored_value = padded_value
85164
165+ parts_list .append (colored_value )
86166
87- # Combine parts with delimiter
88167 formatted_message = delimiter .join (parts_list )
89168 return formatted_message + '\n '
90169
0 commit comments