Skip to content

Commit 811f412

Browse files
authored
V4.0.6 (#9)
* V4.0.6 * V4.0.6 * V4.0.6 * Update README.md * Update README.md * V4.0.6 * V4.0.6 * V4.0.6 * V4.0.6 --------- Co-authored-by: ddc <[email protected]>
1 parent e85a683 commit 811f412

17 files changed

+292
-246
lines changed

.github/PULL_REQUEST_TEMPLATE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
- [ ] I have updated the documentation to reflect the changes.
88
- [ ] I have thought about how this code may affect other services.
99
- [ ] This PR fixes an issue.
10+
- [ ] This PR add/remove/change unit tests.
1011
- [ ] This PR adds something new (e.g. new method or parameters).
11-
- [ ] This PR have unit tests (e.g. tests added/removed/changed)
1212
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)
1313
- [ ] This PR is **not** a code change (e.g. documentation, README, ...)
1414

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
[![PyPi](https://img.shields.io/pypi/v/pythonLogs.svg)](https://pypi.python.org/pypi/pythonLogs)
66
[![PyPI Downloads](https://static.pepy.tech/badge/pythonLogs)](https://pepy.tech/projects/pythonLogs)
77
[![codecov](https://codecov.io/gh/ddc/pythonLogs/graph/badge.svg?token=QsjwsmYzgD)](https://codecov.io/gh/ddc/pythonLogs)
8-
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
8+
[![CI/CD Pipeline](https://github.com/ddc/pythonLogs/actions/workflows/workflow.yml/badge.svg)](https://github.com/ddc/pythonLogs/actions/workflows/workflow.yml)
9+
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ddc_pythonLogs&metric=alert_status)](https://sonarcloud.io/dashboard?id=ddc_pythonLogs)
910
[![Build Status](https://img.shields.io/endpoint.svg?url=https%3A//actions-badge.atrox.dev/ddc/pythonLogs/badge?ref=main&label=build&logo=none)](https://actions-badge.atrox.dev/ddc/pythonLogs/goto?ref=main)
11+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
1012
[![Python](https://img.shields.io/pypi/pyversions/pythonLogs.svg)](https://www.python.org/downloads)
1113

1214
[![Support me on GitHub](https://img.shields.io/badge/Support_me_on_GitHub-154c79?style=for-the-badge&logo=github)](https://github.com/sponsors/ddc)

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.5"
7+
version = "4.0.6"
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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ def init(self):
3434
logger.setLevel(self.level)
3535
logging.Formatter.converter = get_timezone_function(self.timezone)
3636
_format = get_format(self.showlocation, self.appname, self.timezone)
37-
logging.basicConfig(datefmt=self.datefmt, encoding=self.encoding, format=_format)
37+
38+
# Only add handler if logger doesn't have any handlers
39+
if not logger.handlers:
40+
handler = logging.StreamHandler()
41+
formatter = logging.Formatter(_format, datefmt=self.datefmt)
42+
handler.setFormatter(formatter)
43+
logger.addHandler(handler)
44+
3845
self.logger = logger
3946
# Register weak reference for memory tracking
4047
register_logger_weakref(logger)

pythonLogs/factory.py

Lines changed: 75 additions & 58 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"
@@ -80,7 +100,7 @@ def get_or_create_logger(
80100

81101
# Check if logger already exists in the registry
82102
if name in cls._logger_registry:
83-
logger, timestamp = cls._logger_registry[name]
103+
logger, _ = cls._logger_registry[name]
84104
# Update timestamp for LRU tracking
85105
cls._logger_registry[name] = (logger, time.time())
86106
return logger
@@ -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: 33 additions & 17 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

@@ -58,34 +59,49 @@ def wrapper(self, *args, **kwargs):
5859
return wrapper
5960

6061

62+
def _get_wrappable_methods(cls: Type) -> list:
63+
"""Helper function to get methods that should be made thread-safe."""
64+
return [
65+
method_name for method_name in dir(cls)
66+
if (callable(getattr(cls, method_name, None)) and
67+
not method_name.startswith('_') and
68+
method_name not in ['__enter__', '__exit__', '__init__'])
69+
]
70+
71+
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+
6185
def auto_thread_safe(thread_safe_methods: list = None):
6286
"""Class decorator that adds automatic thread safety to specified methods."""
6387

6488
def decorator(cls: Type) -> Type:
65-
# Add lock to class if not present
66-
if not hasattr(cls, '_lock'):
67-
cls._lock = threading.RLock()
89+
_ensure_class_has_lock(cls)
6890

6991
# Store thread-safe methods list
7092
if thread_safe_methods:
7193
cls._thread_safe_methods = thread_safe_methods
7294

7395
# 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-
]
96+
methods_to_wrap = thread_safe_methods or _get_wrappable_methods(cls)
8097

8198
# Wrap each method
8299
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)
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)
89105

90106
return cls
91107

@@ -132,4 +148,4 @@ def __enter__(self):
132148
return self
133149

134150
def __exit__(self, exc_type, exc_val, exc_tb):
135-
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"

0 commit comments

Comments
 (0)