Skip to content

Commit 9a17974

Browse files
committed
v7.0.0
1 parent ecf1be2 commit 9a17974

4 files changed

Lines changed: 90 additions & 167 deletions

File tree

pythonlogs/core/factory.py

Lines changed: 60 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import atexit
2+
import dataclasses
23
import logging
34
import threading
45
import time
@@ -10,7 +11,6 @@
1011
from pythonlogs.core.settings import get_log_settings
1112
from pythonlogs.size_rotating import SizeRotatingLog as _SizeRotatingLogImpl
1213
from pythonlogs.timed_rotating import TimedRotatingLog as _TimedRotatingLogImpl
13-
from typing import assert_never
1414

1515

1616
@dataclass
@@ -217,6 +217,48 @@ def get_memory_limits(cls) -> dict[str, int]:
217217
with cls._registry_lock:
218218
return {"max_loggers": cls._max_loggers, "ttl_seconds": cls._logger_ttl}
219219

220+
# Mapping of logger types to their implementation classes and accepted fields
221+
_LOGGER_IMPL = {
222+
LoggerType.BASIC: (
223+
_BasicLogImpl,
224+
{"level", "name", "encoding", "datefmt", "timezone", "showlocation"},
225+
),
226+
LoggerType.SIZE_ROTATING: (
227+
_SizeRotatingLogImpl,
228+
{
229+
"level",
230+
"name",
231+
"directory",
232+
"filenames",
233+
"maxmbytes",
234+
"daystokeep",
235+
"encoding",
236+
"datefmt",
237+
"timezone",
238+
"streamhandler",
239+
"showlocation",
240+
},
241+
),
242+
LoggerType.TIMED_ROTATING: (
243+
_TimedRotatingLogImpl,
244+
{
245+
"level",
246+
"name",
247+
"directory",
248+
"filenames",
249+
"when",
250+
"sufix",
251+
"daystokeep",
252+
"encoding",
253+
"datefmt",
254+
"timezone",
255+
"streamhandler",
256+
"showlocation",
257+
"rotateatutc",
258+
},
259+
),
260+
}
261+
220262
@staticmethod
221263
def create_logger(logger_type: LoggerType | str, config: LoggerConfig | None = None, **kwargs) -> logging.Logger:
222264
"""
@@ -225,7 +267,7 @@ def create_logger(logger_type: LoggerType | str, config: LoggerConfig | None = N
225267
Args:
226268
logger_type: Type of logger to create (LoggerType enum or string)
227269
config: LoggerConfig object with logger parameters
228-
**kwargs: Individual logger parameters (for backward compatibility)
270+
**kwargs: Individual logger parameters (kwargs take precedence over config)
229271
230272
Returns:
231273
Configured logger instance
@@ -242,159 +284,21 @@ def create_logger(logger_type: LoggerType | str, config: LoggerConfig | None = N
242284
f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}"
243285
) from err
244286

245-
# Merge config and kwargs (kwargs take precedence for backward compatibility)
287+
# Merge config and kwargs (kwargs take precedence)
246288
if config is None:
247289
config = LoggerConfig()
248-
249-
# Create a new config with kwargs overriding config values
250-
final_config = LoggerConfig(
251-
level=kwargs.get("level", config.level),
252-
name=kwargs.get("name", config.name),
253-
directory=kwargs.get("directory", config.directory),
254-
filenames=kwargs.get("filenames", config.filenames),
255-
encoding=kwargs.get("encoding", config.encoding),
256-
datefmt=kwargs.get("datefmt", config.datefmt),
257-
timezone=kwargs.get("timezone", config.timezone),
258-
streamhandler=kwargs.get("streamhandler", config.streamhandler),
259-
showlocation=kwargs.get("showlocation", config.showlocation),
260-
maxmbytes=kwargs.get("maxmbytes", config.maxmbytes),
261-
when=kwargs.get("when", config.when),
262-
sufix=kwargs.get("sufix", config.sufix),
263-
rotateatutc=kwargs.get("rotateatutc", config.rotateatutc),
264-
daystokeep=kwargs.get("daystokeep", config.daystokeep),
265-
)
290+
merged = {f.name: kwargs.get(f.name, getattr(config, f.name)) for f in dataclasses.fields(LoggerConfig)}
266291

267292
# Convert enum values to strings for logger classes
268-
level_str = final_config.level.value if isinstance(final_config.level, LogLevel) else final_config.level
269-
when_str = final_config.when.value if isinstance(final_config.when, RotateWhen) else final_config.when
270-
271-
# Create logger based on type
272-
match logger_type:
273-
case LoggerType.BASIC:
274-
return _BasicLogImpl(
275-
level=level_str,
276-
name=final_config.name,
277-
encoding=final_config.encoding,
278-
datefmt=final_config.datefmt,
279-
timezone=final_config.timezone,
280-
showlocation=final_config.showlocation,
281-
).init()
282-
case LoggerType.SIZE_ROTATING:
283-
return _SizeRotatingLogImpl(
284-
level=level_str,
285-
name=final_config.name,
286-
directory=final_config.directory,
287-
filenames=final_config.filenames,
288-
maxmbytes=final_config.maxmbytes,
289-
daystokeep=final_config.daystokeep,
290-
encoding=final_config.encoding,
291-
datefmt=final_config.datefmt,
292-
timezone=final_config.timezone,
293-
streamhandler=final_config.streamhandler,
294-
showlocation=final_config.showlocation,
295-
).init()
296-
case LoggerType.TIMED_ROTATING:
297-
return _TimedRotatingLogImpl(
298-
level=level_str,
299-
name=final_config.name,
300-
directory=final_config.directory,
301-
filenames=final_config.filenames,
302-
when=when_str,
303-
sufix=final_config.sufix,
304-
daystokeep=final_config.daystokeep,
305-
encoding=final_config.encoding,
306-
datefmt=final_config.datefmt,
307-
timezone=final_config.timezone,
308-
streamhandler=final_config.streamhandler,
309-
showlocation=final_config.showlocation,
310-
rotateatutc=final_config.rotateatutc,
311-
).init()
312-
case _ as unreachable: # pragma: no cover
313-
assert_never(unreachable)
314-
315-
@staticmethod
316-
def create_basic_logger(
317-
level: LogLevel | str | None = None,
318-
name: str | None = None,
319-
encoding: str | None = None,
320-
datefmt: str | None = None,
321-
timezone: str | None = None,
322-
showlocation: bool | None = None,
323-
) -> logging.Logger:
324-
"""Convenience method for creating a basic logger"""
325-
return LoggerFactory.create_logger(
326-
LoggerType.BASIC,
327-
level=level,
328-
name=name,
329-
encoding=encoding,
330-
datefmt=datefmt,
331-
timezone=timezone,
332-
showlocation=showlocation,
333-
)
334-
335-
@staticmethod
336-
def create_size_rotating_logger(
337-
level: LogLevel | str | None = None,
338-
name: str | None = None,
339-
directory: str | None = None,
340-
filenames: list | tuple | None = None,
341-
maxmbytes: int | None = None,
342-
daystokeep: int | None = None,
343-
encoding: str | None = None,
344-
datefmt: str | None = None,
345-
timezone: str | None = None,
346-
streamhandler: bool | None = None,
347-
showlocation: bool | None = None,
348-
) -> logging.Logger:
349-
"""Convenience method for creating a size rotating logger"""
350-
return LoggerFactory.create_logger(
351-
LoggerType.SIZE_ROTATING,
352-
level=level,
353-
name=name,
354-
directory=directory,
355-
filenames=filenames,
356-
maxmbytes=maxmbytes,
357-
daystokeep=daystokeep,
358-
encoding=encoding,
359-
datefmt=datefmt,
360-
timezone=timezone,
361-
streamhandler=streamhandler,
362-
showlocation=showlocation,
363-
)
293+
if isinstance(merged.get("level"), LogLevel):
294+
merged["level"] = merged["level"].value
295+
if isinstance(merged.get("when"), RotateWhen):
296+
merged["when"] = merged["when"].value
364297

365-
@staticmethod
366-
def create_timed_rotating_logger(
367-
level: LogLevel | str | None = None,
368-
name: str | None = None,
369-
directory: str | None = None,
370-
filenames: list | tuple | None = None,
371-
when: RotateWhen | str | None = None,
372-
sufix: str | None = None,
373-
daystokeep: int | None = None,
374-
encoding: str | None = None,
375-
datefmt: str | None = None,
376-
timezone: str | None = None,
377-
streamhandler: bool | None = None,
378-
showlocation: bool | None = None,
379-
rotateatutc: bool | None = None,
380-
) -> logging.Logger:
381-
"""Convenience method for creating a timed rotating logger"""
382-
return LoggerFactory.create_logger(
383-
LoggerType.TIMED_ROTATING,
384-
level=level,
385-
name=name,
386-
directory=directory,
387-
filenames=filenames,
388-
when=when,
389-
sufix=sufix,
390-
daystokeep=daystokeep,
391-
encoding=encoding,
392-
datefmt=datefmt,
393-
timezone=timezone,
394-
streamhandler=streamhandler,
395-
showlocation=showlocation,
396-
rotateatutc=rotateatutc,
397-
)
298+
# Create logger using table-driven dispatch
299+
impl_class, valid_fields = LoggerFactory._LOGGER_IMPL[logger_type]
300+
logger_kwargs = {k: v for k, v in merged.items() if k in valid_fields}
301+
return impl_class(**logger_kwargs).init()
398302

399303

400304
# Public API wrapper classes - act like logging.Logger with context manager support
@@ -439,7 +343,8 @@ def __init__(
439343
timezone: str | None = None,
440344
showlocation: bool | None = None,
441345
):
442-
self._logger = LoggerFactory.create_basic_logger(
346+
self._logger = LoggerFactory.create_logger(
347+
LoggerType.BASIC,
443348
level=level,
444349
name=name,
445350
encoding=encoding,
@@ -477,7 +382,8 @@ def __init__(
477382
streamhandler: bool | None = None,
478383
showlocation: bool | None = None,
479384
):
480-
self._logger = LoggerFactory.create_size_rotating_logger(
385+
self._logger = LoggerFactory.create_logger(
386+
LoggerType.SIZE_ROTATING,
481387
level=level,
482388
name=name,
483389
directory=directory,
@@ -522,7 +428,8 @@ def __init__(
522428
showlocation: bool | None = None,
523429
rotateatutc: bool | None = None,
524430
):
525-
self._logger = LoggerFactory.create_timed_rotating_logger(
431+
self._logger = LoggerFactory.create_logger(
432+
LoggerType.TIMED_ROTATING,
526433
level=level,
527434
name=name,
528435
directory=directory,

tests/context_management/test_resource_management.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def test_factory_registry_cleanup(self):
5151
logger_name = "test_registry_cleanup"
5252

5353
# Create logger through factory (returns logging.Logger)
54-
logger = LoggerFactory.create_size_rotating_logger(
54+
logger = LoggerFactory.create_logger(
55+
LoggerType.SIZE_ROTATING,
5556
name=logger_name,
5657
directory=self.temp_dir,
5758
filenames=[self.log_file],
@@ -150,7 +151,8 @@ def test_registry_clear_with_file_handlers(self):
150151
logger_name = "test_file_handlers"
151152

152153
# Create logger with file handlers (returns logging.Logger)
153-
logger = LoggerFactory.create_size_rotating_logger(
154+
logger = LoggerFactory.create_logger(
155+
LoggerType.SIZE_ROTATING,
154156
name=logger_name,
155157
directory=self.temp_dir,
156158
filenames=[self.log_file, "second.log"],
@@ -186,7 +188,8 @@ def test_resource_cleanup_performance(self):
186188
# Create multiple loggers using factory
187189
start_time = time.time()
188190
for name in logger_names:
189-
logger = LoggerFactory.create_size_rotating_logger(
191+
logger = LoggerFactory.create_logger(
192+
LoggerType.SIZE_ROTATING,
190193
name=name,
191194
directory=self.temp_dir,
192195
filenames=[f"{name}.log"],
@@ -218,7 +221,8 @@ def test_memory_usage_after_cleanup(self):
218221
logger_name = "memory_test_logger"
219222

220223
# Create logger using factory (returns logging.Logger)
221-
logger = LoggerFactory.create_size_rotating_logger(
224+
logger = LoggerFactory.create_logger(
225+
LoggerType.SIZE_ROTATING,
222226
name=logger_name,
223227
directory=self.temp_dir,
224228
filenames=[self.log_file],
@@ -258,7 +262,7 @@ def create_and_cleanup_logger(index):
258262
"""Create a logger and immediately clean it up."""
259263
logger_name = f"concurrent_test_{index}"
260264
# Use factory to create logger (returns logging.Logger)
261-
logger = LoggerFactory.create_basic_logger(name=logger_name, level=LogLevel.INFO.value)
265+
logger = LoggerFactory.create_logger(LoggerType.BASIC, name=logger_name, level=LogLevel.INFO.value)
262266

263267
# Add to registry
264268
LoggerFactory._logger_registry[logger_name] = (logger, time.time())

tests/factory/test_factory.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,12 @@ def test_factory_create_logger_invalid_type(self):
164164
def test_factory_create_size_rotating_logger(self):
165165
"""Test factory create_size_rotating_logger method."""
166166
with tempfile.TemporaryDirectory() as temp_dir:
167-
logger = LoggerFactory.create_size_rotating_logger(
168-
name="size_factory_test", directory=temp_dir, maxmbytes=10, level=LogLevel.INFO
167+
logger = LoggerFactory.create_logger(
168+
LoggerType.SIZE_ROTATING,
169+
name="size_factory_test",
170+
directory=temp_dir,
171+
maxmbytes=10,
172+
level=LogLevel.INFO,
169173
)
170174
assert logger.name == "size_factory_test"
171175

@@ -176,14 +180,18 @@ def test_factory_create_size_rotating_logger(self):
176180
def test_factory_create_timed_rotating_logger(self):
177181
"""Test factory create_timed_rotating_logger method."""
178182
with tempfile.TemporaryDirectory() as temp_dir:
179-
logger = LoggerFactory.create_timed_rotating_logger(
180-
name="timed_factory_test", directory=temp_dir, when=RotateWhen.DAILY, level="ERROR"
183+
logger = LoggerFactory.create_logger(
184+
LoggerType.TIMED_ROTATING,
185+
name="timed_factory_test",
186+
directory=temp_dir,
187+
when=RotateWhen.DAILY,
188+
level="ERROR",
181189
)
182190
assert logger.name == "timed_factory_test"
183191

184192
def test_factory_create_basic_logger(self):
185193
"""Test factory create_basic_logger method."""
186-
logger = LoggerFactory.create_basic_logger(name="basic_factory_test", level=LogLevel.CRITICAL)
194+
logger = LoggerFactory.create_logger(LoggerType.BASIC, name="basic_factory_test", level=LogLevel.CRITICAL)
187195
assert logger.name == "basic_factory_test"
188196
assert logger.level == 50 # CRITICAL level
189197

tests/factory/test_factory_windows.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ def test_logger_with_file_output_windows(self):
6969
def test_factory_create_size_rotating_logger_windows(self):
7070
"""Test factory create_size_rotating_logger method on Windows."""
7171
with windows_safe_temp_directory() as temp_dir:
72-
logger = LoggerFactory.create_size_rotating_logger(
72+
logger = LoggerFactory.create_logger(
73+
LoggerType.SIZE_ROTATING,
7374
name="size_factory_test_win",
7475
directory=temp_dir,
7576
maxmbytes=10,
@@ -81,7 +82,8 @@ def test_factory_create_size_rotating_logger_windows(self):
8182
def test_factory_create_timed_rotating_logger_windows(self):
8283
"""Test factory create_timed_rotating_logger method on Windows."""
8384
with windows_safe_temp_directory() as temp_dir:
84-
logger = LoggerFactory.create_timed_rotating_logger(
85+
logger = LoggerFactory.create_logger(
86+
LoggerType.TIMED_ROTATING,
8587
name="timed_factory_test_win",
8688
directory=temp_dir,
8789
when=RotateWhen.DAILY,
@@ -379,15 +381,17 @@ def test_windows_file_locking_resilience_factory(self):
379381
"""Test that Windows file locking resilience works with factory-created loggers."""
380382
with windows_safe_temp_directory() as temp_dir:
381383
# Create multiple loggers that write to the same directory
382-
logger1 = LoggerFactory.create_size_rotating_logger(
384+
logger1 = LoggerFactory.create_logger(
385+
LoggerType.SIZE_ROTATING,
383386
name="resilience_test1_win",
384387
directory=temp_dir,
385388
filenames=["resilience1.log"],
386389
maxmbytes=1,
387390
level=LogLevel.INFO,
388391
)
389392

390-
logger2 = LoggerFactory.create_timed_rotating_logger(
393+
logger2 = LoggerFactory.create_logger(
394+
LoggerType.TIMED_ROTATING,
391395
name="resilience_test2_win",
392396
directory=temp_dir,
393397
filenames=["resilience2.log"],

0 commit comments

Comments
 (0)