ominfra 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.
@@ -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
@@ -2125,8 +2127,6 @@ class AttrOps(ta.Generic[T]):
2125
2127
  self._eq = _eq
2126
2128
  return _eq
2127
2129
 
2128
- #
2129
-
2130
2130
  @property
2131
2131
  def hash_eq(self) -> ta.Tuple[
2132
2132
  ta.Callable[[T], int],
@@ -2134,6 +2134,8 @@ class AttrOps(ta.Generic[T]):
2134
2134
  ]:
2135
2135
  return (self.hash, self.eq)
2136
2136
 
2137
+ #
2138
+
2137
2139
  @property
2138
2140
  def repr_hash_eq(self) -> ta.Tuple[
2139
2141
  ta.Callable[[T], str],
@@ -2144,20 +2146,25 @@ class AttrOps(ta.Generic[T]):
2144
2146
 
2145
2147
  #
2146
2148
 
2149
+ class NOT_SET: # noqa
2150
+ def __new__(cls, *args, **kwargs): # noqa
2151
+ raise TypeError
2152
+
2147
2153
  def install(
2148
2154
  self,
2149
2155
  locals_dct: ta.MutableMapping[str, ta.Any],
2150
2156
  *,
2151
- all: bool = False, # noqa
2152
- repr: bool = False, # noqa
2153
- hash: bool = False, # noqa
2154
- eq: bool = False,
2157
+ repr: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
2158
+ hash: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
2159
+ eq: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET,
2155
2160
  ) -> 'AttrOps[T]':
2156
- if repr or all:
2161
+ if all(a is self.NOT_SET for a in (repr, hash, eq)):
2162
+ repr = hash = eq = True # noqa
2163
+ if repr:
2157
2164
  locals_dct.update(__repr__=self.repr)
2158
- if hash or all:
2165
+ if hash:
2159
2166
  locals_dct.update(__hash__=self.hash)
2160
- if eq or all:
2167
+ if eq:
2161
2168
  locals_dct.update(__eq__=self.eq)
2162
2169
  return self
2163
2170
 
@@ -3069,124 +3076,6 @@ def typing_annotations_attr() -> str:
3069
3076
  return _TYPING_ANNOTATIONS_ATTR
3070
3077
 
3071
3078
 
3072
- ########################################
3073
- # ../../../omlish/logs/infos.py
3074
-
3075
-
3076
- ##
3077
-
3078
-
3079
- def logging_context_info(cls):
3080
- return cls
3081
-
3082
-
3083
- ##
3084
-
3085
-
3086
- @logging_context_info
3087
- @ta.final
3088
- class LoggingSourceFileInfo(ta.NamedTuple):
3089
- file_name: str
3090
- module: str
3091
-
3092
- @classmethod
3093
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
3094
- if file_path is None:
3095
- return None
3096
-
3097
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
3098
- try:
3099
- file_name = os.path.basename(file_path)
3100
- module = os.path.splitext(file_name)[0]
3101
- except (TypeError, ValueError, AttributeError):
3102
- return None
3103
-
3104
- return cls(
3105
- file_name,
3106
- module,
3107
- )
3108
-
3109
-
3110
- ##
3111
-
3112
-
3113
- @logging_context_info
3114
- @ta.final
3115
- class LoggingThreadInfo(ta.NamedTuple):
3116
- ident: int
3117
- native_id: ta.Optional[int]
3118
- name: str
3119
-
3120
- @classmethod
3121
- def build(cls) -> 'LoggingThreadInfo':
3122
- return cls(
3123
- threading.get_ident(),
3124
- threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
3125
- threading.current_thread().name,
3126
- )
3127
-
3128
-
3129
- ##
3130
-
3131
-
3132
- @logging_context_info
3133
- @ta.final
3134
- class LoggingProcessInfo(ta.NamedTuple):
3135
- pid: int
3136
-
3137
- @classmethod
3138
- def build(cls) -> 'LoggingProcessInfo':
3139
- return cls(
3140
- os.getpid(),
3141
- )
3142
-
3143
-
3144
- ##
3145
-
3146
-
3147
- @logging_context_info
3148
- @ta.final
3149
- class LoggingMultiprocessingInfo(ta.NamedTuple):
3150
- process_name: str
3151
-
3152
- @classmethod
3153
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
3154
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
3155
- if (mp := sys.modules.get('multiprocessing')) is None:
3156
- return None
3157
-
3158
- return cls(
3159
- mp.current_process().name,
3160
- )
3161
-
3162
-
3163
- ##
3164
-
3165
-
3166
- @logging_context_info
3167
- @ta.final
3168
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
3169
- name: str
3170
-
3171
- @classmethod
3172
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
3173
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
3174
- if (asyncio := sys.modules.get('asyncio')) is None:
3175
- return None
3176
-
3177
- try:
3178
- task = asyncio.current_task()
3179
- except Exception: # noqa
3180
- return None
3181
-
3182
- if task is None:
3183
- return None
3184
-
3185
- return cls(
3186
- task.get_name(), # Always non-None
3187
- )
3188
-
3189
-
3190
3079
  ########################################
3191
3080
  # ../../../omlish/logs/levels.py
3192
3081
 
@@ -3206,36 +3095,66 @@ class NamedLogLevel(int):
3206
3095
 
3207
3096
  #
3208
3097
 
3209
- @property
3210
- def exact_name(self) -> ta.Optional[str]:
3211
- return self._NAMES_BY_INT.get(self)
3098
+ _CACHE: ta.ClassVar[ta.MutableMapping[int, 'NamedLogLevel']] = {}
3099
+
3100
+ @ta.overload
3101
+ def __new__(cls, name: str, offset: int = 0, /) -> 'NamedLogLevel':
3102
+ ...
3103
+
3104
+ @ta.overload
3105
+ def __new__(cls, i: int, /) -> 'NamedLogLevel':
3106
+ ...
3212
3107
 
3213
- _effective_name: ta.Optional[str]
3108
+ def __new__(cls, x, offset=0, /):
3109
+ if isinstance(x, str):
3110
+ return cls(cls._INTS_BY_NAME[x.upper()] + offset)
3111
+ elif not offset and (c := cls._CACHE.get(x)) is not None:
3112
+ return c
3113
+ else:
3114
+ return super().__new__(cls, x + offset)
3115
+
3116
+ #
3117
+
3118
+ _name_and_offset: ta.Tuple[str, int]
3214
3119
 
3215
3120
  @property
3216
- def effective_name(self) -> ta.Optional[str]:
3121
+ def name_and_offset(self) -> ta.Tuple[str, int]:
3217
3122
  try:
3218
- return self._effective_name
3123
+ return self._name_and_offset
3219
3124
  except AttributeError:
3220
3125
  pass
3221
3126
 
3222
- if (n := self.exact_name) is None:
3127
+ if (n := self._NAMES_BY_INT.get(self)) is not None:
3128
+ t = (n, 0)
3129
+ else:
3223
3130
  for n, i in self._NAME_INT_PAIRS: # noqa
3224
3131
  if self >= i:
3132
+ t = (n, (self - i))
3225
3133
  break
3226
3134
  else:
3227
- n = None
3135
+ t = ('NOTSET', int(self))
3136
+
3137
+ self._name_and_offset = t
3138
+ return t
3139
+
3140
+ @property
3141
+ def exact_name(self) -> ta.Optional[str]:
3142
+ n, o = self.name_and_offset
3143
+ return n if not o else None
3228
3144
 
3229
- self._effective_name = n
3145
+ @property
3146
+ def effective_name(self) -> str:
3147
+ n, _ = self.name_and_offset
3230
3148
  return n
3231
3149
 
3232
3150
  #
3233
3151
 
3234
- def __repr__(self) -> str:
3235
- return f'{self.__class__.__name__}({int(self)})'
3236
-
3237
3152
  def __str__(self) -> str:
3238
- return self.exact_name or f'{self.effective_name or "INVALID"}:{int(self)}'
3153
+ return self.exact_name or f'{self.effective_name}{int(self):+}'
3154
+
3155
+ def __repr__(self) -> str:
3156
+ n, o = self.name_and_offset
3157
+ return f'{self.__class__.__name__}({n!r}{f", {int(o)}" if o else ""})'
3239
3158
 
3240
3159
  #
3241
3160
 
@@ -3255,6 +3174,9 @@ NamedLogLevel.DEBUG = NamedLogLevel(logging.DEBUG)
3255
3174
  NamedLogLevel.NOTSET = NamedLogLevel(logging.NOTSET)
3256
3175
 
3257
3176
 
3177
+ NamedLogLevel._CACHE.update({i: NamedLogLevel(i) for i in NamedLogLevel._NAMES_BY_INT}) # noqa
3178
+
3179
+
3258
3180
  ########################################
3259
3181
  # ../../../omlish/logs/std/filters.py
3260
3182
 
@@ -6172,223 +6094,338 @@ def check_lite_runtime_version() -> None:
6172
6094
 
6173
6095
 
6174
6096
  ########################################
6175
- # ../../../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
+ """
6176
6102
 
6177
6103
 
6178
6104
  ##
6179
6105
 
6180
6106
 
6181
- @logging_context_info
6182
- @ta.final
6183
- class LoggingCaller(ta.NamedTuple):
6184
- file_path: str
6185
- line_no: int
6186
- name: str
6187
- stack_info: ta.Optional[str]
6107
+ def logging_context_info(cls):
6108
+ return cls
6188
6109
 
6189
- @classmethod
6190
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
6191
- file_path = os.path.normcase(frame.f_code.co_filename)
6192
6110
 
6193
- # Yes, really.
6194
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
6195
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
6196
- if 'importlib' in file_path and '_bootstrap' in file_path:
6197
- return True
6111
+ @ta.final
6112
+ class LoggingContextInfos:
6113
+ def __new__(cls, *args, **kwargs): # noqa
6114
+ raise TypeError
6198
6115
 
6199
- return False
6116
+ #
6200
6117
 
6201
- @classmethod
6202
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
6203
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
6118
+ @logging_context_info
6119
+ @ta.final
6120
+ class Name(ta.NamedTuple):
6121
+ name: str
6204
6122
 
6205
- while f is not None:
6206
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
6207
- # stack_offset management.
6208
- if hasattr(f, 'f_code'):
6209
- return f
6123
+ @logging_context_info
6124
+ @ta.final
6125
+ class Level(ta.NamedTuple):
6126
+ level: NamedLogLevel
6127
+ name: str
6210
6128
 
6211
- f = f.f_back
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
+ )
6212
6136
 
6213
- return None
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]
6214
6142
 
6215
- @classmethod
6216
- def find(
6217
- cls,
6218
- ofs: int = 0,
6219
- *,
6220
- stack_info: bool = False,
6221
- ) -> ta.Optional['LoggingCaller']:
6222
- if (f := cls.find_frame(ofs + 1)) is None:
6223
- return None
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)
6224
6165
 
6225
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
6226
- sinfo = None
6227
- if stack_info:
6228
- sio = io.StringIO()
6229
- traceback.print_stack(f, file=sio)
6230
- sinfo = sio.getvalue()
6231
- sio.close()
6232
- if sinfo[-1] == '\n':
6233
- sinfo = sinfo[:-1]
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 = '', ()
6234
6173
 
6235
- return cls(
6236
- f.f_code.co_filename,
6237
- f.f_lineno or 0,
6238
- f.f_code.co_name,
6239
- sinfo,
6240
- )
6174
+ elif isinstance(msg, str):
6175
+ s, a = msg, args
6241
6176
 
6177
+ else:
6178
+ raise TypeError(msg)
6242
6179
 
6243
- ########################################
6244
- # ../../../omlish/logs/protocols.py
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]
6245
6183
 
6184
+ return cls(
6185
+ msg=s,
6186
+ args=a,
6187
+ )
6246
6188
 
6247
- ##
6189
+ @logging_context_info
6190
+ @ta.final
6191
+ class Extra(ta.NamedTuple):
6192
+ extra: ta.Mapping[ta.Any, ta.Any]
6248
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
6249
6201
 
6250
- class LoggerLike(ta.Protocol):
6251
- """Satisfied by both our Logger and stdlib logging.Logger."""
6202
+ @classmethod
6203
+ def get_std_start_ns(cls) -> int:
6204
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
6252
6205
 
6253
- def isEnabledFor(self, level: LogLevel) -> bool: ... # noqa
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
6254
6216
 
6255
- def getEffectiveLevel(self) -> LogLevel: ... # noqa
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
+ )
6256
6248
 
6257
- #
6249
+ @logging_context_info
6250
+ @ta.final
6251
+ class Exc(ta.NamedTuple):
6252
+ info: LoggingExcInfo
6253
+ info_tuple: LoggingExcInfoTuple
6258
6254
 
6259
- def log(self, level: LogLevel, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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
6260
6270
 
6261
- def debug(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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
6262
6276
 
6263
- def info(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6277
+ return cls(
6278
+ info=info,
6279
+ info_tuple=info_tuple,
6280
+ )
6264
6281
 
6265
- def warning(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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]
6266
6289
 
6267
- def error(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6290
+ @classmethod
6291
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
6292
+ file_path = os.path.normcase(frame.f_code.co_filename)
6268
6293
 
6269
- def exception(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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
6270
6299
 
6271
- def critical(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6300
+ return False
6272
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
6273
6305
 
6274
- ########################################
6275
- # ../../../omlish/logs/std/json.py
6276
- """
6277
- TODO:
6278
- - translate json keys
6279
- """
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
6280
6311
 
6312
+ f = f.f_back
6281
6313
 
6282
- ##
6314
+ return None
6283
6315
 
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
6284
6325
 
6285
- class JsonLoggingFormatter(logging.Formatter):
6286
- KEYS: ta.Mapping[str, bool] = {
6287
- 'name': False,
6288
- 'msg': False,
6289
- 'args': False,
6290
- 'levelname': False,
6291
- 'levelno': False,
6292
- 'pathname': False,
6293
- 'filename': False,
6294
- 'module': False,
6295
- 'exc_info': True,
6296
- 'exc_text': True,
6297
- 'stack_info': True,
6298
- 'lineno': False,
6299
- 'funcName': False,
6300
- 'created': False,
6301
- 'msecs': False,
6302
- 'relativeCreated': False,
6303
- 'thread': False,
6304
- 'threadName': False,
6305
- 'processName': False,
6306
- 'process': False,
6307
- }
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
+ )
6308
6342
 
6309
- def __init__(
6310
- self,
6311
- *args: ta.Any,
6312
- json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
6313
- **kwargs: ta.Any,
6314
- ) -> None:
6315
- super().__init__(*args, **kwargs)
6343
+ @logging_context_info
6344
+ @ta.final
6345
+ class SourceFile(ta.NamedTuple):
6346
+ file_name: str
6347
+ module: str
6316
6348
 
6317
- if json_dumps is None:
6318
- json_dumps = json_dumps_compact
6319
- self._json_dumps = json_dumps
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
6320
6353
 
6321
- def format(self, record: logging.LogRecord) -> str:
6322
- dct = {
6323
- k: v
6324
- for k, o in self.KEYS.items()
6325
- for v in [getattr(record, k)]
6326
- if not (o and v is None)
6327
- }
6328
- return self._json_dumps(dct)
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
6329
6360
 
6361
+ return cls(
6362
+ file_name=file_name,
6363
+ module=module,
6364
+ )
6330
6365
 
6331
- ########################################
6332
- # ../../../omlish/logs/times.py
6366
+ @logging_context_info
6367
+ @ta.final
6368
+ class Thread(ta.NamedTuple):
6369
+ ident: int
6370
+ native_id: ta.Optional[int]
6371
+ name: str
6333
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
+ )
6334
6380
 
6335
- ##
6381
+ @logging_context_info
6382
+ @ta.final
6383
+ class Process(ta.NamedTuple):
6384
+ pid: int
6336
6385
 
6386
+ @classmethod
6387
+ def build(cls) -> 'LoggingContextInfos.Process':
6388
+ return cls(
6389
+ pid=os.getpid(),
6390
+ )
6337
6391
 
6338
- @logging_context_info
6339
- @ta.final
6340
- class LoggingTimeFields(ta.NamedTuple):
6341
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
6392
+ @logging_context_info
6393
+ @ta.final
6394
+ class Multiprocessing(ta.NamedTuple):
6395
+ process_name: str
6342
6396
 
6343
- created: float
6344
- msecs: float
6345
- relative_created: float
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
6346
6402
 
6347
- @classmethod
6348
- def get_std_start_time_ns(cls) -> int:
6349
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
6403
+ return cls(
6404
+ process_name=mp.current_process().name,
6405
+ )
6350
6406
 
6351
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
6352
- # int.
6353
- #
6354
- # See:
6355
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6356
- #
6357
- if isinstance(x, float):
6358
- return int(x * 1e9)
6359
- else:
6360
- return x
6407
+ @logging_context_info
6408
+ @ta.final
6409
+ class AsyncioTask(ta.NamedTuple):
6410
+ name: str
6361
6411
 
6362
- @classmethod
6363
- def build(
6364
- cls,
6365
- time_ns: int,
6366
- *,
6367
- start_time_ns: ta.Optional[int] = None,
6368
- ) -> 'LoggingTimeFields':
6369
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6370
- created = time_ns / 1e9 # ns to float seconds
6371
-
6372
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
6373
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
6374
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
6375
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
6376
-
6377
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6378
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
6379
- # ns -> sec conversion can round up, e.g:
6380
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
6381
- msecs = 0.0
6382
-
6383
- if start_time_ns is None:
6384
- start_time_ns = cls.get_std_start_time_ns()
6385
- relative_created = (time_ns - start_time_ns) / 1e6
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
6386
6417
 
6387
- return cls(
6388
- created,
6389
- msecs,
6390
- relative_created,
6391
- )
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
+ )
6392
6429
 
6393
6430
 
6394
6431
  ##
@@ -6399,12 +6436,12 @@ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
6399
6436
 
6400
6437
 
6401
6438
  def _check_logging_start_time() -> None:
6402
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
6439
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
6403
6440
  import warnings # noqa
6404
6441
 
6405
6442
  warnings.warn(
6406
6443
  f'Unexpected logging start time detected: '
6407
- f'get_std_start_time_ns={x}, '
6444
+ f'get_std_start_ns={x}, '
6408
6445
  f'time.time()={t}',
6409
6446
  UnexpectedLoggingStartTimeWarning,
6410
6447
  )
@@ -6413,6 +6450,95 @@ def _check_logging_start_time() -> None:
6413
6450
  _check_logging_start_time()
6414
6451
 
6415
6452
 
6453
+ ########################################
6454
+ # ../../../omlish/logs/protocols.py
6455
+
6456
+
6457
+ ##
6458
+
6459
+
6460
+ @ta.runtime_checkable
6461
+ class LoggerLike(ta.Protocol):
6462
+ """Satisfied by both our Logger and stdlib logging.Logger."""
6463
+
6464
+ def isEnabledFor(self, level: LogLevel) -> bool: ... # noqa
6465
+
6466
+ def getEffectiveLevel(self) -> LogLevel: ... # noqa
6467
+
6468
+ #
6469
+
6470
+ def log(self, level: LogLevel, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6471
+
6472
+ def debug(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6473
+
6474
+ def info(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6475
+
6476
+ def warning(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6477
+
6478
+ def error(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6479
+
6480
+ def exception(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6481
+
6482
+ def critical(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
6483
+
6484
+
6485
+ ########################################
6486
+ # ../../../omlish/logs/std/json.py
6487
+ """
6488
+ TODO:
6489
+ - translate json keys
6490
+ """
6491
+
6492
+
6493
+ ##
6494
+
6495
+
6496
+ class JsonLoggingFormatter(logging.Formatter):
6497
+ KEYS: ta.Mapping[str, bool] = {
6498
+ 'name': False,
6499
+ 'msg': False,
6500
+ 'args': False,
6501
+ 'levelname': False,
6502
+ 'levelno': False,
6503
+ 'pathname': False,
6504
+ 'filename': False,
6505
+ 'module': False,
6506
+ 'exc_info': True,
6507
+ 'exc_text': True,
6508
+ 'stack_info': True,
6509
+ 'lineno': False,
6510
+ 'funcName': False,
6511
+ 'created': False,
6512
+ 'msecs': False,
6513
+ 'relativeCreated': False,
6514
+ 'thread': False,
6515
+ 'threadName': False,
6516
+ 'processName': False,
6517
+ 'process': False,
6518
+ }
6519
+
6520
+ def __init__(
6521
+ self,
6522
+ *args: ta.Any,
6523
+ json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
6524
+ **kwargs: ta.Any,
6525
+ ) -> None:
6526
+ super().__init__(*args, **kwargs)
6527
+
6528
+ if json_dumps is None:
6529
+ json_dumps = json_dumps_compact
6530
+ self._json_dumps = json_dumps
6531
+
6532
+ def format(self, record: logging.LogRecord) -> str:
6533
+ dct = {
6534
+ k: v
6535
+ for k, o in self.KEYS.items()
6536
+ for v in [getattr(record, k)]
6537
+ if not (o and v is None)
6538
+ }
6539
+ return self._json_dumps(dct)
6540
+
6541
+
6416
6542
  ########################################
6417
6543
  # ../../../omlish/os/journald.py
6418
6544
 
@@ -8451,68 +8577,36 @@ inj = InjectionApi()
8451
8577
 
8452
8578
 
8453
8579
  class LoggingContext(Abstract):
8454
- @property
8455
8580
  @abc.abstractmethod
8456
- def level(self) -> NamedLogLevel:
8581
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8457
8582
  raise NotImplementedError
8458
8583
 
8459
- #
8460
-
8461
- @property
8462
- @abc.abstractmethod
8463
- def time_ns(self) -> int:
8464
- raise NotImplementedError
8465
-
8466
- @property
8467
- @abc.abstractmethod
8468
- def times(self) -> LoggingTimeFields:
8469
- raise NotImplementedError
8470
-
8471
- #
8584
+ @ta.final
8585
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8586
+ return self.get_info(ty)
8472
8587
 
8473
- @property
8474
- @abc.abstractmethod
8475
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
8476
- 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
8477
8593
 
8478
- @property
8479
- @abc.abstractmethod
8480
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
8481
- raise NotImplementedError
8594
+ ##
8482
8595
 
8483
- #
8484
8596
 
8597
+ class CaptureLoggingContext(LoggingContext, Abstract):
8485
8598
  @abc.abstractmethod
8486
- def caller(self) -> ta.Optional[LoggingCaller]:
8487
- raise NotImplementedError
8599
+ def set_basic(
8600
+ self,
8601
+ name: str,
8488
8602
 
8489
- @abc.abstractmethod
8490
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
8603
+ msg: ta.Union[str, tuple, LoggingMsgFn],
8604
+ args: tuple,
8605
+ ) -> 'CaptureLoggingContext':
8491
8606
  raise NotImplementedError
8492
8607
 
8493
8608
  #
8494
8609
 
8495
- @abc.abstractmethod
8496
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
8497
- raise NotImplementedError
8498
-
8499
- @abc.abstractmethod
8500
- def process(self) -> ta.Optional[LoggingProcessInfo]:
8501
- raise NotImplementedError
8502
-
8503
- @abc.abstractmethod
8504
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
8505
- raise NotImplementedError
8506
-
8507
- @abc.abstractmethod
8508
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
8509
- raise NotImplementedError
8510
-
8511
-
8512
- ##
8513
-
8514
-
8515
- class CaptureLoggingContext(LoggingContext, Abstract):
8516
8610
  class AlreadyCapturedError(Exception):
8517
8611
  pass
8518
8612
 
@@ -8543,80 +8637,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
8543
8637
 
8544
8638
  exc_info: LoggingExcInfoArg = False,
8545
8639
 
8546
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
8640
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
8547
8641
  stack_offset: int = 0,
8548
8642
  stack_info: bool = False,
8549
8643
  ) -> None:
8550
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
8551
-
8552
- #
8644
+ # TODO: Name, Msg, Extra
8553
8645
 
8554
8646
  if time_ns is None:
8555
8647
  time_ns = time.time_ns()
8556
- self._time_ns: int = time_ns
8557
-
8558
- #
8559
-
8560
- if exc_info is True:
8561
- sys_exc_info = sys.exc_info()
8562
- if sys_exc_info[0] is not None:
8563
- exc_info = sys_exc_info
8564
- else:
8565
- exc_info = None
8566
- elif exc_info is False:
8567
- exc_info = None
8568
-
8569
- if exc_info is not None:
8570
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
8571
- if isinstance(exc_info, BaseException):
8572
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
8573
- else:
8574
- self._exc_info_tuple = exc_info
8575
8648
 
8576
- #
8649
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
8650
+ self._set_info(
8651
+ LoggingContextInfos.Level.build(level),
8652
+ LoggingContextInfos.Time.build(time_ns),
8653
+ LoggingContextInfos.Exc.build(exc_info),
8654
+ )
8577
8655
 
8578
8656
  if caller is not CaptureLoggingContextImpl.NOT_SET:
8579
- self._caller = caller # type: ignore[assignment]
8657
+ self._infos[LoggingContextInfos.Caller] = caller
8580
8658
  else:
8581
8659
  self._stack_offset = stack_offset
8582
8660
  self._stack_info = stack_info
8583
8661
 
8584
- ##
8585
-
8586
- @property
8587
- def level(self) -> NamedLogLevel:
8588
- return self._level
8589
-
8590
- #
8591
-
8592
- @property
8593
- def time_ns(self) -> int:
8594
- return self._time_ns
8595
-
8596
- _times: LoggingTimeFields
8597
-
8598
- @property
8599
- def times(self) -> LoggingTimeFields:
8600
- try:
8601
- return self._times
8602
- except AttributeError:
8603
- pass
8604
-
8605
- times = self._times = LoggingTimeFields.build(self.time_ns)
8606
- return times
8662
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
8663
+ for info in infos:
8664
+ if info is not None:
8665
+ self._infos[type(info)] = info
8666
+ return self
8607
8667
 
8608
- #
8668
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8669
+ return self._infos.get(ty)
8609
8670
 
8610
- _exc_info: ta.Optional[LoggingExcInfo] = None
8611
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
8671
+ ##
8612
8672
 
8613
- @property
8614
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
8615
- return self._exc_info
8673
+ def set_basic(
8674
+ self,
8675
+ name: str,
8616
8676
 
8617
- @property
8618
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
8619
- return self._exc_info_tuple
8677
+ msg: ta.Union[str, tuple, LoggingMsgFn],
8678
+ args: tuple,
8679
+ ) -> 'CaptureLoggingContextImpl':
8680
+ return self._set_info(
8681
+ LoggingContextInfos.Name(name),
8682
+ LoggingContextInfos.Msg.build(msg, *args),
8683
+ )
8620
8684
 
8621
8685
  ##
8622
8686
 
@@ -8630,74 +8694,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
8630
8694
 
8631
8695
  _has_captured: bool = False
8632
8696
 
8633
- _caller: ta.Optional[LoggingCaller]
8634
- _source_file: ta.Optional[LoggingSourceFileInfo]
8635
-
8636
- _thread: ta.Optional[LoggingThreadInfo]
8637
- _process: ta.Optional[LoggingProcessInfo]
8638
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
8639
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
8640
-
8641
8697
  def capture(self) -> None:
8642
8698
  if self._has_captured:
8643
8699
  raise CaptureLoggingContextImpl.AlreadyCapturedError
8644
8700
  self._has_captured = True
8645
8701
 
8646
- if not hasattr(self, '_caller'):
8647
- self._caller = LoggingCaller.find(
8702
+ if LoggingContextInfos.Caller not in self._infos:
8703
+ self._set_info(LoggingContextInfos.Caller.build(
8648
8704
  self._stack_offset + 1,
8649
8705
  stack_info=self._stack_info,
8650
- )
8651
-
8652
- if (caller := self._caller) is not None:
8653
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
8654
- else:
8655
- self._source_file = None
8656
-
8657
- self._thread = LoggingThreadInfo.build()
8658
- self._process = LoggingProcessInfo.build()
8659
- self._multiprocessing = LoggingMultiprocessingInfo.build()
8660
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
8661
-
8662
- #
8663
-
8664
- def caller(self) -> ta.Optional[LoggingCaller]:
8665
- try:
8666
- return self._caller
8667
- except AttributeError:
8668
- raise CaptureLoggingContext.NotCapturedError from None
8669
-
8670
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
8671
- try:
8672
- return self._source_file
8673
- except AttributeError:
8674
- raise CaptureLoggingContext.NotCapturedError from None
8675
-
8676
- #
8677
-
8678
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
8679
- try:
8680
- return self._thread
8681
- except AttributeError:
8682
- raise CaptureLoggingContext.NotCapturedError from None
8683
-
8684
- def process(self) -> ta.Optional[LoggingProcessInfo]:
8685
- try:
8686
- return self._process
8687
- except AttributeError:
8688
- raise CaptureLoggingContext.NotCapturedError from None
8706
+ ))
8689
8707
 
8690
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
8691
- try:
8692
- return self._multiprocessing
8693
- except AttributeError:
8694
- raise CaptureLoggingContext.NotCapturedError from None
8708
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
8709
+ self._set_info(LoggingContextInfos.SourceFile.build(
8710
+ caller.file_path,
8711
+ ))
8695
8712
 
8696
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
8697
- try:
8698
- return self._asyncio_task
8699
- except AttributeError:
8700
- raise CaptureLoggingContext.NotCapturedError from None
8713
+ self._set_info(
8714
+ LoggingContextInfos.Thread.build(),
8715
+ LoggingContextInfos.Process.build(),
8716
+ LoggingContextInfos.Multiprocessing.build(),
8717
+ LoggingContextInfos.AsyncioTask.build(),
8718
+ )
8701
8719
 
8702
8720
 
8703
8721
  ########################################
@@ -9578,7 +9596,6 @@ class CoroHttpServer:
9578
9596
 
9579
9597
 
9580
9598
  class AnyLogger(Abstract, ta.Generic[T]):
9581
- @ta.final
9582
9599
  def is_enabled_for(self, level: LogLevel) -> bool:
9583
9600
  return level >= self.get_effective_level()
9584
9601
 
@@ -9724,36 +9741,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
9724
9741
 
9725
9742
  ##
9726
9743
 
9727
- @classmethod
9728
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
9729
- if callable(msg):
9730
- if args:
9731
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
9732
- x = msg()
9733
- if isinstance(x, str):
9734
- return x, ()
9735
- elif isinstance(x, tuple):
9736
- if x:
9737
- return x[0], x[1:]
9738
- else:
9739
- return '', ()
9740
- else:
9741
- raise TypeError(x)
9742
-
9743
- elif isinstance(msg, tuple):
9744
- if args:
9745
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
9746
- if msg:
9747
- return msg[0], msg[1:]
9748
- else:
9749
- return '', ()
9750
-
9751
- elif isinstance(msg, str):
9752
- return msg, args
9753
-
9754
- else:
9755
- raise TypeError(msg)
9756
-
9757
9744
  @abc.abstractmethod
9758
9745
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
9759
9746
  raise NotImplementedError
@@ -9777,7 +9764,7 @@ class AsyncLogger(AnyLogger[ta.Awaitable[None]], Abstract):
9777
9764
  class AnyNopLogger(AnyLogger[T], Abstract):
9778
9765
  @ta.final
9779
9766
  def get_effective_level(self) -> LogLevel:
9780
- return 999
9767
+ return -999
9781
9768
 
9782
9769
 
9783
9770
  @ta.final
@@ -9794,137 +9781,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
9794
9781
 
9795
9782
  ########################################
9796
9783
  # ../../../omlish/logs/std/records.py
9784
+ """
9785
+ TODO:
9786
+ - TypedDict?
9787
+ """
9797
9788
 
9798
9789
 
9799
9790
  ##
9800
9791
 
9801
9792
 
9802
- # Ref:
9803
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9804
- #
9805
- # LogRecord:
9806
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
9807
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9808
- #
9809
- # LogRecord.__init__ args:
9810
- # - name: str
9811
- # - level: int
9812
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
9813
- # - lineno: int - May be 0.
9814
- # - msg: str
9815
- # - args: tuple | dict | 1-tuple[dict]
9816
- # - exc_info: LoggingExcInfoTuple | None
9817
- # - func: str | None = None -> funcName
9818
- # - sinfo: str | None = None -> stack_info
9819
- #
9820
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9821
- # Name of the logger used to log the call. Unmodified by ctor.
9822
- name=str,
9793
+ class LoggingContextInfoRecordAdapters:
9794
+ # Ref:
9795
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9796
+ #
9797
+ # LogRecord:
9798
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
9799
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9800
+ #
9823
9801
 
9824
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
9825
- # (see Using arbitrary objects as messages). Unmodified by ctor.
9826
- msg=str,
9802
+ def __new__(cls, *args, **kwargs): # noqa
9803
+ raise TypeError
9827
9804
 
9828
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
9829
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
9830
- # the mapping, but is otherwise unmodified.
9831
- args=ta.Union[tuple, dict],
9805
+ class Adapter(Abstract, ta.Generic[T]):
9806
+ @property
9807
+ @abc.abstractmethod
9808
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
9809
+ raise NotImplementedError
9832
9810
 
9833
- #
9811
+ #
9834
9812
 
9835
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9836
- # `getLevelName(level)`.
9837
- levelname=str,
9813
+ @ta.final
9814
+ class NOT_SET: # noqa
9815
+ def __new__(cls, *args, **kwargs): # noqa
9816
+ raise TypeError
9838
9817
 
9839
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9840
- levelno=int,
9818
+ class RecordAttr(ta.NamedTuple):
9819
+ name: str
9820
+ type: ta.Any
9821
+ default: ta.Any
9841
9822
 
9842
- #
9823
+ # @abc.abstractmethod
9824
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
9843
9825
 
9844
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
9845
- # to "(unknown file)" by Logger.findCaller / Logger._log.
9846
- pathname=str,
9826
+ @property
9827
+ @abc.abstractmethod
9828
+ def _record_attrs(self) -> ta.Union[
9829
+ ta.Mapping[str, ta.Any],
9830
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
9831
+ ]:
9832
+ raise NotImplementedError
9847
9833
 
9848
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
9849
- filename=str,
9834
+ #
9850
9835
 
9851
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
9852
- # "Unknown module".
9853
- module=str,
9836
+ @abc.abstractmethod
9837
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9838
+ raise NotImplementedError
9854
9839
 
9855
- #
9840
+ #
9856
9841
 
9857
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9858
- exc_info=ta.Optional[LoggingExcInfoTuple],
9842
+ @abc.abstractmethod
9843
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
9844
+ raise NotImplementedError
9859
9845
 
9860
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9861
- exc_text=ta.Optional[str],
9846
+ #
9862
9847
 
9863
- #
9848
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9849
+ super().__init_subclass__(**kwargs)
9850
+
9851
+ if Abstract in cls.__bases__:
9852
+ return
9864
9853
 
9865
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
9866
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
9867
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
9868
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
9869
- stack_info=ta.Optional[str],
9854
+ if 'record_attrs' in cls.__dict__:
9855
+ raise TypeError(cls)
9856
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
9857
+ raise TypeError(ra)
9858
+
9859
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
9860
+ for n, v in ra.items():
9861
+ if not n or not isinstance(n, str) or n in rd:
9862
+ raise AttributeError(n)
9863
+ if isinstance(v, tuple):
9864
+ t, d = v
9865
+ else:
9866
+ t, d = v, cls.NOT_SET
9867
+ rd[n] = cls.RecordAttr(
9868
+ name=n,
9869
+ type=t,
9870
+ default=d,
9871
+ )
9872
+ cls.record_attrs = rd
9870
9873
 
9871
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
9872
- # Logger.findCaller / Logger._log.
9873
- lineno=int,
9874
+ class RequiredAdapter(Adapter[T], Abstract):
9875
+ @property
9876
+ @abc.abstractmethod
9877
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
9878
+ raise NotImplementedError
9874
9879
 
9875
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
9876
- # "(unknown function)" by Logger.findCaller / Logger._log.
9877
- funcName=str,
9880
+ #
9878
9881
 
9879
- #
9882
+ @ta.final
9883
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9884
+ if (info := ctx.get_info(self.info_cls)) is not None:
9885
+ return self._info_to_record(info)
9886
+ else:
9887
+ raise TypeError # FIXME: fallback?
9880
9888
 
9881
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
9882
- #
9883
- # See:
9884
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
9885
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
9886
- #
9887
- created=float,
9889
+ @abc.abstractmethod
9890
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9891
+ raise NotImplementedError
9888
9892
 
9889
- # Millisecond portion of the time when the LogRecord was created.
9890
- msecs=float,
9893
+ #
9891
9894
 
9892
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9893
- relativeCreated=float,
9895
+ @abc.abstractmethod
9896
+ def record_to_info(self, rec: logging.LogRecord) -> T:
9897
+ raise NotImplementedError
9894
9898
 
9895
- #
9899
+ #
9896
9900
 
9897
- # Thread ID if available, and `logging.logThreads` is truthy.
9898
- thread=ta.Optional[int],
9901
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9902
+ super().__init_subclass__(**kwargs)
9899
9903
 
9900
- # Thread name if available, and `logging.logThreads` is truthy.
9901
- threadName=ta.Optional[str],
9904
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
9905
+ raise TypeError(cls.record_attrs)
9902
9906
 
9903
- #
9907
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
9908
+ @property
9909
+ @abc.abstractmethod
9910
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
9911
+ raise NotImplementedError
9912
+
9913
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
9914
+
9915
+ #
9916
+
9917
+ @ta.final
9918
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9919
+ if (info := ctx.get_info(self.info_cls)) is not None:
9920
+ return self._info_to_record(info)
9921
+ else:
9922
+ return self.record_defaults
9923
+
9924
+ @abc.abstractmethod
9925
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9926
+ raise NotImplementedError
9927
+
9928
+ #
9929
+
9930
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9931
+ super().__init_subclass__(**kwargs)
9932
+
9933
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
9934
+ if any(d is cls.NOT_SET for d in dd.values()):
9935
+ raise TypeError(cls.record_attrs)
9936
+ cls.record_defaults = dd
9904
9937
 
9905
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
9906
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
9907
- # as 'MainProcess'.
9908
9938
  #
9909
- # As noted by stdlib:
9939
+
9940
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
9941
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
9942
+
9943
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9944
+ # Name of the logger used to log the call. Unmodified by ctor.
9945
+ name=str,
9946
+ )
9947
+
9948
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
9949
+ return dict(
9950
+ name=info.name,
9951
+ )
9952
+
9953
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
9954
+ return LoggingContextInfos.Name(
9955
+ name=rec.name,
9956
+ )
9957
+
9958
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
9959
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
9960
+
9961
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9962
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9963
+ # `getLevelName(level)`.
9964
+ levelname=str,
9965
+
9966
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9967
+ levelno=int,
9968
+ )
9969
+
9970
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
9971
+ return dict(
9972
+ levelname=info.name,
9973
+ levelno=int(info.level),
9974
+ )
9975
+
9976
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
9977
+ return LoggingContextInfos.Level.build(rec.levelno)
9978
+
9979
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
9980
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
9981
+
9982
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9983
+ # The format string passed in the original logging call. Merged with args to produce message, or an
9984
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
9985
+ msg=str,
9986
+
9987
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
9988
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
9989
+ # Mapping into just the mapping, but is otherwise unmodified.
9990
+ args=ta.Union[tuple, dict, None],
9991
+ )
9992
+
9993
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
9994
+ return dict(
9995
+ msg=info.msg,
9996
+ args=info.args,
9997
+ )
9998
+
9999
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
10000
+ return LoggingContextInfos.Msg(
10001
+ msg=rec.msg,
10002
+ args=rec.args,
10003
+ )
10004
+
10005
+ # FIXME: handled specially - all unknown attrs on LogRecord
10006
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
10007
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
9910
10008
  #
9911
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
9912
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
10009
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
10010
+ # # FIXME:
10011
+ # # if extra is not None:
10012
+ # # for key in extra:
10013
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
10014
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
10015
+ # # rv.__dict__[key] = extra[key]
10016
+ # return dict()
9913
10017
  #
9914
- processName=ta.Optional[str],
10018
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
10019
+ # return None
9915
10020
 
9916
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
9917
- # None.
9918
- process=ta.Optional[int],
10021
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
10022
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
9919
10023
 
9920
- #
10024
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
10025
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
10026
+ # `time.time()`.
10027
+ #
10028
+ # See:
10029
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
10030
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
10031
+ #
10032
+ created=float,
9921
10033
 
9922
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
9923
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
9924
- taskName=ta.Optional[str],
9925
- )
10034
+ # Millisecond portion of the time when the LogRecord was created.
10035
+ msecs=float,
10036
+
10037
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
10038
+ relativeCreated=float,
10039
+ )
10040
+
10041
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
10042
+ return dict(
10043
+ created=info.secs,
10044
+ msecs=info.msecs,
10045
+ relativeCreated=info.relative_secs,
10046
+ )
10047
+
10048
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
10049
+ return LoggingContextInfos.Time.build(
10050
+ int(rec.created * 1e9),
10051
+ )
10052
+
10053
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
10054
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
10055
+
10056
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10057
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
10058
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
10059
+
10060
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
10061
+ exc_text=(ta.Optional[str], None),
10062
+ )
10063
+
10064
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
10065
+ return dict(
10066
+ exc_info=info.info_tuple,
10067
+ exc_text=None,
10068
+ )
10069
+
10070
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
10071
+ # FIXME:
10072
+ # error: Argument 1 to "build" of "Exc" has incompatible type
10073
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
10074
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
10075
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
10076
+
10077
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
10078
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
10079
+
10080
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
10081
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
10082
+
10083
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
10084
+
10085
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10086
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
10087
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
10088
+ pathname=(str, _UNKNOWN_PATH_NAME),
10089
+
10090
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
10091
+ # y Logger.findCaller / Logger._log.
10092
+ lineno=(int, 0),
10093
+
10094
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
10095
+ # "(unknown function)" by Logger.findCaller / Logger._log.
10096
+ funcName=(str, _UNKNOWN_FUNC_NAME),
10097
+
10098
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
10099
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
10100
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
10101
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
10102
+ # stripped of exactly one trailing `\n` if present.
10103
+ stack_info=(ta.Optional[str], None),
10104
+ )
10105
+
10106
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
10107
+ if (sinfo := caller.stack_info) is not None:
10108
+ stack_info: ta.Optional[str] = '\n'.join([
10109
+ self._STACK_INFO_PREFIX,
10110
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
10111
+ ])
10112
+ else:
10113
+ stack_info = None
10114
+
10115
+ return dict(
10116
+ pathname=caller.file_path,
10117
+
10118
+ lineno=caller.line_no,
10119
+ funcName=caller.func_name,
10120
+
10121
+ stack_info=stack_info,
10122
+ )
10123
+
10124
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
10125
+ # FIXME: piecemeal?
10126
+ # FIXME: strip _STACK_INFO_PREFIX
10127
+ raise NotImplementedError
10128
+
10129
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
10130
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
10131
+
10132
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
10133
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
10134
+ # pathname.
10135
+ filename=str,
10136
+
10137
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
10138
+ # "Unknown module".
10139
+ module=str,
10140
+ )
10141
+
10142
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
10143
+
10144
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
10145
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
10146
+ return dict(
10147
+ filename=info.file_name,
10148
+ module=info.module,
10149
+ )
10150
+
10151
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
10152
+ return dict(
10153
+ filename=caller.file_path,
10154
+ module=self._UNKNOWN_MODULE,
10155
+ )
10156
+
10157
+ return dict(
10158
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
10159
+ module=self._UNKNOWN_MODULE,
10160
+ )
10161
+
10162
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
10163
+ if not (
10164
+ rec.module is None or
10165
+ rec.module == self._UNKNOWN_MODULE
10166
+ ):
10167
+ return LoggingContextInfos.SourceFile(
10168
+ file_name=rec.filename,
10169
+ module=rec.module, # FIXME: piecemeal?
10170
+ )
10171
+
10172
+ return None
10173
+
10174
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
10175
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
10176
+
10177
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10178
+ # Thread ID if available, and `logging.logThreads` is truthy.
10179
+ thread=(ta.Optional[int], None),
10180
+
10181
+ # Thread name if available, and `logging.logThreads` is truthy.
10182
+ threadName=(ta.Optional[str], None),
10183
+ )
10184
+
10185
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
10186
+ if logging.logThreads:
10187
+ return dict(
10188
+ thread=info.ident,
10189
+ threadName=info.name,
10190
+ )
10191
+
10192
+ return self.record_defaults
10193
+
10194
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
10195
+ if (
10196
+ (ident := rec.thread) is not None and
10197
+ (name := rec.threadName) is not None
10198
+ ):
10199
+ return LoggingContextInfos.Thread(
10200
+ ident=ident,
10201
+ native_id=None,
10202
+ name=name,
10203
+ )
10204
+
10205
+ return None
10206
+
10207
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
10208
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
10209
+
10210
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10211
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
10212
+ # otherwise None.
10213
+ process=(ta.Optional[int], None),
10214
+ )
10215
+
10216
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
10217
+ if logging.logProcesses:
10218
+ return dict(
10219
+ process=info.pid,
10220
+ )
10221
+
10222
+ return self.record_defaults
10223
+
10224
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
10225
+ if (
10226
+ (pid := rec.process) is not None
10227
+ ):
10228
+ return LoggingContextInfos.Process(
10229
+ pid=pid,
10230
+ )
10231
+
10232
+ return None
10233
+
10234
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
10235
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
10236
+
10237
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10238
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
10239
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
10240
+ # remains as 'MainProcess'.
10241
+ #
10242
+ # As noted by stdlib:
10243
+ #
10244
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
10245
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
10246
+ #
10247
+ processName=(ta.Optional[str], None),
10248
+ )
10249
+
10250
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
10251
+ if logging.logMultiprocessing:
10252
+ return dict(
10253
+ processName=info.process_name,
10254
+ )
10255
+
10256
+ return self.record_defaults
10257
+
10258
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
10259
+ if (
10260
+ (process_name := rec.processName) is not None
10261
+ ):
10262
+ return LoggingContextInfos.Multiprocessing(
10263
+ process_name=process_name,
10264
+ )
10265
+
10266
+ return None
10267
+
10268
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
10269
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
10270
+
10271
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
10272
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
10273
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
10274
+ taskName=(ta.Optional[str], None),
10275
+ )
10276
+
10277
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
10278
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
10279
+ return dict(
10280
+ taskName=info.name,
10281
+ )
10282
+
10283
+ return self.record_defaults
9926
10284
 
9927
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
10285
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
10286
+ if (
10287
+ (name := getattr(rec, 'taskName', None)) is not None
10288
+ ):
10289
+ return LoggingContextInfos.AsyncioTask(
10290
+ name=name,
10291
+ )
10292
+
10293
+ return None
10294
+
10295
+
10296
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
10297
+ LoggingContextInfoRecordAdapters.Name(),
10298
+ LoggingContextInfoRecordAdapters.Level(),
10299
+ LoggingContextInfoRecordAdapters.Msg(),
10300
+ LoggingContextInfoRecordAdapters.Time(),
10301
+ LoggingContextInfoRecordAdapters.Exc(),
10302
+ LoggingContextInfoRecordAdapters.Caller(),
10303
+ LoggingContextInfoRecordAdapters.SourceFile(),
10304
+ LoggingContextInfoRecordAdapters.Thread(),
10305
+ LoggingContextInfoRecordAdapters.Process(),
10306
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
10307
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
10308
+ ]
10309
+
10310
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
10311
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
10312
+ }
10313
+
10314
+
10315
+ ##
10316
+
10317
+
10318
+ KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
10319
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
10320
+ )
9928
10321
 
9929
10322
 
9930
10323
  # Formatter:
@@ -9948,14 +10341,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9948
10341
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
9949
10342
 
9950
10343
 
9951
- ##
9952
-
9953
-
9954
10344
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
9955
10345
  pass
9956
10346
 
9957
10347
 
9958
10348
  def _check_std_logging_record_attrs() -> None:
10349
+ if (
10350
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
10351
+ len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
10352
+ ):
10353
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
10354
+
9959
10355
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
9960
10356
 
9961
10357
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -9974,116 +10370,20 @@ _check_std_logging_record_attrs()
9974
10370
 
9975
10371
 
9976
10372
  class LoggingContextLogRecord(logging.LogRecord):
9977
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
9978
-
9979
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
9980
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
9981
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
9982
-
9983
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
9984
-
9985
- def __init__( # noqa
9986
- self,
9987
- # name,
9988
- # level,
9989
- # pathname,
9990
- # lineno,
9991
- # msg,
9992
- # args,
9993
- # exc_info,
9994
- # func=None,
9995
- # sinfo=None,
9996
- # **kwargs,
9997
- *,
9998
- name: str,
9999
- msg: str,
10000
- args: ta.Union[tuple, dict],
10001
-
10002
- _logging_context: LoggingContext,
10003
- ) -> None:
10004
- ctx = _logging_context
10005
-
10006
- self.name: str = name
10007
-
10008
- self.msg: str = msg
10009
-
10010
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
10011
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
10012
- args = args[0] # type: ignore[assignment]
10013
- self.args: ta.Union[tuple, dict] = args
10014
-
10015
- self.levelname: str = logging.getLevelName(ctx.level)
10016
- self.levelno: int = ctx.level
10017
-
10018
- if (caller := ctx.caller()) is not None:
10019
- self.pathname: str = caller.file_path
10020
- else:
10021
- self.pathname = self._UNKNOWN_PATH_NAME
10022
-
10023
- if (src_file := ctx.source_file()) is not None:
10024
- self.filename: str = src_file.file_name
10025
- self.module: str = src_file.module
10026
- else:
10027
- self.filename = self.pathname
10028
- self.module = self._UNKNOWN_MODULE
10029
-
10030
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
10031
- self.exc_text: ta.Optional[str] = None
10032
-
10033
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
10034
- if caller is not None:
10035
- if (sinfo := caller.stack_info) is not None:
10036
- self.stack_info: ta.Optional[str] = '\n'.join([
10037
- self._STACK_INFO_PREFIX,
10038
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
10039
- ])
10040
- else:
10041
- self.stack_info = None
10042
-
10043
- self.lineno: int = caller.line_no
10044
- self.funcName: str = caller.name
10045
-
10046
- else:
10047
- self.stack_info = None
10048
-
10049
- self.lineno = 0
10050
- self.funcName = self._UNKNOWN_FUNC_NAME
10051
-
10052
- times = ctx.times
10053
- self.created: float = times.created
10054
- self.msecs: float = times.msecs
10055
- self.relativeCreated: float = times.relative_created
10056
-
10057
- if logging.logThreads:
10058
- thread = check.not_none(ctx.thread())
10059
- self.thread: ta.Optional[int] = thread.ident
10060
- self.threadName: ta.Optional[str] = thread.name
10061
- else:
10062
- self.thread = None
10063
- self.threadName = None
10064
-
10065
- if logging.logProcesses:
10066
- process = check.not_none(ctx.process())
10067
- self.process: ta.Optional[int] = process.pid
10068
- else:
10069
- self.process = None
10070
-
10071
- if logging.logMultiprocessing:
10072
- if (mp := ctx.multiprocessing()) is not None:
10073
- self.processName: ta.Optional[str] = mp.process_name
10074
- else:
10075
- self.processName = None
10076
- else:
10077
- self.processName = None
10078
-
10079
- # Absent <3.12
10080
- if getattr(logging, 'logAsyncioTasks', None):
10081
- if (at := ctx.asyncio_task()) is not None:
10082
- self.taskName: ta.Optional[str] = at.name
10083
- else:
10084
- self.taskName = None
10085
- else:
10086
- self.taskName = None
10373
+ # LogRecord.__init__ args:
10374
+ # - name: str
10375
+ # - level: int
10376
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
10377
+ # - lineno: int - May be 0.
10378
+ # - msg: str
10379
+ # - args: tuple | dict | 1-tuple[dict]
10380
+ # - exc_info: LoggingExcInfoTuple | None
10381
+ # - func: str | None = None -> funcName
10382
+ # - sinfo: str | None = None -> stack_info
10383
+
10384
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
10385
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
10386
+ self.__dict__.update(ad.context_to_record(_logging_context))
10087
10387
 
10088
10388
 
10089
10389
  ########################################
@@ -10360,7 +10660,7 @@ class CoroHttpServerConnectionFdioHandler(SocketFdioHandler):
10360
10660
 
10361
10661
 
10362
10662
  ########################################
10363
- # ../../../omlish/logs/std/adapters.py
10663
+ # ../../../omlish/logs/std/loggers.py
10364
10664
 
10365
10665
 
10366
10666
  ##
@@ -10376,25 +10676,27 @@ class StdLogger(Logger):
10376
10676
  def std(self) -> logging.Logger:
10377
10677
  return self._std
10378
10678
 
10679
+ def is_enabled_for(self, level: LogLevel) -> bool:
10680
+ return self._std.isEnabledFor(level)
10681
+
10379
10682
  def get_effective_level(self) -> LogLevel:
10380
10683
  return self._std.getEffectiveLevel()
10381
10684
 
10382
10685
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
10383
- if not self.is_enabled_for(ctx.level):
10686
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
10384
10687
  return
10385
10688
 
10386
- ctx.capture()
10387
-
10388
- ms, args = self._prepare_msg_args(msg, *args)
10389
-
10390
- rec = LoggingContextLogRecord(
10689
+ ctx.set_basic(
10391
10690
  name=self._std.name,
10392
- msg=ms,
10393
- args=args,
10394
10691
 
10395
- _logging_context=ctx,
10692
+ msg=msg,
10693
+ args=args,
10396
10694
  )
10397
10695
 
10696
+ ctx.capture()
10697
+
10698
+ rec = LoggingContextLogRecord(_logging_context=ctx)
10699
+
10398
10700
  self._std.handle(rec)
10399
10701
 
10400
10702