omlish 0.0.0.dev430__py3-none-any.whl → 0.0.0.dev432__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.
@@ -1,152 +1,574 @@
1
1
  # ruff: noqa: UP006 UP007 UP045
2
2
  # @omlish-lite
3
+ """
4
+ TODO:
5
+ - TypedDict?
6
+ """
7
+ import abc
3
8
  import collections.abc
4
9
  import logging
5
- import sys
6
10
  import typing as ta
7
11
 
8
- from ...lite.check import check
12
+ from ...lite.abstract import Abstract
9
13
  from ..contexts import LoggingContext
10
- from ..contexts import LoggingExcInfoTuple
14
+ from ..contexts import LoggingContextInfoT
15
+ from ..infos import LoggingContextInfo
16
+ from ..infos import LoggingContextInfos
17
+ from ..infos import LoggingExcInfoTuple
11
18
  from ..warnings import LoggingSetupWarning
12
19
 
13
20
 
21
+ T = ta.TypeVar('T')
22
+
23
+
14
24
  ##
15
25
 
16
26
 
17
- # Ref:
18
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
19
- #
20
- # LogRecord:
21
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
22
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
23
- #
24
- # LogRecord.__init__ args:
25
- # - name: str
26
- # - level: int
27
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
28
- # - lineno: int - May be 0.
29
- # - msg: str
30
- # - args: tuple | dict | 1-tuple[dict]
31
- # - exc_info: LoggingExcInfoTuple | None
32
- # - func: str | None = None -> funcName
33
- # - sinfo: str | None = None -> stack_info
34
- #
35
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
36
- # Name of the logger used to log the call. Unmodified by ctor.
37
- name=str,
27
+ class LoggingContextInfoRecordAdapters:
28
+ # Ref:
29
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
30
+ #
31
+ # LogRecord:
32
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
33
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
34
+ #
38
35
 
39
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
40
- # (see Using arbitrary objects as messages). Unmodified by ctor.
41
- msg=str,
36
+ def __new__(cls, *args, **kwargs): # noqa
37
+ raise TypeError
38
+
39
+ class Adapter(Abstract, ta.Generic[T]):
40
+ @property
41
+ @abc.abstractmethod
42
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
43
+ raise NotImplementedError
44
+
45
+ #
46
+
47
+ @ta.final
48
+ class NOT_SET: # noqa
49
+ def __new__(cls, *args, **kwargs): # noqa
50
+ raise TypeError
51
+
52
+ class RecordAttr(ta.NamedTuple):
53
+ name: str
54
+ type: ta.Any
55
+ default: ta.Any
56
+
57
+ # @abc.abstractmethod
58
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
59
+
60
+ @property
61
+ @abc.abstractmethod
62
+ def _record_attrs(self) -> ta.Union[
63
+ ta.Mapping[str, ta.Any],
64
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
65
+ ]:
66
+ raise NotImplementedError
67
+
68
+ #
69
+
70
+ @abc.abstractmethod
71
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
72
+ raise NotImplementedError
73
+
74
+ #
75
+
76
+ @abc.abstractmethod
77
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
78
+ raise NotImplementedError
79
+
80
+ #
81
+
82
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
83
+ super().__init_subclass__(**kwargs)
84
+
85
+ if Abstract in cls.__bases__:
86
+ return
87
+
88
+ if 'record_attrs' in cls.__dict__:
89
+ raise TypeError(cls)
90
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
91
+ raise TypeError(ra)
92
+
93
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
94
+ for n, v in ra.items():
95
+ if not n or not isinstance(n, str) or n in rd:
96
+ raise AttributeError(n)
97
+ if isinstance(v, tuple):
98
+ t, d = v
99
+ else:
100
+ t, d = v, cls.NOT_SET
101
+ rd[n] = cls.RecordAttr(
102
+ name=n,
103
+ type=t,
104
+ default=d,
105
+ )
106
+ cls.record_attrs = rd
107
+
108
+ class RequiredAdapter(Adapter[T], Abstract):
109
+ @property
110
+ @abc.abstractmethod
111
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
112
+ raise NotImplementedError
113
+
114
+ #
115
+
116
+ @ta.final
117
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
118
+ if (info := ctx.get_info(self.info_cls)) is not None:
119
+ return self._info_to_record(info)
120
+ else:
121
+ raise TypeError # FIXME: fallback?
42
122
 
43
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
44
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
45
- # the mapping, but is otherwise unmodified.
46
- args=ta.Union[tuple, dict],
123
+ @abc.abstractmethod
124
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
125
+ raise NotImplementedError
47
126
 
48
- #
127
+ #
49
128
 
50
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
51
- # `getLevelName(level)`.
52
- levelname=str,
129
+ @abc.abstractmethod
130
+ def record_to_info(self, rec: logging.LogRecord) -> T:
131
+ raise NotImplementedError
53
132
 
54
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
55
- levelno=int,
133
+ #
56
134
 
57
- #
135
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
136
+ super().__init_subclass__(**kwargs)
58
137
 
59
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
60
- # to "(unknown file)" by Logger.findCaller / Logger._log.
61
- pathname=str,
138
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
139
+ raise TypeError(cls.record_attrs)
62
140
 
63
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
64
- filename=str,
141
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
142
+ @property
143
+ @abc.abstractmethod
144
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
145
+ raise NotImplementedError
65
146
 
66
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
67
- # "Unknown module".
68
- module=str,
147
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
69
148
 
70
- #
149
+ #
150
+
151
+ @ta.final
152
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
153
+ if (info := ctx.get_info(self.info_cls)) is not None:
154
+ return self._info_to_record(info)
155
+ else:
156
+ return self.record_defaults
71
157
 
72
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
73
- exc_info=ta.Optional[LoggingExcInfoTuple],
158
+ @abc.abstractmethod
159
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
160
+ raise NotImplementedError
74
161
 
75
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
76
- exc_text=ta.Optional[str],
162
+ #
163
+
164
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
165
+ super().__init_subclass__(**kwargs)
166
+
167
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
168
+ if any(d is cls.NOT_SET for d in dd.values()):
169
+ raise TypeError(cls.record_attrs)
170
+ cls.record_defaults = dd
77
171
 
78
172
  #
79
173
 
80
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
81
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
82
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
83
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
84
- stack_info=ta.Optional[str],
174
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
175
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
85
176
 
86
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
87
- # Logger.findCaller / Logger._log.
88
- lineno=int,
177
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
178
+ # Name of the logger used to log the call. Unmodified by ctor.
179
+ name=str,
180
+ )
89
181
 
90
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
91
- # "(unknown function)" by Logger.findCaller / Logger._log.
92
- funcName=str,
182
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
183
+ return dict(
184
+ name=info.name,
185
+ )
93
186
 
94
- #
187
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
188
+ return LoggingContextInfos.Name(
189
+ name=rec.name,
190
+ )
95
191
 
96
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
97
- #
98
- # See:
99
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
100
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
101
- #
102
- created=float,
192
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
193
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
103
194
 
104
- # Millisecond portion of the time when the LogRecord was created.
105
- msecs=float,
195
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
196
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
197
+ # `getLevelName(level)`.
198
+ levelname=str,
106
199
 
107
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
108
- relativeCreated=float,
200
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
201
+ levelno=int,
202
+ )
109
203
 
110
- #
204
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
205
+ return dict(
206
+ levelname=info.name,
207
+ levelno=int(info.level),
208
+ )
111
209
 
112
- # Thread ID if available, and `logging.logThreads` is truthy.
113
- thread=ta.Optional[int],
210
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
211
+ return LoggingContextInfos.Level.build(rec.levelno)
114
212
 
115
- # Thread name if available, and `logging.logThreads` is truthy.
116
- threadName=ta.Optional[str],
213
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
214
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
117
215
 
118
- #
216
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
217
+ # The format string passed in the original logging call. Merged with args to produce message, or an
218
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
219
+ msg=str,
119
220
 
120
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
121
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
122
- # as 'MainProcess'.
123
- #
124
- # As noted by stdlib:
221
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
222
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
223
+ # Mapping into just the mapping, but is otherwise unmodified.
224
+ args=ta.Union[tuple, dict, None],
225
+ )
226
+
227
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
228
+ return dict(
229
+ msg=info.msg,
230
+ args=info.args,
231
+ )
232
+
233
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
234
+ return LoggingContextInfos.Msg(
235
+ msg=rec.msg,
236
+ args=rec.args,
237
+ )
238
+
239
+ # FIXME: handled specially - all unknown attrs on LogRecord
240
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
241
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
125
242
  #
126
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
127
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
243
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
244
+ # # FIXME:
245
+ # # if extra is not None:
246
+ # # for key in extra:
247
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
248
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
249
+ # # rv.__dict__[key] = extra[key]
250
+ # return dict()
128
251
  #
129
- processName=ta.Optional[str],
252
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
253
+ # return None
254
+
255
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
256
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
257
+
258
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
259
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
260
+ # `time.time()`.
261
+ #
262
+ # See:
263
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
264
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
265
+ #
266
+ created=float,
267
+
268
+ # Millisecond portion of the time when the LogRecord was created.
269
+ msecs=float,
270
+
271
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
272
+ relativeCreated=float,
273
+ )
130
274
 
131
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
132
- # None.
133
- process=ta.Optional[int],
275
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
276
+ return dict(
277
+ created=info.secs,
278
+ msecs=info.msecs,
279
+ relativeCreated=info.relative_secs,
280
+ )
134
281
 
135
- #
282
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
283
+ return LoggingContextInfos.Time.build(
284
+ int(rec.created * 1e9),
285
+ )
136
286
 
137
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
138
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
139
- taskName=ta.Optional[str],
140
- )
287
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
288
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
289
+
290
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
291
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
292
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
293
+
294
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
295
+ exc_text=(ta.Optional[str], None),
296
+ )
297
+
298
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
299
+ return dict(
300
+ exc_info=info.info_tuple,
301
+ exc_text=None,
302
+ )
303
+
304
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
305
+ # FIXME:
306
+ # error: Argument 1 to "build" of "Exc" has incompatible type
307
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
308
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
309
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
310
+
311
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
312
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
313
+
314
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
315
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
316
+
317
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
318
+
319
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
320
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
321
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
322
+ pathname=(str, _UNKNOWN_PATH_NAME),
323
+
324
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
325
+ # y Logger.findCaller / Logger._log.
326
+ lineno=(int, 0),
327
+
328
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
329
+ # "(unknown function)" by Logger.findCaller / Logger._log.
330
+ funcName=(str, _UNKNOWN_FUNC_NAME),
331
+
332
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
333
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
334
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
335
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
336
+ # stripped of exactly one trailing `\n` if present.
337
+ stack_info=(ta.Optional[str], None),
338
+ )
339
+
340
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
341
+ if (sinfo := caller.stack_info) is not None:
342
+ stack_info: ta.Optional[str] = '\n'.join([
343
+ self._STACK_INFO_PREFIX,
344
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
345
+ ])
346
+ else:
347
+ stack_info = None
348
+
349
+ return dict(
350
+ pathname=caller.file_path,
351
+
352
+ lineno=caller.line_no,
353
+ funcName=caller.func_name,
354
+
355
+ stack_info=stack_info,
356
+ )
357
+
358
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
359
+ # FIXME: piecemeal?
360
+ if (
361
+ rec.pathname != self._UNKNOWN_PATH_NAME and
362
+ rec.lineno != 0 and
363
+ rec.funcName != self._UNKNOWN_FUNC_NAME
364
+ ):
365
+ if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
366
+ sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
367
+ return LoggingContextInfos.Caller(
368
+ file_path=rec.pathname,
369
+
370
+ line_no=rec.lineno,
371
+ func_name=rec.funcName,
372
+
373
+ stack_info=sinfo,
374
+ )
375
+
376
+ return None
377
+
378
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
379
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
141
380
 
142
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
381
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
382
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
383
+ # pathname.
384
+ filename=str,
385
+
386
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
387
+ # "Unknown module".
388
+ module=str,
389
+ )
390
+
391
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
392
+
393
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
394
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
395
+ return dict(
396
+ filename=info.file_name,
397
+ module=info.module,
398
+ )
399
+
400
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
401
+ return dict(
402
+ filename=caller.file_path,
403
+ module=self._UNKNOWN_MODULE,
404
+ )
405
+
406
+ return dict(
407
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
408
+ module=self._UNKNOWN_MODULE,
409
+ )
410
+
411
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
412
+ if (
413
+ rec.module is not None and
414
+ rec.module != self._UNKNOWN_MODULE
415
+ ):
416
+ return LoggingContextInfos.SourceFile(
417
+ file_name=rec.filename,
418
+ module=rec.module, # FIXME: piecemeal?
419
+ )
420
+
421
+ return None
422
+
423
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
424
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
425
+
426
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
427
+ # Thread ID if available, and `logging.logThreads` is truthy.
428
+ thread=(ta.Optional[int], None),
429
+
430
+ # Thread name if available, and `logging.logThreads` is truthy.
431
+ threadName=(ta.Optional[str], None),
432
+ )
433
+
434
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
435
+ if logging.logThreads:
436
+ return dict(
437
+ thread=info.ident,
438
+ threadName=info.name,
439
+ )
440
+
441
+ return self.record_defaults
442
+
443
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
444
+ if (
445
+ (ident := rec.thread) is not None and
446
+ (name := rec.threadName) is not None
447
+ ):
448
+ return LoggingContextInfos.Thread(
449
+ ident=ident,
450
+ native_id=None,
451
+ name=name,
452
+ )
453
+
454
+ return None
455
+
456
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
457
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
458
+
459
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
460
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
461
+ # otherwise None.
462
+ process=(ta.Optional[int], None),
463
+ )
464
+
465
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
466
+ if logging.logProcesses:
467
+ return dict(
468
+ process=info.pid,
469
+ )
470
+
471
+ return self.record_defaults
472
+
473
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
474
+ if (
475
+ (pid := rec.process) is not None
476
+ ):
477
+ return LoggingContextInfos.Process(
478
+ pid=pid,
479
+ )
480
+
481
+ return None
482
+
483
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
484
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
485
+
486
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
487
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
488
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
489
+ # remains as 'MainProcess'.
490
+ #
491
+ # As noted by stdlib:
492
+ #
493
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
494
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
495
+ #
496
+ processName=(ta.Optional[str], None),
497
+ )
498
+
499
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
500
+ if logging.logMultiprocessing:
501
+ return dict(
502
+ processName=info.process_name,
503
+ )
504
+
505
+ return self.record_defaults
506
+
507
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
508
+ if (
509
+ (process_name := rec.processName) is not None
510
+ ):
511
+ return LoggingContextInfos.Multiprocessing(
512
+ process_name=process_name,
513
+ )
514
+
515
+ return None
516
+
517
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
518
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
519
+
520
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
521
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
522
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
523
+ taskName=(ta.Optional[str], None),
524
+ )
525
+
526
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
527
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
528
+ return dict(
529
+ taskName=info.name,
530
+ )
531
+
532
+ return self.record_defaults
533
+
534
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
535
+ if (
536
+ (name := getattr(rec, 'taskName', None)) is not None
537
+ ):
538
+ return LoggingContextInfos.AsyncioTask(
539
+ name=name,
540
+ )
541
+
542
+ return None
543
+
544
+
545
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
546
+ LoggingContextInfoRecordAdapters.Name(),
547
+ LoggingContextInfoRecordAdapters.Level(),
548
+ LoggingContextInfoRecordAdapters.Msg(),
549
+ LoggingContextInfoRecordAdapters.Time(),
550
+ LoggingContextInfoRecordAdapters.Exc(),
551
+ LoggingContextInfoRecordAdapters.Caller(),
552
+ LoggingContextInfoRecordAdapters.SourceFile(),
553
+ LoggingContextInfoRecordAdapters.Thread(),
554
+ LoggingContextInfoRecordAdapters.Process(),
555
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
556
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
557
+ ]
558
+
559
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
560
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
561
+ }
562
+
563
+
564
+ ##
143
565
 
144
566
 
145
567
  # Formatter:
146
568
  # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
147
569
  # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
148
570
  #
149
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
571
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
150
572
  # The logged message, computed as msg % args. Set to `record.getMessage()`.
151
573
  message=str,
152
574
 
@@ -160,20 +582,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
160
582
  exc_text=ta.Optional[str],
161
583
  )
162
584
 
163
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
164
-
165
585
 
166
586
  ##
167
587
 
168
588
 
589
+ _KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
590
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
591
+ )
592
+
593
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
594
+
595
+
169
596
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
170
597
  pass
171
598
 
172
599
 
173
600
  def _check_std_logging_record_attrs() -> None:
601
+ if (
602
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
603
+ len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
604
+ ):
605
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
606
+
174
607
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
175
608
 
176
- if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
609
+ if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
177
610
  import warnings # noqa
178
611
 
179
612
  warnings.warn(
@@ -189,113 +622,40 @@ _check_std_logging_record_attrs()
189
622
 
190
623
 
191
624
  class LoggingContextLogRecord(logging.LogRecord):
192
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
193
-
194
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
195
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
196
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
197
-
198
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
199
-
200
- def __init__( # noqa
201
- self,
202
- # name,
203
- # level,
204
- # pathname,
205
- # lineno,
206
- # msg,
207
- # args,
208
- # exc_info,
209
- # func=None,
210
- # sinfo=None,
211
- # **kwargs,
212
- *,
213
- name: str,
214
- msg: str,
215
- args: ta.Union[tuple, dict],
216
-
217
- _logging_context: LoggingContext,
218
- ) -> None:
219
- ctx = _logging_context
220
-
221
- self.name: str = name
222
-
223
- self.msg: str = msg
224
-
225
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
226
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
227
- args = args[0] # type: ignore[assignment]
228
- self.args: ta.Union[tuple, dict] = args
229
-
230
- self.levelname: str = logging.getLevelName(ctx.level)
231
- self.levelno: int = ctx.level
232
-
233
- if (caller := ctx.caller()) is not None:
234
- self.pathname: str = caller.file_path
235
- else:
236
- self.pathname = self._UNKNOWN_PATH_NAME
237
-
238
- if (src_file := ctx.source_file()) is not None:
239
- self.filename: str = src_file.file_name
240
- self.module: str = src_file.module
241
- else:
242
- self.filename = self.pathname
243
- self.module = self._UNKNOWN_MODULE
244
-
245
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
246
- self.exc_text: ta.Optional[str] = None
247
-
248
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
249
- if caller is not None:
250
- if (sinfo := caller.stack_info) is not None:
251
- self.stack_info: ta.Optional[str] = '\n'.join([
252
- self._STACK_INFO_PREFIX,
253
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
254
- ])
255
- else:
256
- self.stack_info = None
257
-
258
- self.lineno: int = caller.line_no
259
- self.funcName: str = caller.name
260
-
261
- else:
262
- self.stack_info = None
263
-
264
- self.lineno = 0
265
- self.funcName = self._UNKNOWN_FUNC_NAME
266
-
267
- times = ctx.times
268
- self.created: float = times.created
269
- self.msecs: float = times.msecs
270
- self.relativeCreated: float = times.relative_created
271
-
272
- if logging.logThreads:
273
- thread = check.not_none(ctx.thread())
274
- self.thread: ta.Optional[int] = thread.ident
275
- self.threadName: ta.Optional[str] = thread.name
276
- else:
277
- self.thread = None
278
- self.threadName = None
279
-
280
- if logging.logProcesses:
281
- process = check.not_none(ctx.process())
282
- self.process: ta.Optional[int] = process.pid
283
- else:
284
- self.process = None
285
-
286
- if logging.logMultiprocessing:
287
- if (mp := ctx.multiprocessing()) is not None:
288
- self.processName: ta.Optional[str] = mp.process_name
289
- else:
290
- self.processName = None
291
- else:
292
- self.processName = None
293
-
294
- # Absent <3.12
295
- if getattr(logging, 'logAsyncioTasks', None):
296
- if (at := ctx.asyncio_task()) is not None:
297
- self.taskName: ta.Optional[str] = at.name
298
- else:
299
- self.taskName = None
300
- else:
301
- self.taskName = None
625
+ # LogRecord.__init__ args:
626
+ # - name: str
627
+ # - level: int
628
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
629
+ # - lineno: int - May be 0.
630
+ # - msg: str
631
+ # - args: tuple | dict | 1-tuple[dict]
632
+ # - exc_info: LoggingExcInfoTuple | None
633
+ # - func: str | None = None -> funcName
634
+ # - sinfo: str | None = None -> stack_info
635
+
636
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
637
+ self._logging_context = _logging_context
638
+
639
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
640
+ self.__dict__.update(ad.context_to_record(_logging_context))
641
+
642
+
643
+ ##
644
+
645
+
646
+ @ta.final
647
+ class LogRecordLoggingContext(LoggingContext):
648
+ def __init__(self, rec: logging.LogRecord) -> None:
649
+ if isinstance(rec, LoggingContextLogRecord):
650
+ raise TypeError(rec)
651
+
652
+ self._rec = rec
653
+
654
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
655
+ type(info): info
656
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
657
+ if (info := ad.record_to_info(rec)) is not None
658
+ }
659
+
660
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
661
+ return self._infos.get(ty)