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