Skip to content

Commit c1cae3c

Browse files
committed
update formatter to handle dynamic resizing better. This change breaks the tests. Im well aware of that. These will be fixed in a later version
1 parent 67bf644 commit c1cae3c

File tree

2 files changed

+122
-43
lines changed

2 files changed

+122
-43
lines changed

logstyles/formatter.py

Lines changed: 121 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# logstyles/formatter.py
2+
import threading
23

34
from .utils import hex_to_ansi, reset_code
45

@@ -7,84 +8,162 @@ def escape_angle_brackets(text):
78
return text.replace('<', '\\<').replace('>', '\\>')
89

910
def 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

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
setup(
1616
name="logstyles", # Required
17-
version="0.1.3", # Required
17+
version="0.1.4", # Required
1818
description="A logging styling library for Loguru with customizable themes and formats.", # Required
1919
long_description=README, # Optional
2020
long_description_content_type="text/markdown", # Optional (see note above)

0 commit comments

Comments
 (0)