python-utils 2.5.6__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
python_utils/logger.py CHANGED
@@ -1,18 +1,165 @@
1
+ """
2
+ This module provides a base class `LoggerBase` and a derived class `Logged`
3
+ for adding logging capabilities to classes. The `LoggerBase` class expects
4
+ a `logger` attribute to be a `logging.Logger` or compatible instance and
5
+ provides methods for logging at various levels. The `Logged` class
6
+ automatically adds a named logger to the class.
7
+
8
+ Classes:
9
+ LoggerBase:
10
+ A base class that adds logging utilities to a class.
11
+ Logged:
12
+ A derived class that automatically adds a named logger to a class.
13
+
14
+ Example:
15
+ >>> class MyClass(Logged):
16
+ ... def __init__(self):
17
+ ... Logged.__init__(self)
18
+
19
+ >>> my_class = MyClass()
20
+ >>> my_class.debug('debug')
21
+ >>> my_class.info('info')
22
+ >>> my_class.warning('warning')
23
+ >>> my_class.error('error')
24
+ >>> my_class.exception('exception')
25
+ >>> my_class.log(0, 'log')
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import abc
31
+ import collections.abc
1
32
  import logging
2
- import functools
33
+ import types
34
+ import typing
35
+
36
+ if typing.TYPE_CHECKING:
37
+ import typing_extensions
38
+
39
+ from . import decorators
3
40
 
4
41
  __all__ = ['Logged']
5
42
 
43
+ #: Accepted values for the logging ``exc_info`` parameter: a bool, an
44
+ #: ``(type, value, traceback)`` triple, a bare exception instance, or ``None``.
45
+ # From the logging typeshed, converted to be compatible with Python 3.8
46
+ # https://github.com/python/typeshed/blob/main/stdlib/logging/__init__.pyi
47
+ _ExcInfoType: typing.TypeAlias = (
48
+ bool
49
+ | tuple[
50
+ type[BaseException],
51
+ BaseException,
52
+ types.TracebackType | None,
53
+ ]
54
+ | tuple[None, None, None]
55
+ | BaseException
56
+ | None
57
+ )
58
+ #: Parameter specification capturing a wrapped logger method's arguments.
59
+ _P = typing.ParamSpec('_P')
60
+ #: Covariant return-type variable for wrapped logger methods.
61
+ _T = typing.TypeVar('_T', covariant=True)
62
+
63
+
64
+ class LoggerProtocol(typing.Protocol):
65
+ """Structural type for any ``logging.Logger``-compatible logger.
66
+
67
+ Objects that provide these methods (such as a ``logging.Logger`` or a
68
+ ``loguru`` logger) can be used as the ``logger`` attribute of a
69
+ ``LoggerBase`` subclass. Only the shape matters; no inheritance required.
70
+ """
71
+
72
+ def debug(
73
+ self,
74
+ msg: object,
75
+ *args: object,
76
+ exc_info: _ExcInfoType = None,
77
+ stack_info: bool = False,
78
+ stacklevel: int = 1,
79
+ extra: collections.abc.Mapping[str, object] | None = None,
80
+ ) -> None:
81
+ """Log ``msg`` at ``DEBUG`` level."""
82
+
83
+ def info(
84
+ self,
85
+ msg: object,
86
+ *args: object,
87
+ exc_info: _ExcInfoType = None,
88
+ stack_info: bool = False,
89
+ stacklevel: int = 1,
90
+ extra: collections.abc.Mapping[str, object] | None = None,
91
+ ) -> None:
92
+ """Log ``msg`` at ``INFO`` level."""
6
93
 
7
- class Logged(object):
8
- '''Class which automatically adds a named logger to your class when
9
- interiting
94
+ def warning(
95
+ self,
96
+ msg: object,
97
+ *args: object,
98
+ exc_info: _ExcInfoType = None,
99
+ stack_info: bool = False,
100
+ stacklevel: int = 1,
101
+ extra: collections.abc.Mapping[str, object] | None = None,
102
+ ) -> None:
103
+ """Log ``msg`` at ``WARNING`` level."""
104
+
105
+ def error(
106
+ self,
107
+ msg: object,
108
+ *args: object,
109
+ exc_info: _ExcInfoType = None,
110
+ stack_info: bool = False,
111
+ stacklevel: int = 1,
112
+ extra: collections.abc.Mapping[str, object] | None = None,
113
+ ) -> None:
114
+ """Log ``msg`` at ``ERROR`` level."""
115
+
116
+ def critical(
117
+ self,
118
+ msg: object,
119
+ *args: object,
120
+ exc_info: _ExcInfoType = None,
121
+ stack_info: bool = False,
122
+ stacklevel: int = 1,
123
+ extra: collections.abc.Mapping[str, object] | None = None,
124
+ ) -> None:
125
+ """Log ``msg`` at ``CRITICAL`` level."""
126
+
127
+ def exception(
128
+ self,
129
+ msg: object,
130
+ *args: object,
131
+ exc_info: _ExcInfoType = None,
132
+ stack_info: bool = False,
133
+ stacklevel: int = 1,
134
+ extra: collections.abc.Mapping[str, object] | None = None,
135
+ ) -> None:
136
+ """Log ``msg`` at ``ERROR`` level, including exception information."""
137
+
138
+ def log(
139
+ self,
140
+ level: int,
141
+ msg: object,
142
+ *args: object,
143
+ exc_info: _ExcInfoType = None,
144
+ stack_info: bool = False,
145
+ stacklevel: int = 1,
146
+ extra: collections.abc.Mapping[str, object] | None = None,
147
+ ) -> None:
148
+ """Log ``msg`` at the integer ``level``."""
149
+
150
+
151
+ class LoggerBase(abc.ABC):
152
+ """Class which automatically adds logging utilities to your class when
153
+ inheriting. Expects `logger` to be a logging.Logger or compatible instance.
10
154
 
11
155
  Adds easy access to debug, info, warning, error, exception and log methods
12
156
 
13
- >>> class MyClass(Logged):
157
+ >>> class MyClass(LoggerBase):
158
+ ... logger = logging.getLogger(__name__)
159
+ ...
14
160
  ... def __init__(self):
15
161
  ... Logged.__init__(self)
162
+
16
163
  >>> my_class = MyClass()
17
164
  >>> my_class.debug('debug')
18
165
  >>> my_class.info('info')
@@ -20,43 +167,219 @@ class Logged(object):
20
167
  >>> my_class.error('error')
21
168
  >>> my_class.exception('exception')
22
169
  >>> my_class.log(0, 'log')
23
- '''
24
- def __new__(cls, *args, **kwargs):
25
- cls.logger = logging.getLogger(
26
- cls.__get_name(cls.__module__, cls.__name__))
27
- return super(Logged, cls).__new__(cls)
170
+ """
171
+
172
+ # I've tried using a protocol to properly type the logger but it gave all
173
+ # sorts of issues with mypy so we're using the lazy solution for now. The
174
+ # actual classes define the correct type anyway
175
+ logger: typing.Any
176
+ # logger: LoggerProtocol
28
177
 
29
178
  @classmethod
30
- def __get_name(cls, *name_parts):
179
+ def __get_name( # pyright: ignore[reportUnusedFunction]
180
+ cls, *name_parts: str
181
+ ) -> str:
182
+ """Join the non-empty, stripped ``name_parts`` into a dotted name."""
31
183
  return '.'.join(n.strip() for n in name_parts if n.strip())
32
184
 
185
+ @decorators.wraps_classmethod(logging.Logger.debug)
186
+ @classmethod
187
+ def debug(
188
+ cls,
189
+ msg: object,
190
+ *args: object,
191
+ exc_info: _ExcInfoType = None,
192
+ stack_info: bool = False,
193
+ stacklevel: int = 1,
194
+ extra: collections.abc.Mapping[str, object] | None = None,
195
+ ) -> None:
196
+ """Log ``msg`` at ``DEBUG`` level on the class logger."""
197
+ return cls.logger.debug( # type: ignore[no-any-return]
198
+ msg,
199
+ *args,
200
+ exc_info=exc_info,
201
+ stack_info=stack_info,
202
+ stacklevel=stacklevel,
203
+ extra=extra,
204
+ )
205
+
206
+ @decorators.wraps_classmethod(logging.Logger.info)
33
207
  @classmethod
34
- @functools.wraps(logging.debug)
35
- def debug(cls, msg, *args, **kwargs):
36
- cls.logger.debug(msg, *args, **kwargs)
208
+ def info(
209
+ cls,
210
+ msg: object,
211
+ *args: object,
212
+ exc_info: _ExcInfoType = None,
213
+ stack_info: bool = False,
214
+ stacklevel: int = 1,
215
+ extra: collections.abc.Mapping[str, object] | None = None,
216
+ ) -> None:
217
+ """Log ``msg`` at ``INFO`` level on the class logger."""
218
+ return cls.logger.info( # type: ignore[no-any-return]
219
+ msg,
220
+ *args,
221
+ exc_info=exc_info,
222
+ stack_info=stack_info,
223
+ stacklevel=stacklevel,
224
+ extra=extra,
225
+ )
37
226
 
227
+ @decorators.wraps_classmethod(logging.Logger.warning)
38
228
  @classmethod
39
- @functools.wraps(logging.info)
40
- def info(cls, msg, *args, **kwargs):
41
- cls.logger.info(msg, *args, **kwargs)
229
+ def warning(
230
+ cls,
231
+ msg: object,
232
+ *args: object,
233
+ exc_info: _ExcInfoType = None,
234
+ stack_info: bool = False,
235
+ stacklevel: int = 1,
236
+ extra: collections.abc.Mapping[str, object] | None = None,
237
+ ) -> None:
238
+ """Log ``msg`` at ``WARNING`` level on the class logger."""
239
+ return cls.logger.warning( # type: ignore[no-any-return]
240
+ msg,
241
+ *args,
242
+ exc_info=exc_info,
243
+ stack_info=stack_info,
244
+ stacklevel=stacklevel,
245
+ extra=extra,
246
+ )
42
247
 
248
+ @decorators.wraps_classmethod(logging.Logger.error)
43
249
  @classmethod
44
- @functools.wraps(logging.warning)
45
- def warning(cls, msg, *args, **kwargs):
46
- cls.logger.warning(msg, *args, **kwargs)
250
+ def error(
251
+ cls,
252
+ msg: object,
253
+ *args: object,
254
+ exc_info: _ExcInfoType = None,
255
+ stack_info: bool = False,
256
+ stacklevel: int = 1,
257
+ extra: collections.abc.Mapping[str, object] | None = None,
258
+ ) -> None:
259
+ """Log ``msg`` at ``ERROR`` level on the class logger."""
260
+ return cls.logger.error( # type: ignore[no-any-return]
261
+ msg,
262
+ *args,
263
+ exc_info=exc_info,
264
+ stack_info=stack_info,
265
+ stacklevel=stacklevel,
266
+ extra=extra,
267
+ )
47
268
 
269
+ @decorators.wraps_classmethod(logging.Logger.critical)
48
270
  @classmethod
49
- @functools.wraps(logging.error)
50
- def error(cls, msg, *args, **kwargs):
51
- cls.logger.error(msg, *args, **kwargs)
271
+ def critical(
272
+ cls,
273
+ msg: object,
274
+ *args: object,
275
+ exc_info: _ExcInfoType = None,
276
+ stack_info: bool = False,
277
+ stacklevel: int = 1,
278
+ extra: collections.abc.Mapping[str, object] | None = None,
279
+ ) -> None:
280
+ """Log ``msg`` at ``CRITICAL`` level on the class logger."""
281
+ return cls.logger.critical( # type: ignore[no-any-return]
282
+ msg,
283
+ *args,
284
+ exc_info=exc_info,
285
+ stack_info=stack_info,
286
+ stacklevel=stacklevel,
287
+ extra=extra,
288
+ )
52
289
 
290
+ @decorators.wraps_classmethod(logging.Logger.exception)
53
291
  @classmethod
54
- @functools.wraps(logging.exception)
55
- def exception(cls, msg, *args, **kwargs):
56
- cls.logger.exception(msg, *args, **kwargs)
292
+ def exception(
293
+ cls,
294
+ msg: object,
295
+ *args: object,
296
+ exc_info: _ExcInfoType = None,
297
+ stack_info: bool = False,
298
+ stacklevel: int = 1,
299
+ extra: collections.abc.Mapping[str, object] | None = None,
300
+ ) -> None:
301
+ """Log ``msg`` at ``ERROR`` level with exception info attached."""
302
+ return cls.logger.exception( # type: ignore[no-any-return]
303
+ msg,
304
+ *args,
305
+ exc_info=exc_info,
306
+ stack_info=stack_info,
307
+ stacklevel=stacklevel,
308
+ extra=extra,
309
+ )
57
310
 
311
+ @decorators.wraps_classmethod(logging.Logger.log)
58
312
  @classmethod
59
- @functools.wraps(logging.log)
60
- def log(cls, lvl, msg, *args, **kwargs):
61
- cls.logger.log(lvl, msg, *args, **kwargs)
313
+ def log(
314
+ cls,
315
+ level: int,
316
+ msg: object,
317
+ *args: object,
318
+ exc_info: _ExcInfoType = None,
319
+ stack_info: bool = False,
320
+ stacklevel: int = 1,
321
+ extra: collections.abc.Mapping[str, object] | None = None,
322
+ ) -> None:
323
+ """Log ``msg`` at the integer ``level`` on the class logger."""
324
+ return cls.logger.log( # type: ignore[no-any-return]
325
+ level,
326
+ msg,
327
+ *args,
328
+ exc_info=exc_info,
329
+ stack_info=stack_info,
330
+ stacklevel=stacklevel,
331
+ extra=extra,
332
+ )
333
+
62
334
 
335
+ class Logged(LoggerBase):
336
+ """Class which automatically adds a named logger to your class when
337
+ inheriting.
338
+
339
+ Adds easy access to debug, info, warning, error, exception and log methods
340
+
341
+ >>> class MyClass(Logged):
342
+ ... def __init__(self):
343
+ ... Logged.__init__(self)
344
+
345
+ >>> my_class = MyClass()
346
+ >>> my_class.debug('debug')
347
+ >>> my_class.info('info')
348
+ >>> my_class.warning('warning')
349
+ >>> my_class.error('error')
350
+ >>> my_class.exception('exception')
351
+ >>> my_class.log(0, 'log')
352
+
353
+ >>> my_class._Logged__get_name('spam')
354
+ 'spam'
355
+ """
356
+
357
+ logger: logging.Logger # pragma: no cover
358
+
359
+ @classmethod
360
+ def __get_name(cls, *name_parts: str) -> str:
361
+ """Build the dotted logger name via ``LoggerBase``'s joiner."""
362
+ return typing.cast(
363
+ str,
364
+ LoggerBase._LoggerBase__get_name(*name_parts), # type: ignore[attr-defined] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
365
+ )
366
+
367
+ def __new__(
368
+ cls, *args: typing.Any, **kwargs: typing.Any
369
+ ) -> typing_extensions.Self:
370
+ """
371
+ Create a new instance of the class and initialize the logger.
372
+
373
+ The logger is named using the module and class name.
374
+
375
+ Args:
376
+ *args: Variable length argument list.
377
+ **kwargs: Arbitrary keyword arguments.
378
+
379
+ Returns:
380
+ An instance of the class.
381
+ """
382
+ cls.logger = logging.getLogger(
383
+ cls.__get_name(cls.__module__, cls.__name__)
384
+ )
385
+ return super().__new__(cls)
python_utils/loguru.py ADDED
@@ -0,0 +1,53 @@
1
+ """
2
+ This module provides a `Logurud` class that integrates the `loguru` logger
3
+ with the base logging functionality defined in `logger_module.LoggerBase`.
4
+
5
+ Classes:
6
+ Logurud: A class that extends `LoggerBase` and uses `loguru` for logging.
7
+
8
+ Usage example:
9
+ >>> from python_utils.loguru import Logurud
10
+ >>> class MyClass(Logurud):
11
+ ... def __init__(self):
12
+ ... Logurud.__init__(self)
13
+ >>> my_class = MyClass()
14
+ >>> my_class.logger.info('This is an info message')
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import typing
20
+
21
+ import loguru
22
+
23
+ from . import logger as logger_module
24
+
25
+ __all__ = ['Logurud']
26
+
27
+
28
+ class Logurud(logger_module.LoggerBase):
29
+ """
30
+ A class that extends `LoggerBase` and uses `loguru` for logging.
31
+
32
+ Attributes:
33
+ logger (loguru.Logger): The `loguru` logger instance.
34
+ """
35
+
36
+ logger: loguru.Logger
37
+
38
+ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Logurud:
39
+ """
40
+ Creates a new instance of `Logurud` and initializes the `loguru`
41
+ logger.
42
+
43
+ Args:
44
+ *args (typing.Any): Variable length argument list.
45
+ **kwargs (typing.Any): Arbitrary keyword arguments.
46
+
47
+ Returns:
48
+ Logurud: A new instance of `Logurud`.
49
+ """
50
+ # `logger` is already declared at class scope; assign without
51
+ # re-annotating to avoid an obscured-declaration error.
52
+ cls.logger = loguru.logger.opt(depth=1)
53
+ return super().__new__(cls)