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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,46 @@ inj = InjectionApi()
10004
10091
 
10005
10092
 
10006
10093
  class LoggingContext(Abstract):
10007
- @property
10008
10094
  @abc.abstractmethod
10009
- def level(self) -> NamedLogLevel:
10095
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10010
10096
  raise NotImplementedError
10011
10097
 
10012
- #
10013
-
10014
- @property
10015
- @abc.abstractmethod
10016
- def time_ns(self) -> int:
10017
- raise NotImplementedError
10018
-
10019
- @property
10020
- @abc.abstractmethod
10021
- def times(self) -> LoggingTimeFields:
10022
- raise NotImplementedError
10098
+ @ta.final
10099
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10100
+ return self.get_info(ty)
10023
10101
 
10024
- #
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
10025
10107
 
10026
- @property
10027
- @abc.abstractmethod
10028
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
10029
- raise NotImplementedError
10030
10108
 
10031
- @property
10032
- @abc.abstractmethod
10033
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
10034
- raise NotImplementedError
10109
+ @ta.final
10110
+ class SimpleLoggingContext(LoggingContext):
10111
+ def __init__(self, *infos: LoggingContextInfo) -> None:
10112
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {type(i): i for i in infos}
10035
10113
 
10036
- #
10114
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10115
+ return self._infos.get(ty)
10037
10116
 
10038
- @abc.abstractmethod
10039
- def caller(self) -> ta.Optional[LoggingCaller]:
10040
- raise NotImplementedError
10041
10117
 
10042
- @abc.abstractmethod
10043
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
10044
- raise NotImplementedError
10118
+ ##
10045
10119
 
10046
- #
10047
-
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
10120
 
10121
+ class CaptureLoggingContext(LoggingContext, Abstract):
10056
10122
  @abc.abstractmethod
10057
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
10058
- raise NotImplementedError
10123
+ def set_basic(
10124
+ self,
10125
+ name: str,
10059
10126
 
10060
- @abc.abstractmethod
10061
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
10127
+ msg: ta.Union[str, tuple, LoggingMsgFn],
10128
+ args: tuple,
10129
+ ) -> 'CaptureLoggingContext':
10062
10130
  raise NotImplementedError
10063
10131
 
10132
+ #
10064
10133
 
10065
- ##
10066
-
10067
-
10068
- class CaptureLoggingContext(LoggingContext, Abstract):
10069
10134
  class AlreadyCapturedError(Exception):
10070
10135
  pass
10071
10136
 
@@ -10096,80 +10161,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
10096
10161
 
10097
10162
  exc_info: LoggingExcInfoArg = False,
10098
10163
 
10099
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
10164
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
10100
10165
  stack_offset: int = 0,
10101
10166
  stack_info: bool = False,
10102
10167
  ) -> None:
10103
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
10104
-
10105
- #
10168
+ # TODO: Name, Msg, Extra
10106
10169
 
10107
10170
  if time_ns is None:
10108
10171
  time_ns = time.time_ns()
10109
- self._time_ns: int = time_ns
10110
-
10111
- #
10112
-
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
10172
 
10129
- #
10173
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
10174
+ self._set_info(
10175
+ LoggingContextInfos.Level.build(level),
10176
+ LoggingContextInfos.Time.build(time_ns),
10177
+ LoggingContextInfos.Exc.build(exc_info),
10178
+ )
10130
10179
 
10131
10180
  if caller is not CaptureLoggingContextImpl.NOT_SET:
10132
- self._caller = caller # type: ignore[assignment]
10181
+ self._infos[LoggingContextInfos.Caller] = caller
10133
10182
  else:
10134
10183
  self._stack_offset = stack_offset
10135
10184
  self._stack_info = stack_info
10136
10185
 
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
10157
-
10158
- times = self._times = LoggingTimeFields.build(self.time_ns)
10159
- return times
10186
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
10187
+ for info in infos:
10188
+ if info is not None:
10189
+ self._infos[type(info)] = info
10190
+ return self
10160
10191
 
10161
- #
10192
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10193
+ return self._infos.get(ty)
10162
10194
 
10163
- _exc_info: ta.Optional[LoggingExcInfo] = None
10164
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
10195
+ ##
10165
10196
 
10166
- @property
10167
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
10168
- return self._exc_info
10197
+ def set_basic(
10198
+ self,
10199
+ name: str,
10169
10200
 
10170
- @property
10171
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
10172
- return self._exc_info_tuple
10201
+ msg: ta.Union[str, tuple, LoggingMsgFn],
10202
+ args: tuple,
10203
+ ) -> 'CaptureLoggingContextImpl':
10204
+ return self._set_info(
10205
+ LoggingContextInfos.Name(name),
10206
+ LoggingContextInfos.Msg.build(msg, *args),
10207
+ )
10173
10208
 
10174
10209
  ##
10175
10210
 
@@ -10183,74 +10218,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
10183
10218
 
10184
10219
  _has_captured: bool = False
10185
10220
 
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
10221
  def capture(self) -> None:
10195
10222
  if self._has_captured:
10196
10223
  raise CaptureLoggingContextImpl.AlreadyCapturedError
10197
10224
  self._has_captured = True
10198
10225
 
10199
- if not hasattr(self, '_caller'):
10200
- self._caller = LoggingCaller.find(
10226
+ if LoggingContextInfos.Caller not in self._infos:
10227
+ self._set_info(LoggingContextInfos.Caller.build(
10201
10228
  self._stack_offset + 1,
10202
10229
  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
10230
+ ))
10242
10231
 
10243
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
10244
- try:
10245
- return self._multiprocessing
10246
- except AttributeError:
10247
- raise CaptureLoggingContext.NotCapturedError from None
10232
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
10233
+ self._set_info(LoggingContextInfos.SourceFile.build(
10234
+ caller.file_path,
10235
+ ))
10248
10236
 
10249
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
10250
- try:
10251
- return self._asyncio_task
10252
- except AttributeError:
10253
- raise CaptureLoggingContext.NotCapturedError from None
10237
+ self._set_info(
10238
+ LoggingContextInfos.Thread.build(),
10239
+ LoggingContextInfos.Process.build(),
10240
+ LoggingContextInfos.Multiprocessing.build(),
10241
+ LoggingContextInfos.AsyncioTask.build(),
10242
+ )
10254
10243
 
10255
10244
 
10256
10245
  ########################################
@@ -10327,10 +10316,14 @@ def _locking_logging_module_lock() -> ta.Iterator[None]:
10327
10316
  def configure_standard_logging(
10328
10317
  level: ta.Union[int, str] = logging.INFO,
10329
10318
  *,
10330
- json: bool = False,
10331
10319
  target: ta.Optional[logging.Logger] = None,
10320
+
10332
10321
  force: bool = False,
10322
+
10333
10323
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
10324
+
10325
+ formatter: ta.Optional[logging.Formatter] = None, # noqa
10326
+ json: bool = False,
10334
10327
  ) -> ta.Optional[StandardConfiguredLoggingHandler]:
10335
10328
  with _locking_logging_module_lock():
10336
10329
  if target is None:
@@ -10351,11 +10344,11 @@ def configure_standard_logging(
10351
10344
 
10352
10345
  #
10353
10346
 
10354
- formatter: logging.Formatter
10355
- if json:
10356
- formatter = JsonLoggingFormatter()
10357
- else:
10358
- formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
10347
+ if formatter is None:
10348
+ if json:
10349
+ formatter = JsonLoggingFormatter()
10350
+ else:
10351
+ formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS)) # noqa
10359
10352
  handler.setFormatter(formatter)
10360
10353
 
10361
10354
  #
@@ -11154,36 +11147,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
11154
11147
 
11155
11148
  ##
11156
11149
 
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
11150
  @abc.abstractmethod
11188
11151
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
11189
11152
  raise NotImplementedError
@@ -11224,144 +11187,560 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
11224
11187
 
11225
11188
  ########################################
11226
11189
  # ../../../omlish/logs/std/records.py
11190
+ """
11191
+ TODO:
11192
+ - TypedDict?
11193
+ """
11227
11194
 
11228
11195
 
11229
11196
  ##
11230
11197
 
11231
11198
 
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,
11199
+ class LoggingContextInfoRecordAdapters:
11200
+ # Ref:
11201
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
11202
+ #
11203
+ # LogRecord:
11204
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
11205
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
11206
+ #
11253
11207
 
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,
11208
+ def __new__(cls, *args, **kwargs): # noqa
11209
+ raise TypeError
11257
11210
 
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],
11211
+ class Adapter(Abstract, ta.Generic[T]):
11212
+ @property
11213
+ @abc.abstractmethod
11214
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
11215
+ raise NotImplementedError
11262
11216
 
11263
- #
11217
+ #
11264
11218
 
11265
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
11266
- # `getLevelName(level)`.
11267
- levelname=str,
11219
+ @ta.final
11220
+ class NOT_SET: # noqa
11221
+ def __new__(cls, *args, **kwargs): # noqa
11222
+ raise TypeError
11268
11223
 
11269
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
11270
- levelno=int,
11224
+ class RecordAttr(ta.NamedTuple):
11225
+ name: str
11226
+ type: ta.Any
11227
+ default: ta.Any
11271
11228
 
11272
- #
11229
+ # @abc.abstractmethod
11230
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
11273
11231
 
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,
11232
+ @property
11233
+ @abc.abstractmethod
11234
+ def _record_attrs(self) -> ta.Union[
11235
+ ta.Mapping[str, ta.Any],
11236
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
11237
+ ]:
11238
+ raise NotImplementedError
11277
11239
 
11278
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
11279
- filename=str,
11240
+ #
11280
11241
 
11281
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
11282
- # "Unknown module".
11283
- module=str,
11242
+ @abc.abstractmethod
11243
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11244
+ raise NotImplementedError
11284
11245
 
11285
- #
11246
+ #
11286
11247
 
11287
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
11288
- exc_info=ta.Optional[LoggingExcInfoTuple],
11248
+ @abc.abstractmethod
11249
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
11250
+ raise NotImplementedError
11289
11251
 
11290
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
11291
- exc_text=ta.Optional[str],
11252
+ #
11292
11253
 
11293
- #
11254
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11255
+ super().__init_subclass__(**kwargs)
11294
11256
 
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],
11257
+ if Abstract in cls.__bases__:
11258
+ return
11300
11259
 
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,
11260
+ if 'record_attrs' in cls.__dict__:
11261
+ raise TypeError(cls)
11262
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
11263
+ raise TypeError(ra)
11264
+
11265
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
11266
+ for n, v in ra.items():
11267
+ if not n or not isinstance(n, str) or n in rd:
11268
+ raise AttributeError(n)
11269
+ if isinstance(v, tuple):
11270
+ t, d = v
11271
+ else:
11272
+ t, d = v, cls.NOT_SET
11273
+ rd[n] = cls.RecordAttr(
11274
+ name=n,
11275
+ type=t,
11276
+ default=d,
11277
+ )
11278
+ cls.record_attrs = rd
11304
11279
 
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,
11280
+ class RequiredAdapter(Adapter[T], Abstract):
11281
+ @property
11282
+ @abc.abstractmethod
11283
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
11284
+ raise NotImplementedError
11308
11285
 
11309
- #
11286
+ #
11310
11287
 
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,
11288
+ @ta.final
11289
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11290
+ if (info := ctx.get_info(self.info_cls)) is not None:
11291
+ return self._info_to_record(info)
11292
+ else:
11293
+ raise TypeError # FIXME: fallback?
11318
11294
 
11319
- # Millisecond portion of the time when the LogRecord was created.
11320
- msecs=float,
11295
+ @abc.abstractmethod
11296
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
11297
+ raise NotImplementedError
11321
11298
 
11322
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
11323
- relativeCreated=float,
11299
+ #
11324
11300
 
11325
- #
11301
+ @abc.abstractmethod
11302
+ def record_to_info(self, rec: logging.LogRecord) -> T:
11303
+ raise NotImplementedError
11326
11304
 
11327
- # Thread ID if available, and `logging.logThreads` is truthy.
11328
- thread=ta.Optional[int],
11305
+ #
11329
11306
 
11330
- # Thread name if available, and `logging.logThreads` is truthy.
11331
- threadName=ta.Optional[str],
11307
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11308
+ super().__init_subclass__(**kwargs)
11332
11309
 
11333
- #
11310
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
11311
+ raise TypeError(cls.record_attrs)
11312
+
11313
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
11314
+ @property
11315
+ @abc.abstractmethod
11316
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
11317
+ raise NotImplementedError
11318
+
11319
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
11320
+
11321
+ #
11322
+
11323
+ @ta.final
11324
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11325
+ if (info := ctx.get_info(self.info_cls)) is not None:
11326
+ return self._info_to_record(info)
11327
+ else:
11328
+ return self.record_defaults
11329
+
11330
+ @abc.abstractmethod
11331
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
11332
+ raise NotImplementedError
11333
+
11334
+ #
11335
+
11336
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11337
+ super().__init_subclass__(**kwargs)
11338
+
11339
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
11340
+ if any(d is cls.NOT_SET for d in dd.values()):
11341
+ raise TypeError(cls.record_attrs)
11342
+ cls.record_defaults = dd
11334
11343
 
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
11344
  #
11339
- # As noted by stdlib:
11345
+
11346
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
11347
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
11348
+
11349
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11350
+ # Name of the logger used to log the call. Unmodified by ctor.
11351
+ name=str,
11352
+ )
11353
+
11354
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
11355
+ return dict(
11356
+ name=info.name,
11357
+ )
11358
+
11359
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
11360
+ return LoggingContextInfos.Name(
11361
+ name=rec.name,
11362
+ )
11363
+
11364
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
11365
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
11366
+
11367
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11368
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
11369
+ # `getLevelName(level)`.
11370
+ levelname=str,
11371
+
11372
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
11373
+ levelno=int,
11374
+ )
11375
+
11376
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
11377
+ return dict(
11378
+ levelname=info.name,
11379
+ levelno=int(info.level),
11380
+ )
11381
+
11382
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
11383
+ return LoggingContextInfos.Level.build(rec.levelno)
11384
+
11385
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
11386
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
11387
+
11388
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11389
+ # The format string passed in the original logging call. Merged with args to produce message, or an
11390
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
11391
+ msg=str,
11392
+
11393
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
11394
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
11395
+ # Mapping into just the mapping, but is otherwise unmodified.
11396
+ args=ta.Union[tuple, dict, None],
11397
+ )
11398
+
11399
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
11400
+ return dict(
11401
+ msg=info.msg,
11402
+ args=info.args,
11403
+ )
11404
+
11405
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
11406
+ return LoggingContextInfos.Msg(
11407
+ msg=rec.msg,
11408
+ args=rec.args,
11409
+ )
11410
+
11411
+ # FIXME: handled specially - all unknown attrs on LogRecord
11412
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
11413
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
11340
11414
  #
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
11415
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
11416
+ # # FIXME:
11417
+ # # if extra is not None:
11418
+ # # for key in extra:
11419
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
11420
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
11421
+ # # rv.__dict__[key] = extra[key]
11422
+ # return dict()
11343
11423
  #
11344
- processName=ta.Optional[str],
11424
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
11425
+ # return None
11345
11426
 
11346
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
11347
- # None.
11348
- process=ta.Optional[int],
11427
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
11428
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
11349
11429
 
11350
- #
11430
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11431
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
11432
+ # `time.time()`.
11433
+ #
11434
+ # See:
11435
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
11436
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
11437
+ #
11438
+ created=float,
11351
11439
 
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
- )
11440
+ # Millisecond portion of the time when the LogRecord was created.
11441
+ msecs=float,
11356
11442
 
11357
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
11443
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
11444
+ relativeCreated=float,
11445
+ )
11446
+
11447
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
11448
+ return dict(
11449
+ created=info.secs,
11450
+ msecs=info.msecs,
11451
+ relativeCreated=info.relative_secs,
11452
+ )
11453
+
11454
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
11455
+ return LoggingContextInfos.Time.build(
11456
+ int(rec.created * 1e9),
11457
+ )
11458
+
11459
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
11460
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
11461
+
11462
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11463
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
11464
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
11465
+
11466
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
11467
+ exc_text=(ta.Optional[str], None),
11468
+ )
11469
+
11470
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
11471
+ return dict(
11472
+ exc_info=info.info_tuple,
11473
+ exc_text=None,
11474
+ )
11475
+
11476
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
11477
+ # FIXME:
11478
+ # error: Argument 1 to "build" of "Exc" has incompatible type
11479
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
11480
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
11481
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
11482
+
11483
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
11484
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
11485
+
11486
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
11487
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
11488
+
11489
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
11490
+
11491
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11492
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
11493
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
11494
+ pathname=(str, _UNKNOWN_PATH_NAME),
11495
+
11496
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
11497
+ # y Logger.findCaller / Logger._log.
11498
+ lineno=(int, 0),
11499
+
11500
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
11501
+ # "(unknown function)" by Logger.findCaller / Logger._log.
11502
+ funcName=(str, _UNKNOWN_FUNC_NAME),
11503
+
11504
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
11505
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
11506
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
11507
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
11508
+ # stripped of exactly one trailing `\n` if present.
11509
+ stack_info=(ta.Optional[str], None),
11510
+ )
11511
+
11512
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
11513
+ if (sinfo := caller.stack_info) is not None:
11514
+ stack_info: ta.Optional[str] = '\n'.join([
11515
+ self._STACK_INFO_PREFIX,
11516
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
11517
+ ])
11518
+ else:
11519
+ stack_info = None
11520
+
11521
+ return dict(
11522
+ pathname=caller.file_path,
11523
+
11524
+ lineno=caller.line_no,
11525
+ funcName=caller.func_name,
11526
+
11527
+ stack_info=stack_info,
11528
+ )
11529
+
11530
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
11531
+ # FIXME: piecemeal?
11532
+ if (
11533
+ rec.pathname != self._UNKNOWN_PATH_NAME and
11534
+ rec.lineno != 0 and
11535
+ rec.funcName != self._UNKNOWN_FUNC_NAME
11536
+ ):
11537
+ if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
11538
+ sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
11539
+ return LoggingContextInfos.Caller(
11540
+ file_path=rec.pathname,
11541
+
11542
+ line_no=rec.lineno,
11543
+ func_name=rec.funcName,
11544
+
11545
+ stack_info=sinfo,
11546
+ )
11547
+
11548
+ return None
11549
+
11550
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
11551
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
11552
+
11553
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11554
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
11555
+ # pathname.
11556
+ filename=str,
11557
+
11558
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
11559
+ # "Unknown module".
11560
+ module=str,
11561
+ )
11562
+
11563
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
11564
+
11565
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11566
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
11567
+ return dict(
11568
+ filename=info.file_name,
11569
+ module=info.module,
11570
+ )
11571
+
11572
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
11573
+ return dict(
11574
+ filename=caller.file_path,
11575
+ module=self._UNKNOWN_MODULE,
11576
+ )
11577
+
11578
+ return dict(
11579
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
11580
+ module=self._UNKNOWN_MODULE,
11581
+ )
11582
+
11583
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
11584
+ if (
11585
+ rec.module is not None and
11586
+ rec.module != self._UNKNOWN_MODULE
11587
+ ):
11588
+ return LoggingContextInfos.SourceFile(
11589
+ file_name=rec.filename,
11590
+ module=rec.module, # FIXME: piecemeal?
11591
+ )
11592
+
11593
+ return None
11594
+
11595
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
11596
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
11597
+
11598
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11599
+ # Thread ID if available, and `logging.logThreads` is truthy.
11600
+ thread=(ta.Optional[int], None),
11601
+
11602
+ # Thread name if available, and `logging.logThreads` is truthy.
11603
+ threadName=(ta.Optional[str], None),
11604
+ )
11605
+
11606
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
11607
+ if logging.logThreads:
11608
+ return dict(
11609
+ thread=info.ident,
11610
+ threadName=info.name,
11611
+ )
11612
+
11613
+ return self.record_defaults
11614
+
11615
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
11616
+ if (
11617
+ (ident := rec.thread) is not None and
11618
+ (name := rec.threadName) is not None
11619
+ ):
11620
+ return LoggingContextInfos.Thread(
11621
+ ident=ident,
11622
+ native_id=None,
11623
+ name=name,
11624
+ )
11625
+
11626
+ return None
11627
+
11628
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
11629
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
11630
+
11631
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11632
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
11633
+ # otherwise None.
11634
+ process=(ta.Optional[int], None),
11635
+ )
11636
+
11637
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
11638
+ if logging.logProcesses:
11639
+ return dict(
11640
+ process=info.pid,
11641
+ )
11642
+
11643
+ return self.record_defaults
11644
+
11645
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
11646
+ if (
11647
+ (pid := rec.process) is not None
11648
+ ):
11649
+ return LoggingContextInfos.Process(
11650
+ pid=pid,
11651
+ )
11652
+
11653
+ return None
11654
+
11655
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
11656
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
11657
+
11658
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11659
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
11660
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
11661
+ # remains as 'MainProcess'.
11662
+ #
11663
+ # As noted by stdlib:
11664
+ #
11665
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
11666
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
11667
+ #
11668
+ processName=(ta.Optional[str], None),
11669
+ )
11670
+
11671
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
11672
+ if logging.logMultiprocessing:
11673
+ return dict(
11674
+ processName=info.process_name,
11675
+ )
11676
+
11677
+ return self.record_defaults
11678
+
11679
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
11680
+ if (
11681
+ (process_name := rec.processName) is not None
11682
+ ):
11683
+ return LoggingContextInfos.Multiprocessing(
11684
+ process_name=process_name,
11685
+ )
11686
+
11687
+ return None
11688
+
11689
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
11690
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
11691
+
11692
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
11693
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
11694
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
11695
+ taskName=(ta.Optional[str], None),
11696
+ )
11697
+
11698
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
11699
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
11700
+ return dict(
11701
+ taskName=info.name,
11702
+ )
11703
+
11704
+ return self.record_defaults
11705
+
11706
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
11707
+ if (
11708
+ (name := getattr(rec, 'taskName', None)) is not None
11709
+ ):
11710
+ return LoggingContextInfos.AsyncioTask(
11711
+ name=name,
11712
+ )
11713
+
11714
+ return None
11715
+
11716
+
11717
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
11718
+ LoggingContextInfoRecordAdapters.Name(),
11719
+ LoggingContextInfoRecordAdapters.Level(),
11720
+ LoggingContextInfoRecordAdapters.Msg(),
11721
+ LoggingContextInfoRecordAdapters.Time(),
11722
+ LoggingContextInfoRecordAdapters.Exc(),
11723
+ LoggingContextInfoRecordAdapters.Caller(),
11724
+ LoggingContextInfoRecordAdapters.SourceFile(),
11725
+ LoggingContextInfoRecordAdapters.Thread(),
11726
+ LoggingContextInfoRecordAdapters.Process(),
11727
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
11728
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
11729
+ ]
11730
+
11731
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
11732
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
11733
+ }
11734
+
11735
+
11736
+ ##
11358
11737
 
11359
11738
 
11360
11739
  # Formatter:
11361
11740
  # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
11362
11741
  # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
11363
11742
  #
11364
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11743
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11365
11744
  # The logged message, computed as msg % args. Set to `record.getMessage()`.
11366
11745
  message=str,
11367
11746
 
@@ -11375,20 +11754,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11375
11754
  exc_text=ta.Optional[str],
11376
11755
  )
11377
11756
 
11378
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
11379
-
11380
11757
 
11381
11758
  ##
11382
11759
 
11383
11760
 
11761
+ _KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
11762
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
11763
+ )
11764
+
11765
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
11766
+
11767
+
11384
11768
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
11385
11769
  pass
11386
11770
 
11387
11771
 
11388
11772
  def _check_std_logging_record_attrs() -> None:
11773
+ if (
11774
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
11775
+ len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
11776
+ ):
11777
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
11778
+
11389
11779
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
11390
11780
 
11391
- if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
11781
+ if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
11392
11782
  import warnings # noqa
11393
11783
 
11394
11784
  warnings.warn(
@@ -11404,116 +11794,43 @@ _check_std_logging_record_attrs()
11404
11794
 
11405
11795
 
11406
11796
  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'
11797
+ # LogRecord.__init__ args:
11798
+ # - name: str
11799
+ # - level: int
11800
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
11801
+ # - lineno: int - May be 0.
11802
+ # - msg: str
11803
+ # - args: tuple | dict | 1-tuple[dict]
11804
+ # - exc_info: LoggingExcInfoTuple | None
11805
+ # - func: str | None = None -> funcName
11806
+ # - sinfo: str | None = None -> stack_info
11414
11807
 
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
11808
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
11809
+ self._logging_context = _logging_context
11439
11810
 
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
11811
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
11812
+ self.__dict__.update(ad.context_to_record(_logging_context))
11444
11813
 
11445
- self.levelname: str = logging.getLevelName(ctx.level)
11446
- self.levelno: int = ctx.level
11447
11814
 
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
11815
+ ##
11481
11816
 
11482
- times = ctx.times
11483
- self.created: float = times.created
11484
- self.msecs: float = times.msecs
11485
- self.relativeCreated: float = times.relative_created
11486
11817
 
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
11818
+ @ta.final
11819
+ class LogRecordLoggingContext(LoggingContext):
11820
+ def __init__(self, rec: logging.LogRecord) -> None:
11821
+ if isinstance(rec, LoggingContextLogRecord):
11822
+ raise TypeError(rec)
11494
11823
 
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
11824
+ self._rec = rec
11500
11825
 
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
11826
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
11827
+ type(info): info
11828
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
11829
+ if (info := ad.record_to_info(rec)) is not None
11830
+ }
11508
11831
 
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
11832
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
11833
+ return self._infos.get(ty)
11517
11834
 
11518
11835
 
11519
11836
  ########################################
@@ -12152,21 +12469,20 @@ class StdLogger(Logger):
12152
12469
  return self._std.getEffectiveLevel()
12153
12470
 
12154
12471
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
12155
- if not self.is_enabled_for(ctx.level):
12472
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
12156
12473
  return
12157
12474
 
12158
- ctx.capture()
12159
-
12160
- ms, args = self._prepare_msg_args(msg, *args)
12161
-
12162
- rec = LoggingContextLogRecord(
12475
+ ctx.set_basic(
12163
12476
  name=self._std.name,
12164
- msg=ms,
12165
- args=args,
12166
12477
 
12167
- _logging_context=ctx,
12478
+ msg=msg,
12479
+ args=args,
12168
12480
  )
12169
12481
 
12482
+ ctx.capture()
12483
+
12484
+ rec = LoggingContextLogRecord(_logging_context=ctx)
12485
+
12170
12486
  self._std.handle(rec)
12171
12487
 
12172
12488