Skip to content

Commit 572a94e

Browse files
committed
V4.0.6
1 parent 17bff0c commit 572a94e

File tree

13 files changed

+249
-226
lines changed

13 files changed

+249
-226
lines changed

pythonLogs/factory.py

Lines changed: 74 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import threading
55
import time
6+
from dataclasses import dataclass
67
from enum import Enum
78
from typing import Dict, Optional, Tuple, Union
89
from pythonLogs.basic_log import BasicLog
@@ -12,6 +13,25 @@
1213
from pythonLogs.timed_rotating import TimedRotatingLog
1314

1415

16+
@dataclass
17+
class LoggerConfig:
18+
"""Configuration class to group logger parameters"""
19+
level: Optional[Union[LogLevel, str]] = None
20+
name: Optional[str] = None
21+
directory: Optional[str] = None
22+
filenames: Optional[list | tuple] = None
23+
encoding: Optional[str] = None
24+
datefmt: Optional[str] = None
25+
timezone: Optional[str] = None
26+
streamhandler: Optional[bool] = None
27+
showlocation: Optional[bool] = None
28+
maxmbytes: Optional[int] = None
29+
when: Optional[Union[RotateWhen, str]] = None
30+
sufix: Optional[str] = None
31+
rotateatutc: Optional[bool] = None
32+
daystokeep: Optional[int] = None
33+
34+
1535
class LoggerType(str, Enum):
1636
"""Available logger types"""
1737
BASIC = "basic"
@@ -189,42 +209,17 @@ def get_registered_loggers(cls) -> dict[str, logging.Logger]:
189209
@staticmethod
190210
def create_logger(
191211
logger_type: Union[LoggerType, str],
192-
level: Optional[Union[LogLevel, str]] = None,
193-
name: Optional[str] = None,
194-
directory: Optional[str] = None,
195-
filenames: Optional[list | tuple] = None,
196-
encoding: Optional[str] = None,
197-
datefmt: Optional[str] = None,
198-
timezone: Optional[str] = None,
199-
streamhandler: Optional[bool] = None,
200-
showlocation: Optional[bool] = None, # Size rotating specific
201-
maxmbytes: Optional[int] = None, # Timed rotating specific
202-
when: Optional[Union[RotateWhen, str]] = None,
203-
sufix: Optional[str] = None,
204-
rotateatutc: Optional[bool] = None,
205-
# Common
206-
daystokeep: Optional[int] = None,
212+
config: Optional[LoggerConfig] = None,
213+
**kwargs
207214
) -> logging.Logger:
208215

209216
"""
210217
Factory method to create loggers based on type.
211218
212219
Args:
213220
logger_type: Type of logger to create (LoggerType enum or string)
214-
level: Log level (LogLevel enum or string: DEBUG, INFO, WARNING, ERROR, CRITICAL)
215-
name: Logger name
216-
directory: Log directory path
217-
filenames: List/tuple of log filenames
218-
encoding: File encoding
219-
datefmt: Date format string
220-
timezone: Timezone for timestamps
221-
streamhandler: Enable console output
222-
showlocation: Show file location in logs
223-
maxmbytes: Max file size in MB (size rotating only)
224-
when: When to rotate (RotateWhen enum or string: MIDNIGHT, HOURLY, DAILY, etc.)
225-
sufix: Date suffix for rotated files (timed rotating only)
226-
rotateatutc: Rotate at UTC time (timed rotating only)
227-
daystokeep: Days to keep old logs
221+
config: LoggerConfig object with logger parameters
222+
**kwargs: Individual logger parameters (for backward compatibility)
228223
229224
Returns:
230225
Configured logger instance
@@ -239,50 +234,72 @@ def create_logger(
239234
except ValueError:
240235
raise ValueError(f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}")
241236

237+
# Merge config and kwargs (kwargs take precedence for backward compatibility)
238+
if config is None:
239+
config = LoggerConfig()
240+
241+
# Create a new config with kwargs overriding config values
242+
final_config = LoggerConfig(
243+
level=kwargs.get('level', config.level),
244+
name=kwargs.get('name', config.name),
245+
directory=kwargs.get('directory', config.directory),
246+
filenames=kwargs.get('filenames', config.filenames),
247+
encoding=kwargs.get('encoding', config.encoding),
248+
datefmt=kwargs.get('datefmt', config.datefmt),
249+
timezone=kwargs.get('timezone', config.timezone),
250+
streamhandler=kwargs.get('streamhandler', config.streamhandler),
251+
showlocation=kwargs.get('showlocation', config.showlocation),
252+
maxmbytes=kwargs.get('maxmbytes', config.maxmbytes),
253+
when=kwargs.get('when', config.when),
254+
sufix=kwargs.get('sufix', config.sufix),
255+
rotateatutc=kwargs.get('rotateatutc', config.rotateatutc),
256+
daystokeep=kwargs.get('daystokeep', config.daystokeep)
257+
)
258+
242259
# Convert enum values to strings for logger classes
243-
level_str = level.value if isinstance(level, LogLevel) else level
244-
when_str = when.value if isinstance(when, RotateWhen) else when
260+
level_str = final_config.level.value if isinstance(final_config.level, LogLevel) else final_config.level
261+
when_str = final_config.when.value if isinstance(final_config.when, RotateWhen) else final_config.when
245262

246263
# Create logger based on type
247264
match logger_type:
248265
case LoggerType.BASIC:
249266
logger_instance = BasicLog(
250267
level=level_str,
251-
name=name,
252-
encoding=encoding,
253-
datefmt=datefmt,
254-
timezone=timezone,
255-
showlocation=showlocation, )
268+
name=final_config.name,
269+
encoding=final_config.encoding,
270+
datefmt=final_config.datefmt,
271+
timezone=final_config.timezone,
272+
showlocation=final_config.showlocation, )
256273

257274
case LoggerType.SIZE_ROTATING:
258275
logger_instance = SizeRotatingLog(
259276
level=level_str,
260-
name=name,
261-
directory=directory,
262-
filenames=filenames,
263-
maxmbytes=maxmbytes,
264-
daystokeep=daystokeep,
265-
encoding=encoding,
266-
datefmt=datefmt,
267-
timezone=timezone,
268-
streamhandler=streamhandler,
269-
showlocation=showlocation, )
277+
name=final_config.name,
278+
directory=final_config.directory,
279+
filenames=final_config.filenames,
280+
maxmbytes=final_config.maxmbytes,
281+
daystokeep=final_config.daystokeep,
282+
encoding=final_config.encoding,
283+
datefmt=final_config.datefmt,
284+
timezone=final_config.timezone,
285+
streamhandler=final_config.streamhandler,
286+
showlocation=final_config.showlocation, )
270287

271288
case LoggerType.TIMED_ROTATING:
272289
logger_instance = TimedRotatingLog(
273290
level=level_str,
274-
name=name,
275-
directory=directory,
276-
filenames=filenames,
291+
name=final_config.name,
292+
directory=final_config.directory,
293+
filenames=final_config.filenames,
277294
when=when_str,
278-
sufix=sufix,
279-
daystokeep=daystokeep,
280-
encoding=encoding,
281-
datefmt=datefmt,
282-
timezone=timezone,
283-
streamhandler=streamhandler,
284-
showlocation=showlocation,
285-
rotateatutc=rotateatutc, )
295+
sufix=final_config.sufix,
296+
daystokeep=final_config.daystokeep,
297+
encoding=final_config.encoding,
298+
datefmt=final_config.datefmt,
299+
timezone=final_config.timezone,
300+
streamhandler=final_config.streamhandler,
301+
showlocation=final_config.showlocation,
302+
rotateatutc=final_config.rotateatutc, )
286303

287304
case _:
288305
raise ValueError(f"Unsupported logger type: {logger_type}")

pythonLogs/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
DEFAULT_ROTATE_SUFFIX,
1212
DEFAULT_TIMEZONE,
1313
LogLevel,
14-
RotateWhen
14+
RotateWhen,
1515
)
1616

1717

pythonLogs/thread_safety.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# -*- encoding: utf-8 -*-
22
import functools
33
import threading
4-
from typing import Any, Callable, Dict, TypeVar, Type
4+
from typing import Any, Callable, Dict, Type, TypeVar
5+
56

67
F = TypeVar('F', bound=Callable[..., Any])
78

@@ -68,13 +69,24 @@ def _get_wrappable_methods(cls: Type) -> list:
6869
]
6970

7071

72+
def _ensure_class_has_lock(cls: Type) -> None:
73+
"""Ensure the class has a lock attribute."""
74+
if not hasattr(cls, '_lock'):
75+
cls._lock = threading.RLock()
76+
77+
78+
def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
79+
"""Check if a method should be wrapped with thread safety."""
80+
return (hasattr(cls, method_name) and
81+
callable(original_method) and
82+
not hasattr(original_method, '_thread_safe_wrapped'))
83+
84+
7185
def auto_thread_safe(thread_safe_methods: list = None):
7286
"""Class decorator that adds automatic thread safety to specified methods."""
7387

7488
def decorator(cls: Type) -> Type:
75-
# Add lock to class if not present
76-
if not hasattr(cls, '_lock'):
77-
cls._lock = threading.RLock()
89+
_ensure_class_has_lock(cls)
7890

7991
# Store thread-safe methods list
8092
if thread_safe_methods:
@@ -85,12 +97,11 @@ def decorator(cls: Type) -> Type:
8597

8698
# Wrap each method
8799
for method_name in methods_to_wrap:
88-
if hasattr(cls, method_name):
89-
original_method = getattr(cls, method_name)
90-
if callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped'):
91-
wrapped_method = thread_safe(original_method)
92-
wrapped_method._thread_safe_wrapped = True
93-
setattr(cls, method_name, wrapped_method)
100+
original_method = getattr(cls, method_name, None)
101+
if _should_wrap_method(cls, method_name, original_method):
102+
wrapped_method = thread_safe(original_method)
103+
wrapped_method._thread_safe_wrapped = True
104+
setattr(cls, method_name, wrapped_method)
94105

95106
return cls
96107

@@ -137,4 +148,4 @@ def __enter__(self):
137148
return self
138149

139150
def __exit__(self, exc_type, exc_val, exc_tb):
140-
self.lock.release()
151+
self.lock.release()

pythonLogs/timed_rotating.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
get_logger_and_formatter,
1111
get_stream_handler,
1212
gzip_file_with_sufix,
13-
remove_old_logs,
13+
remove_old_logs,
1414
)
1515
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
1616
from pythonLogs.settings import get_log_settings

tests/context_management/test_context_managers.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,21 @@
2323
class TestContextManagers:
2424
"""Test context manager functionality for resource management."""
2525

26-
def setup_method(self):
26+
@pytest.fixture(autouse=True)
27+
def setup_temp_dir(self):
2728
"""Set up test fixtures before each test method."""
2829
# Clear any existing loggers
2930
clear_logger_registry()
3031

31-
# Create temporary directory for log files
32-
self.temp_dir = tempfile.mkdtemp()
33-
self.log_file = "test.log"
34-
35-
def teardown_method(self):
36-
"""Clean up after each test method."""
32+
# Create temporary directory for log files using context manager
33+
with tempfile.TemporaryDirectory() as temp_dir:
34+
self.temp_dir = temp_dir
35+
self.log_file = "test.log"
36+
yield
37+
3738
# Clear registry after each test
3839
clear_logger_registry()
3940

40-
# Clean up temporary files
41-
import shutil
42-
if os.path.exists(self.temp_dir):
43-
shutil.rmtree(self.temp_dir, ignore_errors=True)
44-
4541
def test_basic_log_context_manager(self):
4642
"""Test BasicLog as context manager."""
4743
logger_name = "test_basic_context"

tests/context_management/test_resource_management.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,21 @@
2323
class TestResourceManagement:
2424
"""Test resource management functionality."""
2525

26-
def setup_method(self):
26+
@pytest.fixture(autouse=True)
27+
def setup_temp_dir(self):
2728
"""Set up test fixtures before each test method."""
2829
# Clear any existing loggers
2930
clear_logger_registry()
3031

31-
# Create temporary directory for log files
32-
self.temp_dir = tempfile.mkdtemp()
33-
self.log_file = "resource_test.log"
34-
35-
def teardown_method(self):
36-
"""Clean up after each test method."""
32+
# Create temporary directory for log files using context manager
33+
with tempfile.TemporaryDirectory() as temp_dir:
34+
self.temp_dir = temp_dir
35+
self.log_file = "resource_test.log"
36+
yield
37+
3738
# Clear registry after each test
3839
clear_logger_registry()
3940

40-
# Clean up temporary files
41-
import shutil
42-
if os.path.exists(self.temp_dir):
43-
shutil.rmtree(self.temp_dir, ignore_errors=True)
44-
4541
def test_factory_registry_cleanup(self):
4642
"""Test that factory registry cleanup properly closes handlers."""
4743
logger_name = "test_registry_cleanup"
@@ -273,7 +269,9 @@ def create_and_cleanup_logger(index):
273269
# Create multiple threads doing concurrent operations
274270
num_threads = 5
275271
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
276-
futures = [executor.submit(create_and_cleanup_logger, i) for i in range(num_threads)]
272+
futures = []
273+
for i in range(num_threads):
274+
futures.append(executor.submit(create_and_cleanup_logger, i))
277275

278276
# Wait for all to complete
279277
results = []

0 commit comments

Comments
 (0)