omdev 0.0.0.dev430__py3-none-any.whl → 0.0.0.dev432__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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=file_name,
2996
- module=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
- ident=threading.get_ident(),
3014
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
3015
- name=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
- pid=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
- process_name=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
- name=task.get_name(), # Always non-None
3077
- )
3078
-
3079
-
3080
2964
  ########################################
3081
2965
  # ../../../omlish/logs/levels.py
3082
2966
 
@@ -5560,74 +5444,362 @@ class PredicateTimeout(Timeout):
5560
5444
 
5561
5445
 
5562
5446
  ########################################
5563
- # ../../../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
+ """
5564
5452
 
5565
5453
 
5566
5454
  ##
5567
5455
 
5568
5456
 
5569
- @logging_context_info
5457
+ def logging_context_info(cls):
5458
+ return cls
5459
+
5460
+
5570
5461
  @ta.final
5571
- class LoggingCaller(ta.NamedTuple):
5572
- file_path: str
5573
- line_no: int
5574
- name: str
5575
- stack_info: ta.Optional[str]
5462
+ class LoggingContextInfos:
5463
+ def __new__(cls, *args, **kwargs): # noqa
5464
+ raise TypeError
5576
5465
 
5577
- @classmethod
5578
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
5579
- file_path = os.path.normcase(frame.f_code.co_filename)
5466
+ #
5580
5467
 
5581
- # Yes, really.
5582
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
5583
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
5584
- if 'importlib' in file_path and '_bootstrap' in file_path:
5585
- return True
5468
+ @logging_context_info
5469
+ @ta.final
5470
+ class Name(ta.NamedTuple):
5471
+ name: str
5586
5472
 
5587
- return False
5473
+ @logging_context_info
5474
+ @ta.final
5475
+ class Level(ta.NamedTuple):
5476
+ level: NamedLogLevel
5477
+ name: str
5588
5478
 
5589
- @classmethod
5590
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
5591
- 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)
5592
5515
 
5593
- while f is not None:
5594
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
5595
- # stack_offset management.
5596
- if hasattr(f, 'f_code'):
5597
- 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 = '', ()
5598
5523
 
5599
- f = f.f_back
5524
+ elif isinstance(msg, str):
5525
+ s, a = msg, args
5600
5526
 
5601
- 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
5602
5663
 
5603
- @classmethod
5604
- def find(
5605
- cls,
5606
- ofs: int = 0,
5607
- *,
5608
- stack_info: bool = False,
5609
- ) -> ta.Optional['LoggingCaller']:
5610
- if (f := cls.find_frame(ofs + 1)) is None:
5611
5664
  return None
5612
5665
 
5613
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5614
- sinfo = None
5615
- if stack_info:
5616
- sio = io.StringIO()
5617
- traceback.print_stack(f, file=sio)
5618
- sinfo = sio.getvalue()
5619
- sio.close()
5620
- if sinfo[-1] == '\n':
5621
- 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
5622
5675
 
5623
- return cls(
5624
- file_path=f.f_code.co_filename,
5625
- line_no=f.f_lineno or 0,
5626
- name=f.f_code.co_name,
5627
- stack_info=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,
5628
5797
  )
5629
5798
 
5630
5799
 
5800
+ _check_logging_start_time()
5801
+
5802
+
5631
5803
  ########################################
5632
5804
  # ../../../omlish/logs/protocols.py
5633
5805
 
@@ -5717,91 +5889,6 @@ class JsonLoggingFormatter(logging.Formatter):
5717
5889
  return self._json_dumps(dct)
5718
5890
 
5719
5891
 
5720
- ########################################
5721
- # ../../../omlish/logs/times.py
5722
-
5723
-
5724
- ##
5725
-
5726
-
5727
- @logging_context_info
5728
- @ta.final
5729
- class LoggingTimeFields(ta.NamedTuple):
5730
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5731
-
5732
- created: float
5733
- msecs: float
5734
- relative_created: float
5735
-
5736
- @classmethod
5737
- def get_std_start_time_ns(cls) -> int:
5738
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5739
-
5740
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5741
- # int.
5742
- #
5743
- # See:
5744
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5745
- #
5746
- if isinstance(x, float):
5747
- return int(x * 1e9)
5748
- else:
5749
- return x
5750
-
5751
- @classmethod
5752
- def build(
5753
- cls,
5754
- time_ns: int,
5755
- *,
5756
- start_time_ns: ta.Optional[int] = None,
5757
- ) -> 'LoggingTimeFields':
5758
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5759
- created = time_ns / 1e9 # ns to float seconds
5760
-
5761
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5762
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5763
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5764
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5765
-
5766
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5767
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5768
- # ns -> sec conversion can round up, e.g:
5769
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5770
- msecs = 0.0
5771
-
5772
- if start_time_ns is None:
5773
- start_time_ns = cls.get_std_start_time_ns()
5774
- relative_created = (time_ns - start_time_ns) / 1e6
5775
-
5776
- return cls(
5777
- created=created,
5778
- msecs=msecs,
5779
- relative_created=relative_created,
5780
- )
5781
-
5782
-
5783
- ##
5784
-
5785
-
5786
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5787
- pass
5788
-
5789
-
5790
- def _check_logging_start_time() -> None:
5791
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5792
- import warnings # noqa
5793
-
5794
- warnings.warn(
5795
- f'Unexpected logging start time detected: '
5796
- f'get_std_start_time_ns={x}, '
5797
- f'time.time()={t}',
5798
- UnexpectedLoggingStartTimeWarning,
5799
- )
5800
-
5801
-
5802
- _check_logging_start_time()
5803
-
5804
-
5805
5892
  ########################################
5806
5893
  # ../../interp/types.py
5807
5894
 
@@ -7015,68 +7102,46 @@ inj = InjectionApi()
7015
7102
 
7016
7103
 
7017
7104
  class LoggingContext(Abstract):
7018
- @property
7019
7105
  @abc.abstractmethod
7020
- def level(self) -> NamedLogLevel:
7106
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7021
7107
  raise NotImplementedError
7022
7108
 
7023
- #
7109
+ @ta.final
7110
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7111
+ return self.get_info(ty)
7024
7112
 
7025
- @property
7026
- @abc.abstractmethod
7027
- def time_ns(self) -> int:
7028
- 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
7029
7118
 
7030
- @property
7031
- @abc.abstractmethod
7032
- def times(self) -> LoggingTimeFields:
7033
- raise NotImplementedError
7034
7119
 
7035
- #
7036
-
7037
- @property
7038
- @abc.abstractmethod
7039
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7040
- raise NotImplementedError
7041
-
7042
- @property
7043
- @abc.abstractmethod
7044
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7045
- raise NotImplementedError
7046
-
7047
- #
7048
-
7049
- @abc.abstractmethod
7050
- def caller(self) -> ta.Optional[LoggingCaller]:
7051
- raise NotImplementedError
7120
+ @ta.final
7121
+ class SimpleLoggingContext(LoggingContext):
7122
+ def __init__(self, *infos: LoggingContextInfo) -> None:
7123
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {type(i): i for i in infos}
7052
7124
 
7053
- @abc.abstractmethod
7054
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7055
- raise NotImplementedError
7125
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7126
+ return self._infos.get(ty)
7056
7127
 
7057
- #
7058
7128
 
7059
- @abc.abstractmethod
7060
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7061
- raise NotImplementedError
7129
+ ##
7062
7130
 
7063
- @abc.abstractmethod
7064
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7065
- raise NotImplementedError
7066
7131
 
7132
+ class CaptureLoggingContext(LoggingContext, Abstract):
7067
7133
  @abc.abstractmethod
7068
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7069
- raise NotImplementedError
7134
+ def set_basic(
7135
+ self,
7136
+ name: str,
7070
7137
 
7071
- @abc.abstractmethod
7072
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7138
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7139
+ args: tuple,
7140
+ ) -> 'CaptureLoggingContext':
7073
7141
  raise NotImplementedError
7074
7142
 
7143
+ #
7075
7144
 
7076
- ##
7077
-
7078
-
7079
- class CaptureLoggingContext(LoggingContext, Abstract):
7080
7145
  class AlreadyCapturedError(Exception):
7081
7146
  pass
7082
7147
 
@@ -7107,80 +7172,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7107
7172
 
7108
7173
  exc_info: LoggingExcInfoArg = False,
7109
7174
 
7110
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
7175
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
7111
7176
  stack_offset: int = 0,
7112
7177
  stack_info: bool = False,
7113
7178
  ) -> None:
7114
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7115
-
7116
- #
7179
+ # TODO: Name, Msg, Extra
7117
7180
 
7118
7181
  if time_ns is None:
7119
7182
  time_ns = time.time_ns()
7120
- self._time_ns: int = time_ns
7121
-
7122
- #
7123
7183
 
7124
- if exc_info is True:
7125
- sys_exc_info = sys.exc_info()
7126
- if sys_exc_info[0] is not None:
7127
- exc_info = sys_exc_info
7128
- else:
7129
- exc_info = None
7130
- elif exc_info is False:
7131
- exc_info = None
7132
-
7133
- if exc_info is not None:
7134
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
7135
- if isinstance(exc_info, BaseException):
7136
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
7137
- else:
7138
- self._exc_info_tuple = exc_info
7139
-
7140
- #
7184
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
7185
+ self._set_info(
7186
+ LoggingContextInfos.Level.build(level),
7187
+ LoggingContextInfos.Time.build(time_ns),
7188
+ LoggingContextInfos.Exc.build(exc_info),
7189
+ )
7141
7190
 
7142
7191
  if caller is not CaptureLoggingContextImpl.NOT_SET:
7143
- self._caller = caller # type: ignore[assignment]
7192
+ self._infos[LoggingContextInfos.Caller] = caller
7144
7193
  else:
7145
7194
  self._stack_offset = stack_offset
7146
7195
  self._stack_info = stack_info
7147
7196
 
7148
- ##
7149
-
7150
- @property
7151
- def level(self) -> NamedLogLevel:
7152
- return self._level
7153
-
7154
- #
7155
-
7156
- @property
7157
- def time_ns(self) -> int:
7158
- return self._time_ns
7159
-
7160
- _times: LoggingTimeFields
7161
-
7162
- @property
7163
- def times(self) -> LoggingTimeFields:
7164
- try:
7165
- return self._times
7166
- except AttributeError:
7167
- pass
7168
-
7169
- times = self._times = LoggingTimeFields.build(self.time_ns)
7170
- return times
7197
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
7198
+ for info in infos:
7199
+ if info is not None:
7200
+ self._infos[type(info)] = info
7201
+ return self
7171
7202
 
7172
- #
7203
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7204
+ return self._infos.get(ty)
7173
7205
 
7174
- _exc_info: ta.Optional[LoggingExcInfo] = None
7175
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
7206
+ ##
7176
7207
 
7177
- @property
7178
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7179
- return self._exc_info
7208
+ def set_basic(
7209
+ self,
7210
+ name: str,
7180
7211
 
7181
- @property
7182
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7183
- return self._exc_info_tuple
7212
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7213
+ args: tuple,
7214
+ ) -> 'CaptureLoggingContextImpl':
7215
+ return self._set_info(
7216
+ LoggingContextInfos.Name(name),
7217
+ LoggingContextInfos.Msg.build(msg, *args),
7218
+ )
7184
7219
 
7185
7220
  ##
7186
7221
 
@@ -7194,74 +7229,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7194
7229
 
7195
7230
  _has_captured: bool = False
7196
7231
 
7197
- _caller: ta.Optional[LoggingCaller]
7198
- _source_file: ta.Optional[LoggingSourceFileInfo]
7199
-
7200
- _thread: ta.Optional[LoggingThreadInfo]
7201
- _process: ta.Optional[LoggingProcessInfo]
7202
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
7203
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
7204
-
7205
7232
  def capture(self) -> None:
7206
7233
  if self._has_captured:
7207
7234
  raise CaptureLoggingContextImpl.AlreadyCapturedError
7208
7235
  self._has_captured = True
7209
7236
 
7210
- if not hasattr(self, '_caller'):
7211
- self._caller = LoggingCaller.find(
7237
+ if LoggingContextInfos.Caller not in self._infos:
7238
+ self._set_info(LoggingContextInfos.Caller.build(
7212
7239
  self._stack_offset + 1,
7213
7240
  stack_info=self._stack_info,
7214
- )
7215
-
7216
- if (caller := self._caller) is not None:
7217
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
7218
- else:
7219
- self._source_file = None
7220
-
7221
- self._thread = LoggingThreadInfo.build()
7222
- self._process = LoggingProcessInfo.build()
7223
- self._multiprocessing = LoggingMultiprocessingInfo.build()
7224
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
7225
-
7226
- #
7227
-
7228
- def caller(self) -> ta.Optional[LoggingCaller]:
7229
- try:
7230
- return self._caller
7231
- except AttributeError:
7232
- raise CaptureLoggingContext.NotCapturedError from None
7233
-
7234
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7235
- try:
7236
- return self._source_file
7237
- except AttributeError:
7238
- raise CaptureLoggingContext.NotCapturedError from None
7239
-
7240
- #
7241
-
7242
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7243
- try:
7244
- return self._thread
7245
- except AttributeError:
7246
- raise CaptureLoggingContext.NotCapturedError from None
7247
-
7248
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7249
- try:
7250
- return self._process
7251
- except AttributeError:
7252
- raise CaptureLoggingContext.NotCapturedError from None
7241
+ ))
7253
7242
 
7254
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7255
- try:
7256
- return self._multiprocessing
7257
- except AttributeError:
7258
- raise CaptureLoggingContext.NotCapturedError from None
7243
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
7244
+ self._set_info(LoggingContextInfos.SourceFile.build(
7245
+ caller.file_path,
7246
+ ))
7259
7247
 
7260
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7261
- try:
7262
- return self._asyncio_task
7263
- except AttributeError:
7264
- raise CaptureLoggingContext.NotCapturedError from None
7248
+ self._set_info(
7249
+ LoggingContextInfos.Thread.build(),
7250
+ LoggingContextInfos.Process.build(),
7251
+ LoggingContextInfos.Multiprocessing.build(),
7252
+ LoggingContextInfos.AsyncioTask.build(),
7253
+ )
7265
7254
 
7266
7255
 
7267
7256
  ########################################
@@ -7338,10 +7327,14 @@ def _locking_logging_module_lock() -> ta.Iterator[None]:
7338
7327
  def configure_standard_logging(
7339
7328
  level: ta.Union[int, str] = logging.INFO,
7340
7329
  *,
7341
- json: bool = False,
7342
7330
  target: ta.Optional[logging.Logger] = None,
7331
+
7343
7332
  force: bool = False,
7333
+
7344
7334
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
7335
+
7336
+ formatter: ta.Optional[logging.Formatter] = None, # noqa
7337
+ json: bool = False,
7345
7338
  ) -> ta.Optional[StandardConfiguredLoggingHandler]:
7346
7339
  with _locking_logging_module_lock():
7347
7340
  if target is None:
@@ -7362,11 +7355,11 @@ def configure_standard_logging(
7362
7355
 
7363
7356
  #
7364
7357
 
7365
- formatter: logging.Formatter
7366
- if json:
7367
- formatter = JsonLoggingFormatter()
7368
- else:
7369
- formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
7358
+ if formatter is None:
7359
+ if json:
7360
+ formatter = JsonLoggingFormatter()
7361
+ else:
7362
+ formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS)) # noqa
7370
7363
  handler.setFormatter(formatter)
7371
7364
 
7372
7365
  #
@@ -7760,36 +7753,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
7760
7753
 
7761
7754
  ##
7762
7755
 
7763
- @classmethod
7764
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
7765
- if callable(msg):
7766
- if args:
7767
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
7768
- x = msg()
7769
- if isinstance(x, str):
7770
- return x, ()
7771
- elif isinstance(x, tuple):
7772
- if x:
7773
- return x[0], x[1:]
7774
- else:
7775
- return '', ()
7776
- else:
7777
- raise TypeError(x)
7778
-
7779
- elif isinstance(msg, tuple):
7780
- if args:
7781
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
7782
- if msg:
7783
- return msg[0], msg[1:]
7784
- else:
7785
- return '', ()
7786
-
7787
- elif isinstance(msg, str):
7788
- return msg, args
7789
-
7790
- else:
7791
- raise TypeError(msg)
7792
-
7793
7756
  @abc.abstractmethod
7794
7757
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
7795
7758
  raise NotImplementedError
@@ -7830,144 +7793,560 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
7830
7793
 
7831
7794
  ########################################
7832
7795
  # ../../../omlish/logs/std/records.py
7796
+ """
7797
+ TODO:
7798
+ - TypedDict?
7799
+ """
7833
7800
 
7834
7801
 
7835
7802
  ##
7836
7803
 
7837
7804
 
7838
- # Ref:
7839
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
7840
- #
7841
- # LogRecord:
7842
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
7843
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
7844
- #
7845
- # LogRecord.__init__ args:
7846
- # - name: str
7847
- # - level: int
7848
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
7849
- # - lineno: int - May be 0.
7850
- # - msg: str
7851
- # - args: tuple | dict | 1-tuple[dict]
7852
- # - exc_info: LoggingExcInfoTuple | None
7853
- # - func: str | None = None -> funcName
7854
- # - sinfo: str | None = None -> stack_info
7855
- #
7856
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
7857
- # Name of the logger used to log the call. Unmodified by ctor.
7858
- name=str,
7805
+ class LoggingContextInfoRecordAdapters:
7806
+ # Ref:
7807
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
7808
+ #
7809
+ # LogRecord:
7810
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
7811
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
7812
+ #
7859
7813
 
7860
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
7861
- # (see Using arbitrary objects as messages). Unmodified by ctor.
7862
- msg=str,
7814
+ def __new__(cls, *args, **kwargs): # noqa
7815
+ raise TypeError
7863
7816
 
7864
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
7865
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
7866
- # the mapping, but is otherwise unmodified.
7867
- args=ta.Union[tuple, dict],
7817
+ class Adapter(Abstract, ta.Generic[T]):
7818
+ @property
7819
+ @abc.abstractmethod
7820
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
7821
+ raise NotImplementedError
7868
7822
 
7869
- #
7823
+ #
7870
7824
 
7871
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
7872
- # `getLevelName(level)`.
7873
- levelname=str,
7825
+ @ta.final
7826
+ class NOT_SET: # noqa
7827
+ def __new__(cls, *args, **kwargs): # noqa
7828
+ raise TypeError
7874
7829
 
7875
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
7876
- levelno=int,
7830
+ class RecordAttr(ta.NamedTuple):
7831
+ name: str
7832
+ type: ta.Any
7833
+ default: ta.Any
7877
7834
 
7878
- #
7835
+ # @abc.abstractmethod
7836
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
7879
7837
 
7880
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
7881
- # to "(unknown file)" by Logger.findCaller / Logger._log.
7882
- pathname=str,
7838
+ @property
7839
+ @abc.abstractmethod
7840
+ def _record_attrs(self) -> ta.Union[
7841
+ ta.Mapping[str, ta.Any],
7842
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
7843
+ ]:
7844
+ raise NotImplementedError
7883
7845
 
7884
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
7885
- filename=str,
7846
+ #
7886
7847
 
7887
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
7888
- # "Unknown module".
7889
- module=str,
7848
+ @abc.abstractmethod
7849
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
7850
+ raise NotImplementedError
7890
7851
 
7891
- #
7852
+ #
7892
7853
 
7893
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
7894
- exc_info=ta.Optional[LoggingExcInfoTuple],
7854
+ @abc.abstractmethod
7855
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
7856
+ raise NotImplementedError
7895
7857
 
7896
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
7897
- exc_text=ta.Optional[str],
7858
+ #
7898
7859
 
7899
- #
7860
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7861
+ super().__init_subclass__(**kwargs)
7900
7862
 
7901
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
7902
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
7903
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
7904
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
7905
- stack_info=ta.Optional[str],
7863
+ if Abstract in cls.__bases__:
7864
+ return
7906
7865
 
7907
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
7908
- # Logger.findCaller / Logger._log.
7909
- lineno=int,
7866
+ if 'record_attrs' in cls.__dict__:
7867
+ raise TypeError(cls)
7868
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
7869
+ raise TypeError(ra)
7870
+
7871
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
7872
+ for n, v in ra.items():
7873
+ if not n or not isinstance(n, str) or n in rd:
7874
+ raise AttributeError(n)
7875
+ if isinstance(v, tuple):
7876
+ t, d = v
7877
+ else:
7878
+ t, d = v, cls.NOT_SET
7879
+ rd[n] = cls.RecordAttr(
7880
+ name=n,
7881
+ type=t,
7882
+ default=d,
7883
+ )
7884
+ cls.record_attrs = rd
7910
7885
 
7911
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
7912
- # "(unknown function)" by Logger.findCaller / Logger._log.
7913
- funcName=str,
7886
+ class RequiredAdapter(Adapter[T], Abstract):
7887
+ @property
7888
+ @abc.abstractmethod
7889
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
7890
+ raise NotImplementedError
7914
7891
 
7915
- #
7892
+ #
7916
7893
 
7917
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
7918
- #
7919
- # See:
7920
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7921
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
7922
- #
7923
- created=float,
7894
+ @ta.final
7895
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
7896
+ if (info := ctx.get_info(self.info_cls)) is not None:
7897
+ return self._info_to_record(info)
7898
+ else:
7899
+ raise TypeError # FIXME: fallback?
7924
7900
 
7925
- # Millisecond portion of the time when the LogRecord was created.
7926
- msecs=float,
7901
+ @abc.abstractmethod
7902
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
7903
+ raise NotImplementedError
7927
7904
 
7928
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
7929
- relativeCreated=float,
7905
+ #
7930
7906
 
7931
- #
7907
+ @abc.abstractmethod
7908
+ def record_to_info(self, rec: logging.LogRecord) -> T:
7909
+ raise NotImplementedError
7910
+
7911
+ #
7932
7912
 
7933
- # Thread ID if available, and `logging.logThreads` is truthy.
7934
- thread=ta.Optional[int],
7913
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7914
+ super().__init_subclass__(**kwargs)
7935
7915
 
7936
- # Thread name if available, and `logging.logThreads` is truthy.
7937
- threadName=ta.Optional[str],
7916
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
7917
+ raise TypeError(cls.record_attrs)
7938
7918
 
7939
- #
7919
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
7920
+ @property
7921
+ @abc.abstractmethod
7922
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
7923
+ raise NotImplementedError
7924
+
7925
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
7926
+
7927
+ #
7928
+
7929
+ @ta.final
7930
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
7931
+ if (info := ctx.get_info(self.info_cls)) is not None:
7932
+ return self._info_to_record(info)
7933
+ else:
7934
+ return self.record_defaults
7935
+
7936
+ @abc.abstractmethod
7937
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
7938
+ raise NotImplementedError
7939
+
7940
+ #
7941
+
7942
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7943
+ super().__init_subclass__(**kwargs)
7944
+
7945
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
7946
+ if any(d is cls.NOT_SET for d in dd.values()):
7947
+ raise TypeError(cls.record_attrs)
7948
+ cls.record_defaults = dd
7940
7949
 
7941
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
7942
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
7943
- # as 'MainProcess'.
7944
7950
  #
7945
- # As noted by stdlib:
7951
+
7952
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
7953
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
7954
+
7955
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
7956
+ # Name of the logger used to log the call. Unmodified by ctor.
7957
+ name=str,
7958
+ )
7959
+
7960
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
7961
+ return dict(
7962
+ name=info.name,
7963
+ )
7964
+
7965
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
7966
+ return LoggingContextInfos.Name(
7967
+ name=rec.name,
7968
+ )
7969
+
7970
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
7971
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
7972
+
7973
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
7974
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
7975
+ # `getLevelName(level)`.
7976
+ levelname=str,
7977
+
7978
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
7979
+ levelno=int,
7980
+ )
7981
+
7982
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
7983
+ return dict(
7984
+ levelname=info.name,
7985
+ levelno=int(info.level),
7986
+ )
7987
+
7988
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
7989
+ return LoggingContextInfos.Level.build(rec.levelno)
7990
+
7991
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
7992
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
7993
+
7994
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
7995
+ # The format string passed in the original logging call. Merged with args to produce message, or an
7996
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
7997
+ msg=str,
7998
+
7999
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
8000
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
8001
+ # Mapping into just the mapping, but is otherwise unmodified.
8002
+ args=ta.Union[tuple, dict, None],
8003
+ )
8004
+
8005
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
8006
+ return dict(
8007
+ msg=info.msg,
8008
+ args=info.args,
8009
+ )
8010
+
8011
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
8012
+ return LoggingContextInfos.Msg(
8013
+ msg=rec.msg,
8014
+ args=rec.args,
8015
+ )
8016
+
8017
+ # FIXME: handled specially - all unknown attrs on LogRecord
8018
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
8019
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
7946
8020
  #
7947
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
7948
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
8021
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
8022
+ # # FIXME:
8023
+ # # if extra is not None:
8024
+ # # for key in extra:
8025
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
8026
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
8027
+ # # rv.__dict__[key] = extra[key]
8028
+ # return dict()
7949
8029
  #
7950
- processName=ta.Optional[str],
8030
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
8031
+ # return None
8032
+
8033
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
8034
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
8035
+
8036
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
8037
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
8038
+ # `time.time()`.
8039
+ #
8040
+ # See:
8041
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
8042
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
8043
+ #
8044
+ created=float,
8045
+
8046
+ # Millisecond portion of the time when the LogRecord was created.
8047
+ msecs=float,
8048
+
8049
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
8050
+ relativeCreated=float,
8051
+ )
7951
8052
 
7952
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
7953
- # None.
7954
- process=ta.Optional[int],
8053
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
8054
+ return dict(
8055
+ created=info.secs,
8056
+ msecs=info.msecs,
8057
+ relativeCreated=info.relative_secs,
8058
+ )
7955
8059
 
7956
- #
8060
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
8061
+ return LoggingContextInfos.Time.build(
8062
+ int(rec.created * 1e9),
8063
+ )
7957
8064
 
7958
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
7959
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
7960
- taskName=ta.Optional[str],
7961
- )
8065
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
8066
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
8067
+
8068
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8069
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
8070
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
8071
+
8072
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
8073
+ exc_text=(ta.Optional[str], None),
8074
+ )
8075
+
8076
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
8077
+ return dict(
8078
+ exc_info=info.info_tuple,
8079
+ exc_text=None,
8080
+ )
8081
+
8082
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
8083
+ # FIXME:
8084
+ # error: Argument 1 to "build" of "Exc" has incompatible type
8085
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
8086
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
8087
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
7962
8088
 
7963
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
8089
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
8090
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
8091
+
8092
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
8093
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
8094
+
8095
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
8096
+
8097
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8098
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
8099
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
8100
+ pathname=(str, _UNKNOWN_PATH_NAME),
8101
+
8102
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
8103
+ # y Logger.findCaller / Logger._log.
8104
+ lineno=(int, 0),
8105
+
8106
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
8107
+ # "(unknown function)" by Logger.findCaller / Logger._log.
8108
+ funcName=(str, _UNKNOWN_FUNC_NAME),
8109
+
8110
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
8111
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
8112
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
8113
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
8114
+ # stripped of exactly one trailing `\n` if present.
8115
+ stack_info=(ta.Optional[str], None),
8116
+ )
8117
+
8118
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
8119
+ if (sinfo := caller.stack_info) is not None:
8120
+ stack_info: ta.Optional[str] = '\n'.join([
8121
+ self._STACK_INFO_PREFIX,
8122
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
8123
+ ])
8124
+ else:
8125
+ stack_info = None
8126
+
8127
+ return dict(
8128
+ pathname=caller.file_path,
8129
+
8130
+ lineno=caller.line_no,
8131
+ funcName=caller.func_name,
8132
+
8133
+ stack_info=stack_info,
8134
+ )
8135
+
8136
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
8137
+ # FIXME: piecemeal?
8138
+ if (
8139
+ rec.pathname != self._UNKNOWN_PATH_NAME and
8140
+ rec.lineno != 0 and
8141
+ rec.funcName != self._UNKNOWN_FUNC_NAME
8142
+ ):
8143
+ if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
8144
+ sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
8145
+ return LoggingContextInfos.Caller(
8146
+ file_path=rec.pathname,
8147
+
8148
+ line_no=rec.lineno,
8149
+ func_name=rec.funcName,
8150
+
8151
+ stack_info=sinfo,
8152
+ )
8153
+
8154
+ return None
8155
+
8156
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
8157
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
8158
+
8159
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
8160
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
8161
+ # pathname.
8162
+ filename=str,
8163
+
8164
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
8165
+ # "Unknown module".
8166
+ module=str,
8167
+ )
8168
+
8169
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
8170
+
8171
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
8172
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
8173
+ return dict(
8174
+ filename=info.file_name,
8175
+ module=info.module,
8176
+ )
8177
+
8178
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
8179
+ return dict(
8180
+ filename=caller.file_path,
8181
+ module=self._UNKNOWN_MODULE,
8182
+ )
8183
+
8184
+ return dict(
8185
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
8186
+ module=self._UNKNOWN_MODULE,
8187
+ )
8188
+
8189
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
8190
+ if (
8191
+ rec.module is not None and
8192
+ rec.module != self._UNKNOWN_MODULE
8193
+ ):
8194
+ return LoggingContextInfos.SourceFile(
8195
+ file_name=rec.filename,
8196
+ module=rec.module, # FIXME: piecemeal?
8197
+ )
8198
+
8199
+ return None
8200
+
8201
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
8202
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
8203
+
8204
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8205
+ # Thread ID if available, and `logging.logThreads` is truthy.
8206
+ thread=(ta.Optional[int], None),
8207
+
8208
+ # Thread name if available, and `logging.logThreads` is truthy.
8209
+ threadName=(ta.Optional[str], None),
8210
+ )
8211
+
8212
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
8213
+ if logging.logThreads:
8214
+ return dict(
8215
+ thread=info.ident,
8216
+ threadName=info.name,
8217
+ )
8218
+
8219
+ return self.record_defaults
8220
+
8221
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
8222
+ if (
8223
+ (ident := rec.thread) is not None and
8224
+ (name := rec.threadName) is not None
8225
+ ):
8226
+ return LoggingContextInfos.Thread(
8227
+ ident=ident,
8228
+ native_id=None,
8229
+ name=name,
8230
+ )
8231
+
8232
+ return None
8233
+
8234
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
8235
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
8236
+
8237
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8238
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
8239
+ # otherwise None.
8240
+ process=(ta.Optional[int], None),
8241
+ )
8242
+
8243
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
8244
+ if logging.logProcesses:
8245
+ return dict(
8246
+ process=info.pid,
8247
+ )
8248
+
8249
+ return self.record_defaults
8250
+
8251
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
8252
+ if (
8253
+ (pid := rec.process) is not None
8254
+ ):
8255
+ return LoggingContextInfos.Process(
8256
+ pid=pid,
8257
+ )
8258
+
8259
+ return None
8260
+
8261
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
8262
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
8263
+
8264
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
8265
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
8266
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
8267
+ # remains as 'MainProcess'.
8268
+ #
8269
+ # As noted by stdlib:
8270
+ #
8271
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
8272
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
8273
+ #
8274
+ processName=(ta.Optional[str], None),
8275
+ )
8276
+
8277
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
8278
+ if logging.logMultiprocessing:
8279
+ return dict(
8280
+ processName=info.process_name,
8281
+ )
8282
+
8283
+ return self.record_defaults
8284
+
8285
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
8286
+ if (
8287
+ (process_name := rec.processName) is not None
8288
+ ):
8289
+ return LoggingContextInfos.Multiprocessing(
8290
+ process_name=process_name,
8291
+ )
8292
+
8293
+ return None
8294
+
8295
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
8296
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
8297
+
8298
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
8299
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
8300
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
8301
+ taskName=(ta.Optional[str], None),
8302
+ )
8303
+
8304
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
8305
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
8306
+ return dict(
8307
+ taskName=info.name,
8308
+ )
8309
+
8310
+ return self.record_defaults
8311
+
8312
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
8313
+ if (
8314
+ (name := getattr(rec, 'taskName', None)) is not None
8315
+ ):
8316
+ return LoggingContextInfos.AsyncioTask(
8317
+ name=name,
8318
+ )
8319
+
8320
+ return None
8321
+
8322
+
8323
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
8324
+ LoggingContextInfoRecordAdapters.Name(),
8325
+ LoggingContextInfoRecordAdapters.Level(),
8326
+ LoggingContextInfoRecordAdapters.Msg(),
8327
+ LoggingContextInfoRecordAdapters.Time(),
8328
+ LoggingContextInfoRecordAdapters.Exc(),
8329
+ LoggingContextInfoRecordAdapters.Caller(),
8330
+ LoggingContextInfoRecordAdapters.SourceFile(),
8331
+ LoggingContextInfoRecordAdapters.Thread(),
8332
+ LoggingContextInfoRecordAdapters.Process(),
8333
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
8334
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
8335
+ ]
8336
+
8337
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
8338
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
8339
+ }
8340
+
8341
+
8342
+ ##
7964
8343
 
7965
8344
 
7966
8345
  # Formatter:
7967
8346
  # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
7968
8347
  # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
7969
8348
  #
7970
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
8349
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
7971
8350
  # The logged message, computed as msg % args. Set to `record.getMessage()`.
7972
8351
  message=str,
7973
8352
 
@@ -7981,20 +8360,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
7981
8360
  exc_text=ta.Optional[str],
7982
8361
  )
7983
8362
 
7984
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
7985
-
7986
8363
 
7987
8364
  ##
7988
8365
 
7989
8366
 
8367
+ _KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
8368
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
8369
+ )
8370
+
8371
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
8372
+
8373
+
7990
8374
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
7991
8375
  pass
7992
8376
 
7993
8377
 
7994
8378
  def _check_std_logging_record_attrs() -> None:
8379
+ if (
8380
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
8381
+ len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
8382
+ ):
8383
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
8384
+
7995
8385
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
7996
8386
 
7997
- if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
8387
+ if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
7998
8388
  import warnings # noqa
7999
8389
 
8000
8390
  warnings.warn(
@@ -8010,116 +8400,43 @@ _check_std_logging_record_attrs()
8010
8400
 
8011
8401
 
8012
8402
  class LoggingContextLogRecord(logging.LogRecord):
8013
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
8014
-
8015
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
8016
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
8017
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
8018
-
8019
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
8020
-
8021
- def __init__( # noqa
8022
- self,
8023
- # name,
8024
- # level,
8025
- # pathname,
8026
- # lineno,
8027
- # msg,
8028
- # args,
8029
- # exc_info,
8030
- # func=None,
8031
- # sinfo=None,
8032
- # **kwargs,
8033
- *,
8034
- name: str,
8035
- msg: str,
8036
- args: ta.Union[tuple, dict],
8037
-
8038
- _logging_context: LoggingContext,
8039
- ) -> None:
8040
- ctx = _logging_context
8041
-
8042
- self.name: str = name
8043
-
8044
- self.msg: str = msg
8403
+ # LogRecord.__init__ args:
8404
+ # - name: str
8405
+ # - level: int
8406
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
8407
+ # - lineno: int - May be 0.
8408
+ # - msg: str
8409
+ # - args: tuple | dict | 1-tuple[dict]
8410
+ # - exc_info: LoggingExcInfoTuple | None
8411
+ # - func: str | None = None -> funcName
8412
+ # - sinfo: str | None = None -> stack_info
8045
8413
 
8046
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
8047
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
8048
- args = args[0] # type: ignore[assignment]
8049
- self.args: ta.Union[tuple, dict] = args
8414
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
8415
+ self._logging_context = _logging_context
8050
8416
 
8051
- self.levelname: str = logging.getLevelName(ctx.level)
8052
- self.levelno: int = ctx.level
8417
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
8418
+ self.__dict__.update(ad.context_to_record(_logging_context))
8053
8419
 
8054
- if (caller := ctx.caller()) is not None:
8055
- self.pathname: str = caller.file_path
8056
- else:
8057
- self.pathname = self._UNKNOWN_PATH_NAME
8058
-
8059
- if (src_file := ctx.source_file()) is not None:
8060
- self.filename: str = src_file.file_name
8061
- self.module: str = src_file.module
8062
- else:
8063
- self.filename = self.pathname
8064
- self.module = self._UNKNOWN_MODULE
8065
-
8066
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
8067
- self.exc_text: ta.Optional[str] = None
8068
-
8069
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
8070
- if caller is not None:
8071
- if (sinfo := caller.stack_info) is not None:
8072
- self.stack_info: ta.Optional[str] = '\n'.join([
8073
- self._STACK_INFO_PREFIX,
8074
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
8075
- ])
8076
- else:
8077
- self.stack_info = None
8078
8420
 
8079
- self.lineno: int = caller.line_no
8080
- self.funcName: str = caller.name
8081
-
8082
- else:
8083
- self.stack_info = None
8084
-
8085
- self.lineno = 0
8086
- self.funcName = self._UNKNOWN_FUNC_NAME
8421
+ ##
8087
8422
 
8088
- times = ctx.times
8089
- self.created: float = times.created
8090
- self.msecs: float = times.msecs
8091
- self.relativeCreated: float = times.relative_created
8092
8423
 
8093
- if logging.logThreads:
8094
- thread = check.not_none(ctx.thread())
8095
- self.thread: ta.Optional[int] = thread.ident
8096
- self.threadName: ta.Optional[str] = thread.name
8097
- else:
8098
- self.thread = None
8099
- self.threadName = None
8424
+ @ta.final
8425
+ class LogRecordLoggingContext(LoggingContext):
8426
+ def __init__(self, rec: logging.LogRecord) -> None:
8427
+ if isinstance(rec, LoggingContextLogRecord):
8428
+ raise TypeError(rec)
8100
8429
 
8101
- if logging.logProcesses:
8102
- process = check.not_none(ctx.process())
8103
- self.process: ta.Optional[int] = process.pid
8104
- else:
8105
- self.process = None
8430
+ self._rec = rec
8106
8431
 
8107
- if logging.logMultiprocessing:
8108
- if (mp := ctx.multiprocessing()) is not None:
8109
- self.processName: ta.Optional[str] = mp.process_name
8110
- else:
8111
- self.processName = None
8112
- else:
8113
- self.processName = None
8432
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
8433
+ type(info): info
8434
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
8435
+ if (info := ad.record_to_info(rec)) is not None
8436
+ }
8114
8437
 
8115
- # Absent <3.12
8116
- if getattr(logging, 'logAsyncioTasks', None):
8117
- if (at := ctx.asyncio_task()) is not None:
8118
- self.taskName: ta.Optional[str] = at.name
8119
- else:
8120
- self.taskName = None
8121
- else:
8122
- self.taskName = None
8438
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
8439
+ return self._infos.get(ty)
8123
8440
 
8124
8441
 
8125
8442
  ########################################
@@ -8446,21 +8763,20 @@ class StdLogger(Logger):
8446
8763
  return self._std.getEffectiveLevel()
8447
8764
 
8448
8765
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
8449
- if not self.is_enabled_for(ctx.level):
8766
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
8450
8767
  return
8451
8768
 
8452
- ctx.capture()
8453
-
8454
- ms, args = self._prepare_msg_args(msg, *args)
8455
-
8456
- rec = LoggingContextLogRecord(
8769
+ ctx.set_basic(
8457
8770
  name=self._std.name,
8458
- msg=ms,
8459
- args=args,
8460
8771
 
8461
- _logging_context=ctx,
8772
+ msg=msg,
8773
+ args=args,
8462
8774
  )
8463
8775
 
8776
+ ctx.capture()
8777
+
8778
+ rec = LoggingContextLogRecord(_logging_context=ctx)
8779
+
8464
8780
  self._std.handle(rec)
8465
8781
 
8466
8782