ominfra 0.0.0.dev430__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.
ominfra/scripts/manage.py CHANGED
@@ -133,6 +133,13 @@ U = ta.TypeVar('U')
133
133
  # ../../omlish/lite/timeouts.py
134
134
  TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.DEFAULT'], ta.Iterable['TimeoutLike'], float, None] # ta.TypeAlias
135
135
 
136
+ # ../../omlish/logs/infos.py
137
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
138
+ LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
139
+ LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
140
+ LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
141
+ LoggingContextInfo = ta.Any # ta.TypeAlias
142
+
136
143
  # ../../omlish/os/atomics.py
137
144
  AtomicPathSwapKind = ta.Literal['dir', 'file']
138
145
  AtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
@@ -151,16 +158,11 @@ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
151
158
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
152
159
 
153
160
  # ../../omlish/logs/contexts.py
154
- LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
155
- LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
156
- LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
161
+ LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
157
162
 
158
163
  # deploy/specs.py
159
164
  KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
160
165
 
161
- # ../../omlish/logs/base.py
162
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
163
-
164
166
  # ../../omlish/subprocesses/base.py
165
167
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
166
168
 
@@ -3991,124 +3993,6 @@ def typing_annotations_attr() -> str:
3991
3993
  return _TYPING_ANNOTATIONS_ATTR
3992
3994
 
3993
3995
 
3994
- ########################################
3995
- # ../../../omlish/logs/infos.py
3996
-
3997
-
3998
- ##
3999
-
4000
-
4001
- def logging_context_info(cls):
4002
- return cls
4003
-
4004
-
4005
- ##
4006
-
4007
-
4008
- @logging_context_info
4009
- @ta.final
4010
- class LoggingSourceFileInfo(ta.NamedTuple):
4011
- file_name: str
4012
- module: str
4013
-
4014
- @classmethod
4015
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
4016
- if file_path is None:
4017
- return None
4018
-
4019
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
4020
- try:
4021
- file_name = os.path.basename(file_path)
4022
- module = os.path.splitext(file_name)[0]
4023
- except (TypeError, ValueError, AttributeError):
4024
- return None
4025
-
4026
- return cls(
4027
- file_name=file_name,
4028
- module=module,
4029
- )
4030
-
4031
-
4032
- ##
4033
-
4034
-
4035
- @logging_context_info
4036
- @ta.final
4037
- class LoggingThreadInfo(ta.NamedTuple):
4038
- ident: int
4039
- native_id: ta.Optional[int]
4040
- name: str
4041
-
4042
- @classmethod
4043
- def build(cls) -> 'LoggingThreadInfo':
4044
- return cls(
4045
- ident=threading.get_ident(),
4046
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
4047
- name=threading.current_thread().name,
4048
- )
4049
-
4050
-
4051
- ##
4052
-
4053
-
4054
- @logging_context_info
4055
- @ta.final
4056
- class LoggingProcessInfo(ta.NamedTuple):
4057
- pid: int
4058
-
4059
- @classmethod
4060
- def build(cls) -> 'LoggingProcessInfo':
4061
- return cls(
4062
- pid=os.getpid(),
4063
- )
4064
-
4065
-
4066
- ##
4067
-
4068
-
4069
- @logging_context_info
4070
- @ta.final
4071
- class LoggingMultiprocessingInfo(ta.NamedTuple):
4072
- process_name: str
4073
-
4074
- @classmethod
4075
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
4076
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
4077
- if (mp := sys.modules.get('multiprocessing')) is None:
4078
- return None
4079
-
4080
- return cls(
4081
- process_name=mp.current_process().name,
4082
- )
4083
-
4084
-
4085
- ##
4086
-
4087
-
4088
- @logging_context_info
4089
- @ta.final
4090
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
4091
- name: str
4092
-
4093
- @classmethod
4094
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
4095
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
4096
- if (asyncio := sys.modules.get('asyncio')) is None:
4097
- return None
4098
-
4099
- try:
4100
- task = asyncio.current_task()
4101
- except Exception: # noqa
4102
- return None
4103
-
4104
- if task is None:
4105
- return None
4106
-
4107
- return cls(
4108
- name=task.get_name(), # Always non-None
4109
- )
4110
-
4111
-
4112
3996
  ########################################
4113
3997
  # ../../../omlish/logs/levels.py
4114
3998
 
@@ -7623,74 +7507,362 @@ class PredicateTimeout(Timeout):
7623
7507
 
7624
7508
 
7625
7509
  ########################################
7626
- # ../../../omlish/logs/callers.py
7510
+ # ../../../omlish/logs/infos.py
7511
+ """
7512
+ TODO:
7513
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
7514
+ """
7627
7515
 
7628
7516
 
7629
7517
  ##
7630
7518
 
7631
7519
 
7632
- @logging_context_info
7520
+ def logging_context_info(cls):
7521
+ return cls
7522
+
7523
+
7633
7524
  @ta.final
7634
- class LoggingCaller(ta.NamedTuple):
7635
- file_path: str
7636
- line_no: int
7637
- name: str
7638
- stack_info: ta.Optional[str]
7525
+ class LoggingContextInfos:
7526
+ def __new__(cls, *args, **kwargs): # noqa
7527
+ raise TypeError
7639
7528
 
7640
- @classmethod
7641
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
7642
- file_path = os.path.normcase(frame.f_code.co_filename)
7529
+ #
7643
7530
 
7644
- # Yes, really.
7645
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
7646
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
7647
- if 'importlib' in file_path and '_bootstrap' in file_path:
7648
- return True
7531
+ @logging_context_info
7532
+ @ta.final
7533
+ class Name(ta.NamedTuple):
7534
+ name: str
7649
7535
 
7650
- return False
7536
+ @logging_context_info
7537
+ @ta.final
7538
+ class Level(ta.NamedTuple):
7539
+ level: NamedLogLevel
7540
+ name: str
7651
7541
 
7652
- @classmethod
7653
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
7654
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
7542
+ @classmethod
7543
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
7544
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7545
+ return cls(
7546
+ level=nl,
7547
+ name=logging.getLevelName(nl),
7548
+ )
7549
+
7550
+ @logging_context_info
7551
+ @ta.final
7552
+ class Msg(ta.NamedTuple):
7553
+ msg: str
7554
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
7655
7555
 
7656
- while f is not None:
7657
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
7658
- # stack_offset management.
7659
- if hasattr(f, 'f_code'):
7660
- return f
7556
+ @classmethod
7557
+ def build(
7558
+ cls,
7559
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7560
+ *args: ta.Any,
7561
+ ) -> 'LoggingContextInfos.Msg':
7562
+ s: str
7563
+ a: ta.Any
7564
+
7565
+ if callable(msg):
7566
+ if args:
7567
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
7568
+ x = msg()
7569
+ if isinstance(x, str):
7570
+ s, a = x, ()
7571
+ elif isinstance(x, tuple):
7572
+ if x:
7573
+ s, a = x[0], x[1:]
7574
+ else:
7575
+ s, a = '', ()
7576
+ else:
7577
+ raise TypeError(x)
7661
7578
 
7662
- f = f.f_back
7579
+ elif isinstance(msg, tuple):
7580
+ if args:
7581
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
7582
+ if msg:
7583
+ s, a = msg[0], msg[1:]
7584
+ else:
7585
+ s, a = '', ()
7663
7586
 
7664
- return None
7587
+ elif isinstance(msg, str):
7588
+ s, a = msg, args
7589
+
7590
+ else:
7591
+ raise TypeError(msg)
7592
+
7593
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
7594
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
7595
+ a = a[0]
7596
+
7597
+ return cls(
7598
+ msg=s,
7599
+ args=a,
7600
+ )
7601
+
7602
+ @logging_context_info
7603
+ @ta.final
7604
+ class Extra(ta.NamedTuple):
7605
+ extra: ta.Mapping[ta.Any, ta.Any]
7606
+
7607
+ @logging_context_info
7608
+ @ta.final
7609
+ class Time(ta.NamedTuple):
7610
+ ns: int
7611
+ secs: float
7612
+ msecs: float
7613
+ relative_secs: float
7614
+
7615
+ @classmethod
7616
+ def get_std_start_ns(cls) -> int:
7617
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
7618
+
7619
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
7620
+ # an int.
7621
+ #
7622
+ # See:
7623
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7624
+ #
7625
+ if isinstance(x, float):
7626
+ return int(x * 1e9)
7627
+ else:
7628
+ return x
7629
+
7630
+ @classmethod
7631
+ def build(
7632
+ cls,
7633
+ ns: int,
7634
+ *,
7635
+ start_ns: ta.Optional[int] = None,
7636
+ ) -> 'LoggingContextInfos.Time':
7637
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7638
+ secs = ns / 1e9 # ns to float seconds
7639
+
7640
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
7641
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
7642
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
7643
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
7644
+
7645
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
7646
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
7647
+ # ns -> sec conversion can round up, e.g:
7648
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
7649
+ msecs = 0.0
7650
+
7651
+ if start_ns is None:
7652
+ start_ns = cls.get_std_start_ns()
7653
+ relative_secs = (ns - start_ns) / 1e6
7654
+
7655
+ return cls(
7656
+ ns=ns,
7657
+ secs=secs,
7658
+ msecs=msecs,
7659
+ relative_secs=relative_secs,
7660
+ )
7661
+
7662
+ @logging_context_info
7663
+ @ta.final
7664
+ class Exc(ta.NamedTuple):
7665
+ info: LoggingExcInfo
7666
+ info_tuple: LoggingExcInfoTuple
7667
+
7668
+ @classmethod
7669
+ def build(
7670
+ cls,
7671
+ arg: LoggingExcInfoArg = False,
7672
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
7673
+ if arg is True:
7674
+ sys_exc_info = sys.exc_info()
7675
+ if sys_exc_info[0] is not None:
7676
+ arg = sys_exc_info
7677
+ else:
7678
+ arg = None
7679
+ elif arg is False:
7680
+ arg = None
7681
+ if arg is None:
7682
+ return None
7683
+
7684
+ info: LoggingExcInfo = arg
7685
+ if isinstance(info, BaseException):
7686
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
7687
+ else:
7688
+ info_tuple = info
7689
+
7690
+ return cls(
7691
+ info=info,
7692
+ info_tuple=info_tuple,
7693
+ )
7694
+
7695
+ @logging_context_info
7696
+ @ta.final
7697
+ class Caller(ta.NamedTuple):
7698
+ file_path: str
7699
+ line_no: int
7700
+ func_name: str
7701
+ stack_info: ta.Optional[str]
7702
+
7703
+ @classmethod
7704
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
7705
+ file_path = os.path.normcase(frame.f_code.co_filename)
7706
+
7707
+ # Yes, really.
7708
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
7709
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
7710
+ if 'importlib' in file_path and '_bootstrap' in file_path:
7711
+ return True
7712
+
7713
+ return False
7714
+
7715
+ @classmethod
7716
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
7717
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
7718
+
7719
+ while f is not None:
7720
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
7721
+ # manual stack_offset management.
7722
+ if hasattr(f, 'f_code'):
7723
+ return f
7724
+
7725
+ f = f.f_back
7665
7726
 
7666
- @classmethod
7667
- def find(
7668
- cls,
7669
- ofs: int = 0,
7670
- *,
7671
- stack_info: bool = False,
7672
- ) -> ta.Optional['LoggingCaller']:
7673
- if (f := cls.find_frame(ofs + 1)) is None:
7674
7727
  return None
7675
7728
 
7676
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
7677
- sinfo = None
7678
- if stack_info:
7679
- sio = io.StringIO()
7680
- traceback.print_stack(f, file=sio)
7681
- sinfo = sio.getvalue()
7682
- sio.close()
7683
- if sinfo[-1] == '\n':
7684
- sinfo = sinfo[:-1]
7729
+ @classmethod
7730
+ def build(
7731
+ cls,
7732
+ stack_offset: int = 0,
7733
+ *,
7734
+ stack_info: bool = False,
7735
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
7736
+ if (f := cls.find_frame(stack_offset + 1)) is None:
7737
+ return None
7738
+
7739
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
7740
+ sinfo = None
7741
+ if stack_info:
7742
+ sio = io.StringIO()
7743
+ traceback.print_stack(f, file=sio)
7744
+ sinfo = sio.getvalue()
7745
+ sio.close()
7746
+ if sinfo[-1] == '\n':
7747
+ sinfo = sinfo[:-1]
7748
+
7749
+ return cls(
7750
+ file_path=f.f_code.co_filename,
7751
+ line_no=f.f_lineno or 0,
7752
+ func_name=f.f_code.co_name,
7753
+ stack_info=sinfo,
7754
+ )
7755
+
7756
+ @logging_context_info
7757
+ @ta.final
7758
+ class SourceFile(ta.NamedTuple):
7759
+ file_name: str
7760
+ module: str
7685
7761
 
7686
- return cls(
7687
- file_path=f.f_code.co_filename,
7688
- line_no=f.f_lineno or 0,
7689
- name=f.f_code.co_name,
7690
- stack_info=sinfo,
7762
+ @classmethod
7763
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
7764
+ if caller_file_path is None:
7765
+ return None
7766
+
7767
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
7768
+ try:
7769
+ file_name = os.path.basename(caller_file_path)
7770
+ module = os.path.splitext(file_name)[0]
7771
+ except (TypeError, ValueError, AttributeError):
7772
+ return None
7773
+
7774
+ return cls(
7775
+ file_name=file_name,
7776
+ module=module,
7777
+ )
7778
+
7779
+ @logging_context_info
7780
+ @ta.final
7781
+ class Thread(ta.NamedTuple):
7782
+ ident: int
7783
+ native_id: ta.Optional[int]
7784
+ name: str
7785
+
7786
+ @classmethod
7787
+ def build(cls) -> 'LoggingContextInfos.Thread':
7788
+ return cls(
7789
+ ident=threading.get_ident(),
7790
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
7791
+ name=threading.current_thread().name,
7792
+ )
7793
+
7794
+ @logging_context_info
7795
+ @ta.final
7796
+ class Process(ta.NamedTuple):
7797
+ pid: int
7798
+
7799
+ @classmethod
7800
+ def build(cls) -> 'LoggingContextInfos.Process':
7801
+ return cls(
7802
+ pid=os.getpid(),
7803
+ )
7804
+
7805
+ @logging_context_info
7806
+ @ta.final
7807
+ class Multiprocessing(ta.NamedTuple):
7808
+ process_name: str
7809
+
7810
+ @classmethod
7811
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
7812
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
7813
+ if (mp := sys.modules.get('multiprocessing')) is None:
7814
+ return None
7815
+
7816
+ return cls(
7817
+ process_name=mp.current_process().name,
7818
+ )
7819
+
7820
+ @logging_context_info
7821
+ @ta.final
7822
+ class AsyncioTask(ta.NamedTuple):
7823
+ name: str
7824
+
7825
+ @classmethod
7826
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
7827
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
7828
+ if (asyncio := sys.modules.get('asyncio')) is None:
7829
+ return None
7830
+
7831
+ try:
7832
+ task = asyncio.current_task()
7833
+ except Exception: # noqa
7834
+ return None
7835
+
7836
+ if task is None:
7837
+ return None
7838
+
7839
+ return cls(
7840
+ name=task.get_name(), # Always non-None
7841
+ )
7842
+
7843
+
7844
+ ##
7845
+
7846
+
7847
+ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
7848
+ pass
7849
+
7850
+
7851
+ def _check_logging_start_time() -> None:
7852
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
7853
+ import warnings # noqa
7854
+
7855
+ warnings.warn(
7856
+ f'Unexpected logging start time detected: '
7857
+ f'get_std_start_ns={x}, '
7858
+ f'time.time()={t}',
7859
+ UnexpectedLoggingStartTimeWarning,
7691
7860
  )
7692
7861
 
7693
7862
 
7863
+ _check_logging_start_time()
7864
+
7865
+
7694
7866
  ########################################
7695
7867
  # ../../../omlish/logs/protocols.py
7696
7868
 
@@ -7781,115 +7953,30 @@ class JsonLoggingFormatter(logging.Formatter):
7781
7953
 
7782
7954
 
7783
7955
  ########################################
7784
- # ../../../omlish/logs/times.py
7956
+ # ../../../omlish/os/atomics.py
7785
7957
 
7786
7958
 
7787
7959
  ##
7788
7960
 
7789
7961
 
7790
- @logging_context_info
7791
- @ta.final
7792
- class LoggingTimeFields(ta.NamedTuple):
7793
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
7962
+ class AtomicPathSwap(Abstract):
7963
+ def __init__(
7964
+ self,
7965
+ kind: AtomicPathSwapKind,
7966
+ dst_path: str,
7967
+ *,
7968
+ auto_commit: bool = False,
7969
+ ) -> None:
7970
+ super().__init__()
7794
7971
 
7795
- created: float
7796
- msecs: float
7797
- relative_created: float
7972
+ self._kind = kind
7973
+ self._dst_path = dst_path
7974
+ self._auto_commit = auto_commit
7798
7975
 
7799
- @classmethod
7800
- def get_std_start_time_ns(cls) -> int:
7801
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
7976
+ self._state: AtomicPathSwapState = 'open'
7802
7977
 
7803
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
7804
- # int.
7805
- #
7806
- # See:
7807
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7808
- #
7809
- if isinstance(x, float):
7810
- return int(x * 1e9)
7811
- else:
7812
- return x
7813
-
7814
- @classmethod
7815
- def build(
7816
- cls,
7817
- time_ns: int,
7818
- *,
7819
- start_time_ns: ta.Optional[int] = None,
7820
- ) -> 'LoggingTimeFields':
7821
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7822
- created = time_ns / 1e9 # ns to float seconds
7823
-
7824
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
7825
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
7826
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
7827
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
7828
-
7829
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
7830
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
7831
- # ns -> sec conversion can round up, e.g:
7832
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
7833
- msecs = 0.0
7834
-
7835
- if start_time_ns is None:
7836
- start_time_ns = cls.get_std_start_time_ns()
7837
- relative_created = (time_ns - start_time_ns) / 1e6
7838
-
7839
- return cls(
7840
- created=created,
7841
- msecs=msecs,
7842
- relative_created=relative_created,
7843
- )
7844
-
7845
-
7846
- ##
7847
-
7848
-
7849
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
7850
- pass
7851
-
7852
-
7853
- def _check_logging_start_time() -> None:
7854
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
7855
- import warnings # noqa
7856
-
7857
- warnings.warn(
7858
- f'Unexpected logging start time detected: '
7859
- f'get_std_start_time_ns={x}, '
7860
- f'time.time()={t}',
7861
- UnexpectedLoggingStartTimeWarning,
7862
- )
7863
-
7864
-
7865
- _check_logging_start_time()
7866
-
7867
-
7868
- ########################################
7869
- # ../../../omlish/os/atomics.py
7870
-
7871
-
7872
- ##
7873
-
7874
-
7875
- class AtomicPathSwap(Abstract):
7876
- def __init__(
7877
- self,
7878
- kind: AtomicPathSwapKind,
7879
- dst_path: str,
7880
- *,
7881
- auto_commit: bool = False,
7882
- ) -> None:
7883
- super().__init__()
7884
-
7885
- self._kind = kind
7886
- self._dst_path = dst_path
7887
- self._auto_commit = auto_commit
7888
-
7889
- self._state: AtomicPathSwapState = 'open'
7890
-
7891
- def __repr__(self) -> str:
7892
- return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
7978
+ def __repr__(self) -> str:
7979
+ return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
7893
7980
 
7894
7981
  @property
7895
7982
  def kind(self) -> AtomicPathSwapKind:
@@ -10004,68 +10091,36 @@ inj = InjectionApi()
10004
10091
 
10005
10092
 
10006
10093
  class LoggingContext(Abstract):
10007
- @property
10008
- @abc.abstractmethod
10009
- def level(self) -> NamedLogLevel:
10010
- raise NotImplementedError
10011
-
10012
- #
10013
-
10014
- @property
10015
- @abc.abstractmethod
10016
- def time_ns(self) -> int:
10017
- raise NotImplementedError
10018
-
10019
- @property
10020
10094
  @abc.abstractmethod
10021
- def times(self) -> LoggingTimeFields:
10095
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10022
10096
  raise NotImplementedError
10023
10097
 
10024
- #
10098
+ @ta.final
10099
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10100
+ return self.get_info(ty)
10025
10101
 
10026
- @property
10027
- @abc.abstractmethod
10028
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
10029
- raise NotImplementedError
10102
+ @ta.final
10103
+ def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
10104
+ if (info := self.get_info(ty)) is None:
10105
+ raise TypeError(f'LoggingContextInfo absent: {ty}')
10106
+ return info
10030
10107
 
10031
- @property
10032
- @abc.abstractmethod
10033
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
10034
- raise NotImplementedError
10108
+ ##
10035
10109
 
10036
- #
10037
10110
 
10111
+ class CaptureLoggingContext(LoggingContext, Abstract):
10038
10112
  @abc.abstractmethod
10039
- def caller(self) -> ta.Optional[LoggingCaller]:
10040
- raise NotImplementedError
10113
+ def set_basic(
10114
+ self,
10115
+ name: str,
10041
10116
 
10042
- @abc.abstractmethod
10043
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
10117
+ msg: ta.Union[str, tuple, LoggingMsgFn],
10118
+ args: tuple,
10119
+ ) -> 'CaptureLoggingContext':
10044
10120
  raise NotImplementedError
10045
10121
 
10046
10122
  #
10047
10123
 
10048
- @abc.abstractmethod
10049
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
10050
- raise NotImplementedError
10051
-
10052
- @abc.abstractmethod
10053
- def process(self) -> ta.Optional[LoggingProcessInfo]:
10054
- raise NotImplementedError
10055
-
10056
- @abc.abstractmethod
10057
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
10058
- raise NotImplementedError
10059
-
10060
- @abc.abstractmethod
10061
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
10062
- raise NotImplementedError
10063
-
10064
-
10065
- ##
10066
-
10067
-
10068
- class CaptureLoggingContext(LoggingContext, Abstract):
10069
10124
  class AlreadyCapturedError(Exception):
10070
10125
  pass
10071
10126
 
@@ -10096,80 +10151,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
10096
10151
 
10097
10152
  exc_info: LoggingExcInfoArg = False,
10098
10153
 
10099
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
10154
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
10100
10155
  stack_offset: int = 0,
10101
10156
  stack_info: bool = False,
10102
10157
  ) -> None:
10103
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
10104
-
10105
- #
10158
+ # TODO: Name, Msg, Extra
10106
10159
 
10107
10160
  if time_ns is None:
10108
10161
  time_ns = time.time_ns()
10109
- self._time_ns: int = time_ns
10110
-
10111
- #
10112
10162
 
10113
- if exc_info is True:
10114
- sys_exc_info = sys.exc_info()
10115
- if sys_exc_info[0] is not None:
10116
- exc_info = sys_exc_info
10117
- else:
10118
- exc_info = None
10119
- elif exc_info is False:
10120
- exc_info = None
10121
-
10122
- if exc_info is not None:
10123
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
10124
- if isinstance(exc_info, BaseException):
10125
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
10126
- else:
10127
- self._exc_info_tuple = exc_info
10128
-
10129
- #
10163
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
10164
+ self._set_info(
10165
+ LoggingContextInfos.Level.build(level),
10166
+ LoggingContextInfos.Time.build(time_ns),
10167
+ LoggingContextInfos.Exc.build(exc_info),
10168
+ )
10130
10169
 
10131
10170
  if caller is not CaptureLoggingContextImpl.NOT_SET:
10132
- self._caller = caller # type: ignore[assignment]
10171
+ self._infos[LoggingContextInfos.Caller] = caller
10133
10172
  else:
10134
10173
  self._stack_offset = stack_offset
10135
10174
  self._stack_info = stack_info
10136
10175
 
10137
- ##
10138
-
10139
- @property
10140
- def level(self) -> NamedLogLevel:
10141
- return self._level
10142
-
10143
- #
10144
-
10145
- @property
10146
- def time_ns(self) -> int:
10147
- return self._time_ns
10148
-
10149
- _times: LoggingTimeFields
10150
-
10151
- @property
10152
- def times(self) -> LoggingTimeFields:
10153
- try:
10154
- return self._times
10155
- except AttributeError:
10156
- pass
10176
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
10177
+ for info in infos:
10178
+ if info is not None:
10179
+ self._infos[type(info)] = info
10180
+ return self
10157
10181
 
10158
- times = self._times = LoggingTimeFields.build(self.time_ns)
10159
- return times
10182
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10183
+ return self._infos.get(ty)
10160
10184
 
10161
- #
10162
-
10163
- _exc_info: ta.Optional[LoggingExcInfo] = None
10164
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
10185
+ ##
10165
10186
 
10166
- @property
10167
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
10168
- return self._exc_info
10187
+ def set_basic(
10188
+ self,
10189
+ name: str,
10169
10190
 
10170
- @property
10171
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
10172
- return self._exc_info_tuple
10191
+ msg: ta.Union[str, tuple, LoggingMsgFn],
10192
+ args: tuple,
10193
+ ) -> 'CaptureLoggingContextImpl':
10194
+ return self._set_info(
10195
+ LoggingContextInfos.Name(name),
10196
+ LoggingContextInfos.Msg.build(msg, *args),
10197
+ )
10173
10198
 
10174
10199
  ##
10175
10200
 
@@ -10183,74 +10208,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
10183
10208
 
10184
10209
  _has_captured: bool = False
10185
10210
 
10186
- _caller: ta.Optional[LoggingCaller]
10187
- _source_file: ta.Optional[LoggingSourceFileInfo]
10188
-
10189
- _thread: ta.Optional[LoggingThreadInfo]
10190
- _process: ta.Optional[LoggingProcessInfo]
10191
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
10192
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
10193
-
10194
10211
  def capture(self) -> None:
10195
10212
  if self._has_captured:
10196
10213
  raise CaptureLoggingContextImpl.AlreadyCapturedError
10197
10214
  self._has_captured = True
10198
10215
 
10199
- if not hasattr(self, '_caller'):
10200
- self._caller = LoggingCaller.find(
10216
+ if LoggingContextInfos.Caller not in self._infos:
10217
+ self._set_info(LoggingContextInfos.Caller.build(
10201
10218
  self._stack_offset + 1,
10202
10219
  stack_info=self._stack_info,
10203
- )
10204
-
10205
- if (caller := self._caller) is not None:
10206
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
10207
- else:
10208
- self._source_file = None
10209
-
10210
- self._thread = LoggingThreadInfo.build()
10211
- self._process = LoggingProcessInfo.build()
10212
- self._multiprocessing = LoggingMultiprocessingInfo.build()
10213
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
10214
-
10215
- #
10216
-
10217
- def caller(self) -> ta.Optional[LoggingCaller]:
10218
- try:
10219
- return self._caller
10220
- except AttributeError:
10221
- raise CaptureLoggingContext.NotCapturedError from None
10222
-
10223
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
10224
- try:
10225
- return self._source_file
10226
- except AttributeError:
10227
- raise CaptureLoggingContext.NotCapturedError from None
10228
-
10229
- #
10230
-
10231
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
10232
- try:
10233
- return self._thread
10234
- except AttributeError:
10235
- raise CaptureLoggingContext.NotCapturedError from None
10236
-
10237
- def process(self) -> ta.Optional[LoggingProcessInfo]:
10238
- try:
10239
- return self._process
10240
- except AttributeError:
10241
- raise CaptureLoggingContext.NotCapturedError from None
10220
+ ))
10242
10221
 
10243
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
10244
- try:
10245
- return self._multiprocessing
10246
- except AttributeError:
10247
- raise CaptureLoggingContext.NotCapturedError from None
10222
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
10223
+ self._set_info(LoggingContextInfos.SourceFile.build(
10224
+ caller.file_path,
10225
+ ))
10248
10226
 
10249
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
10250
- try:
10251
- return self._asyncio_task
10252
- except AttributeError:
10253
- raise CaptureLoggingContext.NotCapturedError from None
10227
+ self._set_info(
10228
+ LoggingContextInfos.Thread.build(),
10229
+ LoggingContextInfos.Process.build(),
10230
+ LoggingContextInfos.Multiprocessing.build(),
10231
+ LoggingContextInfos.AsyncioTask.build(),
10232
+ )
10254
10233
 
10255
10234
 
10256
10235
  ########################################
@@ -11154,36 +11133,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
11154
11133
 
11155
11134
  ##
11156
11135
 
11157
- @classmethod
11158
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
11159
- if callable(msg):
11160
- if args:
11161
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
11162
- x = msg()
11163
- if isinstance(x, str):
11164
- return x, ()
11165
- elif isinstance(x, tuple):
11166
- if x:
11167
- return x[0], x[1:]
11168
- else:
11169
- return '', ()
11170
- else:
11171
- raise TypeError(x)
11172
-
11173
- elif isinstance(msg, tuple):
11174
- if args:
11175
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
11176
- if msg:
11177
- return msg[0], msg[1:]
11178
- else:
11179
- return '', ()
11180
-
11181
- elif isinstance(msg, str):
11182
- return msg, args
11183
-
11184
- else:
11185
- raise TypeError(msg)
11186
-
11187
11136
  @abc.abstractmethod
11188
11137
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
11189
11138
  raise NotImplementedError
@@ -11224,137 +11173,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
11224
11173
 
11225
11174
  ########################################
11226
11175
  # ../../../omlish/logs/std/records.py
11176
+ """
11177
+ TODO:
11178
+ - TypedDict?
11179
+ """
11227
11180
 
11228
11181
 
11229
11182
  ##
11230
11183
 
11231
11184
 
11232
- # Ref:
11233
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
11234
- #
11235
- # LogRecord:
11236
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
11237
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
11238
- #
11239
- # LogRecord.__init__ args:
11240
- # - name: str
11241
- # - level: int
11242
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
11243
- # - lineno: int - May be 0.
11244
- # - msg: str
11245
- # - args: tuple | dict | 1-tuple[dict]
11246
- # - exc_info: LoggingExcInfoTuple | None
11247
- # - func: str | None = None -> funcName
11248
- # - sinfo: str | None = None -> stack_info
11249
- #
11250
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11251
- # Name of the logger used to log the call. Unmodified by ctor.
11252
- name=str,
11185
+ class LoggingContextInfoRecordAdapters:
11186
+ # Ref:
11187
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
11188
+ #
11189
+ # LogRecord:
11190
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
11191
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
11192
+ #
11253
11193
 
11254
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
11255
- # (see Using arbitrary objects as messages). Unmodified by ctor.
11256
- msg=str,
11194
+ def __new__(cls, *args, **kwargs): # noqa
11195
+ raise TypeError
11257
11196
 
11258
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
11259
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
11260
- # the mapping, but is otherwise unmodified.
11261
- args=ta.Union[tuple, dict],
11197
+ class Adapter(Abstract, ta.Generic[T]):
11198
+ @property
11199
+ @abc.abstractmethod
11200
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
11201
+ raise NotImplementedError
11262
11202
 
11263
- #
11203
+ #
11264
11204
 
11265
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
11266
- # `getLevelName(level)`.
11267
- levelname=str,
11205
+ @ta.final
11206
+ class NOT_SET: # noqa
11207
+ def __new__(cls, *args, **kwargs): # noqa
11208
+ raise TypeError
11268
11209
 
11269
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
11270
- levelno=int,
11210
+ class RecordAttr(ta.NamedTuple):
11211
+ name: str
11212
+ type: ta.Any
11213
+ default: ta.Any
11271
11214
 
11272
- #
11215
+ # @abc.abstractmethod
11216
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
11273
11217
 
11274
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
11275
- # to "(unknown file)" by Logger.findCaller / Logger._log.
11276
- pathname=str,
11218
+ @property
11219
+ @abc.abstractmethod
11220
+ def _record_attrs(self) -> ta.Union[
11221
+ ta.Mapping[str, ta.Any],
11222
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
11223
+ ]:
11224
+ raise NotImplementedError
11277
11225
 
11278
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
11279
- filename=str,
11226
+ #
11280
11227
 
11281
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
11282
- # "Unknown module".
11283
- module=str,
11228
+ @abc.abstractmethod
11229
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11230
+ raise NotImplementedError
11284
11231
 
11285
- #
11232
+ #
11286
11233
 
11287
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
11288
- exc_info=ta.Optional[LoggingExcInfoTuple],
11234
+ @abc.abstractmethod
11235
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
11236
+ raise NotImplementedError
11289
11237
 
11290
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
11291
- exc_text=ta.Optional[str],
11238
+ #
11292
11239
 
11293
- #
11240
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11241
+ super().__init_subclass__(**kwargs)
11294
11242
 
11295
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
11296
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
11297
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
11298
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
11299
- stack_info=ta.Optional[str],
11243
+ if Abstract in cls.__bases__:
11244
+ return
11300
11245
 
11301
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
11302
- # Logger.findCaller / Logger._log.
11303
- lineno=int,
11246
+ if 'record_attrs' in cls.__dict__:
11247
+ raise TypeError(cls)
11248
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
11249
+ raise TypeError(ra)
11250
+
11251
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
11252
+ for n, v in ra.items():
11253
+ if not n or not isinstance(n, str) or n in rd:
11254
+ raise AttributeError(n)
11255
+ if isinstance(v, tuple):
11256
+ t, d = v
11257
+ else:
11258
+ t, d = v, cls.NOT_SET
11259
+ rd[n] = cls.RecordAttr(
11260
+ name=n,
11261
+ type=t,
11262
+ default=d,
11263
+ )
11264
+ cls.record_attrs = rd
11304
11265
 
11305
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
11306
- # "(unknown function)" by Logger.findCaller / Logger._log.
11307
- funcName=str,
11266
+ class RequiredAdapter(Adapter[T], Abstract):
11267
+ @property
11268
+ @abc.abstractmethod
11269
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
11270
+ raise NotImplementedError
11308
11271
 
11309
- #
11272
+ #
11310
11273
 
11311
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
11312
- #
11313
- # See:
11314
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
11315
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
11316
- #
11317
- created=float,
11274
+ @ta.final
11275
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11276
+ if (info := ctx.get_info(self.info_cls)) is not None:
11277
+ return self._info_to_record(info)
11278
+ else:
11279
+ raise TypeError # FIXME: fallback?
11318
11280
 
11319
- # Millisecond portion of the time when the LogRecord was created.
11320
- msecs=float,
11281
+ @abc.abstractmethod
11282
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
11283
+ raise NotImplementedError
11321
11284
 
11322
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
11323
- relativeCreated=float,
11285
+ #
11324
11286
 
11325
- #
11287
+ @abc.abstractmethod
11288
+ def record_to_info(self, rec: logging.LogRecord) -> T:
11289
+ raise NotImplementedError
11326
11290
 
11327
- # Thread ID if available, and `logging.logThreads` is truthy.
11328
- thread=ta.Optional[int],
11291
+ #
11329
11292
 
11330
- # Thread name if available, and `logging.logThreads` is truthy.
11331
- threadName=ta.Optional[str],
11293
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11294
+ super().__init_subclass__(**kwargs)
11332
11295
 
11333
- #
11296
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
11297
+ raise TypeError(cls.record_attrs)
11298
+
11299
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
11300
+ @property
11301
+ @abc.abstractmethod
11302
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
11303
+ raise NotImplementedError
11304
+
11305
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
11306
+
11307
+ #
11308
+
11309
+ @ta.final
11310
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11311
+ if (info := ctx.get_info(self.info_cls)) is not None:
11312
+ return self._info_to_record(info)
11313
+ else:
11314
+ return self.record_defaults
11315
+
11316
+ @abc.abstractmethod
11317
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
11318
+ raise NotImplementedError
11319
+
11320
+ #
11321
+
11322
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11323
+ super().__init_subclass__(**kwargs)
11324
+
11325
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
11326
+ if any(d is cls.NOT_SET for d in dd.values()):
11327
+ raise TypeError(cls.record_attrs)
11328
+ cls.record_defaults = dd
11334
11329
 
11335
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
11336
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
11337
- # as 'MainProcess'.
11338
11330
  #
11339
- # As noted by stdlib:
11331
+
11332
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
11333
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
11334
+
11335
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11336
+ # Name of the logger used to log the call. Unmodified by ctor.
11337
+ name=str,
11338
+ )
11339
+
11340
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
11341
+ return dict(
11342
+ name=info.name,
11343
+ )
11344
+
11345
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
11346
+ return LoggingContextInfos.Name(
11347
+ name=rec.name,
11348
+ )
11349
+
11350
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
11351
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
11352
+
11353
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11354
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
11355
+ # `getLevelName(level)`.
11356
+ levelname=str,
11357
+
11358
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
11359
+ levelno=int,
11360
+ )
11361
+
11362
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
11363
+ return dict(
11364
+ levelname=info.name,
11365
+ levelno=int(info.level),
11366
+ )
11367
+
11368
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
11369
+ return LoggingContextInfos.Level.build(rec.levelno)
11370
+
11371
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
11372
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
11373
+
11374
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11375
+ # The format string passed in the original logging call. Merged with args to produce message, or an
11376
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
11377
+ msg=str,
11378
+
11379
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
11380
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
11381
+ # Mapping into just the mapping, but is otherwise unmodified.
11382
+ args=ta.Union[tuple, dict, None],
11383
+ )
11384
+
11385
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
11386
+ return dict(
11387
+ msg=info.msg,
11388
+ args=info.args,
11389
+ )
11390
+
11391
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
11392
+ return LoggingContextInfos.Msg(
11393
+ msg=rec.msg,
11394
+ args=rec.args,
11395
+ )
11396
+
11397
+ # FIXME: handled specially - all unknown attrs on LogRecord
11398
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
11399
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
11340
11400
  #
11341
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
11342
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
11401
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
11402
+ # # FIXME:
11403
+ # # if extra is not None:
11404
+ # # for key in extra:
11405
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
11406
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
11407
+ # # rv.__dict__[key] = extra[key]
11408
+ # return dict()
11343
11409
  #
11344
- processName=ta.Optional[str],
11410
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
11411
+ # return None
11345
11412
 
11346
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
11347
- # None.
11348
- process=ta.Optional[int],
11413
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
11414
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
11349
11415
 
11350
- #
11416
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11417
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
11418
+ # `time.time()`.
11419
+ #
11420
+ # See:
11421
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
11422
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
11423
+ #
11424
+ created=float,
11351
11425
 
11352
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
11353
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
11354
- taskName=ta.Optional[str],
11355
- )
11426
+ # Millisecond portion of the time when the LogRecord was created.
11427
+ msecs=float,
11428
+
11429
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
11430
+ relativeCreated=float,
11431
+ )
11432
+
11433
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
11434
+ return dict(
11435
+ created=info.secs,
11436
+ msecs=info.msecs,
11437
+ relativeCreated=info.relative_secs,
11438
+ )
11439
+
11440
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
11441
+ return LoggingContextInfos.Time.build(
11442
+ int(rec.created * 1e9),
11443
+ )
11444
+
11445
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
11446
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
11447
+
11448
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11449
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
11450
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
11356
11451
 
11357
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
11452
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
11453
+ exc_text=(ta.Optional[str], None),
11454
+ )
11455
+
11456
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
11457
+ return dict(
11458
+ exc_info=info.info_tuple,
11459
+ exc_text=None,
11460
+ )
11461
+
11462
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
11463
+ # FIXME:
11464
+ # error: Argument 1 to "build" of "Exc" has incompatible type
11465
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
11466
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
11467
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
11468
+
11469
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
11470
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
11471
+
11472
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
11473
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
11474
+
11475
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
11476
+
11477
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11478
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
11479
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
11480
+ pathname=(str, _UNKNOWN_PATH_NAME),
11481
+
11482
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
11483
+ # y Logger.findCaller / Logger._log.
11484
+ lineno=(int, 0),
11485
+
11486
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
11487
+ # "(unknown function)" by Logger.findCaller / Logger._log.
11488
+ funcName=(str, _UNKNOWN_FUNC_NAME),
11489
+
11490
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
11491
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
11492
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
11493
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
11494
+ # stripped of exactly one trailing `\n` if present.
11495
+ stack_info=(ta.Optional[str], None),
11496
+ )
11497
+
11498
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
11499
+ if (sinfo := caller.stack_info) is not None:
11500
+ stack_info: ta.Optional[str] = '\n'.join([
11501
+ self._STACK_INFO_PREFIX,
11502
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
11503
+ ])
11504
+ else:
11505
+ stack_info = None
11506
+
11507
+ return dict(
11508
+ pathname=caller.file_path,
11509
+
11510
+ lineno=caller.line_no,
11511
+ funcName=caller.func_name,
11512
+
11513
+ stack_info=stack_info,
11514
+ )
11515
+
11516
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
11517
+ # FIXME: piecemeal?
11518
+ # FIXME: strip _STACK_INFO_PREFIX
11519
+ raise NotImplementedError
11520
+
11521
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
11522
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
11523
+
11524
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11525
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
11526
+ # pathname.
11527
+ filename=str,
11528
+
11529
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
11530
+ # "Unknown module".
11531
+ module=str,
11532
+ )
11533
+
11534
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
11535
+
11536
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11537
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
11538
+ return dict(
11539
+ filename=info.file_name,
11540
+ module=info.module,
11541
+ )
11542
+
11543
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
11544
+ return dict(
11545
+ filename=caller.file_path,
11546
+ module=self._UNKNOWN_MODULE,
11547
+ )
11548
+
11549
+ return dict(
11550
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
11551
+ module=self._UNKNOWN_MODULE,
11552
+ )
11553
+
11554
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
11555
+ if not (
11556
+ rec.module is None or
11557
+ rec.module == self._UNKNOWN_MODULE
11558
+ ):
11559
+ return LoggingContextInfos.SourceFile(
11560
+ file_name=rec.filename,
11561
+ module=rec.module, # FIXME: piecemeal?
11562
+ )
11563
+
11564
+ return None
11565
+
11566
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
11567
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
11568
+
11569
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11570
+ # Thread ID if available, and `logging.logThreads` is truthy.
11571
+ thread=(ta.Optional[int], None),
11572
+
11573
+ # Thread name if available, and `logging.logThreads` is truthy.
11574
+ threadName=(ta.Optional[str], None),
11575
+ )
11576
+
11577
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
11578
+ if logging.logThreads:
11579
+ return dict(
11580
+ thread=info.ident,
11581
+ threadName=info.name,
11582
+ )
11583
+
11584
+ return self.record_defaults
11585
+
11586
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
11587
+ if (
11588
+ (ident := rec.thread) is not None and
11589
+ (name := rec.threadName) is not None
11590
+ ):
11591
+ return LoggingContextInfos.Thread(
11592
+ ident=ident,
11593
+ native_id=None,
11594
+ name=name,
11595
+ )
11596
+
11597
+ return None
11598
+
11599
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
11600
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
11601
+
11602
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11603
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
11604
+ # otherwise None.
11605
+ process=(ta.Optional[int], None),
11606
+ )
11607
+
11608
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
11609
+ if logging.logProcesses:
11610
+ return dict(
11611
+ process=info.pid,
11612
+ )
11613
+
11614
+ return self.record_defaults
11615
+
11616
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
11617
+ if (
11618
+ (pid := rec.process) is not None
11619
+ ):
11620
+ return LoggingContextInfos.Process(
11621
+ pid=pid,
11622
+ )
11623
+
11624
+ return None
11625
+
11626
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
11627
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
11628
+
11629
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11630
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
11631
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
11632
+ # remains as 'MainProcess'.
11633
+ #
11634
+ # As noted by stdlib:
11635
+ #
11636
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
11637
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
11638
+ #
11639
+ processName=(ta.Optional[str], None),
11640
+ )
11641
+
11642
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
11643
+ if logging.logMultiprocessing:
11644
+ return dict(
11645
+ processName=info.process_name,
11646
+ )
11647
+
11648
+ return self.record_defaults
11649
+
11650
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
11651
+ if (
11652
+ (process_name := rec.processName) is not None
11653
+ ):
11654
+ return LoggingContextInfos.Multiprocessing(
11655
+ process_name=process_name,
11656
+ )
11657
+
11658
+ return None
11659
+
11660
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
11661
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
11662
+
11663
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
11664
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
11665
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
11666
+ taskName=(ta.Optional[str], None),
11667
+ )
11668
+
11669
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
11670
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
11671
+ return dict(
11672
+ taskName=info.name,
11673
+ )
11674
+
11675
+ return self.record_defaults
11676
+
11677
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
11678
+ if (
11679
+ (name := getattr(rec, 'taskName', None)) is not None
11680
+ ):
11681
+ return LoggingContextInfos.AsyncioTask(
11682
+ name=name,
11683
+ )
11684
+
11685
+ return None
11686
+
11687
+
11688
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
11689
+ LoggingContextInfoRecordAdapters.Name(),
11690
+ LoggingContextInfoRecordAdapters.Level(),
11691
+ LoggingContextInfoRecordAdapters.Msg(),
11692
+ LoggingContextInfoRecordAdapters.Time(),
11693
+ LoggingContextInfoRecordAdapters.Exc(),
11694
+ LoggingContextInfoRecordAdapters.Caller(),
11695
+ LoggingContextInfoRecordAdapters.SourceFile(),
11696
+ LoggingContextInfoRecordAdapters.Thread(),
11697
+ LoggingContextInfoRecordAdapters.Process(),
11698
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
11699
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
11700
+ ]
11701
+
11702
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
11703
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
11704
+ }
11705
+
11706
+
11707
+ ##
11708
+
11709
+
11710
+ KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
11711
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
11712
+ )
11358
11713
 
11359
11714
 
11360
11715
  # Formatter:
@@ -11378,14 +11733,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11378
11733
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
11379
11734
 
11380
11735
 
11381
- ##
11382
-
11383
-
11384
11736
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
11385
11737
  pass
11386
11738
 
11387
11739
 
11388
11740
  def _check_std_logging_record_attrs() -> None:
11741
+ if (
11742
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
11743
+ len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
11744
+ ):
11745
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
11746
+
11389
11747
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
11390
11748
 
11391
11749
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -11404,116 +11762,20 @@ _check_std_logging_record_attrs()
11404
11762
 
11405
11763
 
11406
11764
  class LoggingContextLogRecord(logging.LogRecord):
11407
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
11408
-
11409
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
11410
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
11411
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
11412
-
11413
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
11414
-
11415
- def __init__( # noqa
11416
- self,
11417
- # name,
11418
- # level,
11419
- # pathname,
11420
- # lineno,
11421
- # msg,
11422
- # args,
11423
- # exc_info,
11424
- # func=None,
11425
- # sinfo=None,
11426
- # **kwargs,
11427
- *,
11428
- name: str,
11429
- msg: str,
11430
- args: ta.Union[tuple, dict],
11431
-
11432
- _logging_context: LoggingContext,
11433
- ) -> None:
11434
- ctx = _logging_context
11435
-
11436
- self.name: str = name
11437
-
11438
- self.msg: str = msg
11439
-
11440
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
11441
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
11442
- args = args[0] # type: ignore[assignment]
11443
- self.args: ta.Union[tuple, dict] = args
11444
-
11445
- self.levelname: str = logging.getLevelName(ctx.level)
11446
- self.levelno: int = ctx.level
11447
-
11448
- if (caller := ctx.caller()) is not None:
11449
- self.pathname: str = caller.file_path
11450
- else:
11451
- self.pathname = self._UNKNOWN_PATH_NAME
11452
-
11453
- if (src_file := ctx.source_file()) is not None:
11454
- self.filename: str = src_file.file_name
11455
- self.module: str = src_file.module
11456
- else:
11457
- self.filename = self.pathname
11458
- self.module = self._UNKNOWN_MODULE
11459
-
11460
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
11461
- self.exc_text: ta.Optional[str] = None
11462
-
11463
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
11464
- if caller is not None:
11465
- if (sinfo := caller.stack_info) is not None:
11466
- self.stack_info: ta.Optional[str] = '\n'.join([
11467
- self._STACK_INFO_PREFIX,
11468
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
11469
- ])
11470
- else:
11471
- self.stack_info = None
11472
-
11473
- self.lineno: int = caller.line_no
11474
- self.funcName: str = caller.name
11475
-
11476
- else:
11477
- self.stack_info = None
11478
-
11479
- self.lineno = 0
11480
- self.funcName = self._UNKNOWN_FUNC_NAME
11481
-
11482
- times = ctx.times
11483
- self.created: float = times.created
11484
- self.msecs: float = times.msecs
11485
- self.relativeCreated: float = times.relative_created
11486
-
11487
- if logging.logThreads:
11488
- thread = check.not_none(ctx.thread())
11489
- self.thread: ta.Optional[int] = thread.ident
11490
- self.threadName: ta.Optional[str] = thread.name
11491
- else:
11492
- self.thread = None
11493
- self.threadName = None
11494
-
11495
- if logging.logProcesses:
11496
- process = check.not_none(ctx.process())
11497
- self.process: ta.Optional[int] = process.pid
11498
- else:
11499
- self.process = None
11500
-
11501
- if logging.logMultiprocessing:
11502
- if (mp := ctx.multiprocessing()) is not None:
11503
- self.processName: ta.Optional[str] = mp.process_name
11504
- else:
11505
- self.processName = None
11506
- else:
11507
- self.processName = None
11508
-
11509
- # Absent <3.12
11510
- if getattr(logging, 'logAsyncioTasks', None):
11511
- if (at := ctx.asyncio_task()) is not None:
11512
- self.taskName: ta.Optional[str] = at.name
11513
- else:
11514
- self.taskName = None
11515
- else:
11516
- self.taskName = None
11765
+ # LogRecord.__init__ args:
11766
+ # - name: str
11767
+ # - level: int
11768
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
11769
+ # - lineno: int - May be 0.
11770
+ # - msg: str
11771
+ # - args: tuple | dict | 1-tuple[dict]
11772
+ # - exc_info: LoggingExcInfoTuple | None
11773
+ # - func: str | None = None -> funcName
11774
+ # - sinfo: str | None = None -> stack_info
11775
+
11776
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
11777
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
11778
+ self.__dict__.update(ad.context_to_record(_logging_context))
11517
11779
 
11518
11780
 
11519
11781
  ########################################
@@ -12152,21 +12414,20 @@ class StdLogger(Logger):
12152
12414
  return self._std.getEffectiveLevel()
12153
12415
 
12154
12416
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
12155
- if not self.is_enabled_for(ctx.level):
12417
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
12156
12418
  return
12157
12419
 
12158
- ctx.capture()
12159
-
12160
- ms, args = self._prepare_msg_args(msg, *args)
12161
-
12162
- rec = LoggingContextLogRecord(
12420
+ ctx.set_basic(
12163
12421
  name=self._std.name,
12164
- msg=ms,
12165
- args=args,
12166
12422
 
12167
- _logging_context=ctx,
12423
+ msg=msg,
12424
+ args=args,
12168
12425
  )
12169
12426
 
12427
+ ctx.capture()
12428
+
12429
+ rec = LoggingContextLogRecord(_logging_context=ctx)
12430
+
12170
12431
  self._std.handle(rec)
12171
12432
 
12172
12433