Skip to content

Commit d4e5b1a

Browse files
authored
V4.0.4 (#7)
* V4.0.4 * V4.0.4 * V4.0.4 --------- Co-authored-by: ddc <[email protected]>
1 parent bf71d77 commit d4e5b1a

28 files changed

+1513
-93
lines changed

.github/PULL_REQUEST_TEMPLATE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [ ] I have thought about how this code may affect other services.
99
- [ ] This PR fixes an issue.
1010
- [ ] This PR adds something new (e.g. new method or parameters).
11+
- [ ] This PR have unit tests (e.g. tests added/removed/changed)
1112
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)
1213
- [ ] This PR is **not** a code change (e.g. documentation, README, ...)
1314

README.md

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ High-performance Python logging library with file rotation and optimized caching
2828
- [Memory Management](#memory-management)
2929
- [Flexible Configuration Options](#flexible-configuration-options)
3030
- [Migration Guide](#migration-guide)
31-
- [Performance Improvements](#performance-improvements)
3231
- [Development](#source-code)
3332
- [Run Tests and Get Coverage Report using Poe](#run-tests-and-get-coverage-report-using-poe)
3433
- [License](#license)
@@ -446,40 +445,6 @@ registered = LoggerFactory.get_registered_loggers()
446445
print(f"Currently registered: {list(registered.keys())}")
447446
```
448447

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-
483448
# Flexible Configuration Options
484449
You can use either enums (for type safety) or strings (for simplicity):
485450

@@ -552,25 +517,6 @@ timed_logger = timed_rotating_logger(level=LogLevel.WARNING, name="app", directo
552517
- 🔧 **Cleaner API** without manual `.init()` calls
553518
- 📚 **Centralized configuration** through factory pattern
554519

555-
# Performance Improvements
556-
557-
## Benchmarks
558-
The factory pattern with optimizations provides significant performance improvements:
559-
560-
| Feature | Improvement | Benefit |
561-
|---------|-------------|---------|
562-
| Logger Registry | 90%+ faster | Cached logger instances |
563-
| Settings Caching | ~85% faster | Reused configuration objects |
564-
| Directory Validation | ~75% faster | Cached permission checks |
565-
| Timezone Operations | ~60% faster | Cached timezone functions |
566-
567-
## Performance Test Results
568-
```python
569-
# Create 100 loggers - Performance comparison
570-
# Legacy method: ~0.045 seconds
571-
# Factory pattern: ~0.004 seconds
572-
# Improvement: 91% faster ⚡
573-
```
574520

575521
# Source Code
576522
### Build
@@ -579,21 +525,17 @@ poetry build -f wheel
579525
```
580526

581527

582-
583528
# Run Tests and Get Coverage Report using Poe
584529
```shell
585530
poetry update --with test
586531
poe test
587532
```
588533

589534

590-
591535
# License
592536
Released under the [MIT License](LICENSE)
593537

594538

595-
596-
597539
# Buy me a cup of coffee
598540
+ [GitHub Sponsor](https://github.com/sponsors/ddc)
599541
+ [ko-fi](https://ko-fi.com/ddcsta)

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)