omdev 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.
@@ -130,6 +130,13 @@ U = ta.TypeVar('U')
130
130
  # ../../omlish/lite/timeouts.py
131
131
  TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.DEFAULT'], ta.Iterable['TimeoutLike'], float, None] # ta.TypeAlias
132
132
 
133
+ # ../../omlish/logs/infos.py
134
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
135
+ LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
136
+ LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
137
+ LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
138
+ LoggingContextInfo = ta.Any # ta.TypeAlias
139
+
133
140
  # ../../omlish/asyncs/asyncio/timeouts.py
134
141
  AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
135
142
 
@@ -140,12 +147,7 @@ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
140
147
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
141
148
 
142
149
  # ../../omlish/logs/contexts.py
143
- LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
144
- LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
145
- LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
146
-
147
- # ../../omlish/logs/base.py
148
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
150
+ LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
149
151
 
150
152
  # ../../omlish/subprocesses/base.py
151
153
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
@@ -2959,124 +2961,6 @@ def typing_annotations_attr() -> str:
2959
2961
  return _TYPING_ANNOTATIONS_ATTR
2960
2962
 
2961
2963
 
2962
- ########################################
2963
- # ../../../omlish/logs/infos.py
2964
-
2965
-
2966
- ##
2967
-
2968
-
2969
- def logging_context_info(cls):
2970
- return cls
2971
-
2972
-
2973
- ##
2974
-
2975
-
2976
- @logging_context_info
2977
- @ta.final
2978
- class LoggingSourceFileInfo(ta.NamedTuple):
2979
- file_name: str
2980
- module: str
2981
-
2982
- @classmethod
2983
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
2984
- if file_path is None:
2985
- return None
2986
-
2987
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
2988
- try:
2989
- file_name = os.path.basename(file_path)
2990
- module = os.path.splitext(file_name)[0]
2991
- except (TypeError, ValueError, AttributeError):
2992
- return None
2993
-
2994
- return cls(
2995
- file_name,
2996
- module,
2997
- )
2998
-
2999
-
3000
- ##
3001
-
3002
-
3003
- @logging_context_info
3004
- @ta.final
3005
- class LoggingThreadInfo(ta.NamedTuple):
3006
- ident: int
3007
- native_id: ta.Optional[int]
3008
- name: str
3009
-
3010
- @classmethod
3011
- def build(cls) -> 'LoggingThreadInfo':
3012
- return cls(
3013
- threading.get_ident(),
3014
- threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
3015
- threading.current_thread().name,
3016
- )
3017
-
3018
-
3019
- ##
3020
-
3021
-
3022
- @logging_context_info
3023
- @ta.final
3024
- class LoggingProcessInfo(ta.NamedTuple):
3025
- pid: int
3026
-
3027
- @classmethod
3028
- def build(cls) -> 'LoggingProcessInfo':
3029
- return cls(
3030
- os.getpid(),
3031
- )
3032
-
3033
-
3034
- ##
3035
-
3036
-
3037
- @logging_context_info
3038
- @ta.final
3039
- class LoggingMultiprocessingInfo(ta.NamedTuple):
3040
- process_name: str
3041
-
3042
- @classmethod
3043
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
3044
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
3045
- if (mp := sys.modules.get('multiprocessing')) is None:
3046
- return None
3047
-
3048
- return cls(
3049
- mp.current_process().name,
3050
- )
3051
-
3052
-
3053
- ##
3054
-
3055
-
3056
- @logging_context_info
3057
- @ta.final
3058
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
3059
- name: str
3060
-
3061
- @classmethod
3062
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
3063
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
3064
- if (asyncio := sys.modules.get('asyncio')) is None:
3065
- return None
3066
-
3067
- try:
3068
- task = asyncio.current_task()
3069
- except Exception: # noqa
3070
- return None
3071
-
3072
- if task is None:
3073
- return None
3074
-
3075
- return cls(
3076
- task.get_name(), # Always non-None
3077
- )
3078
-
3079
-
3080
2964
  ########################################
3081
2965
  # ../../../omlish/logs/levels.py
3082
2966
 
@@ -3096,36 +2980,66 @@ class NamedLogLevel(int):
3096
2980
 
3097
2981
  #
3098
2982
 
3099
- @property
3100
- def exact_name(self) -> ta.Optional[str]:
3101
- return self._NAMES_BY_INT.get(self)
2983
+ _CACHE: ta.ClassVar[ta.MutableMapping[int, 'NamedLogLevel']] = {}
2984
+
2985
+ @ta.overload
2986
+ def __new__(cls, name: str, offset: int = 0, /) -> 'NamedLogLevel':
2987
+ ...
2988
+
2989
+ @ta.overload
2990
+ def __new__(cls, i: int, /) -> 'NamedLogLevel':
2991
+ ...
2992
+
2993
+ def __new__(cls, x, offset=0, /):
2994
+ if isinstance(x, str):
2995
+ return cls(cls._INTS_BY_NAME[x.upper()] + offset)
2996
+ elif not offset and (c := cls._CACHE.get(x)) is not None:
2997
+ return c
2998
+ else:
2999
+ return super().__new__(cls, x + offset)
3000
+
3001
+ #
3102
3002
 
3103
- _effective_name: ta.Optional[str]
3003
+ _name_and_offset: ta.Tuple[str, int]
3104
3004
 
3105
3005
  @property
3106
- def effective_name(self) -> ta.Optional[str]:
3006
+ def name_and_offset(self) -> ta.Tuple[str, int]:
3107
3007
  try:
3108
- return self._effective_name
3008
+ return self._name_and_offset
3109
3009
  except AttributeError:
3110
3010
  pass
3111
3011
 
3112
- if (n := self.exact_name) is None:
3012
+ if (n := self._NAMES_BY_INT.get(self)) is not None:
3013
+ t = (n, 0)
3014
+ else:
3113
3015
  for n, i in self._NAME_INT_PAIRS: # noqa
3114
3016
  if self >= i:
3017
+ t = (n, (self - i))
3115
3018
  break
3116
3019
  else:
3117
- n = None
3020
+ t = ('NOTSET', int(self))
3021
+
3022
+ self._name_and_offset = t
3023
+ return t
3024
+
3025
+ @property
3026
+ def exact_name(self) -> ta.Optional[str]:
3027
+ n, o = self.name_and_offset
3028
+ return n if not o else None
3118
3029
 
3119
- self._effective_name = n
3030
+ @property
3031
+ def effective_name(self) -> str:
3032
+ n, _ = self.name_and_offset
3120
3033
  return n
3121
3034
 
3122
3035
  #
3123
3036
 
3124
- def __repr__(self) -> str:
3125
- return f'{self.__class__.__name__}({int(self)})'
3126
-
3127
3037
  def __str__(self) -> str:
3128
- return self.exact_name or f'{self.effective_name or "INVALID"}:{int(self)}'
3038
+ return self.exact_name or f'{self.effective_name}{int(self):+}'
3039
+
3040
+ def __repr__(self) -> str:
3041
+ n, o = self.name_and_offset
3042
+ return f'{self.__class__.__name__}({n!r}{f", {int(o)}" if o else ""})'
3129
3043
 
3130
3044
  #
3131
3045
 
@@ -3145,6 +3059,9 @@ NamedLogLevel.DEBUG = NamedLogLevel(logging.DEBUG)
3145
3059
  NamedLogLevel.NOTSET = NamedLogLevel(logging.NOTSET)
3146
3060
 
3147
3061
 
3062
+ NamedLogLevel._CACHE.update({i: NamedLogLevel(i) for i in NamedLogLevel._NAMES_BY_INT}) # noqa
3063
+
3064
+
3148
3065
  ########################################
3149
3066
  # ../../../omlish/logs/std/filters.py
3150
3067
 
@@ -5527,74 +5444,362 @@ class PredicateTimeout(Timeout):
5527
5444
 
5528
5445
 
5529
5446
  ########################################
5530
- # ../../../omlish/logs/callers.py
5447
+ # ../../../omlish/logs/infos.py
5448
+ """
5449
+ TODO:
5450
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
5451
+ """
5531
5452
 
5532
5453
 
5533
5454
  ##
5534
5455
 
5535
5456
 
5536
- @logging_context_info
5457
+ def logging_context_info(cls):
5458
+ return cls
5459
+
5460
+
5537
5461
  @ta.final
5538
- class LoggingCaller(ta.NamedTuple):
5539
- file_path: str
5540
- line_no: int
5541
- name: str
5542
- stack_info: ta.Optional[str]
5462
+ class LoggingContextInfos:
5463
+ def __new__(cls, *args, **kwargs): # noqa
5464
+ raise TypeError
5543
5465
 
5544
- @classmethod
5545
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
5546
- file_path = os.path.normcase(frame.f_code.co_filename)
5466
+ #
5547
5467
 
5548
- # Yes, really.
5549
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
5550
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
5551
- if 'importlib' in file_path and '_bootstrap' in file_path:
5552
- return True
5468
+ @logging_context_info
5469
+ @ta.final
5470
+ class Name(ta.NamedTuple):
5471
+ name: str
5553
5472
 
5554
- return False
5473
+ @logging_context_info
5474
+ @ta.final
5475
+ class Level(ta.NamedTuple):
5476
+ level: NamedLogLevel
5477
+ name: str
5555
5478
 
5556
- @classmethod
5557
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
5558
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
5479
+ @classmethod
5480
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
5481
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
5482
+ return cls(
5483
+ level=nl,
5484
+ name=logging.getLevelName(nl),
5485
+ )
5486
+
5487
+ @logging_context_info
5488
+ @ta.final
5489
+ class Msg(ta.NamedTuple):
5490
+ msg: str
5491
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
5492
+
5493
+ @classmethod
5494
+ def build(
5495
+ cls,
5496
+ msg: ta.Union[str, tuple, LoggingMsgFn],
5497
+ *args: ta.Any,
5498
+ ) -> 'LoggingContextInfos.Msg':
5499
+ s: str
5500
+ a: ta.Any
5501
+
5502
+ if callable(msg):
5503
+ if args:
5504
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
5505
+ x = msg()
5506
+ if isinstance(x, str):
5507
+ s, a = x, ()
5508
+ elif isinstance(x, tuple):
5509
+ if x:
5510
+ s, a = x[0], x[1:]
5511
+ else:
5512
+ s, a = '', ()
5513
+ else:
5514
+ raise TypeError(x)
5559
5515
 
5560
- while f is not None:
5561
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
5562
- # stack_offset management.
5563
- if hasattr(f, 'f_code'):
5564
- return f
5516
+ elif isinstance(msg, tuple):
5517
+ if args:
5518
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
5519
+ if msg:
5520
+ s, a = msg[0], msg[1:]
5521
+ else:
5522
+ s, a = '', ()
5565
5523
 
5566
- f = f.f_back
5524
+ elif isinstance(msg, str):
5525
+ s, a = msg, args
5567
5526
 
5568
- return None
5527
+ else:
5528
+ raise TypeError(msg)
5529
+
5530
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
5531
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
5532
+ a = a[0]
5533
+
5534
+ return cls(
5535
+ msg=s,
5536
+ args=a,
5537
+ )
5538
+
5539
+ @logging_context_info
5540
+ @ta.final
5541
+ class Extra(ta.NamedTuple):
5542
+ extra: ta.Mapping[ta.Any, ta.Any]
5543
+
5544
+ @logging_context_info
5545
+ @ta.final
5546
+ class Time(ta.NamedTuple):
5547
+ ns: int
5548
+ secs: float
5549
+ msecs: float
5550
+ relative_secs: float
5551
+
5552
+ @classmethod
5553
+ def get_std_start_ns(cls) -> int:
5554
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5555
+
5556
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
5557
+ # an int.
5558
+ #
5559
+ # See:
5560
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5561
+ #
5562
+ if isinstance(x, float):
5563
+ return int(x * 1e9)
5564
+ else:
5565
+ return x
5566
+
5567
+ @classmethod
5568
+ def build(
5569
+ cls,
5570
+ ns: int,
5571
+ *,
5572
+ start_ns: ta.Optional[int] = None,
5573
+ ) -> 'LoggingContextInfos.Time':
5574
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5575
+ secs = ns / 1e9 # ns to float seconds
5576
+
5577
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5578
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5579
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
5580
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
5581
+
5582
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5583
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
5584
+ # ns -> sec conversion can round up, e.g:
5585
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5586
+ msecs = 0.0
5587
+
5588
+ if start_ns is None:
5589
+ start_ns = cls.get_std_start_ns()
5590
+ relative_secs = (ns - start_ns) / 1e6
5591
+
5592
+ return cls(
5593
+ ns=ns,
5594
+ secs=secs,
5595
+ msecs=msecs,
5596
+ relative_secs=relative_secs,
5597
+ )
5598
+
5599
+ @logging_context_info
5600
+ @ta.final
5601
+ class Exc(ta.NamedTuple):
5602
+ info: LoggingExcInfo
5603
+ info_tuple: LoggingExcInfoTuple
5604
+
5605
+ @classmethod
5606
+ def build(
5607
+ cls,
5608
+ arg: LoggingExcInfoArg = False,
5609
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
5610
+ if arg is True:
5611
+ sys_exc_info = sys.exc_info()
5612
+ if sys_exc_info[0] is not None:
5613
+ arg = sys_exc_info
5614
+ else:
5615
+ arg = None
5616
+ elif arg is False:
5617
+ arg = None
5618
+ if arg is None:
5619
+ return None
5620
+
5621
+ info: LoggingExcInfo = arg
5622
+ if isinstance(info, BaseException):
5623
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
5624
+ else:
5625
+ info_tuple = info
5626
+
5627
+ return cls(
5628
+ info=info,
5629
+ info_tuple=info_tuple,
5630
+ )
5631
+
5632
+ @logging_context_info
5633
+ @ta.final
5634
+ class Caller(ta.NamedTuple):
5635
+ file_path: str
5636
+ line_no: int
5637
+ func_name: str
5638
+ stack_info: ta.Optional[str]
5639
+
5640
+ @classmethod
5641
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
5642
+ file_path = os.path.normcase(frame.f_code.co_filename)
5643
+
5644
+ # Yes, really.
5645
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
5646
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
5647
+ if 'importlib' in file_path and '_bootstrap' in file_path:
5648
+ return True
5649
+
5650
+ return False
5651
+
5652
+ @classmethod
5653
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
5654
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
5655
+
5656
+ while f is not None:
5657
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
5658
+ # manual stack_offset management.
5659
+ if hasattr(f, 'f_code'):
5660
+ return f
5661
+
5662
+ f = f.f_back
5569
5663
 
5570
- @classmethod
5571
- def find(
5572
- cls,
5573
- ofs: int = 0,
5574
- *,
5575
- stack_info: bool = False,
5576
- ) -> ta.Optional['LoggingCaller']:
5577
- if (f := cls.find_frame(ofs + 1)) is None:
5578
5664
  return None
5579
5665
 
5580
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5581
- sinfo = None
5582
- if stack_info:
5583
- sio = io.StringIO()
5584
- traceback.print_stack(f, file=sio)
5585
- sinfo = sio.getvalue()
5586
- sio.close()
5587
- if sinfo[-1] == '\n':
5588
- sinfo = sinfo[:-1]
5666
+ @classmethod
5667
+ def build(
5668
+ cls,
5669
+ stack_offset: int = 0,
5670
+ *,
5671
+ stack_info: bool = False,
5672
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
5673
+ if (f := cls.find_frame(stack_offset + 1)) is None:
5674
+ return None
5589
5675
 
5590
- return cls(
5591
- f.f_code.co_filename,
5592
- f.f_lineno or 0,
5593
- f.f_code.co_name,
5594
- sinfo,
5676
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5677
+ sinfo = None
5678
+ if stack_info:
5679
+ sio = io.StringIO()
5680
+ traceback.print_stack(f, file=sio)
5681
+ sinfo = sio.getvalue()
5682
+ sio.close()
5683
+ if sinfo[-1] == '\n':
5684
+ sinfo = sinfo[:-1]
5685
+
5686
+ return cls(
5687
+ file_path=f.f_code.co_filename,
5688
+ line_no=f.f_lineno or 0,
5689
+ func_name=f.f_code.co_name,
5690
+ stack_info=sinfo,
5691
+ )
5692
+
5693
+ @logging_context_info
5694
+ @ta.final
5695
+ class SourceFile(ta.NamedTuple):
5696
+ file_name: str
5697
+ module: str
5698
+
5699
+ @classmethod
5700
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
5701
+ if caller_file_path is None:
5702
+ return None
5703
+
5704
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
5705
+ try:
5706
+ file_name = os.path.basename(caller_file_path)
5707
+ module = os.path.splitext(file_name)[0]
5708
+ except (TypeError, ValueError, AttributeError):
5709
+ return None
5710
+
5711
+ return cls(
5712
+ file_name=file_name,
5713
+ module=module,
5714
+ )
5715
+
5716
+ @logging_context_info
5717
+ @ta.final
5718
+ class Thread(ta.NamedTuple):
5719
+ ident: int
5720
+ native_id: ta.Optional[int]
5721
+ name: str
5722
+
5723
+ @classmethod
5724
+ def build(cls) -> 'LoggingContextInfos.Thread':
5725
+ return cls(
5726
+ ident=threading.get_ident(),
5727
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
5728
+ name=threading.current_thread().name,
5729
+ )
5730
+
5731
+ @logging_context_info
5732
+ @ta.final
5733
+ class Process(ta.NamedTuple):
5734
+ pid: int
5735
+
5736
+ @classmethod
5737
+ def build(cls) -> 'LoggingContextInfos.Process':
5738
+ return cls(
5739
+ pid=os.getpid(),
5740
+ )
5741
+
5742
+ @logging_context_info
5743
+ @ta.final
5744
+ class Multiprocessing(ta.NamedTuple):
5745
+ process_name: str
5746
+
5747
+ @classmethod
5748
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
5749
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
5750
+ if (mp := sys.modules.get('multiprocessing')) is None:
5751
+ return None
5752
+
5753
+ return cls(
5754
+ process_name=mp.current_process().name,
5755
+ )
5756
+
5757
+ @logging_context_info
5758
+ @ta.final
5759
+ class AsyncioTask(ta.NamedTuple):
5760
+ name: str
5761
+
5762
+ @classmethod
5763
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
5764
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
5765
+ if (asyncio := sys.modules.get('asyncio')) is None:
5766
+ return None
5767
+
5768
+ try:
5769
+ task = asyncio.current_task()
5770
+ except Exception: # noqa
5771
+ return None
5772
+
5773
+ if task is None:
5774
+ return None
5775
+
5776
+ return cls(
5777
+ name=task.get_name(), # Always non-None
5778
+ )
5779
+
5780
+
5781
+ ##
5782
+
5783
+
5784
+ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5785
+ pass
5786
+
5787
+
5788
+ def _check_logging_start_time() -> None:
5789
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
5790
+ import warnings # noqa
5791
+
5792
+ warnings.warn(
5793
+ f'Unexpected logging start time detected: '
5794
+ f'get_std_start_ns={x}, '
5795
+ f'time.time()={t}',
5796
+ UnexpectedLoggingStartTimeWarning,
5595
5797
  )
5596
5798
 
5597
5799
 
5800
+ _check_logging_start_time()
5801
+
5802
+
5598
5803
  ########################################
5599
5804
  # ../../../omlish/logs/protocols.py
5600
5805
 
@@ -5602,6 +5807,7 @@ class LoggingCaller(ta.NamedTuple):
5602
5807
  ##
5603
5808
 
5604
5809
 
5810
+ @ta.runtime_checkable
5605
5811
  class LoggerLike(ta.Protocol):
5606
5812
  """Satisfied by both our Logger and stdlib logging.Logger."""
5607
5813
 
@@ -5684,119 +5890,34 @@ class JsonLoggingFormatter(logging.Formatter):
5684
5890
 
5685
5891
 
5686
5892
  ########################################
5687
- # ../../../omlish/logs/times.py
5893
+ # ../../interp/types.py
5688
5894
 
5689
5895
 
5690
5896
  ##
5691
5897
 
5692
5898
 
5693
- @logging_context_info
5694
- @ta.final
5695
- class LoggingTimeFields(ta.NamedTuple):
5696
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5697
-
5698
- created: float
5699
- msecs: float
5700
- relative_created: float
5899
+ # See https://peps.python.org/pep-3149/
5900
+ INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
5901
+ ('debug', 'd'),
5902
+ ('threaded', 't'),
5903
+ ])
5701
5904
 
5702
- @classmethod
5703
- def get_std_start_time_ns(cls) -> int:
5704
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5905
+ INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
5906
+ (g, a) for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items()
5907
+ )
5705
5908
 
5706
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5707
- # int.
5708
- #
5709
- # See:
5710
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5711
- #
5712
- if isinstance(x, float):
5713
- return int(x * 1e9)
5714
- else:
5715
- return x
5716
5909
 
5717
- @classmethod
5718
- def build(
5719
- cls,
5720
- time_ns: int,
5721
- *,
5722
- start_time_ns: ta.Optional[int] = None,
5723
- ) -> 'LoggingTimeFields':
5724
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5725
- created = time_ns / 1e9 # ns to float seconds
5726
-
5727
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5728
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5729
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5730
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5731
-
5732
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5733
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5734
- # ns -> sec conversion can round up, e.g:
5735
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5736
- msecs = 0.0
5737
-
5738
- if start_time_ns is None:
5739
- start_time_ns = cls.get_std_start_time_ns()
5740
- relative_created = (time_ns - start_time_ns) / 1e6
5910
+ @dc.dataclass(frozen=True)
5911
+ class InterpOpts:
5912
+ threaded: bool = False
5913
+ debug: bool = False
5741
5914
 
5742
- return cls(
5743
- created,
5744
- msecs,
5745
- relative_created,
5746
- )
5915
+ def __str__(self) -> str:
5916
+ return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
5747
5917
 
5748
-
5749
- ##
5750
-
5751
-
5752
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5753
- pass
5754
-
5755
-
5756
- def _check_logging_start_time() -> None:
5757
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5758
- import warnings # noqa
5759
-
5760
- warnings.warn(
5761
- f'Unexpected logging start time detected: '
5762
- f'get_std_start_time_ns={x}, '
5763
- f'time.time()={t}',
5764
- UnexpectedLoggingStartTimeWarning,
5765
- )
5766
-
5767
-
5768
- _check_logging_start_time()
5769
-
5770
-
5771
- ########################################
5772
- # ../../interp/types.py
5773
-
5774
-
5775
- ##
5776
-
5777
-
5778
- # See https://peps.python.org/pep-3149/
5779
- INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
5780
- ('debug', 'd'),
5781
- ('threaded', 't'),
5782
- ])
5783
-
5784
- INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
5785
- (g, a) for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items()
5786
- )
5787
-
5788
-
5789
- @dc.dataclass(frozen=True)
5790
- class InterpOpts:
5791
- threaded: bool = False
5792
- debug: bool = False
5793
-
5794
- def __str__(self) -> str:
5795
- return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
5796
-
5797
- @classmethod
5798
- def parse(cls, s: str) -> 'InterpOpts':
5799
- return cls(**{INTERP_OPT_ATTRS_BY_GLYPH[g]: True for g in s})
5918
+ @classmethod
5919
+ def parse(cls, s: str) -> 'InterpOpts':
5920
+ return cls(**{INTERP_OPT_ATTRS_BY_GLYPH[g]: True for g in s})
5800
5921
 
5801
5922
  @classmethod
5802
5923
  def parse_suffix(cls, s: str) -> ta.Tuple[str, 'InterpOpts']:
@@ -6981,68 +7102,36 @@ inj = InjectionApi()
6981
7102
 
6982
7103
 
6983
7104
  class LoggingContext(Abstract):
6984
- @property
6985
- @abc.abstractmethod
6986
- def level(self) -> NamedLogLevel:
6987
- raise NotImplementedError
6988
-
6989
- #
6990
-
6991
- @property
6992
- @abc.abstractmethod
6993
- def time_ns(self) -> int:
6994
- raise NotImplementedError
6995
-
6996
- @property
6997
7105
  @abc.abstractmethod
6998
- def times(self) -> LoggingTimeFields:
7106
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
6999
7107
  raise NotImplementedError
7000
7108
 
7001
- #
7109
+ @ta.final
7110
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7111
+ return self.get_info(ty)
7002
7112
 
7003
- @property
7004
- @abc.abstractmethod
7005
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7006
- raise NotImplementedError
7113
+ @ta.final
7114
+ def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
7115
+ if (info := self.get_info(ty)) is None:
7116
+ raise TypeError(f'LoggingContextInfo absent: {ty}')
7117
+ return info
7007
7118
 
7008
- @property
7009
- @abc.abstractmethod
7010
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7011
- raise NotImplementedError
7119
+ ##
7012
7120
 
7013
- #
7014
7121
 
7122
+ class CaptureLoggingContext(LoggingContext, Abstract):
7015
7123
  @abc.abstractmethod
7016
- def caller(self) -> ta.Optional[LoggingCaller]:
7017
- raise NotImplementedError
7124
+ def set_basic(
7125
+ self,
7126
+ name: str,
7018
7127
 
7019
- @abc.abstractmethod
7020
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7128
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7129
+ args: tuple,
7130
+ ) -> 'CaptureLoggingContext':
7021
7131
  raise NotImplementedError
7022
7132
 
7023
7133
  #
7024
7134
 
7025
- @abc.abstractmethod
7026
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7027
- raise NotImplementedError
7028
-
7029
- @abc.abstractmethod
7030
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7031
- raise NotImplementedError
7032
-
7033
- @abc.abstractmethod
7034
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7035
- raise NotImplementedError
7036
-
7037
- @abc.abstractmethod
7038
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7039
- raise NotImplementedError
7040
-
7041
-
7042
- ##
7043
-
7044
-
7045
- class CaptureLoggingContext(LoggingContext, Abstract):
7046
7135
  class AlreadyCapturedError(Exception):
7047
7136
  pass
7048
7137
 
@@ -7073,80 +7162,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7073
7162
 
7074
7163
  exc_info: LoggingExcInfoArg = False,
7075
7164
 
7076
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
7165
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
7077
7166
  stack_offset: int = 0,
7078
7167
  stack_info: bool = False,
7079
7168
  ) -> None:
7080
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7081
-
7082
- #
7169
+ # TODO: Name, Msg, Extra
7083
7170
 
7084
7171
  if time_ns is None:
7085
7172
  time_ns = time.time_ns()
7086
- self._time_ns: int = time_ns
7087
-
7088
- #
7089
-
7090
- if exc_info is True:
7091
- sys_exc_info = sys.exc_info()
7092
- if sys_exc_info[0] is not None:
7093
- exc_info = sys_exc_info
7094
- else:
7095
- exc_info = None
7096
- elif exc_info is False:
7097
- exc_info = None
7098
-
7099
- if exc_info is not None:
7100
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
7101
- if isinstance(exc_info, BaseException):
7102
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
7103
- else:
7104
- self._exc_info_tuple = exc_info
7105
7173
 
7106
- #
7174
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
7175
+ self._set_info(
7176
+ LoggingContextInfos.Level.build(level),
7177
+ LoggingContextInfos.Time.build(time_ns),
7178
+ LoggingContextInfos.Exc.build(exc_info),
7179
+ )
7107
7180
 
7108
7181
  if caller is not CaptureLoggingContextImpl.NOT_SET:
7109
- self._caller = caller # type: ignore[assignment]
7182
+ self._infos[LoggingContextInfos.Caller] = caller
7110
7183
  else:
7111
7184
  self._stack_offset = stack_offset
7112
7185
  self._stack_info = stack_info
7113
7186
 
7114
- ##
7115
-
7116
- @property
7117
- def level(self) -> NamedLogLevel:
7118
- return self._level
7119
-
7120
- #
7121
-
7122
- @property
7123
- def time_ns(self) -> int:
7124
- return self._time_ns
7125
-
7126
- _times: LoggingTimeFields
7127
-
7128
- @property
7129
- def times(self) -> LoggingTimeFields:
7130
- try:
7131
- return self._times
7132
- except AttributeError:
7133
- pass
7134
-
7135
- times = self._times = LoggingTimeFields.build(self.time_ns)
7136
- return times
7187
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
7188
+ for info in infos:
7189
+ if info is not None:
7190
+ self._infos[type(info)] = info
7191
+ return self
7137
7192
 
7138
- #
7193
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7194
+ return self._infos.get(ty)
7139
7195
 
7140
- _exc_info: ta.Optional[LoggingExcInfo] = None
7141
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
7196
+ ##
7142
7197
 
7143
- @property
7144
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7145
- return self._exc_info
7198
+ def set_basic(
7199
+ self,
7200
+ name: str,
7146
7201
 
7147
- @property
7148
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7149
- return self._exc_info_tuple
7202
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7203
+ args: tuple,
7204
+ ) -> 'CaptureLoggingContextImpl':
7205
+ return self._set_info(
7206
+ LoggingContextInfos.Name(name),
7207
+ LoggingContextInfos.Msg.build(msg, *args),
7208
+ )
7150
7209
 
7151
7210
  ##
7152
7211
 
@@ -7160,74 +7219,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7160
7219
 
7161
7220
  _has_captured: bool = False
7162
7221
 
7163
- _caller: ta.Optional[LoggingCaller]
7164
- _source_file: ta.Optional[LoggingSourceFileInfo]
7165
-
7166
- _thread: ta.Optional[LoggingThreadInfo]
7167
- _process: ta.Optional[LoggingProcessInfo]
7168
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
7169
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
7170
-
7171
7222
  def capture(self) -> None:
7172
7223
  if self._has_captured:
7173
7224
  raise CaptureLoggingContextImpl.AlreadyCapturedError
7174
7225
  self._has_captured = True
7175
7226
 
7176
- if not hasattr(self, '_caller'):
7177
- self._caller = LoggingCaller.find(
7227
+ if LoggingContextInfos.Caller not in self._infos:
7228
+ self._set_info(LoggingContextInfos.Caller.build(
7178
7229
  self._stack_offset + 1,
7179
7230
  stack_info=self._stack_info,
7180
- )
7181
-
7182
- if (caller := self._caller) is not None:
7183
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
7184
- else:
7185
- self._source_file = None
7186
-
7187
- self._thread = LoggingThreadInfo.build()
7188
- self._process = LoggingProcessInfo.build()
7189
- self._multiprocessing = LoggingMultiprocessingInfo.build()
7190
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
7191
-
7192
- #
7193
-
7194
- def caller(self) -> ta.Optional[LoggingCaller]:
7195
- try:
7196
- return self._caller
7197
- except AttributeError:
7198
- raise CaptureLoggingContext.NotCapturedError from None
7199
-
7200
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7201
- try:
7202
- return self._source_file
7203
- except AttributeError:
7204
- raise CaptureLoggingContext.NotCapturedError from None
7205
-
7206
- #
7207
-
7208
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7209
- try:
7210
- return self._thread
7211
- except AttributeError:
7212
- raise CaptureLoggingContext.NotCapturedError from None
7213
-
7214
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7215
- try:
7216
- return self._process
7217
- except AttributeError:
7218
- raise CaptureLoggingContext.NotCapturedError from None
7231
+ ))
7219
7232
 
7220
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7221
- try:
7222
- return self._multiprocessing
7223
- except AttributeError:
7224
- raise CaptureLoggingContext.NotCapturedError from None
7233
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
7234
+ self._set_info(LoggingContextInfos.SourceFile.build(
7235
+ caller.file_path,
7236
+ ))
7225
7237
 
7226
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7227
- try:
7228
- return self._asyncio_task
7229
- except AttributeError:
7230
- raise CaptureLoggingContext.NotCapturedError from None
7238
+ self._set_info(
7239
+ LoggingContextInfos.Thread.build(),
7240
+ LoggingContextInfos.Process.build(),
7241
+ LoggingContextInfos.Multiprocessing.build(),
7242
+ LoggingContextInfos.AsyncioTask.build(),
7243
+ )
7231
7244
 
7232
7245
 
7233
7246
  ########################################
@@ -7581,7 +7594,6 @@ InterpProviders = ta.NewType('InterpProviders', ta.Sequence[InterpProvider])
7581
7594
 
7582
7595
 
7583
7596
  class AnyLogger(Abstract, ta.Generic[T]):
7584
- @ta.final
7585
7597
  def is_enabled_for(self, level: LogLevel) -> bool:
7586
7598
  return level >= self.get_effective_level()
7587
7599
 
@@ -7727,36 +7739,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
7727
7739
 
7728
7740
  ##
7729
7741
 
7730
- @classmethod
7731
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
7732
- if callable(msg):
7733
- if args:
7734
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
7735
- x = msg()
7736
- if isinstance(x, str):
7737
- return x, ()
7738
- elif isinstance(x, tuple):
7739
- if x:
7740
- return x[0], x[1:]
7741
- else:
7742
- return '', ()
7743
- else:
7744
- raise TypeError(x)
7745
-
7746
- elif isinstance(msg, tuple):
7747
- if args:
7748
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
7749
- if msg:
7750
- return msg[0], msg[1:]
7751
- else:
7752
- return '', ()
7753
-
7754
- elif isinstance(msg, str):
7755
- return msg, args
7756
-
7757
- else:
7758
- raise TypeError(msg)
7759
-
7760
7742
  @abc.abstractmethod
7761
7743
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
7762
7744
  raise NotImplementedError
@@ -7780,7 +7762,7 @@ class AsyncLogger(AnyLogger[ta.Awaitable[None]], Abstract):
7780
7762
  class AnyNopLogger(AnyLogger[T], Abstract):
7781
7763
  @ta.final
7782
7764
  def get_effective_level(self) -> LogLevel:
7783
- return 999
7765
+ return -999
7784
7766
 
7785
7767
 
7786
7768
  @ta.final
@@ -7797,137 +7779,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
7797
7779
 
7798
7780
  ########################################
7799
7781
  # ../../../omlish/logs/std/records.py
7782
+ """
7783
+ TODO:
7784
+ - TypedDict?
7785
+ """
7800
7786
 
7801
7787
 
7802
7788
  ##
7803
7789
 
7804
7790
 
7805
- # Ref:
7806
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
7807
- #
7808
- # LogRecord:
7809
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
7810
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
7811
- #
7812
- # LogRecord.__init__ args:
7813
- # - name: str
7814
- # - level: int
7815
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
7816
- # - lineno: int - May be 0.
7817
- # - msg: str
7818
- # - args: tuple | dict | 1-tuple[dict]
7819
- # - exc_info: LoggingExcInfoTuple | None
7820
- # - func: str | None = None -> funcName
7821
- # - sinfo: str | None = None -> stack_info
7822
- #
7823
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
7824
- # Name of the logger used to log the call. Unmodified by ctor.
7825
- name=str,
7791
+ class LoggingContextInfoRecordAdapters:
7792
+ # Ref:
7793
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
7794
+ #
7795
+ # LogRecord:
7796
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
7797
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
7798
+ #
7826
7799
 
7827
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
7828
- # (see Using arbitrary objects as messages). Unmodified by ctor.
7829
- msg=str,
7800
+ def __new__(cls, *args, **kwargs): # noqa
7801
+ raise TypeError
7830
7802
 
7831
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
7832
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
7833
- # the mapping, but is otherwise unmodified.
7834
- args=ta.Union[tuple, dict],
7803
+ class Adapter(Abstract, ta.Generic[T]):
7804
+ @property
7805
+ @abc.abstractmethod
7806
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
7807
+ raise NotImplementedError
7835
7808
 
7836
- #
7809
+ #
7837
7810
 
7838
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
7839
- # `getLevelName(level)`.
7840
- levelname=str,
7811
+ @ta.final
7812
+ class NOT_SET: # noqa
7813
+ def __new__(cls, *args, **kwargs): # noqa
7814
+ raise TypeError
7841
7815
 
7842
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
7843
- levelno=int,
7816
+ class RecordAttr(ta.NamedTuple):
7817
+ name: str
7818
+ type: ta.Any
7819
+ default: ta.Any
7844
7820
 
7845
- #
7821
+ # @abc.abstractmethod
7822
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
7846
7823
 
7847
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
7848
- # to "(unknown file)" by Logger.findCaller / Logger._log.
7849
- pathname=str,
7824
+ @property
7825
+ @abc.abstractmethod
7826
+ def _record_attrs(self) -> ta.Union[
7827
+ ta.Mapping[str, ta.Any],
7828
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
7829
+ ]:
7830
+ raise NotImplementedError
7850
7831
 
7851
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
7852
- filename=str,
7832
+ #
7853
7833
 
7854
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
7855
- # "Unknown module".
7856
- module=str,
7834
+ @abc.abstractmethod
7835
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
7836
+ raise NotImplementedError
7857
7837
 
7858
- #
7838
+ #
7859
7839
 
7860
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
7861
- exc_info=ta.Optional[LoggingExcInfoTuple],
7840
+ @abc.abstractmethod
7841
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
7842
+ raise NotImplementedError
7862
7843
 
7863
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
7864
- exc_text=ta.Optional[str],
7844
+ #
7865
7845
 
7866
- #
7846
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7847
+ super().__init_subclass__(**kwargs)
7867
7848
 
7868
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
7869
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
7870
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
7871
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
7872
- stack_info=ta.Optional[str],
7849
+ if Abstract in cls.__bases__:
7850
+ return
7873
7851
 
7874
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
7875
- # Logger.findCaller / Logger._log.
7876
- lineno=int,
7852
+ if 'record_attrs' in cls.__dict__:
7853
+ raise TypeError(cls)
7854
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
7855
+ raise TypeError(ra)
7856
+
7857
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
7858
+ for n, v in ra.items():
7859
+ if not n or not isinstance(n, str) or n in rd:
7860
+ raise AttributeError(n)
7861
+ if isinstance(v, tuple):
7862
+ t, d = v
7863
+ else:
7864
+ t, d = v, cls.NOT_SET
7865
+ rd[n] = cls.RecordAttr(
7866
+ name=n,
7867
+ type=t,
7868
+ default=d,
7869
+ )
7870
+ cls.record_attrs = rd
7877
7871
 
7878
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
7879
- # "(unknown function)" by Logger.findCaller / Logger._log.
7880
- funcName=str,
7872
+ class RequiredAdapter(Adapter[T], Abstract):
7873
+ @property
7874
+ @abc.abstractmethod
7875
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
7876
+ raise NotImplementedError
7881
7877
 
7882
- #
7878
+ #
7883
7879
 
7884
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
7885
- #
7886
- # See:
7887
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7888
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
7889
- #
7890
- created=float,
7880
+ @ta.final
7881
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
7882
+ if (info := ctx.get_info(self.info_cls)) is not None:
7883
+ return self._info_to_record(info)
7884
+ else:
7885
+ raise TypeError # FIXME: fallback?
7891
7886
 
7892
- # Millisecond portion of the time when the LogRecord was created.
7893
- msecs=float,
7887
+ @abc.abstractmethod
7888
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
7889
+ raise NotImplementedError
7894
7890
 
7895
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
7896
- relativeCreated=float,
7891
+ #
7897
7892
 
7898
- #
7893
+ @abc.abstractmethod
7894
+ def record_to_info(self, rec: logging.LogRecord) -> T:
7895
+ raise NotImplementedError
7896
+
7897
+ #
7899
7898
 
7900
- # Thread ID if available, and `logging.logThreads` is truthy.
7901
- thread=ta.Optional[int],
7899
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7900
+ super().__init_subclass__(**kwargs)
7902
7901
 
7903
- # Thread name if available, and `logging.logThreads` is truthy.
7904
- threadName=ta.Optional[str],
7902
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
7903
+ raise TypeError(cls.record_attrs)
7905
7904
 
7906
- #
7905
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
7906
+ @property
7907
+ @abc.abstractmethod
7908
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
7909
+ raise NotImplementedError
7910
+
7911
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
7912
+
7913
+ #
7914
+
7915
+ @ta.final
7916
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
7917
+ if (info := ctx.get_info(self.info_cls)) is not None:
7918
+ return self._info_to_record(info)
7919
+ else:
7920
+ return self.record_defaults
7921
+
7922
+ @abc.abstractmethod
7923
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
7924
+ raise NotImplementedError
7925
+
7926
+ #
7927
+
7928
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7929
+ super().__init_subclass__(**kwargs)
7930
+
7931
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
7932
+ if any(d is cls.NOT_SET for d in dd.values()):
7933
+ raise TypeError(cls.record_attrs)
7934
+ cls.record_defaults = dd
7907
7935
 
7908
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
7909
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
7910
- # as 'MainProcess'.
7911
7936
  #
7912
- # As noted by stdlib:
7937
+
7938
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
7939
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
7940
+
7941
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
7942
+ # Name of the logger used to log the call. Unmodified by ctor.
7943
+ name=str,
7944
+ )
7945
+
7946
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
7947
+ return dict(
7948
+ name=info.name,
7949
+ )
7950
+
7951
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
7952
+ return LoggingContextInfos.Name(
7953
+ name=rec.name,
7954
+ )
7955
+
7956
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
7957
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
7958
+
7959
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
7960
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
7961
+ # `getLevelName(level)`.
7962
+ levelname=str,
7963
+
7964
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
7965
+ levelno=int,
7966
+ )
7967
+
7968
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
7969
+ return dict(
7970
+ levelname=info.name,
7971
+ levelno=int(info.level),
7972
+ )
7973
+
7974
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
7975
+ return LoggingContextInfos.Level.build(rec.levelno)
7976
+
7977
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
7978
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
7979
+
7980
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
7981
+ # The format string passed in the original logging call. Merged with args to produce message, or an
7982
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
7983
+ msg=str,
7984
+
7985
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
7986
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
7987
+ # Mapping into just the mapping, but is otherwise unmodified.
7988
+ args=ta.Union[tuple, dict, None],
7989
+ )
7990
+
7991
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
7992
+ return dict(
7993
+ msg=info.msg,
7994
+ args=info.args,
7995
+ )
7996
+
7997
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
7998
+ return LoggingContextInfos.Msg(
7999
+ msg=rec.msg,
8000
+ args=rec.args,
8001
+ )
8002
+
8003
+ # FIXME: handled specially - all unknown attrs on LogRecord
8004
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
8005
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
7913
8006
  #
7914
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
7915
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
8007
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
8008
+ # # FIXME:
8009
+ # # if extra is not None:
8010
+ # # for key in extra:
8011
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
8012
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
8013
+ # # rv.__dict__[key] = extra[key]
8014
+ # return dict()
7916
8015
  #
7917
- processName=ta.Optional[str],
8016
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
8017
+ # return None
8018
+
8019
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
8020
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
8021
+
8022
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
8023
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
8024
+ # `time.time()`.
8025
+ #
8026
+ # See:
8027
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
8028
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
8029
+ #
8030
+ created=float,
8031
+
8032
+ # Millisecond portion of the time when the LogRecord was created.
8033
+ msecs=float,
8034
+
8035
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
8036
+ relativeCreated=float,
8037
+ )
7918
8038
 
7919
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
7920
- # None.
7921
- process=ta.Optional[int],
8039
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
8040
+ return dict(
8041
+ created=info.secs,
8042
+ msecs=info.msecs,
8043
+ relativeCreated=info.relative_secs,
8044
+ )
7922
8045
 
7923
- #
8046
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
8047
+ return LoggingContextInfos.Time.build(
8048
+ int(rec.created * 1e9),
8049
+ )
7924
8050
 
7925
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
7926
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
7927
- taskName=ta.Optional[str],
7928
- )
8051
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
8052
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
8053
+
8054
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8055
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
8056
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
8057
+
8058
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
8059
+ exc_text=(ta.Optional[str], None),
8060
+ )
8061
+
8062
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
8063
+ return dict(
8064
+ exc_info=info.info_tuple,
8065
+ exc_text=None,
8066
+ )
8067
+
8068
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
8069
+ # FIXME:
8070
+ # error: Argument 1 to "build" of "Exc" has incompatible type
8071
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
8072
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
8073
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
8074
+
8075
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
8076
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
7929
8077
 
7930
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
8078
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
8079
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
8080
+
8081
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
8082
+
8083
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8084
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
8085
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
8086
+ pathname=(str, _UNKNOWN_PATH_NAME),
8087
+
8088
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
8089
+ # y Logger.findCaller / Logger._log.
8090
+ lineno=(int, 0),
8091
+
8092
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
8093
+ # "(unknown function)" by Logger.findCaller / Logger._log.
8094
+ funcName=(str, _UNKNOWN_FUNC_NAME),
8095
+
8096
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
8097
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
8098
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
8099
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
8100
+ # stripped of exactly one trailing `\n` if present.
8101
+ stack_info=(ta.Optional[str], None),
8102
+ )
8103
+
8104
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
8105
+ if (sinfo := caller.stack_info) is not None:
8106
+ stack_info: ta.Optional[str] = '\n'.join([
8107
+ self._STACK_INFO_PREFIX,
8108
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
8109
+ ])
8110
+ else:
8111
+ stack_info = None
8112
+
8113
+ return dict(
8114
+ pathname=caller.file_path,
8115
+
8116
+ lineno=caller.line_no,
8117
+ funcName=caller.func_name,
8118
+
8119
+ stack_info=stack_info,
8120
+ )
8121
+
8122
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
8123
+ # FIXME: piecemeal?
8124
+ # FIXME: strip _STACK_INFO_PREFIX
8125
+ raise NotImplementedError
8126
+
8127
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
8128
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
8129
+
8130
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
8131
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
8132
+ # pathname.
8133
+ filename=str,
8134
+
8135
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
8136
+ # "Unknown module".
8137
+ module=str,
8138
+ )
8139
+
8140
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
8141
+
8142
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
8143
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
8144
+ return dict(
8145
+ filename=info.file_name,
8146
+ module=info.module,
8147
+ )
8148
+
8149
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
8150
+ return dict(
8151
+ filename=caller.file_path,
8152
+ module=self._UNKNOWN_MODULE,
8153
+ )
8154
+
8155
+ return dict(
8156
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
8157
+ module=self._UNKNOWN_MODULE,
8158
+ )
8159
+
8160
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
8161
+ if not (
8162
+ rec.module is None or
8163
+ rec.module == self._UNKNOWN_MODULE
8164
+ ):
8165
+ return LoggingContextInfos.SourceFile(
8166
+ file_name=rec.filename,
8167
+ module=rec.module, # FIXME: piecemeal?
8168
+ )
8169
+
8170
+ return None
8171
+
8172
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
8173
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
8174
+
8175
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8176
+ # Thread ID if available, and `logging.logThreads` is truthy.
8177
+ thread=(ta.Optional[int], None),
8178
+
8179
+ # Thread name if available, and `logging.logThreads` is truthy.
8180
+ threadName=(ta.Optional[str], None),
8181
+ )
8182
+
8183
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
8184
+ if logging.logThreads:
8185
+ return dict(
8186
+ thread=info.ident,
8187
+ threadName=info.name,
8188
+ )
8189
+
8190
+ return self.record_defaults
8191
+
8192
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
8193
+ if (
8194
+ (ident := rec.thread) is not None and
8195
+ (name := rec.threadName) is not None
8196
+ ):
8197
+ return LoggingContextInfos.Thread(
8198
+ ident=ident,
8199
+ native_id=None,
8200
+ name=name,
8201
+ )
8202
+
8203
+ return None
8204
+
8205
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
8206
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
8207
+
8208
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8209
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
8210
+ # otherwise None.
8211
+ process=(ta.Optional[int], None),
8212
+ )
8213
+
8214
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
8215
+ if logging.logProcesses:
8216
+ return dict(
8217
+ process=info.pid,
8218
+ )
8219
+
8220
+ return self.record_defaults
8221
+
8222
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
8223
+ if (
8224
+ (pid := rec.process) is not None
8225
+ ):
8226
+ return LoggingContextInfos.Process(
8227
+ pid=pid,
8228
+ )
8229
+
8230
+ return None
8231
+
8232
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
8233
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
8234
+
8235
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8236
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
8237
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
8238
+ # remains as 'MainProcess'.
8239
+ #
8240
+ # As noted by stdlib:
8241
+ #
8242
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
8243
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
8244
+ #
8245
+ processName=(ta.Optional[str], None),
8246
+ )
8247
+
8248
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
8249
+ if logging.logMultiprocessing:
8250
+ return dict(
8251
+ processName=info.process_name,
8252
+ )
8253
+
8254
+ return self.record_defaults
8255
+
8256
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
8257
+ if (
8258
+ (process_name := rec.processName) is not None
8259
+ ):
8260
+ return LoggingContextInfos.Multiprocessing(
8261
+ process_name=process_name,
8262
+ )
8263
+
8264
+ return None
8265
+
8266
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
8267
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
8268
+
8269
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
8270
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
8271
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
8272
+ taskName=(ta.Optional[str], None),
8273
+ )
8274
+
8275
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
8276
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
8277
+ return dict(
8278
+ taskName=info.name,
8279
+ )
8280
+
8281
+ return self.record_defaults
8282
+
8283
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
8284
+ if (
8285
+ (name := getattr(rec, 'taskName', None)) is not None
8286
+ ):
8287
+ return LoggingContextInfos.AsyncioTask(
8288
+ name=name,
8289
+ )
8290
+
8291
+ return None
8292
+
8293
+
8294
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
8295
+ LoggingContextInfoRecordAdapters.Name(),
8296
+ LoggingContextInfoRecordAdapters.Level(),
8297
+ LoggingContextInfoRecordAdapters.Msg(),
8298
+ LoggingContextInfoRecordAdapters.Time(),
8299
+ LoggingContextInfoRecordAdapters.Exc(),
8300
+ LoggingContextInfoRecordAdapters.Caller(),
8301
+ LoggingContextInfoRecordAdapters.SourceFile(),
8302
+ LoggingContextInfoRecordAdapters.Thread(),
8303
+ LoggingContextInfoRecordAdapters.Process(),
8304
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
8305
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
8306
+ ]
8307
+
8308
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
8309
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
8310
+ }
8311
+
8312
+
8313
+ ##
8314
+
8315
+
8316
+ KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
8317
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
8318
+ )
7931
8319
 
7932
8320
 
7933
8321
  # Formatter:
@@ -7951,14 +8339,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
7951
8339
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
7952
8340
 
7953
8341
 
7954
- ##
7955
-
7956
-
7957
8342
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
7958
8343
  pass
7959
8344
 
7960
8345
 
7961
8346
  def _check_std_logging_record_attrs() -> None:
8347
+ if (
8348
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
8349
+ len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
8350
+ ):
8351
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
8352
+
7962
8353
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
7963
8354
 
7964
8355
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -7977,116 +8368,20 @@ _check_std_logging_record_attrs()
7977
8368
 
7978
8369
 
7979
8370
  class LoggingContextLogRecord(logging.LogRecord):
7980
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
7981
-
7982
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
7983
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
7984
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
7985
-
7986
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
7987
-
7988
- def __init__( # noqa
7989
- self,
7990
- # name,
7991
- # level,
7992
- # pathname,
7993
- # lineno,
7994
- # msg,
7995
- # args,
7996
- # exc_info,
7997
- # func=None,
7998
- # sinfo=None,
7999
- # **kwargs,
8000
- *,
8001
- name: str,
8002
- msg: str,
8003
- args: ta.Union[tuple, dict],
8004
-
8005
- _logging_context: LoggingContext,
8006
- ) -> None:
8007
- ctx = _logging_context
8008
-
8009
- self.name: str = name
8010
-
8011
- self.msg: str = msg
8012
-
8013
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
8014
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
8015
- args = args[0] # type: ignore[assignment]
8016
- self.args: ta.Union[tuple, dict] = args
8017
-
8018
- self.levelname: str = logging.getLevelName(ctx.level)
8019
- self.levelno: int = ctx.level
8020
-
8021
- if (caller := ctx.caller()) is not None:
8022
- self.pathname: str = caller.file_path
8023
- else:
8024
- self.pathname = self._UNKNOWN_PATH_NAME
8025
-
8026
- if (src_file := ctx.source_file()) is not None:
8027
- self.filename: str = src_file.file_name
8028
- self.module: str = src_file.module
8029
- else:
8030
- self.filename = self.pathname
8031
- self.module = self._UNKNOWN_MODULE
8032
-
8033
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
8034
- self.exc_text: ta.Optional[str] = None
8035
-
8036
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
8037
- if caller is not None:
8038
- if (sinfo := caller.stack_info) is not None:
8039
- self.stack_info: ta.Optional[str] = '\n'.join([
8040
- self._STACK_INFO_PREFIX,
8041
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
8042
- ])
8043
- else:
8044
- self.stack_info = None
8045
-
8046
- self.lineno: int = caller.line_no
8047
- self.funcName: str = caller.name
8048
-
8049
- else:
8050
- self.stack_info = None
8051
-
8052
- self.lineno = 0
8053
- self.funcName = self._UNKNOWN_FUNC_NAME
8054
-
8055
- times = ctx.times
8056
- self.created: float = times.created
8057
- self.msecs: float = times.msecs
8058
- self.relativeCreated: float = times.relative_created
8059
-
8060
- if logging.logThreads:
8061
- thread = check.not_none(ctx.thread())
8062
- self.thread: ta.Optional[int] = thread.ident
8063
- self.threadName: ta.Optional[str] = thread.name
8064
- else:
8065
- self.thread = None
8066
- self.threadName = None
8067
-
8068
- if logging.logProcesses:
8069
- process = check.not_none(ctx.process())
8070
- self.process: ta.Optional[int] = process.pid
8071
- else:
8072
- self.process = None
8073
-
8074
- if logging.logMultiprocessing:
8075
- if (mp := ctx.multiprocessing()) is not None:
8076
- self.processName: ta.Optional[str] = mp.process_name
8077
- else:
8078
- self.processName = None
8079
- else:
8080
- self.processName = None
8081
-
8082
- # Absent <3.12
8083
- if getattr(logging, 'logAsyncioTasks', None):
8084
- if (at := ctx.asyncio_task()) is not None:
8085
- self.taskName: ta.Optional[str] = at.name
8086
- else:
8087
- self.taskName = None
8088
- else:
8089
- self.taskName = None
8371
+ # LogRecord.__init__ args:
8372
+ # - name: str
8373
+ # - level: int
8374
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
8375
+ # - lineno: int - May be 0.
8376
+ # - msg: str
8377
+ # - args: tuple | dict | 1-tuple[dict]
8378
+ # - exc_info: LoggingExcInfoTuple | None
8379
+ # - func: str | None = None -> funcName
8380
+ # - sinfo: str | None = None -> stack_info
8381
+
8382
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
8383
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
8384
+ self.__dict__.update(ad.context_to_record(_logging_context))
8090
8385
 
8091
8386
 
8092
8387
  ########################################
@@ -8390,7 +8685,7 @@ class InterpResolver:
8390
8685
 
8391
8686
 
8392
8687
  ########################################
8393
- # ../../../omlish/logs/std/adapters.py
8688
+ # ../../../omlish/logs/std/loggers.py
8394
8689
 
8395
8690
 
8396
8691
  ##
@@ -8406,25 +8701,27 @@ class StdLogger(Logger):
8406
8701
  def std(self) -> logging.Logger:
8407
8702
  return self._std
8408
8703
 
8704
+ def is_enabled_for(self, level: LogLevel) -> bool:
8705
+ return self._std.isEnabledFor(level)
8706
+
8409
8707
  def get_effective_level(self) -> LogLevel:
8410
8708
  return self._std.getEffectiveLevel()
8411
8709
 
8412
8710
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
8413
- if not self.is_enabled_for(ctx.level):
8711
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
8414
8712
  return
8415
8713
 
8416
- ctx.capture()
8417
-
8418
- ms, args = self._prepare_msg_args(msg, *args)
8419
-
8420
- rec = LoggingContextLogRecord(
8714
+ ctx.set_basic(
8421
8715
  name=self._std.name,
8422
- msg=ms,
8423
- args=args,
8424
8716
 
8425
- _logging_context=ctx,
8717
+ msg=msg,
8718
+ args=args,
8426
8719
  )
8427
8720
 
8721
+ ctx.capture()
8722
+
8723
+ rec = LoggingContextLogRecord(_logging_context=ctx)
8724
+
8428
8725
  self._std.handle(rec)
8429
8726
 
8430
8727