Skip to content

Commit 918bd99

Browse files
committed
V4.0.4
1 parent bf71d77 commit 918bd99

27 files changed

+1512
-69
lines changed

README.md

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -446,40 +446,6 @@ registered = LoggerFactory.get_registered_loggers()
446446
print(f"Currently registered: {list(registered.keys())}")
447447
```
448448

449-
## Thread-Safe Operations
450-
All memory management operations are thread-safe and can be used safely in multi-threaded applications:
451-
452-
```python
453-
import threading
454-
from pythonLogs import size_rotating_logger, clear_logger_registry
455-
456-
def worker_function(worker_id):
457-
# Each thread can safely create and use loggers
458-
logger = size_rotating_logger(
459-
name=f"worker_{worker_id}",
460-
directory="/app/logs"
461-
)
462-
463-
with logger as log:
464-
log.info(f"Worker {worker_id} started")
465-
# Automatic cleanup per thread
466-
467-
# Create multiple threads - all operations are thread-safe
468-
threads = []
469-
for i in range(10):
470-
thread = threading.Thread(target=worker_function, args=(i,))
471-
threads.append(thread)
472-
thread.start()
473-
474-
# Wait for completion and clean up
475-
for thread in threads:
476-
thread.join()
477-
478-
# Safe to clear registry from main thread
479-
clear_logger_registry()
480-
```
481-
482-
483449
# Flexible Configuration Options
484450
You can use either enums (for type safety) or strings (for simplicity):
485451

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pythonLogs"
7-
version = "4.0.3"
7+
version = "4.0.4"
88
description = "High-performance Python logging library with file rotation and optimized caching for better performance"
99
license = "MIT"
1010
readme = "README.md"

pythonLogs/basic_log.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# -*- encoding: utf-8 -*-
22
import logging
3-
import threading
43
from typing import Optional
54
from pythonLogs.log_utils import get_format, get_level, get_timezone_function
65
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
76
from pythonLogs.settings import get_log_settings
7+
from pythonLogs.thread_safety import auto_thread_safe
88

99

10+
@auto_thread_safe(['init', '_cleanup_logger'])
1011
class BasicLog:
1112
"""Basic logger with context manager support for automatic resource cleanup."""
1213

@@ -27,8 +28,6 @@ def __init__(
2728
self.timezone = timezone or _settings.timezone
2829
self.showlocation = showlocation or _settings.show_location
2930
self.logger = None
30-
# Instance-level lock for thread safety
31-
self._lock = threading.Lock()
3231

3332
def init(self):
3433
logger = logging.getLogger(self.appname)
@@ -54,8 +53,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
5453

5554
def _cleanup_logger(self, logger: logging.Logger) -> None:
5655
"""Clean up logger resources by closing all handlers with thread safety."""
57-
with self._lock:
58-
cleanup_logger_handlers(logger)
56+
cleanup_logger_handlers(logger)
5957

6058
@staticmethod
6159
def cleanup_logger(logger: logging.Logger) -> None:

pythonLogs/size_rotating.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import logging.handlers
33
import os
44
import re
5-
import threading
65
from typing import Optional
76
from pythonLogs.constants import MB_TO_BYTES
87
from pythonLogs.log_utils import (
@@ -18,8 +17,10 @@
1817
)
1918
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
2019
from pythonLogs.settings import get_log_settings
20+
from pythonLogs.thread_safety import auto_thread_safe
2121

2222

23+
@auto_thread_safe(['init', '_cleanup_logger'])
2324
class SizeRotatingLog:
2425
"""Size-based rotating logger with context manager support for automatic resource cleanup."""
2526
def __init__(
@@ -49,8 +50,6 @@ def __init__(
4950
self.streamhandler = streamhandler or _settings.stream_handler
5051
self.showlocation = showlocation or _settings.show_location
5152
self.logger = None
52-
# Instance-level lock for thread safety
53-
self._lock = threading.Lock()
5453

5554
def init(self):
5655
check_filename_instance(self.filenames)
@@ -98,8 +97,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
9897

9998
def _cleanup_logger(self, logger: logging.Logger) -> None:
10099
"""Clean up logger resources by closing all handlers with thread safety."""
101-
with self._lock:
102-
cleanup_logger_handlers(logger)
100+
cleanup_logger_handlers(logger)
103101

104102
@staticmethod
105103
def cleanup_logger(logger: logging.Logger) -> None:

pythonLogs/thread_safety.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# -*- encoding: utf-8 -*-
2+
import functools
3+
import threading
4+
from typing import Any, Callable, Dict, TypeVar, Type
5+
6+
F = TypeVar('F', bound=Callable[..., Any])
7+
8+
9+
class ThreadSafeMeta(type):
10+
"""Metaclass that automatically adds thread safety to class methods."""
11+
12+
def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
13+
# Create the class first
14+
cls = super().__new__(mcs, name, bases, namespace)
15+
16+
# Add a class-level lock if not already present
17+
if not hasattr(cls, '_lock'):
18+
cls._lock = threading.RLock()
19+
20+
# Get methods that should be thread-safe (exclude private/dunder methods)
21+
thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
22+
if thread_safe_methods is None:
23+
# Auto-detect public methods that modify state
24+
thread_safe_methods = [
25+
method_name for method_name in namespace
26+
if (callable(getattr(cls, method_name, None)) and
27+
not method_name.startswith('_') and
28+
method_name not in ['__enter__', '__exit__', '__init__'])
29+
]
30+
31+
# Wrap each method with automatic locking
32+
for method_name in thread_safe_methods:
33+
if hasattr(cls, method_name):
34+
original_method = getattr(cls, method_name)
35+
if callable(original_method):
36+
wrapped_method = thread_safe(original_method)
37+
setattr(cls, method_name, wrapped_method)
38+
39+
return cls
40+
41+
42+
def thread_safe(func: F) -> F:
43+
"""Decorator that automatically adds thread safety to methods."""
44+
45+
@functools.wraps(func)
46+
def wrapper(self, *args, **kwargs):
47+
# Use instance lock if available, otherwise class lock
48+
lock = getattr(self, '_lock', None)
49+
if lock is None:
50+
# Check if class has lock, if not create one
51+
if not hasattr(self.__class__, '_lock'):
52+
self.__class__._lock = threading.RLock()
53+
lock = self.__class__._lock
54+
55+
with lock:
56+
return func(self, *args, **kwargs)
57+
58+
return wrapper
59+
60+
61+
def auto_thread_safe(thread_safe_methods: list = None):
62+
"""Class decorator that adds automatic thread safety to specified methods."""
63+
64+
def decorator(cls: Type) -> Type:
65+
# Add lock to class if not present
66+
if not hasattr(cls, '_lock'):
67+
cls._lock = threading.RLock()
68+
69+
# Store thread-safe methods list
70+
if thread_safe_methods:
71+
cls._thread_safe_methods = thread_safe_methods
72+
73+
# Get methods to make thread-safe
74+
methods_to_wrap = thread_safe_methods or [
75+
method_name for method_name in dir(cls)
76+
if (callable(getattr(cls, method_name, None)) and
77+
not method_name.startswith('_') and
78+
method_name not in ['__enter__', '__exit__', '__init__'])
79+
]
80+
81+
# Wrap each method
82+
for method_name in methods_to_wrap:
83+
if hasattr(cls, method_name):
84+
original_method = getattr(cls, method_name)
85+
if callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped'):
86+
wrapped_method = thread_safe(original_method)
87+
wrapped_method._thread_safe_wrapped = True
88+
setattr(cls, method_name, wrapped_method)
89+
90+
return cls
91+
92+
return decorator
93+
94+
95+
class AutoThreadSafe:
96+
"""Base class that provides automatic thread safety for all public methods."""
97+
98+
def __init__(self):
99+
if not hasattr(self, '_lock'):
100+
self._lock = threading.RLock()
101+
102+
def __init_subclass__(cls, **kwargs):
103+
super().__init_subclass__(**kwargs)
104+
105+
# Add class-level lock
106+
if not hasattr(cls, '_lock'):
107+
cls._lock = threading.RLock()
108+
109+
# Auto-wrap public methods
110+
for attr_name in dir(cls):
111+
if not attr_name.startswith('_'):
112+
attr = getattr(cls, attr_name)
113+
if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
114+
wrapped_attr = thread_safe(attr)
115+
wrapped_attr._thread_safe_wrapped = True
116+
setattr(cls, attr_name, wrapped_attr)
117+
118+
119+
def synchronized_method(func: F) -> F:
120+
"""Decorator for individual methods that need thread safety."""
121+
return thread_safe(func)
122+
123+
124+
class ThreadSafeContext:
125+
"""Context manager for thread-safe operations."""
126+
127+
def __init__(self, lock: threading.Lock):
128+
self.lock = lock
129+
130+
def __enter__(self):
131+
self.lock.acquire()
132+
return self
133+
134+
def __exit__(self, exc_type, exc_val, exc_tb):
135+
self.lock.release()

pythonLogs/timed_rotating.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# -*- encoding: utf-8 -*-
22
import logging.handlers
33
import os
4-
import threading
54
from typing import Optional
65
from pythonLogs.log_utils import (
76
check_directory_permissions,
@@ -15,8 +14,10 @@
1514
)
1615
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
1716
from pythonLogs.settings import get_log_settings
17+
from pythonLogs.thread_safety import auto_thread_safe
1818

1919

20+
@auto_thread_safe(['init', '_cleanup_logger'])
2021
class TimedRotatingLog:
2122
"""
2223
Time-based rotating logger with context manager support for automatic resource cleanup.
@@ -60,8 +61,6 @@ def __init__(
6061
self.showlocation = showlocation or _settings.show_location
6162
self.rotateatutc = rotateatutc or _settings.rotate_at_utc
6263
self.logger = None
63-
# Instance-level lock for thread safety
64-
self._lock = threading.Lock()
6564

6665
def init(self):
6766
check_filename_instance(self.filenames)
@@ -107,8 +106,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
107106

108107
def _cleanup_logger(self, logger: logging.Logger) -> None:
109108
"""Clean up logger resources by closing all handlers with thread safety."""
110-
with self._lock:
111-
cleanup_logger_handlers(logger)
109+
cleanup_logger_handlers(logger)
112110

113111
@staticmethod
114112
def cleanup_logger(logger: logging.Logger) -> None:

0 commit comments

Comments
 (0)