annotated-logger 1.2.1__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.
@@ -0,0 +1,966 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import inspect
5
+ import logging
6
+ import logging.config
7
+ import time
8
+ import uuid
9
+ from collections.abc import Iterator
10
+ from copy import copy, deepcopy
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Callable,
15
+ Concatenate,
16
+ Literal,
17
+ ParamSpec,
18
+ Protocol,
19
+ TypeVar,
20
+ cast,
21
+ overload,
22
+ )
23
+
24
+ from makefun import wraps
25
+
26
+ from annotated_logger.filter import AnnotatedFilter
27
+ from annotated_logger.plugins import BasePlugin
28
+
29
+ if TYPE_CHECKING: # pragma: no cover
30
+ from collections.abc import MutableMapping
31
+
32
+ # Use 0.0.0.dev1 and so on when working in a PR
33
+ # Each push attempts to upload to testpypi, but it only works with a unique version
34
+ # https://test.pypi.org/project/annotated-logger/
35
+ # The dev versions in testpypi can then be pulled in to whatever project needed
36
+ # the new feature.
37
+ VERSION = "1.2.1" # pragma: no mutate
38
+
39
+ T = TypeVar("T")
40
+ P = ParamSpec("P")
41
+ P2 = ParamSpec("P2")
42
+ P3 = ParamSpec("P3")
43
+ R = TypeVar("R")
44
+ S = TypeVar("S")
45
+ S2 = TypeVar("S2")
46
+ C_co = TypeVar("C_co", covariant=True)
47
+
48
+
49
+ class AnnotatedClass(Protocol[C_co]):
50
+ """Protocol for typing classes that we annotate and add the logger to."""
51
+
52
+ annotated_logger: AnnotatedAdapter
53
+
54
+
55
+ PreCall = Callable[Concatenate[S, "AnnotatedAdapter", P], None] | None
56
+ PostCall = Callable[Concatenate[S, "AnnotatedAdapter", P], None] | None
57
+ SelfLoggerAndParams = Callable[Concatenate[S, "AnnotatedAdapter", P], R]
58
+ LoggerAndParams = Callable[Concatenate["AnnotatedAdapter", P], R]
59
+ SelfAndParams = Callable[Concatenate[S, P], R]
60
+ ParamsOnly = Callable[P, R]
61
+ SelfAndLogger = Callable[[S, "AnnotatedAdapter"], R]
62
+ LoggerOnly = Callable[["AnnotatedAdapter"], R]
63
+ SelfOnly = Callable[[S], R]
64
+ Empty = Callable[[], R]
65
+
66
+ NoInjectionSelf = Callable[[SelfAndParams[S, P, R]], SelfAndParams[S, P, R]]
67
+ NoInjectionBare = Callable[[ParamsOnly[P, R]], ParamsOnly[P, R]]
68
+ InjectionSelf = Callable[[SelfLoggerAndParams[S, P, R]], SelfAndParams[S, P, R]]
69
+ InjectionSelfProvide = Callable[
70
+ [SelfLoggerAndParams[S, P, R]], SelfLoggerAndParams[S, P, R]
71
+ ]
72
+ InjectionBare = Callable[[LoggerAndParams[P, R]], ParamsOnly[P, R]]
73
+ InjectionBareProvide = Callable[[LoggerAndParams[P, R]], LoggerAndParams[P, R]]
74
+
75
+ Function = (
76
+ SelfLoggerAndParams[S, P, R]
77
+ | SelfAndParams[S, P, R]
78
+ | SelfAndLogger[S, R]
79
+ | SelfOnly[S, R]
80
+ | LoggerAndParams[P, R]
81
+ | ParamsOnly[P, R]
82
+ | LoggerOnly[R]
83
+ | Empty[R]
84
+ )
85
+ Decorator = (
86
+ NoInjectionSelf[S, P, R]
87
+ | InjectionSelf[S, P, R]
88
+ | InjectionSelfProvide[S, P, R]
89
+ | NoInjectionBare[P, R]
90
+ | InjectionBare[P, R]
91
+ | InjectionBareProvide[P, R]
92
+ )
93
+ Annotations = dict[str, Any]
94
+
95
+
96
+ DEFAULT_LOGGING_CONFIG = {
97
+ "version": 1,
98
+ "disable_existing_loggers": False, # pragma: no mutate
99
+ "filters": {
100
+ "annotated_filter": {
101
+ "annotated_filter": True, # pragma: no mutate
102
+ }
103
+ },
104
+ "handlers": {
105
+ "annotated_handler": {
106
+ "class": "logging.StreamHandler",
107
+ "formatter": "annotated_formatter",
108
+ },
109
+ },
110
+ "formatters": {
111
+ "annotated_formatter": {
112
+ "class": "pythonjsonlogger.jsonlogger.JsonFormatter", # pragma: no mutate
113
+ "format": "{created} {levelname} {name} {message}", # pragma: no mutate
114
+ "style": "{",
115
+ },
116
+ },
117
+ "loggers": {
118
+ "annotated_logger": {
119
+ "level": "DEBUG",
120
+ "handlers": ["annotated_handler"],
121
+ "propagate": False, # pragma: no mutate
122
+ },
123
+ },
124
+ }
125
+
126
+
127
+ class AnnotatedIterator(Iterator[T]):
128
+ """Iterator that logs as it iterates."""
129
+
130
+ def __init__(
131
+ self,
132
+ logger: AnnotatedAdapter,
133
+ name: str,
134
+ wrapped: Iterator[T],
135
+ *,
136
+ value: bool,
137
+ level: str,
138
+ ) -> None:
139
+ """Store the wrapped iterator, the logger and note if we log the value."""
140
+ self.wrapped = wrapped
141
+ self.logger = logger
142
+ self.extras: dict[str, T | str] = {"iterator": name}
143
+ self.value = value
144
+ log_methods = {
145
+ "debug": self.logger.debug,
146
+ "info": self.logger.info,
147
+ "warning": self.logger.warning,
148
+ "error": self.logger.error,
149
+ "exception": self.logger.exception,
150
+ }
151
+ self.log_method = log_methods[level]
152
+
153
+ def __iter__(self) -> AnnotatedIterator[T]:
154
+ """Log the start of the iteration."""
155
+ self.log_method("Starting iteration", extra=self.extras)
156
+ return self
157
+
158
+ def __next__(self) -> T:
159
+ """Log that we are at the next iteration."""
160
+ try:
161
+ value = next(self.wrapped)
162
+ if self.value:
163
+ self.extras["value"] = value
164
+ except StopIteration:
165
+ self.log_method("Execution complete", extra=self.extras)
166
+ raise
167
+
168
+ self.log_method("next", extra=self.extras)
169
+ return value
170
+
171
+
172
+ class AnnotatedAdapter(logging.LoggerAdapter): # pyright: ignore[reportMissingTypeArgument]
173
+ """Adapter that provides extra methods."""
174
+
175
+ def __init__(
176
+ self,
177
+ logger: logging.Logger,
178
+ annotated_filter: AnnotatedFilter,
179
+ max_length: int | None = None,
180
+ ) -> None:
181
+ """Adapter that acts like a LogRecord, but allows for annotations."""
182
+ self.filter = annotated_filter
183
+ self.logger = logger
184
+ self.logger.addFilter(annotated_filter)
185
+ self.max_length = max_length
186
+
187
+ # We don't need to send in contextual information here
188
+ # as we do it in the filter for runtime stuff
189
+ super().__init__(logger)
190
+
191
+ def iterator(
192
+ self,
193
+ name: str,
194
+ wrapped: Iterator[T],
195
+ *,
196
+ value: bool = True,
197
+ level: str = "info",
198
+ ) -> AnnotatedIterator[T]:
199
+ """Return an iterator that logs as it iterates."""
200
+ return AnnotatedIterator(self, name, wrapped, value=value, level=level)
201
+
202
+ def process(
203
+ self, msg: str, kwargs: MutableMapping[str, Any]
204
+ ) -> tuple[str, MutableMapping[str, Any]]:
205
+ """Override default LoggerAdapter process behavior.
206
+
207
+ By default a LoggerAdapter replaces the extras passed in a logger call with
208
+ the ones given at it's initialization. That's not the behavior we want.
209
+ So, we just return the kwargs as provided instead.
210
+
211
+ 3.13 adds a `merge_extra` argument which should make this method unneeded.
212
+ But, it doesn't make sense to force everyone to use python 3.13.
213
+ """
214
+ return msg, kwargs
215
+
216
+ def log(
217
+ self,
218
+ level: int,
219
+ msg: object,
220
+ *args: object,
221
+ **kwargs: object,
222
+ ) -> None:
223
+ """Override log method to allow for message splitting."""
224
+ if not self.max_length or not isinstance(msg, str):
225
+ return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType]
226
+
227
+ msg_len = len(msg) # pyright: ignore[reportArgumentType]
228
+ if msg_len <= self.max_length:
229
+ return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType]
230
+
231
+ msg_chunks = []
232
+ while len(msg) > self.max_length: # pyright: ignore[reportArgumentType] # pragma: no mutate
233
+ msg_chunks.append(msg[: self.max_length]) # pyright: ignore[reportArgumentType]
234
+ msg = msg[self.max_length :] # pyright: ignore[reportArgumentType]
235
+ kwargs["extra"] = {"message_parts": len(msg_chunks) + 1, "split": True}
236
+ kwargs["extra"]["split_complete"] = False
237
+ for i, part in enumerate(msg_chunks):
238
+ kwargs["extra"]["message_part"] = i + 1
239
+ super().log(
240
+ level,
241
+ part,
242
+ *args,
243
+ **kwargs, # pyright: ignore[reportArgumentType]
244
+ )
245
+ kwargs["extra"]["message_part"] = len(msg_chunks) + 1
246
+ kwargs["extra"]["split_complete"] = True
247
+ return super().log(level, msg, *args, **kwargs) # pyright: ignore[reportArgumentType]
248
+
249
+ def annotate(self, *, persist: bool = False, **kwargs: Any) -> None:
250
+ """Add an annotation to the filter."""
251
+ if persist:
252
+ self.filter.class_annotations.update(kwargs)
253
+ else:
254
+ self.filter.annotations.update(kwargs)
255
+
256
+
257
+ class AnnotatedLogger:
258
+ """Class that contains settings and the decorator method.
259
+
260
+ Args:
261
+ ----
262
+ annotations: Dictionary of annotations to be added to every log message
263
+ plugins: list of plugins to use
264
+
265
+ Methods:
266
+ -------
267
+ annotate_logs: Decorator that will insert the `annotated_logger` argument if
268
+ asked for in the method signature or let a provided AnnotatedAdapter to be
269
+ passed. Creates a new AnnotatedAdapter instance for each invocation of a
270
+ annotated function to isolate any annotations that are set during execution.
271
+
272
+ """
273
+
274
+ def __init__( # noqa: PLR0913
275
+ self,
276
+ annotations: dict[str, Any] | None = None,
277
+ plugins: list[BasePlugin] | None = None,
278
+ max_length: int | None = None,
279
+ log_level: int = logging.INFO,
280
+ name: str = "annotated_logger",
281
+ config: dict[str, Any] | Literal[False] | None = None,
282
+ ) -> None:
283
+ """Store the settings.
284
+
285
+ Args:
286
+ ----
287
+ annotations: Dictionary of static annotations - default None
288
+ plugins: List of plugins to be applied - default [BasePlugin]
289
+ is created and used - default None
290
+ max_length: Integer, maximum length of a message before it's broken into
291
+ multiple message and log calls. - default None
292
+ log_level: Integer, log level set for the shared root logger of the package.
293
+ - default logging.INFO (20)
294
+ name: Name of the shared root logger of the package. If more than one
295
+ `AnnotatedLogger` object is created in a project this should be set,
296
+ otherwise settings like level will be overwritten by the second to execute
297
+ - default 'annotated_logger'
298
+ config: Optional - logging config dictionary to be passed to
299
+ logging.config.dictConfig or False. If false dictConfig will not be called.
300
+ If not passed the DEFAULT_LOGGING_CONFIG will be used. A special
301
+ `annotated_filter` keyword is looked for, if present it will be
302
+ replaced with a `()` filter config to generate a filter for this
303
+ instance of `AnnotatedLogger`.
304
+
305
+ """
306
+ if plugins is None:
307
+ plugins = []
308
+
309
+ self.log_level = log_level
310
+ self.logger_root_name = name
311
+ self.logger_base = logging.getLogger(self.logger_root_name)
312
+ self.logger_base.setLevel(self.log_level)
313
+ self.annotations = annotations or {}
314
+ self.plugins = [BasePlugin()]
315
+ self.plugins.extend(plugins)
316
+
317
+ if config is None:
318
+ config = deepcopy(DEFAULT_LOGGING_CONFIG)
319
+ if config:
320
+ for config_filter in config["filters"].values():
321
+ if config_filter.get("annotated_filter"):
322
+ del config_filter["annotated_filter"]
323
+ config_filter["()"] = self.generate_filter
324
+
325
+ # If we pass in config=False we don't want to configure.
326
+ # This is typically because we have another AnnotatedLogger
327
+ # object which did run the config and the dict config had config
328
+ # for both.
329
+ if config:
330
+ logging.config.dictConfig(config)
331
+
332
+ self.max_length = max_length
333
+
334
+ def _generate_logger(
335
+ self,
336
+ function: Function[S, P, R] | None = None,
337
+ cls: type | None = None,
338
+ logger_base_name: str | None = None,
339
+ ) -> AnnotatedAdapter:
340
+ """Generate a unique adapter with a unique logger object.
341
+
342
+ This is required because the AnnotatedAdapter adds a filter to the logger.
343
+ The filter stores the annotations inside it, so they will mix if a new filter
344
+ and logger are not created each time.
345
+ """
346
+ root_name = logger_base_name or self.logger_root_name
347
+ logger = logging.getLogger(
348
+ f"{root_name}.{uuid.uuid4()}" # pragma: no mutate
349
+ )
350
+
351
+ annotated_filter = self.generate_filter(function=function, cls=cls)
352
+
353
+ return AnnotatedAdapter(logger, annotated_filter, self.max_length)
354
+
355
+ def _action_annotation(
356
+ self, function: Function[S, P, R], key: str = "action"
357
+ ) -> dict[str, str]:
358
+ return {key: f"{function.__module__}:{function.__qualname__}"}
359
+
360
+ def generate_filter(
361
+ self,
362
+ function: Function[S, P, R] | None = None,
363
+ cls: type[C_co] | None = None,
364
+ annotations: dict[str, Any] | None = None,
365
+ ) -> AnnotatedFilter:
366
+ """Create a AnnotatedFilter with the correct annotations and plugins."""
367
+ annotations_passed = annotations
368
+ annotations = annotations or {}
369
+ if function:
370
+ annotations.update(self._action_annotation(function))
371
+ class_annotations = {}
372
+ elif cls:
373
+ class_annotations = {"class": f"{cls.__module__}:{cls.__qualname__}"}
374
+ else:
375
+ class_annotations = {}
376
+ if not annotations_passed:
377
+ annotations.update(self.annotations)
378
+
379
+ return AnnotatedFilter(
380
+ annotations=annotations,
381
+ class_annotations=class_annotations,
382
+ plugins=self.plugins,
383
+ )
384
+
385
+ #### Defaults
386
+ @overload
387
+ def annotate_logs(
388
+ self,
389
+ logger_name: str | None = None,
390
+ *,
391
+ success_info: bool = True, # pragma: no mutate
392
+ pre_call: PreCall[S2, P2] = None,
393
+ post_call: PostCall[S2, P3] = None,
394
+ ) -> NoInjectionSelf[S, P, R]: ...
395
+
396
+ @overload
397
+ def annotate_logs(
398
+ self,
399
+ logger_name: str | None = None,
400
+ *,
401
+ success_info: bool = True, # pragma: no mutate
402
+ pre_call: PreCall[S2, P2] = None,
403
+ post_call: PostCall[S2, P3] = None,
404
+ _typing_requested: Literal[False],
405
+ ) -> NoInjectionSelf[S, P, R]: ...
406
+
407
+ @overload
408
+ def annotate_logs(
409
+ self,
410
+ logger_name: str | None = None,
411
+ *,
412
+ success_info: bool = True, # pragma: no mutate
413
+ pre_call: PreCall[S2, P2] = None,
414
+ post_call: PostCall[S2, P3] = None,
415
+ provided: Literal[False],
416
+ ) -> NoInjectionSelf[S, P, R]: ...
417
+
418
+ @overload
419
+ def annotate_logs(
420
+ self,
421
+ logger_name: str | None = None,
422
+ *,
423
+ success_info: bool = True, # pragma: no mutate
424
+ pre_call: PreCall[S2, P2] = None,
425
+ post_call: PostCall[S2, P3] = None,
426
+ _typing_self: Literal[True],
427
+ ) -> NoInjectionSelf[S, P, R]: ...
428
+
429
+ @overload
430
+ def annotate_logs(
431
+ self,
432
+ logger_name: str | None = None,
433
+ *,
434
+ success_info: bool = True, # pragma: no mutate
435
+ pre_call: PreCall[S2, P2] = None,
436
+ post_call: PostCall[S2, P3] = None,
437
+ ) -> NoInjectionSelf[S, P, R]: ...
438
+
439
+ @overload
440
+ def annotate_logs(
441
+ self,
442
+ logger_name: str | None = None,
443
+ *,
444
+ success_info: bool = True, # pragma: no mutate
445
+ pre_call: PreCall[S2, P2] = None,
446
+ post_call: PostCall[S2, P3] = None,
447
+ _typing_self: Literal[True],
448
+ _typing_requested: Literal[False],
449
+ ) -> NoInjectionSelf[S, P, R]: ...
450
+
451
+ @overload
452
+ def annotate_logs(
453
+ self,
454
+ logger_name: str | None = None,
455
+ *,
456
+ success_info: bool = True, # pragma: no mutate
457
+ pre_call: PreCall[S2, P2] = None,
458
+ post_call: PostCall[S2, P3] = None,
459
+ provided: Literal[False],
460
+ _typing_requested: Literal[False],
461
+ ) -> NoInjectionSelf[S, P, R]: ...
462
+
463
+ @overload
464
+ def annotate_logs(
465
+ self,
466
+ logger_name: str | None = None,
467
+ *,
468
+ success_info: bool = True, # pragma: no mutate
469
+ pre_call: PreCall[S2, P2] = None,
470
+ post_call: PostCall[S2, P3] = None,
471
+ _typing_self: Literal[True],
472
+ provided: Literal[False],
473
+ ) -> NoInjectionSelf[S, P, R]: ...
474
+
475
+ @overload
476
+ def annotate_logs(
477
+ self,
478
+ logger_name: str | None = None,
479
+ *,
480
+ success_info: bool = True, # pragma: no mutate
481
+ pre_call: PreCall[S2, P2] = None,
482
+ post_call: PostCall[S2, P3] = None,
483
+ _typing_self: Literal[True],
484
+ _typing_requested: Literal[False],
485
+ provided: Literal[False],
486
+ ) -> NoInjectionSelf[S, P, R]: ...
487
+
488
+ #### Class True
489
+ @overload
490
+ def annotate_logs(
491
+ self,
492
+ logger_name: str | None = None,
493
+ *,
494
+ _typing_class: Literal[True],
495
+ success_info: bool = True, # pragma: no mutate
496
+ pre_call: PreCall[S, P2] = None,
497
+ post_call: PostCall[S, P3] = None,
498
+ ) -> Callable[[type[C_co]], type[C_co]]: ...
499
+
500
+ ### Instance False
501
+ @overload
502
+ def annotate_logs(
503
+ self,
504
+ logger_name: str | None = None,
505
+ *,
506
+ success_info: bool = True, # pragma: no mutate
507
+ pre_call: PreCall[S2, P2] = None,
508
+ post_call: PostCall[S2, P3] = None,
509
+ _typing_self: Literal[False],
510
+ ) -> NoInjectionBare[P, R]: ...
511
+
512
+ @overload
513
+ def annotate_logs(
514
+ self,
515
+ logger_name: str | None = None,
516
+ *,
517
+ success_info: bool = True, # pragma: no mutate
518
+ pre_call: PreCall[S2, P2] = None,
519
+ post_call: PostCall[S2, P3] = None,
520
+ _typing_self: Literal[False],
521
+ _typing_requested: Literal[False],
522
+ ) -> NoInjectionBare[P, R]: ...
523
+
524
+ @overload
525
+ def annotate_logs(
526
+ self,
527
+ logger_name: str | None = None,
528
+ *,
529
+ success_info: bool = True, # pragma: no mutate
530
+ pre_call: PreCall[S2, P2] = None,
531
+ post_call: PostCall[S2, P3] = None,
532
+ _typing_self: Literal[False],
533
+ provided: Literal[False],
534
+ ) -> NoInjectionBare[P, R]: ...
535
+
536
+ @overload
537
+ def annotate_logs(
538
+ self,
539
+ logger_name: str | None = None,
540
+ *,
541
+ success_info: bool = True, # pragma: no mutate
542
+ pre_call: PreCall[S2, P2] = None,
543
+ post_call: PostCall[S2, P3] = None,
544
+ _typing_self: Literal[False],
545
+ _typing_requested: Literal[False],
546
+ provided: Literal[False],
547
+ ) -> NoInjectionBare[P, R]: ...
548
+
549
+ ### Requested True
550
+ @overload
551
+ def annotate_logs(
552
+ self,
553
+ logger_name: str | None = None,
554
+ *,
555
+ success_info: bool = True, # pragma: no mutate
556
+ pre_call: PreCall[S2, P2] = None,
557
+ post_call: PostCall[S2, P3] = None,
558
+ _typing_requested: Literal[True],
559
+ ) -> InjectionSelf[S, P, R]: ...
560
+
561
+ @overload
562
+ def annotate_logs(
563
+ self,
564
+ logger_name: str | None = None,
565
+ *,
566
+ success_info: bool = True, # pragma: no mutate
567
+ pre_call: PreCall[S2, P2] = None,
568
+ post_call: PostCall[S2, P3] = None,
569
+ _typing_self: Literal[True],
570
+ _typing_requested: Literal[True],
571
+ ) -> InjectionSelf[S, P, R]: ...
572
+
573
+ @overload
574
+ def annotate_logs(
575
+ self,
576
+ logger_name: str | None = None,
577
+ *,
578
+ success_info: bool = True, # pragma: no mutate
579
+ pre_call: PreCall[S2, P2] = None,
580
+ post_call: PostCall[S2, P3] = None,
581
+ provided: Literal[False],
582
+ _typing_requested: Literal[True],
583
+ ) -> InjectionSelf[S, P, R]: ...
584
+
585
+ @overload
586
+ def annotate_logs(
587
+ self,
588
+ logger_name: str | None = None,
589
+ *,
590
+ success_info: bool = True, # pragma: no mutate
591
+ pre_call: PreCall[S2, P2] = None,
592
+ post_call: PostCall[S2, P3] = None,
593
+ _typing_self: Literal[True],
594
+ _typing_requested: Literal[True],
595
+ provided: Literal[False],
596
+ ) -> InjectionSelf[S, P, R]: ...
597
+
598
+ ### Provided True, Requested True
599
+ # Can't provide it if it was not requested,
600
+ # so no overloads for not requested, but provided
601
+ @overload
602
+ def annotate_logs(
603
+ self,
604
+ logger_name: str | None = None,
605
+ *,
606
+ success_info: bool = True, # pragma: no mutate
607
+ pre_call: PreCall[S2, P2] = None,
608
+ post_call: PostCall[S2, P3] = None,
609
+ _typing_requested: Literal[True],
610
+ provided: Literal[True],
611
+ ) -> InjectionSelfProvide[S, P, R]: ...
612
+
613
+ @overload
614
+ def annotate_logs(
615
+ self,
616
+ logger_name: str | None = None,
617
+ *,
618
+ success_info: bool = True, # pragma: no mutate
619
+ pre_call: PreCall[S2, P2] = None,
620
+ post_call: PostCall[S2, P3] = None,
621
+ _typing_self: Literal[True],
622
+ _typing_requested: Literal[True],
623
+ provided: Literal[True],
624
+ ) -> InjectionSelfProvide[S, P, R]: ...
625
+
626
+ ### Instance False, Requested True
627
+ @overload
628
+ def annotate_logs(
629
+ self,
630
+ logger_name: str | None = None,
631
+ *,
632
+ success_info: bool = True, # pragma: no mutate
633
+ pre_call: PreCall[S2, P2] = None,
634
+ post_call: PostCall[S2, P3] = None,
635
+ _typing_self: Literal[False],
636
+ _typing_requested: Literal[True],
637
+ ) -> InjectionBare[P, R]: ...
638
+
639
+ ### Instance False, Requested True, Provided True
640
+ # Same not as above that you can't provide if not requested
641
+ @overload
642
+ def annotate_logs(
643
+ self,
644
+ logger_name: str | None = None,
645
+ *,
646
+ success_info: bool = True, # pragma: no mutate
647
+ pre_call: PreCall[S2, P2] = None,
648
+ post_call: PostCall[S2, P2] = None,
649
+ _typing_self: Literal[False],
650
+ _typing_requested: Literal[True],
651
+ provided: Literal[True],
652
+ ) -> InjectionBareProvide[P, R]: ...
653
+
654
+ # Between the overloads and the two inner method definitions,
655
+ # there's not much I can do to reduce the complexity more.
656
+ # So, ignoring the complexity metric
657
+ def annotate_logs( # noqa: C901
658
+ self,
659
+ logger_name: str | None = None,
660
+ *,
661
+ success_info: bool = True,
662
+ pre_call: PreCall[S2, P2] = None,
663
+ post_call: PostCall[S2, P3] = None,
664
+ provided: bool = False,
665
+ _typing_self: bool = True, # pragma: no mutate
666
+ _typing_requested: bool = False, # pragma: no mutate
667
+ _typing_class: bool = False, # pragma: no mutate
668
+ ) -> Decorator[S, P, R] | Callable[[type[C_co]], type[C_co]]:
669
+ """Log start and end of function and provide an annotated logger if requested.
670
+
671
+ Args:
672
+ ----
673
+ logger_name: Optional - Specify the name of the logger attached to
674
+ the decorated function.
675
+ success_info: Log success at an info level, if falsey success will be
676
+ logged at debug. Default: True
677
+ provided: Boolean that indicates the caller will be providing it's
678
+ own annotated_logger. Default: False
679
+ pre_call: Method that takes the same arguments as the decorated function
680
+ and does something. Called before the function and the `start` log message.
681
+ post_call: Method that takes the same arguments as the decorated function
682
+ and does something. Called after the function and before the `success`
683
+ log message or in the exception handling.
684
+ _typing_self: Used only for type hint overloads. Indicates that the
685
+ decorated method is an instance method and has a self parameter.
686
+ Default: True
687
+ _typing_requested: Used only for type hint overloads. Indicates that the
688
+ decorated method is expecting an annotated_logger to be provided.
689
+ Default: False
690
+
691
+ Notes:
692
+ -----
693
+ In order to fully support type hinting, the annotated_logger argument
694
+ must be the first argument (after self/cls). Type hinting will only work
695
+ correctly if the _typing arguments are set correctly, but the code will
696
+ work fine at runtime without the _typing arguments.
697
+
698
+ """
699
+
700
+ @overload
701
+ def decorator(
702
+ wrapped: SelfLoggerAndParams[S, P, R],
703
+ ) -> SelfAndParams[S, P, R] | SelfLoggerAndParams[S, P, R]: ...
704
+
705
+ @overload
706
+ def decorator(
707
+ wrapped: LoggerAndParams[P, R],
708
+ ) -> ParamsOnly[P, R] | LoggerAndParams[P, R]: ...
709
+
710
+ @overload
711
+ def decorator(
712
+ wrapped: SelfAndParams[S, P, R],
713
+ ) -> SelfAndParams[S, P, R]: ...
714
+
715
+ @overload
716
+ def decorator(
717
+ wrapped: ParamsOnly[P, R],
718
+ ) -> ParamsOnly[P, R] | Empty[R]: ...
719
+
720
+ @overload
721
+ def decorator(wrapped: type[C_co]) -> Callable[P, AnnotatedClass[C_co]]: ...
722
+
723
+ def decorator( # noqa: C901
724
+ wrapped: Function[S, P, R] | type[C_co],
725
+ ) -> Function[S, P, R] | Callable[P, AnnotatedClass[C_co]]:
726
+ if isinstance(wrapped, type):
727
+
728
+ def wrap_class(
729
+ *args: P.args, **kwargs: P.kwargs
730
+ ) -> AnnotatedClass[C_co]:
731
+ logger = self._generate_logger(
732
+ cls=wrapped, logger_base_name=logger_name
733
+ )
734
+ logger.debug("init")
735
+ new = cast(AnnotatedClass[C_co], wrapped(*args, **kwargs))
736
+ new.annotated_logger = logger
737
+ return new
738
+
739
+ return wrap_class
740
+
741
+ (remove_args, inject_logger) = self._determine_signature_adjustments(
742
+ wrapped, provided=provided
743
+ )
744
+
745
+ @wraps(
746
+ wrapped,
747
+ remove_args=remove_args,
748
+ )
749
+ def wrap_function(*args: P.args, **kwargs: P.kwargs) -> R:
750
+ __tracebackhide__ = True # pragma: no mutate
751
+
752
+ post_call_attempted = False # pragma: no mutate
753
+
754
+ new_args, new_kwargs, logger, pre_execution_annotations = inject_logger(
755
+ list(args), kwargs, logger_base_name=logger_name
756
+ )
757
+ try:
758
+ start_time = time.perf_counter()
759
+ if pre_call:
760
+ pre_call(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue]
761
+ logger.debug("start")
762
+
763
+ result = wrapped(*new_args, **new_kwargs) # pyright: ignore[reportCallIssue]
764
+ logger.annotate(success=True)
765
+ if post_call:
766
+ post_call_attempted = True
767
+ _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue]
768
+ end_time = time.perf_counter()
769
+ logger.annotate(run_time=f"{end_time - start_time :.1f}")
770
+ with contextlib.suppress(TypeError):
771
+ logger.annotate(count=len(result)) # pyright: ignore[reportArgumentType]
772
+
773
+ if success_info:
774
+ logger.info("success")
775
+ else:
776
+ logger.debug("success")
777
+
778
+ # If we were provided with a logger object, set the annotations
779
+ # back to what they were before the wrapped method was called.
780
+ if pre_execution_annotations:
781
+ logger.filter.annotations = pre_execution_annotations
782
+ except Exception as e:
783
+ for plugin in logger.filter.plugins:
784
+ logger = plugin.uncaught_exception(e, logger)
785
+ logger.exception(
786
+ "Uncaught Exception in logged function",
787
+ )
788
+ if post_call and not post_call_attempted:
789
+ _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue]
790
+ raise
791
+ return result
792
+
793
+ return wrap_function
794
+
795
+ return decorator
796
+
797
+ def _determine_signature_adjustments(
798
+ self,
799
+ function: Function[S, P, R],
800
+ *,
801
+ provided: bool,
802
+ ) -> tuple[
803
+ list[str],
804
+ Callable[
805
+ Concatenate[list[Any], dict[str, Any], ...],
806
+ tuple[list[Any], dict[str, Any], AnnotatedAdapter, Annotations | None],
807
+ ],
808
+ ]:
809
+ written_signature = inspect.signature(function)
810
+ logger_requested = False # pragma: no mutate
811
+ remove_args = []
812
+ index, instance_method = self._check_parameters_for_self_and_cls(
813
+ written_signature
814
+ )
815
+ if "annotated_logger" in written_signature.parameters:
816
+ if list(written_signature.parameters.keys())[index] != "annotated_logger":
817
+ error_message = "annotated_logger must be the first argument"
818
+ raise TypeError(error_message)
819
+
820
+ logger_requested = True
821
+ if not provided:
822
+ remove_args = ["annotated_logger"]
823
+
824
+ def inject_logger(
825
+ args: list[Any],
826
+ kwargs: dict[str, Any],
827
+ logger_base_name: str | None = None,
828
+ ) -> tuple[list[Any], dict[str, Any], AnnotatedAdapter, Annotations | None]:
829
+ if not logger_requested:
830
+ logger = self._generate_logger(
831
+ function, logger_base_name=logger_base_name
832
+ )
833
+ return (args, kwargs, logger, None)
834
+
835
+ by_index = False # pragma: no mutate
836
+ # Check for a var positional or positional only
837
+ # If present that means we'll have values in args when invoking
838
+ # but, if not everything will be in kwargs
839
+ for v in written_signature.parameters.values():
840
+ if v.kind == inspect.Parameter.VAR_POSITIONAL:
841
+ by_index = True
842
+
843
+ new_args = copy(args)
844
+ new_kwargs = copy(kwargs)
845
+ if by_index:
846
+ logger, annotations, new_args = self._inject_by_index(
847
+ provided=provided,
848
+ instance_method=instance_method,
849
+ args=new_args,
850
+ index=index,
851
+ function=function,
852
+ logger_base_name=logger_base_name,
853
+ )
854
+ else:
855
+ logger, annotations, new_kwargs = self._inject_by_kwarg(
856
+ provided=provided,
857
+ instance_method=instance_method,
858
+ kwargs=new_kwargs,
859
+ function=function,
860
+ logger_base_name=logger_base_name,
861
+ )
862
+
863
+ return new_args, new_kwargs, logger, annotations
864
+
865
+ return remove_args, inject_logger
866
+
867
+ def _inject_by_kwarg(
868
+ self,
869
+ *,
870
+ provided: bool,
871
+ instance_method: bool,
872
+ function: Function[S, P, R],
873
+ kwargs: dict[str, Any],
874
+ logger_base_name: str | None = None,
875
+ ) -> tuple[AnnotatedAdapter, Annotations | None, dict[str, Any]]:
876
+ if provided:
877
+ instance = kwargs["annotated_logger"]
878
+ elif instance_method:
879
+ instance = kwargs["self"]
880
+ else:
881
+ instance = False # pragma: no mutate
882
+ logger, annotations = self._pick_correct_logger(
883
+ function, instance, logger_base_name=logger_base_name
884
+ )
885
+ if not provided:
886
+ kwargs["annotated_logger"] = logger
887
+
888
+ return logger, annotations, kwargs
889
+
890
+ def _inject_by_index( # noqa: PLR0913
891
+ self,
892
+ *,
893
+ provided: bool,
894
+ instance_method: bool,
895
+ function: Function[S, P, R],
896
+ args: list[Any],
897
+ index: int,
898
+ logger_base_name: str | None = None,
899
+ ) -> tuple[AnnotatedAdapter, Annotations | None, list[Any]]:
900
+ if provided:
901
+ instance = args[index]
902
+ elif instance_method:
903
+ instance = args[0]
904
+ else:
905
+ instance = False # pragma: no mutate
906
+ logger, annotations = self._pick_correct_logger(
907
+ function, instance, logger_base_name=logger_base_name
908
+ )
909
+ if not provided:
910
+ args.insert(index, logger)
911
+ return logger, annotations, args
912
+
913
+ def _check_parameters_for_self_and_cls(
914
+ self, sig: inspect.Signature
915
+ ) -> tuple[int, bool]:
916
+ parameters = sig.parameters
917
+ index = 0
918
+ instance_method = False
919
+ if "self" in parameters:
920
+ index = 1
921
+ instance_method = True
922
+ if "cls" in parameters:
923
+ index = 1
924
+
925
+ return index, instance_method
926
+
927
+ def _pick_correct_logger(
928
+ self,
929
+ function: Function[S, P, R],
930
+ instance: object | bool,
931
+ logger_base_name: str | None = None,
932
+ ) -> tuple[AnnotatedAdapter, Annotations | None]:
933
+ """Use the instance's logger and annotations if present."""
934
+ if instance and hasattr(instance, "annotated_logger"):
935
+ logger = instance.annotated_logger # pyright: ignore[reportAttributeAccessIssue]
936
+ annotations = copy(logger.filter.annotations)
937
+ logger.filter.annotations.update(self._action_annotation(function))
938
+ return (logger, annotations)
939
+
940
+ if isinstance(instance, AnnotatedAdapter):
941
+ logger = instance
942
+ annotations = copy(logger.filter.annotations)
943
+ logger.filter.annotations.update(
944
+ self._action_annotation(function, key="subaction")
945
+ )
946
+ return (logger, annotations)
947
+
948
+ return (
949
+ self._generate_logger(function, logger_base_name=logger_base_name),
950
+ None,
951
+ )
952
+
953
+
954
+ def _attempt_post_call(
955
+ post_call: Callable[P, None],
956
+ logger: AnnotatedAdapter,
957
+ *args: P.args,
958
+ **kwargs: P.kwargs,
959
+ ) -> None:
960
+ try:
961
+ if post_call:
962
+ post_call(*args, **kwargs) # pyright: ignore[reportCallIssue]
963
+ except Exception:
964
+ logger.annotate(success=False)
965
+ logger.exception("Post call failed")
966
+ raise