ominfra 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.
@@ -154,6 +154,13 @@ HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
154
154
  # ../../omlish/lite/maybes.py
155
155
  U = ta.TypeVar('U')
156
156
 
157
+ # ../../omlish/logs/infos.py
158
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
159
+ LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
160
+ LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
161
+ LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
162
+ LoggingContextInfo = ta.Any # ta.TypeAlias
163
+
157
164
  # ../../omlish/http/handlers.py
158
165
  HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse'] # ta.TypeAlias
159
166
  HttpHandlerResponseData = ta.Union[bytes, 'HttpHandlerResponseStreamedData'] # ta.TypeAlias # noqa
@@ -165,16 +172,11 @@ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
165
172
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
166
173
 
167
174
  # ../../omlish/logs/contexts.py
168
- LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
169
- LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
170
- LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
175
+ LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
171
176
 
172
177
  # ../../omlish/http/coro/server/server.py
173
178
  CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
174
179
 
175
- # ../../omlish/logs/base.py
176
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
177
-
178
180
 
179
181
  ########################################
180
182
  # ../errors.py
@@ -3074,124 +3076,6 @@ def typing_annotations_attr() -> str:
3074
3076
  return _TYPING_ANNOTATIONS_ATTR
3075
3077
 
3076
3078
 
3077
- ########################################
3078
- # ../../../omlish/logs/infos.py
3079
-
3080
-
3081
- ##
3082
-
3083
-
3084
- def logging_context_info(cls):
3085
- return cls
3086
-
3087
-
3088
- ##
3089
-
3090
-
3091
- @logging_context_info
3092
- @ta.final
3093
- class LoggingSourceFileInfo(ta.NamedTuple):
3094
- file_name: str
3095
- module: str
3096
-
3097
- @classmethod
3098
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
3099
- if file_path is None:
3100
- return None
3101
-
3102
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
3103
- try:
3104
- file_name = os.path.basename(file_path)
3105
- module = os.path.splitext(file_name)[0]
3106
- except (TypeError, ValueError, AttributeError):
3107
- return None
3108
-
3109
- return cls(
3110
- file_name=file_name,
3111
- module=module,
3112
- )
3113
-
3114
-
3115
- ##
3116
-
3117
-
3118
- @logging_context_info
3119
- @ta.final
3120
- class LoggingThreadInfo(ta.NamedTuple):
3121
- ident: int
3122
- native_id: ta.Optional[int]
3123
- name: str
3124
-
3125
- @classmethod
3126
- def build(cls) -> 'LoggingThreadInfo':
3127
- return cls(
3128
- ident=threading.get_ident(),
3129
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
3130
- name=threading.current_thread().name,
3131
- )
3132
-
3133
-
3134
- ##
3135
-
3136
-
3137
- @logging_context_info
3138
- @ta.final
3139
- class LoggingProcessInfo(ta.NamedTuple):
3140
- pid: int
3141
-
3142
- @classmethod
3143
- def build(cls) -> 'LoggingProcessInfo':
3144
- return cls(
3145
- pid=os.getpid(),
3146
- )
3147
-
3148
-
3149
- ##
3150
-
3151
-
3152
- @logging_context_info
3153
- @ta.final
3154
- class LoggingMultiprocessingInfo(ta.NamedTuple):
3155
- process_name: str
3156
-
3157
- @classmethod
3158
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
3159
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
3160
- if (mp := sys.modules.get('multiprocessing')) is None:
3161
- return None
3162
-
3163
- return cls(
3164
- process_name=mp.current_process().name,
3165
- )
3166
-
3167
-
3168
- ##
3169
-
3170
-
3171
- @logging_context_info
3172
- @ta.final
3173
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
3174
- name: str
3175
-
3176
- @classmethod
3177
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
3178
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
3179
- if (asyncio := sys.modules.get('asyncio')) is None:
3180
- return None
3181
-
3182
- try:
3183
- task = asyncio.current_task()
3184
- except Exception: # noqa
3185
- return None
3186
-
3187
- if task is None:
3188
- return None
3189
-
3190
- return cls(
3191
- name=task.get_name(), # Always non-None
3192
- )
3193
-
3194
-
3195
3079
  ########################################
3196
3080
  # ../../../omlish/logs/levels.py
3197
3081
 
@@ -6210,74 +6094,362 @@ def check_lite_runtime_version() -> None:
6210
6094
 
6211
6095
 
6212
6096
  ########################################
6213
- # ../../../omlish/logs/callers.py
6097
+ # ../../../omlish/logs/infos.py
6098
+ """
6099
+ TODO:
6100
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
6101
+ """
6214
6102
 
6215
6103
 
6216
6104
  ##
6217
6105
 
6218
6106
 
6219
- @logging_context_info
6107
+ def logging_context_info(cls):
6108
+ return cls
6109
+
6110
+
6220
6111
  @ta.final
6221
- class LoggingCaller(ta.NamedTuple):
6222
- file_path: str
6223
- line_no: int
6224
- name: str
6225
- stack_info: ta.Optional[str]
6112
+ class LoggingContextInfos:
6113
+ def __new__(cls, *args, **kwargs): # noqa
6114
+ raise TypeError
6226
6115
 
6227
- @classmethod
6228
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
6229
- file_path = os.path.normcase(frame.f_code.co_filename)
6116
+ #
6230
6117
 
6231
- # Yes, really.
6232
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
6233
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
6234
- if 'importlib' in file_path and '_bootstrap' in file_path:
6235
- return True
6118
+ @logging_context_info
6119
+ @ta.final
6120
+ class Name(ta.NamedTuple):
6121
+ name: str
6236
6122
 
6237
- return False
6123
+ @logging_context_info
6124
+ @ta.final
6125
+ class Level(ta.NamedTuple):
6126
+ level: NamedLogLevel
6127
+ name: str
6238
6128
 
6239
- @classmethod
6240
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
6241
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
6129
+ @classmethod
6130
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
6131
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
6132
+ return cls(
6133
+ level=nl,
6134
+ name=logging.getLevelName(nl),
6135
+ )
6136
+
6137
+ @logging_context_info
6138
+ @ta.final
6139
+ class Msg(ta.NamedTuple):
6140
+ msg: str
6141
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
6142
+
6143
+ @classmethod
6144
+ def build(
6145
+ cls,
6146
+ msg: ta.Union[str, tuple, LoggingMsgFn],
6147
+ *args: ta.Any,
6148
+ ) -> 'LoggingContextInfos.Msg':
6149
+ s: str
6150
+ a: ta.Any
6151
+
6152
+ if callable(msg):
6153
+ if args:
6154
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
6155
+ x = msg()
6156
+ if isinstance(x, str):
6157
+ s, a = x, ()
6158
+ elif isinstance(x, tuple):
6159
+ if x:
6160
+ s, a = x[0], x[1:]
6161
+ else:
6162
+ s, a = '', ()
6163
+ else:
6164
+ raise TypeError(x)
6242
6165
 
6243
- while f is not None:
6244
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
6245
- # stack_offset management.
6246
- if hasattr(f, 'f_code'):
6247
- return f
6166
+ elif isinstance(msg, tuple):
6167
+ if args:
6168
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
6169
+ if msg:
6170
+ s, a = msg[0], msg[1:]
6171
+ else:
6172
+ s, a = '', ()
6248
6173
 
6249
- f = f.f_back
6174
+ elif isinstance(msg, str):
6175
+ s, a = msg, args
6250
6176
 
6251
- return None
6177
+ else:
6178
+ raise TypeError(msg)
6179
+
6180
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
6181
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
6182
+ a = a[0]
6183
+
6184
+ return cls(
6185
+ msg=s,
6186
+ args=a,
6187
+ )
6188
+
6189
+ @logging_context_info
6190
+ @ta.final
6191
+ class Extra(ta.NamedTuple):
6192
+ extra: ta.Mapping[ta.Any, ta.Any]
6193
+
6194
+ @logging_context_info
6195
+ @ta.final
6196
+ class Time(ta.NamedTuple):
6197
+ ns: int
6198
+ secs: float
6199
+ msecs: float
6200
+ relative_secs: float
6201
+
6202
+ @classmethod
6203
+ def get_std_start_ns(cls) -> int:
6204
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
6205
+
6206
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
6207
+ # an int.
6208
+ #
6209
+ # See:
6210
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6211
+ #
6212
+ if isinstance(x, float):
6213
+ return int(x * 1e9)
6214
+ else:
6215
+ return x
6216
+
6217
+ @classmethod
6218
+ def build(
6219
+ cls,
6220
+ ns: int,
6221
+ *,
6222
+ start_ns: ta.Optional[int] = None,
6223
+ ) -> 'LoggingContextInfos.Time':
6224
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6225
+ secs = ns / 1e9 # ns to float seconds
6226
+
6227
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
6228
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
6229
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
6230
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
6231
+
6232
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6233
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
6234
+ # ns -> sec conversion can round up, e.g:
6235
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
6236
+ msecs = 0.0
6237
+
6238
+ if start_ns is None:
6239
+ start_ns = cls.get_std_start_ns()
6240
+ relative_secs = (ns - start_ns) / 1e6
6241
+
6242
+ return cls(
6243
+ ns=ns,
6244
+ secs=secs,
6245
+ msecs=msecs,
6246
+ relative_secs=relative_secs,
6247
+ )
6248
+
6249
+ @logging_context_info
6250
+ @ta.final
6251
+ class Exc(ta.NamedTuple):
6252
+ info: LoggingExcInfo
6253
+ info_tuple: LoggingExcInfoTuple
6254
+
6255
+ @classmethod
6256
+ def build(
6257
+ cls,
6258
+ arg: LoggingExcInfoArg = False,
6259
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
6260
+ if arg is True:
6261
+ sys_exc_info = sys.exc_info()
6262
+ if sys_exc_info[0] is not None:
6263
+ arg = sys_exc_info
6264
+ else:
6265
+ arg = None
6266
+ elif arg is False:
6267
+ arg = None
6268
+ if arg is None:
6269
+ return None
6270
+
6271
+ info: LoggingExcInfo = arg
6272
+ if isinstance(info, BaseException):
6273
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
6274
+ else:
6275
+ info_tuple = info
6276
+
6277
+ return cls(
6278
+ info=info,
6279
+ info_tuple=info_tuple,
6280
+ )
6281
+
6282
+ @logging_context_info
6283
+ @ta.final
6284
+ class Caller(ta.NamedTuple):
6285
+ file_path: str
6286
+ line_no: int
6287
+ func_name: str
6288
+ stack_info: ta.Optional[str]
6289
+
6290
+ @classmethod
6291
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
6292
+ file_path = os.path.normcase(frame.f_code.co_filename)
6293
+
6294
+ # Yes, really.
6295
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
6296
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
6297
+ if 'importlib' in file_path and '_bootstrap' in file_path:
6298
+ return True
6299
+
6300
+ return False
6301
+
6302
+ @classmethod
6303
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
6304
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
6305
+
6306
+ while f is not None:
6307
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
6308
+ # manual stack_offset management.
6309
+ if hasattr(f, 'f_code'):
6310
+ return f
6311
+
6312
+ f = f.f_back
6252
6313
 
6253
- @classmethod
6254
- def find(
6255
- cls,
6256
- ofs: int = 0,
6257
- *,
6258
- stack_info: bool = False,
6259
- ) -> ta.Optional['LoggingCaller']:
6260
- if (f := cls.find_frame(ofs + 1)) is None:
6261
6314
  return None
6262
6315
 
6263
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
6264
- sinfo = None
6265
- if stack_info:
6266
- sio = io.StringIO()
6267
- traceback.print_stack(f, file=sio)
6268
- sinfo = sio.getvalue()
6269
- sio.close()
6270
- if sinfo[-1] == '\n':
6271
- sinfo = sinfo[:-1]
6316
+ @classmethod
6317
+ def build(
6318
+ cls,
6319
+ stack_offset: int = 0,
6320
+ *,
6321
+ stack_info: bool = False,
6322
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
6323
+ if (f := cls.find_frame(stack_offset + 1)) is None:
6324
+ return None
6272
6325
 
6273
- return cls(
6274
- file_path=f.f_code.co_filename,
6275
- line_no=f.f_lineno or 0,
6276
- name=f.f_code.co_name,
6277
- stack_info=sinfo,
6326
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
6327
+ sinfo = None
6328
+ if stack_info:
6329
+ sio = io.StringIO()
6330
+ traceback.print_stack(f, file=sio)
6331
+ sinfo = sio.getvalue()
6332
+ sio.close()
6333
+ if sinfo[-1] == '\n':
6334
+ sinfo = sinfo[:-1]
6335
+
6336
+ return cls(
6337
+ file_path=f.f_code.co_filename,
6338
+ line_no=f.f_lineno or 0,
6339
+ func_name=f.f_code.co_name,
6340
+ stack_info=sinfo,
6341
+ )
6342
+
6343
+ @logging_context_info
6344
+ @ta.final
6345
+ class SourceFile(ta.NamedTuple):
6346
+ file_name: str
6347
+ module: str
6348
+
6349
+ @classmethod
6350
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
6351
+ if caller_file_path is None:
6352
+ return None
6353
+
6354
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
6355
+ try:
6356
+ file_name = os.path.basename(caller_file_path)
6357
+ module = os.path.splitext(file_name)[0]
6358
+ except (TypeError, ValueError, AttributeError):
6359
+ return None
6360
+
6361
+ return cls(
6362
+ file_name=file_name,
6363
+ module=module,
6364
+ )
6365
+
6366
+ @logging_context_info
6367
+ @ta.final
6368
+ class Thread(ta.NamedTuple):
6369
+ ident: int
6370
+ native_id: ta.Optional[int]
6371
+ name: str
6372
+
6373
+ @classmethod
6374
+ def build(cls) -> 'LoggingContextInfos.Thread':
6375
+ return cls(
6376
+ ident=threading.get_ident(),
6377
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
6378
+ name=threading.current_thread().name,
6379
+ )
6380
+
6381
+ @logging_context_info
6382
+ @ta.final
6383
+ class Process(ta.NamedTuple):
6384
+ pid: int
6385
+
6386
+ @classmethod
6387
+ def build(cls) -> 'LoggingContextInfos.Process':
6388
+ return cls(
6389
+ pid=os.getpid(),
6390
+ )
6391
+
6392
+ @logging_context_info
6393
+ @ta.final
6394
+ class Multiprocessing(ta.NamedTuple):
6395
+ process_name: str
6396
+
6397
+ @classmethod
6398
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
6399
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
6400
+ if (mp := sys.modules.get('multiprocessing')) is None:
6401
+ return None
6402
+
6403
+ return cls(
6404
+ process_name=mp.current_process().name,
6405
+ )
6406
+
6407
+ @logging_context_info
6408
+ @ta.final
6409
+ class AsyncioTask(ta.NamedTuple):
6410
+ name: str
6411
+
6412
+ @classmethod
6413
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
6414
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
6415
+ if (asyncio := sys.modules.get('asyncio')) is None:
6416
+ return None
6417
+
6418
+ try:
6419
+ task = asyncio.current_task()
6420
+ except Exception: # noqa
6421
+ return None
6422
+
6423
+ if task is None:
6424
+ return None
6425
+
6426
+ return cls(
6427
+ name=task.get_name(), # Always non-None
6428
+ )
6429
+
6430
+
6431
+ ##
6432
+
6433
+
6434
+ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
6435
+ pass
6436
+
6437
+
6438
+ def _check_logging_start_time() -> None:
6439
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
6440
+ import warnings # noqa
6441
+
6442
+ warnings.warn(
6443
+ f'Unexpected logging start time detected: '
6444
+ f'get_std_start_ns={x}, '
6445
+ f'time.time()={t}',
6446
+ UnexpectedLoggingStartTimeWarning,
6278
6447
  )
6279
6448
 
6280
6449
 
6450
+ _check_logging_start_time()
6451
+
6452
+
6281
6453
  ########################################
6282
6454
  # ../../../omlish/logs/protocols.py
6283
6455
 
@@ -6368,118 +6540,33 @@ class JsonLoggingFormatter(logging.Formatter):
6368
6540
 
6369
6541
 
6370
6542
  ########################################
6371
- # ../../../omlish/logs/times.py
6543
+ # ../../../omlish/os/journald.py
6372
6544
 
6373
6545
 
6374
6546
  ##
6375
6547
 
6376
6548
 
6377
- @logging_context_info
6378
- @ta.final
6379
- class LoggingTimeFields(ta.NamedTuple):
6380
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
6381
-
6382
- created: float
6383
- msecs: float
6384
- relative_created: float
6549
+ class sd_iovec(ct.Structure): # noqa
6550
+ pass
6385
6551
 
6386
- @classmethod
6387
- def get_std_start_time_ns(cls) -> int:
6388
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
6389
6552
 
6390
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
6391
- # int.
6392
- #
6393
- # See:
6394
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6395
- #
6396
- if isinstance(x, float):
6397
- return int(x * 1e9)
6398
- else:
6399
- return x
6553
+ sd_iovec._fields_ = [
6554
+ ('iov_base', ct.c_void_p), # Pointer to data.
6555
+ ('iov_len', ct.c_size_t), # Length of data.
6556
+ ]
6400
6557
 
6401
- @classmethod
6402
- def build(
6403
- cls,
6404
- time_ns: int,
6405
- *,
6406
- start_time_ns: ta.Optional[int] = None,
6407
- ) -> 'LoggingTimeFields':
6408
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6409
- created = time_ns / 1e9 # ns to float seconds
6410
-
6411
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
6412
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
6413
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
6414
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
6415
-
6416
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6417
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
6418
- # ns -> sec conversion can round up, e.g:
6419
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
6420
- msecs = 0.0
6421
-
6422
- if start_time_ns is None:
6423
- start_time_ns = cls.get_std_start_time_ns()
6424
- relative_created = (time_ns - start_time_ns) / 1e6
6425
6558
 
6426
- return cls(
6427
- created=created,
6428
- msecs=msecs,
6429
- relative_created=relative_created,
6430
- )
6559
+ ##
6431
6560
 
6432
6561
 
6433
- ##
6562
+ @cached_nullary
6563
+ def sd_libsystemd() -> ta.Any:
6564
+ lib = ct.CDLL('libsystemd.so.0')
6434
6565
 
6566
+ lib.sd_journal_sendv.restype = ct.c_int
6567
+ lib.sd_journal_sendv.argtypes = [ct.POINTER(sd_iovec), ct.c_int]
6435
6568
 
6436
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
6437
- pass
6438
-
6439
-
6440
- def _check_logging_start_time() -> None:
6441
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
6442
- import warnings # noqa
6443
-
6444
- warnings.warn(
6445
- f'Unexpected logging start time detected: '
6446
- f'get_std_start_time_ns={x}, '
6447
- f'time.time()={t}',
6448
- UnexpectedLoggingStartTimeWarning,
6449
- )
6450
-
6451
-
6452
- _check_logging_start_time()
6453
-
6454
-
6455
- ########################################
6456
- # ../../../omlish/os/journald.py
6457
-
6458
-
6459
- ##
6460
-
6461
-
6462
- class sd_iovec(ct.Structure): # noqa
6463
- pass
6464
-
6465
-
6466
- sd_iovec._fields_ = [
6467
- ('iov_base', ct.c_void_p), # Pointer to data.
6468
- ('iov_len', ct.c_size_t), # Length of data.
6469
- ]
6470
-
6471
-
6472
- ##
6473
-
6474
-
6475
- @cached_nullary
6476
- def sd_libsystemd() -> ta.Any:
6477
- lib = ct.CDLL('libsystemd.so.0')
6478
-
6479
- lib.sd_journal_sendv.restype = ct.c_int
6480
- lib.sd_journal_sendv.argtypes = [ct.POINTER(sd_iovec), ct.c_int]
6481
-
6482
- return lib
6569
+ return lib
6483
6570
 
6484
6571
 
6485
6572
  @cached_nullary
@@ -8490,68 +8577,46 @@ inj = InjectionApi()
8490
8577
 
8491
8578
 
8492
8579
  class LoggingContext(Abstract):
8493
- @property
8494
- @abc.abstractmethod
8495
- def level(self) -> NamedLogLevel:
8496
- raise NotImplementedError
8497
-
8498
- #
8499
-
8500
- @property
8501
- @abc.abstractmethod
8502
- def time_ns(self) -> int:
8503
- raise NotImplementedError
8504
-
8505
- @property
8506
8580
  @abc.abstractmethod
8507
- def times(self) -> LoggingTimeFields:
8581
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8508
8582
  raise NotImplementedError
8509
8583
 
8510
- #
8511
-
8512
- @property
8513
- @abc.abstractmethod
8514
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
8515
- raise NotImplementedError
8584
+ @ta.final
8585
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8586
+ return self.get_info(ty)
8516
8587
 
8517
- @property
8518
- @abc.abstractmethod
8519
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
8520
- raise NotImplementedError
8588
+ @ta.final
8589
+ def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
8590
+ if (info := self.get_info(ty)) is None:
8591
+ raise TypeError(f'LoggingContextInfo absent: {ty}')
8592
+ return info
8521
8593
 
8522
- #
8523
8594
 
8524
- @abc.abstractmethod
8525
- def caller(self) -> ta.Optional[LoggingCaller]:
8526
- raise NotImplementedError
8595
+ @ta.final
8596
+ class SimpleLoggingContext(LoggingContext):
8597
+ def __init__(self, *infos: LoggingContextInfo) -> None:
8598
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {type(i): i for i in infos}
8527
8599
 
8528
- @abc.abstractmethod
8529
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
8530
- raise NotImplementedError
8600
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8601
+ return self._infos.get(ty)
8531
8602
 
8532
- #
8533
8603
 
8534
- @abc.abstractmethod
8535
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
8536
- raise NotImplementedError
8604
+ ##
8537
8605
 
8538
- @abc.abstractmethod
8539
- def process(self) -> ta.Optional[LoggingProcessInfo]:
8540
- raise NotImplementedError
8541
8606
 
8607
+ class CaptureLoggingContext(LoggingContext, Abstract):
8542
8608
  @abc.abstractmethod
8543
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
8544
- raise NotImplementedError
8609
+ def set_basic(
8610
+ self,
8611
+ name: str,
8545
8612
 
8546
- @abc.abstractmethod
8547
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
8613
+ msg: ta.Union[str, tuple, LoggingMsgFn],
8614
+ args: tuple,
8615
+ ) -> 'CaptureLoggingContext':
8548
8616
  raise NotImplementedError
8549
8617
 
8618
+ #
8550
8619
 
8551
- ##
8552
-
8553
-
8554
- class CaptureLoggingContext(LoggingContext, Abstract):
8555
8620
  class AlreadyCapturedError(Exception):
8556
8621
  pass
8557
8622
 
@@ -8582,80 +8647,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
8582
8647
 
8583
8648
  exc_info: LoggingExcInfoArg = False,
8584
8649
 
8585
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
8650
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
8586
8651
  stack_offset: int = 0,
8587
8652
  stack_info: bool = False,
8588
8653
  ) -> None:
8589
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
8590
-
8591
- #
8654
+ # TODO: Name, Msg, Extra
8592
8655
 
8593
8656
  if time_ns is None:
8594
8657
  time_ns = time.time_ns()
8595
- self._time_ns: int = time_ns
8596
-
8597
- #
8598
8658
 
8599
- if exc_info is True:
8600
- sys_exc_info = sys.exc_info()
8601
- if sys_exc_info[0] is not None:
8602
- exc_info = sys_exc_info
8603
- else:
8604
- exc_info = None
8605
- elif exc_info is False:
8606
- exc_info = None
8607
-
8608
- if exc_info is not None:
8609
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
8610
- if isinstance(exc_info, BaseException):
8611
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
8612
- else:
8613
- self._exc_info_tuple = exc_info
8614
-
8615
- #
8659
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
8660
+ self._set_info(
8661
+ LoggingContextInfos.Level.build(level),
8662
+ LoggingContextInfos.Time.build(time_ns),
8663
+ LoggingContextInfos.Exc.build(exc_info),
8664
+ )
8616
8665
 
8617
8666
  if caller is not CaptureLoggingContextImpl.NOT_SET:
8618
- self._caller = caller # type: ignore[assignment]
8667
+ self._infos[LoggingContextInfos.Caller] = caller
8619
8668
  else:
8620
8669
  self._stack_offset = stack_offset
8621
8670
  self._stack_info = stack_info
8622
8671
 
8623
- ##
8624
-
8625
- @property
8626
- def level(self) -> NamedLogLevel:
8627
- return self._level
8628
-
8629
- #
8630
-
8631
- @property
8632
- def time_ns(self) -> int:
8633
- return self._time_ns
8634
-
8635
- _times: LoggingTimeFields
8636
-
8637
- @property
8638
- def times(self) -> LoggingTimeFields:
8639
- try:
8640
- return self._times
8641
- except AttributeError:
8642
- pass
8643
-
8644
- times = self._times = LoggingTimeFields.build(self.time_ns)
8645
- return times
8672
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
8673
+ for info in infos:
8674
+ if info is not None:
8675
+ self._infos[type(info)] = info
8676
+ return self
8646
8677
 
8647
- #
8678
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8679
+ return self._infos.get(ty)
8648
8680
 
8649
- _exc_info: ta.Optional[LoggingExcInfo] = None
8650
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
8681
+ ##
8651
8682
 
8652
- @property
8653
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
8654
- return self._exc_info
8683
+ def set_basic(
8684
+ self,
8685
+ name: str,
8655
8686
 
8656
- @property
8657
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
8658
- return self._exc_info_tuple
8687
+ msg: ta.Union[str, tuple, LoggingMsgFn],
8688
+ args: tuple,
8689
+ ) -> 'CaptureLoggingContextImpl':
8690
+ return self._set_info(
8691
+ LoggingContextInfos.Name(name),
8692
+ LoggingContextInfos.Msg.build(msg, *args),
8693
+ )
8659
8694
 
8660
8695
  ##
8661
8696
 
@@ -8669,74 +8704,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
8669
8704
 
8670
8705
  _has_captured: bool = False
8671
8706
 
8672
- _caller: ta.Optional[LoggingCaller]
8673
- _source_file: ta.Optional[LoggingSourceFileInfo]
8674
-
8675
- _thread: ta.Optional[LoggingThreadInfo]
8676
- _process: ta.Optional[LoggingProcessInfo]
8677
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
8678
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
8679
-
8680
8707
  def capture(self) -> None:
8681
8708
  if self._has_captured:
8682
8709
  raise CaptureLoggingContextImpl.AlreadyCapturedError
8683
8710
  self._has_captured = True
8684
8711
 
8685
- if not hasattr(self, '_caller'):
8686
- self._caller = LoggingCaller.find(
8712
+ if LoggingContextInfos.Caller not in self._infos:
8713
+ self._set_info(LoggingContextInfos.Caller.build(
8687
8714
  self._stack_offset + 1,
8688
8715
  stack_info=self._stack_info,
8689
- )
8690
-
8691
- if (caller := self._caller) is not None:
8692
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
8693
- else:
8694
- self._source_file = None
8695
-
8696
- self._thread = LoggingThreadInfo.build()
8697
- self._process = LoggingProcessInfo.build()
8698
- self._multiprocessing = LoggingMultiprocessingInfo.build()
8699
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
8700
-
8701
- #
8702
-
8703
- def caller(self) -> ta.Optional[LoggingCaller]:
8704
- try:
8705
- return self._caller
8706
- except AttributeError:
8707
- raise CaptureLoggingContext.NotCapturedError from None
8708
-
8709
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
8710
- try:
8711
- return self._source_file
8712
- except AttributeError:
8713
- raise CaptureLoggingContext.NotCapturedError from None
8714
-
8715
- #
8716
-
8717
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
8718
- try:
8719
- return self._thread
8720
- except AttributeError:
8721
- raise CaptureLoggingContext.NotCapturedError from None
8722
-
8723
- def process(self) -> ta.Optional[LoggingProcessInfo]:
8724
- try:
8725
- return self._process
8726
- except AttributeError:
8727
- raise CaptureLoggingContext.NotCapturedError from None
8716
+ ))
8728
8717
 
8729
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
8730
- try:
8731
- return self._multiprocessing
8732
- except AttributeError:
8733
- raise CaptureLoggingContext.NotCapturedError from None
8718
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
8719
+ self._set_info(LoggingContextInfos.SourceFile.build(
8720
+ caller.file_path,
8721
+ ))
8734
8722
 
8735
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
8736
- try:
8737
- return self._asyncio_task
8738
- except AttributeError:
8739
- raise CaptureLoggingContext.NotCapturedError from None
8723
+ self._set_info(
8724
+ LoggingContextInfos.Thread.build(),
8725
+ LoggingContextInfos.Process.build(),
8726
+ LoggingContextInfos.Multiprocessing.build(),
8727
+ LoggingContextInfos.AsyncioTask.build(),
8728
+ )
8740
8729
 
8741
8730
 
8742
8731
  ########################################
@@ -8813,10 +8802,14 @@ def _locking_logging_module_lock() -> ta.Iterator[None]:
8813
8802
  def configure_standard_logging(
8814
8803
  level: ta.Union[int, str] = logging.INFO,
8815
8804
  *,
8816
- json: bool = False,
8817
8805
  target: ta.Optional[logging.Logger] = None,
8806
+
8818
8807
  force: bool = False,
8808
+
8819
8809
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
8810
+
8811
+ formatter: ta.Optional[logging.Formatter] = None, # noqa
8812
+ json: bool = False,
8820
8813
  ) -> ta.Optional[StandardConfiguredLoggingHandler]:
8821
8814
  with _locking_logging_module_lock():
8822
8815
  if target is None:
@@ -8837,11 +8830,11 @@ def configure_standard_logging(
8837
8830
 
8838
8831
  #
8839
8832
 
8840
- formatter: logging.Formatter
8841
- if json:
8842
- formatter = JsonLoggingFormatter()
8843
- else:
8844
- formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
8833
+ if formatter is None:
8834
+ if json:
8835
+ formatter = JsonLoggingFormatter()
8836
+ else:
8837
+ formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS)) # noqa
8845
8838
  handler.setFormatter(formatter)
8846
8839
 
8847
8840
  #
@@ -9762,36 +9755,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
9762
9755
 
9763
9756
  ##
9764
9757
 
9765
- @classmethod
9766
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
9767
- if callable(msg):
9768
- if args:
9769
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
9770
- x = msg()
9771
- if isinstance(x, str):
9772
- return x, ()
9773
- elif isinstance(x, tuple):
9774
- if x:
9775
- return x[0], x[1:]
9776
- else:
9777
- return '', ()
9778
- else:
9779
- raise TypeError(x)
9780
-
9781
- elif isinstance(msg, tuple):
9782
- if args:
9783
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
9784
- if msg:
9785
- return msg[0], msg[1:]
9786
- else:
9787
- return '', ()
9788
-
9789
- elif isinstance(msg, str):
9790
- return msg, args
9791
-
9792
- else:
9793
- raise TypeError(msg)
9794
-
9795
9758
  @abc.abstractmethod
9796
9759
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
9797
9760
  raise NotImplementedError
@@ -9832,144 +9795,560 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
9832
9795
 
9833
9796
  ########################################
9834
9797
  # ../../../omlish/logs/std/records.py
9798
+ """
9799
+ TODO:
9800
+ - TypedDict?
9801
+ """
9835
9802
 
9836
9803
 
9837
9804
  ##
9838
9805
 
9839
9806
 
9840
- # Ref:
9841
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9842
- #
9843
- # LogRecord:
9844
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
9845
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9846
- #
9847
- # LogRecord.__init__ args:
9848
- # - name: str
9849
- # - level: int
9850
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
9851
- # - lineno: int - May be 0.
9852
- # - msg: str
9853
- # - args: tuple | dict | 1-tuple[dict]
9854
- # - exc_info: LoggingExcInfoTuple | None
9855
- # - func: str | None = None -> funcName
9856
- # - sinfo: str | None = None -> stack_info
9857
- #
9858
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9859
- # Name of the logger used to log the call. Unmodified by ctor.
9860
- name=str,
9807
+ class LoggingContextInfoRecordAdapters:
9808
+ # Ref:
9809
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9810
+ #
9811
+ # LogRecord:
9812
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
9813
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9814
+ #
9815
+
9816
+ def __new__(cls, *args, **kwargs): # noqa
9817
+ raise TypeError
9861
9818
 
9862
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
9863
- # (see Using arbitrary objects as messages). Unmodified by ctor.
9864
- msg=str,
9819
+ class Adapter(Abstract, ta.Generic[T]):
9820
+ @property
9821
+ @abc.abstractmethod
9822
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
9823
+ raise NotImplementedError
9865
9824
 
9866
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
9867
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
9868
- # the mapping, but is otherwise unmodified.
9869
- args=ta.Union[tuple, dict],
9825
+ #
9870
9826
 
9871
- #
9827
+ @ta.final
9828
+ class NOT_SET: # noqa
9829
+ def __new__(cls, *args, **kwargs): # noqa
9830
+ raise TypeError
9872
9831
 
9873
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9874
- # `getLevelName(level)`.
9875
- levelname=str,
9832
+ class RecordAttr(ta.NamedTuple):
9833
+ name: str
9834
+ type: ta.Any
9835
+ default: ta.Any
9876
9836
 
9877
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9878
- levelno=int,
9837
+ # @abc.abstractmethod
9838
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
9879
9839
 
9880
- #
9840
+ @property
9841
+ @abc.abstractmethod
9842
+ def _record_attrs(self) -> ta.Union[
9843
+ ta.Mapping[str, ta.Any],
9844
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
9845
+ ]:
9846
+ raise NotImplementedError
9881
9847
 
9882
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
9883
- # to "(unknown file)" by Logger.findCaller / Logger._log.
9884
- pathname=str,
9848
+ #
9885
9849
 
9886
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
9887
- filename=str,
9850
+ @abc.abstractmethod
9851
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9852
+ raise NotImplementedError
9888
9853
 
9889
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
9890
- # "Unknown module".
9891
- module=str,
9854
+ #
9892
9855
 
9893
- #
9856
+ @abc.abstractmethod
9857
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
9858
+ raise NotImplementedError
9894
9859
 
9895
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9896
- exc_info=ta.Optional[LoggingExcInfoTuple],
9860
+ #
9897
9861
 
9898
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9899
- exc_text=ta.Optional[str],
9862
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9863
+ super().__init_subclass__(**kwargs)
9900
9864
 
9901
- #
9865
+ if Abstract in cls.__bases__:
9866
+ return
9902
9867
 
9903
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
9904
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
9905
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
9906
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
9907
- stack_info=ta.Optional[str],
9868
+ if 'record_attrs' in cls.__dict__:
9869
+ raise TypeError(cls)
9870
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
9871
+ raise TypeError(ra)
9872
+
9873
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
9874
+ for n, v in ra.items():
9875
+ if not n or not isinstance(n, str) or n in rd:
9876
+ raise AttributeError(n)
9877
+ if isinstance(v, tuple):
9878
+ t, d = v
9879
+ else:
9880
+ t, d = v, cls.NOT_SET
9881
+ rd[n] = cls.RecordAttr(
9882
+ name=n,
9883
+ type=t,
9884
+ default=d,
9885
+ )
9886
+ cls.record_attrs = rd
9908
9887
 
9909
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
9910
- # Logger.findCaller / Logger._log.
9911
- lineno=int,
9888
+ class RequiredAdapter(Adapter[T], Abstract):
9889
+ @property
9890
+ @abc.abstractmethod
9891
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
9892
+ raise NotImplementedError
9912
9893
 
9913
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
9914
- # "(unknown function)" by Logger.findCaller / Logger._log.
9915
- funcName=str,
9894
+ #
9916
9895
 
9917
- #
9896
+ @ta.final
9897
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9898
+ if (info := ctx.get_info(self.info_cls)) is not None:
9899
+ return self._info_to_record(info)
9900
+ else:
9901
+ raise TypeError # FIXME: fallback?
9918
9902
 
9919
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
9920
- #
9921
- # See:
9922
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
9923
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
9924
- #
9925
- created=float,
9903
+ @abc.abstractmethod
9904
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9905
+ raise NotImplementedError
9926
9906
 
9927
- # Millisecond portion of the time when the LogRecord was created.
9928
- msecs=float,
9907
+ #
9929
9908
 
9930
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9931
- relativeCreated=float,
9909
+ @abc.abstractmethod
9910
+ def record_to_info(self, rec: logging.LogRecord) -> T:
9911
+ raise NotImplementedError
9932
9912
 
9933
- #
9913
+ #
9934
9914
 
9935
- # Thread ID if available, and `logging.logThreads` is truthy.
9936
- thread=ta.Optional[int],
9915
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9916
+ super().__init_subclass__(**kwargs)
9937
9917
 
9938
- # Thread name if available, and `logging.logThreads` is truthy.
9939
- threadName=ta.Optional[str],
9918
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
9919
+ raise TypeError(cls.record_attrs)
9940
9920
 
9941
- #
9921
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
9922
+ @property
9923
+ @abc.abstractmethod
9924
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
9925
+ raise NotImplementedError
9926
+
9927
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
9928
+
9929
+ #
9930
+
9931
+ @ta.final
9932
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9933
+ if (info := ctx.get_info(self.info_cls)) is not None:
9934
+ return self._info_to_record(info)
9935
+ else:
9936
+ return self.record_defaults
9937
+
9938
+ @abc.abstractmethod
9939
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9940
+ raise NotImplementedError
9941
+
9942
+ #
9943
+
9944
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9945
+ super().__init_subclass__(**kwargs)
9946
+
9947
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
9948
+ if any(d is cls.NOT_SET for d in dd.values()):
9949
+ raise TypeError(cls.record_attrs)
9950
+ cls.record_defaults = dd
9942
9951
 
9943
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
9944
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
9945
- # as 'MainProcess'.
9946
9952
  #
9947
- # As noted by stdlib:
9953
+
9954
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
9955
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
9956
+
9957
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9958
+ # Name of the logger used to log the call. Unmodified by ctor.
9959
+ name=str,
9960
+ )
9961
+
9962
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
9963
+ return dict(
9964
+ name=info.name,
9965
+ )
9966
+
9967
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
9968
+ return LoggingContextInfos.Name(
9969
+ name=rec.name,
9970
+ )
9971
+
9972
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
9973
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
9974
+
9975
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9976
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9977
+ # `getLevelName(level)`.
9978
+ levelname=str,
9979
+
9980
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9981
+ levelno=int,
9982
+ )
9983
+
9984
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
9985
+ return dict(
9986
+ levelname=info.name,
9987
+ levelno=int(info.level),
9988
+ )
9989
+
9990
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
9991
+ return LoggingContextInfos.Level.build(rec.levelno)
9992
+
9993
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
9994
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
9995
+
9996
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9997
+ # The format string passed in the original logging call. Merged with args to produce message, or an
9998
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
9999
+ msg=str,
10000
+
10001
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
10002
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
10003
+ # Mapping into just the mapping, but is otherwise unmodified.
10004
+ args=ta.Union[tuple, dict, None],
10005
+ )
10006
+
10007
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
10008
+ return dict(
10009
+ msg=info.msg,
10010
+ args=info.args,
10011
+ )
10012
+
10013
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
10014
+ return LoggingContextInfos.Msg(
10015
+ msg=rec.msg,
10016
+ args=rec.args,
10017
+ )
10018
+
10019
+ # FIXME: handled specially - all unknown attrs on LogRecord
10020
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
10021
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
9948
10022
  #
9949
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
9950
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
10023
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
10024
+ # # FIXME:
10025
+ # # if extra is not None:
10026
+ # # for key in extra:
10027
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
10028
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
10029
+ # # rv.__dict__[key] = extra[key]
10030
+ # return dict()
9951
10031
  #
9952
- processName=ta.Optional[str],
10032
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
10033
+ # return None
9953
10034
 
9954
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
9955
- # None.
9956
- process=ta.Optional[int],
10035
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
10036
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
9957
10037
 
9958
- #
10038
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
10039
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
10040
+ # `time.time()`.
10041
+ #
10042
+ # See:
10043
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
10044
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
10045
+ #
10046
+ created=float,
9959
10047
 
9960
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
9961
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
9962
- taskName=ta.Optional[str],
9963
- )
10048
+ # Millisecond portion of the time when the LogRecord was created.
10049
+ msecs=float,
10050
+
10051
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
10052
+ relativeCreated=float,
10053
+ )
10054
+
10055
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
10056
+ return dict(
10057
+ created=info.secs,
10058
+ msecs=info.msecs,
10059
+ relativeCreated=info.relative_secs,
10060
+ )
10061
+
10062
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
10063
+ return LoggingContextInfos.Time.build(
10064
+ int(rec.created * 1e9),
10065
+ )
10066
+
10067
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
10068
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
10069
+
10070
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10071
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
10072
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
10073
+
10074
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
10075
+ exc_text=(ta.Optional[str], None),
10076
+ )
10077
+
10078
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
10079
+ return dict(
10080
+ exc_info=info.info_tuple,
10081
+ exc_text=None,
10082
+ )
10083
+
10084
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
10085
+ # FIXME:
10086
+ # error: Argument 1 to "build" of "Exc" has incompatible type
10087
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
10088
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
10089
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
9964
10090
 
9965
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
10091
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
10092
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
10093
+
10094
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
10095
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
10096
+
10097
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
10098
+
10099
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10100
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
10101
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
10102
+ pathname=(str, _UNKNOWN_PATH_NAME),
10103
+
10104
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
10105
+ # y Logger.findCaller / Logger._log.
10106
+ lineno=(int, 0),
10107
+
10108
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
10109
+ # "(unknown function)" by Logger.findCaller / Logger._log.
10110
+ funcName=(str, _UNKNOWN_FUNC_NAME),
10111
+
10112
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
10113
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
10114
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
10115
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
10116
+ # stripped of exactly one trailing `\n` if present.
10117
+ stack_info=(ta.Optional[str], None),
10118
+ )
10119
+
10120
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
10121
+ if (sinfo := caller.stack_info) is not None:
10122
+ stack_info: ta.Optional[str] = '\n'.join([
10123
+ self._STACK_INFO_PREFIX,
10124
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
10125
+ ])
10126
+ else:
10127
+ stack_info = None
10128
+
10129
+ return dict(
10130
+ pathname=caller.file_path,
10131
+
10132
+ lineno=caller.line_no,
10133
+ funcName=caller.func_name,
10134
+
10135
+ stack_info=stack_info,
10136
+ )
10137
+
10138
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
10139
+ # FIXME: piecemeal?
10140
+ if (
10141
+ rec.pathname != self._UNKNOWN_PATH_NAME and
10142
+ rec.lineno != 0 and
10143
+ rec.funcName != self._UNKNOWN_FUNC_NAME
10144
+ ):
10145
+ if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
10146
+ sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
10147
+ return LoggingContextInfos.Caller(
10148
+ file_path=rec.pathname,
10149
+
10150
+ line_no=rec.lineno,
10151
+ func_name=rec.funcName,
10152
+
10153
+ stack_info=sinfo,
10154
+ )
10155
+
10156
+ return None
10157
+
10158
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
10159
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
10160
+
10161
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
10162
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
10163
+ # pathname.
10164
+ filename=str,
10165
+
10166
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
10167
+ # "Unknown module".
10168
+ module=str,
10169
+ )
10170
+
10171
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
10172
+
10173
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
10174
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
10175
+ return dict(
10176
+ filename=info.file_name,
10177
+ module=info.module,
10178
+ )
10179
+
10180
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
10181
+ return dict(
10182
+ filename=caller.file_path,
10183
+ module=self._UNKNOWN_MODULE,
10184
+ )
10185
+
10186
+ return dict(
10187
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
10188
+ module=self._UNKNOWN_MODULE,
10189
+ )
10190
+
10191
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
10192
+ if (
10193
+ rec.module is not None and
10194
+ rec.module != self._UNKNOWN_MODULE
10195
+ ):
10196
+ return LoggingContextInfos.SourceFile(
10197
+ file_name=rec.filename,
10198
+ module=rec.module, # FIXME: piecemeal?
10199
+ )
10200
+
10201
+ return None
10202
+
10203
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
10204
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
10205
+
10206
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10207
+ # Thread ID if available, and `logging.logThreads` is truthy.
10208
+ thread=(ta.Optional[int], None),
10209
+
10210
+ # Thread name if available, and `logging.logThreads` is truthy.
10211
+ threadName=(ta.Optional[str], None),
10212
+ )
10213
+
10214
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
10215
+ if logging.logThreads:
10216
+ return dict(
10217
+ thread=info.ident,
10218
+ threadName=info.name,
10219
+ )
10220
+
10221
+ return self.record_defaults
10222
+
10223
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
10224
+ if (
10225
+ (ident := rec.thread) is not None and
10226
+ (name := rec.threadName) is not None
10227
+ ):
10228
+ return LoggingContextInfos.Thread(
10229
+ ident=ident,
10230
+ native_id=None,
10231
+ name=name,
10232
+ )
10233
+
10234
+ return None
10235
+
10236
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
10237
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
10238
+
10239
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10240
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
10241
+ # otherwise None.
10242
+ process=(ta.Optional[int], None),
10243
+ )
10244
+
10245
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
10246
+ if logging.logProcesses:
10247
+ return dict(
10248
+ process=info.pid,
10249
+ )
10250
+
10251
+ return self.record_defaults
10252
+
10253
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
10254
+ if (
10255
+ (pid := rec.process) is not None
10256
+ ):
10257
+ return LoggingContextInfos.Process(
10258
+ pid=pid,
10259
+ )
10260
+
10261
+ return None
10262
+
10263
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
10264
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
10265
+
10266
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10267
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
10268
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
10269
+ # remains as 'MainProcess'.
10270
+ #
10271
+ # As noted by stdlib:
10272
+ #
10273
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
10274
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
10275
+ #
10276
+ processName=(ta.Optional[str], None),
10277
+ )
10278
+
10279
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
10280
+ if logging.logMultiprocessing:
10281
+ return dict(
10282
+ processName=info.process_name,
10283
+ )
10284
+
10285
+ return self.record_defaults
10286
+
10287
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
10288
+ if (
10289
+ (process_name := rec.processName) is not None
10290
+ ):
10291
+ return LoggingContextInfos.Multiprocessing(
10292
+ process_name=process_name,
10293
+ )
10294
+
10295
+ return None
10296
+
10297
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
10298
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
10299
+
10300
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
10301
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
10302
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
10303
+ taskName=(ta.Optional[str], None),
10304
+ )
10305
+
10306
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
10307
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
10308
+ return dict(
10309
+ taskName=info.name,
10310
+ )
10311
+
10312
+ return self.record_defaults
10313
+
10314
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
10315
+ if (
10316
+ (name := getattr(rec, 'taskName', None)) is not None
10317
+ ):
10318
+ return LoggingContextInfos.AsyncioTask(
10319
+ name=name,
10320
+ )
10321
+
10322
+ return None
10323
+
10324
+
10325
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
10326
+ LoggingContextInfoRecordAdapters.Name(),
10327
+ LoggingContextInfoRecordAdapters.Level(),
10328
+ LoggingContextInfoRecordAdapters.Msg(),
10329
+ LoggingContextInfoRecordAdapters.Time(),
10330
+ LoggingContextInfoRecordAdapters.Exc(),
10331
+ LoggingContextInfoRecordAdapters.Caller(),
10332
+ LoggingContextInfoRecordAdapters.SourceFile(),
10333
+ LoggingContextInfoRecordAdapters.Thread(),
10334
+ LoggingContextInfoRecordAdapters.Process(),
10335
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
10336
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
10337
+ ]
10338
+
10339
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
10340
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
10341
+ }
10342
+
10343
+
10344
+ ##
9966
10345
 
9967
10346
 
9968
10347
  # Formatter:
9969
10348
  # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
9970
10349
  # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
9971
10350
  #
9972
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
10351
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9973
10352
  # The logged message, computed as msg % args. Set to `record.getMessage()`.
9974
10353
  message=str,
9975
10354
 
@@ -9983,20 +10362,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9983
10362
  exc_text=ta.Optional[str],
9984
10363
  )
9985
10364
 
9986
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
9987
-
9988
10365
 
9989
10366
  ##
9990
10367
 
9991
10368
 
10369
+ _KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
10370
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
10371
+ )
10372
+
10373
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
10374
+
10375
+
9992
10376
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
9993
10377
  pass
9994
10378
 
9995
10379
 
9996
10380
  def _check_std_logging_record_attrs() -> None:
10381
+ if (
10382
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
10383
+ len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
10384
+ ):
10385
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
10386
+
9997
10387
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
9998
10388
 
9999
- if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
10389
+ if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
10000
10390
  import warnings # noqa
10001
10391
 
10002
10392
  warnings.warn(
@@ -10012,116 +10402,43 @@ _check_std_logging_record_attrs()
10012
10402
 
10013
10403
 
10014
10404
  class LoggingContextLogRecord(logging.LogRecord):
10015
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
10016
-
10017
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
10018
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
10019
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
10020
-
10021
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
10022
-
10023
- def __init__( # noqa
10024
- self,
10025
- # name,
10026
- # level,
10027
- # pathname,
10028
- # lineno,
10029
- # msg,
10030
- # args,
10031
- # exc_info,
10032
- # func=None,
10033
- # sinfo=None,
10034
- # **kwargs,
10035
- *,
10036
- name: str,
10037
- msg: str,
10038
- args: ta.Union[tuple, dict],
10039
-
10040
- _logging_context: LoggingContext,
10041
- ) -> None:
10042
- ctx = _logging_context
10043
-
10044
- self.name: str = name
10045
-
10046
- self.msg: str = msg
10047
-
10048
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
10049
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
10050
- args = args[0] # type: ignore[assignment]
10051
- self.args: ta.Union[tuple, dict] = args
10052
-
10053
- self.levelname: str = logging.getLevelName(ctx.level)
10054
- self.levelno: int = ctx.level
10055
-
10056
- if (caller := ctx.caller()) is not None:
10057
- self.pathname: str = caller.file_path
10058
- else:
10059
- self.pathname = self._UNKNOWN_PATH_NAME
10060
-
10061
- if (src_file := ctx.source_file()) is not None:
10062
- self.filename: str = src_file.file_name
10063
- self.module: str = src_file.module
10064
- else:
10065
- self.filename = self.pathname
10066
- self.module = self._UNKNOWN_MODULE
10405
+ # LogRecord.__init__ args:
10406
+ # - name: str
10407
+ # - level: int
10408
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
10409
+ # - lineno: int - May be 0.
10410
+ # - msg: str
10411
+ # - args: tuple | dict | 1-tuple[dict]
10412
+ # - exc_info: LoggingExcInfoTuple | None
10413
+ # - func: str | None = None -> funcName
10414
+ # - sinfo: str | None = None -> stack_info
10067
10415
 
10068
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
10069
- self.exc_text: ta.Optional[str] = None
10416
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
10417
+ self._logging_context = _logging_context
10070
10418
 
10071
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
10072
- if caller is not None:
10073
- if (sinfo := caller.stack_info) is not None:
10074
- self.stack_info: ta.Optional[str] = '\n'.join([
10075
- self._STACK_INFO_PREFIX,
10076
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
10077
- ])
10078
- else:
10079
- self.stack_info = None
10419
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
10420
+ self.__dict__.update(ad.context_to_record(_logging_context))
10080
10421
 
10081
- self.lineno: int = caller.line_no
10082
- self.funcName: str = caller.name
10083
10422
 
10084
- else:
10085
- self.stack_info = None
10086
-
10087
- self.lineno = 0
10088
- self.funcName = self._UNKNOWN_FUNC_NAME
10423
+ ##
10089
10424
 
10090
- times = ctx.times
10091
- self.created: float = times.created
10092
- self.msecs: float = times.msecs
10093
- self.relativeCreated: float = times.relative_created
10094
10425
 
10095
- if logging.logThreads:
10096
- thread = check.not_none(ctx.thread())
10097
- self.thread: ta.Optional[int] = thread.ident
10098
- self.threadName: ta.Optional[str] = thread.name
10099
- else:
10100
- self.thread = None
10101
- self.threadName = None
10426
+ @ta.final
10427
+ class LogRecordLoggingContext(LoggingContext):
10428
+ def __init__(self, rec: logging.LogRecord) -> None:
10429
+ if isinstance(rec, LoggingContextLogRecord):
10430
+ raise TypeError(rec)
10102
10431
 
10103
- if logging.logProcesses:
10104
- process = check.not_none(ctx.process())
10105
- self.process: ta.Optional[int] = process.pid
10106
- else:
10107
- self.process = None
10432
+ self._rec = rec
10108
10433
 
10109
- if logging.logMultiprocessing:
10110
- if (mp := ctx.multiprocessing()) is not None:
10111
- self.processName: ta.Optional[str] = mp.process_name
10112
- else:
10113
- self.processName = None
10114
- else:
10115
- self.processName = None
10434
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
10435
+ type(info): info
10436
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
10437
+ if (info := ad.record_to_info(rec)) is not None
10438
+ }
10116
10439
 
10117
- # Absent <3.12
10118
- if getattr(logging, 'logAsyncioTasks', None):
10119
- if (at := ctx.asyncio_task()) is not None:
10120
- self.taskName: ta.Optional[str] = at.name
10121
- else:
10122
- self.taskName = None
10123
- else:
10124
- self.taskName = None
10440
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10441
+ return self._infos.get(ty)
10125
10442
 
10126
10443
 
10127
10444
  ########################################
@@ -10421,21 +10738,20 @@ class StdLogger(Logger):
10421
10738
  return self._std.getEffectiveLevel()
10422
10739
 
10423
10740
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
10424
- if not self.is_enabled_for(ctx.level):
10741
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
10425
10742
  return
10426
10743
 
10427
- ctx.capture()
10428
-
10429
- ms, args = self._prepare_msg_args(msg, *args)
10430
-
10431
- rec = LoggingContextLogRecord(
10744
+ ctx.set_basic(
10432
10745
  name=self._std.name,
10433
- msg=ms,
10434
- args=args,
10435
10746
 
10436
- _logging_context=ctx,
10747
+ msg=msg,
10748
+ args=args,
10437
10749
  )
10438
10750
 
10751
+ ctx.capture()
10752
+
10753
+ rec = LoggingContextLogRecord(_logging_context=ctx)
10754
+
10439
10755
  self._std.handle(rec)
10440
10756
 
10441
10757