ominfra 0.0.0.dev429__py3-none-any.whl → 0.0.0.dev431__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,13 +92,15 @@ LogLevel = int # ta.TypeAlias
92
92
  # ../../../../omlish/configs/formats.py
93
93
  ConfigDataT = ta.TypeVar('ConfigDataT', bound='ConfigData')
94
94
 
95
- # ../../../../omlish/logs/contexts.py
95
+ # ../../../../omlish/logs/infos.py
96
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
96
97
  LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
97
98
  LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
98
99
  LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
100
+ LoggingContextInfo = ta.Any # ta.TypeAlias
99
101
 
100
- # ../../../../omlish/logs/base.py
101
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
102
+ # ../../../../omlish/logs/contexts.py
103
+ LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
102
104
 
103
105
  # ../../../threadworkers.py
104
106
  ThreadWorkerT = ta.TypeVar('ThreadWorkerT', bound='ThreadWorker')
@@ -1600,8 +1602,6 @@ class AttrOps(ta.Generic[T]):
1600
1602
  self._eq = _eq
1601
1603
  return _eq
1602
1604
 
1603
- #
1604
-
1605
1605
  @property
1606
1606
  def hash_eq(self) -> ta.Tuple[
1607
1607
  ta.Callable[[T], int],
@@ -1609,6 +1609,8 @@ class AttrOps(ta.Generic[T]):
1609
1609
  ]:
1610
1610
  return (self.hash, self.eq)
1611
1611
 
1612
+ #
1613
+
1612
1614
  @property
1613
1615
  def repr_hash_eq(self) -> ta.Tuple[
1614
1616
  ta.Callable[[T], str],
@@ -1619,20 +1621,25 @@ class AttrOps(ta.Generic[T]):
1619
1621
 
1620
1622
  #
1621
1623
 
1624
+ class NOT_SET: # noqa
1625
+ def __new__(cls, *args, **kwargs): # noqa
1626
+ raise TypeError
1627
+
1622
1628
  def install(
1623
1629
  self,
1624
1630
  locals_dct: ta.MutableMapping[str, ta.Any],
1625
1631
  *,
1626
- all: bool = False, # noqa
1627
- repr: bool = False, # noqa
1628
- hash: bool = False, # noqa
1629
- eq: bool = False,
1632
+ repr: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
1633
+ hash: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
1634
+ eq: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET,
1630
1635
  ) -> 'AttrOps[T]':
1631
- if repr or all:
1636
+ if all(a is self.NOT_SET for a in (repr, hash, eq)):
1637
+ repr = hash = eq = True # noqa
1638
+ if repr:
1632
1639
  locals_dct.update(__repr__=self.repr)
1633
- if hash or all:
1640
+ if hash:
1634
1641
  locals_dct.update(__hash__=self.hash)
1635
- if eq or all:
1642
+ if eq:
1636
1643
  locals_dct.update(__eq__=self.eq)
1637
1644
  return self
1638
1645
 
@@ -2684,124 +2691,6 @@ def format_num_bytes(num_bytes: int) -> str:
2684
2691
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
2685
2692
 
2686
2693
 
2687
- ########################################
2688
- # ../../../../../omlish/logs/infos.py
2689
-
2690
-
2691
- ##
2692
-
2693
-
2694
- def logging_context_info(cls):
2695
- return cls
2696
-
2697
-
2698
- ##
2699
-
2700
-
2701
- @logging_context_info
2702
- @ta.final
2703
- class LoggingSourceFileInfo(ta.NamedTuple):
2704
- file_name: str
2705
- module: str
2706
-
2707
- @classmethod
2708
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
2709
- if file_path is None:
2710
- return None
2711
-
2712
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
2713
- try:
2714
- file_name = os.path.basename(file_path)
2715
- module = os.path.splitext(file_name)[0]
2716
- except (TypeError, ValueError, AttributeError):
2717
- return None
2718
-
2719
- return cls(
2720
- file_name,
2721
- module,
2722
- )
2723
-
2724
-
2725
- ##
2726
-
2727
-
2728
- @logging_context_info
2729
- @ta.final
2730
- class LoggingThreadInfo(ta.NamedTuple):
2731
- ident: int
2732
- native_id: ta.Optional[int]
2733
- name: str
2734
-
2735
- @classmethod
2736
- def build(cls) -> 'LoggingThreadInfo':
2737
- return cls(
2738
- threading.get_ident(),
2739
- threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
2740
- threading.current_thread().name,
2741
- )
2742
-
2743
-
2744
- ##
2745
-
2746
-
2747
- @logging_context_info
2748
- @ta.final
2749
- class LoggingProcessInfo(ta.NamedTuple):
2750
- pid: int
2751
-
2752
- @classmethod
2753
- def build(cls) -> 'LoggingProcessInfo':
2754
- return cls(
2755
- os.getpid(),
2756
- )
2757
-
2758
-
2759
- ##
2760
-
2761
-
2762
- @logging_context_info
2763
- @ta.final
2764
- class LoggingMultiprocessingInfo(ta.NamedTuple):
2765
- process_name: str
2766
-
2767
- @classmethod
2768
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
2769
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
2770
- if (mp := sys.modules.get('multiprocessing')) is None:
2771
- return None
2772
-
2773
- return cls(
2774
- mp.current_process().name,
2775
- )
2776
-
2777
-
2778
- ##
2779
-
2780
-
2781
- @logging_context_info
2782
- @ta.final
2783
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
2784
- name: str
2785
-
2786
- @classmethod
2787
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
2788
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
2789
- if (asyncio := sys.modules.get('asyncio')) is None:
2790
- return None
2791
-
2792
- try:
2793
- task = asyncio.current_task()
2794
- except Exception: # noqa
2795
- return None
2796
-
2797
- if task is None:
2798
- return None
2799
-
2800
- return cls(
2801
- task.get_name(), # Always non-None
2802
- )
2803
-
2804
-
2805
2694
  ########################################
2806
2695
  # ../../../../../omlish/logs/levels.py
2807
2696
 
@@ -2821,36 +2710,66 @@ class NamedLogLevel(int):
2821
2710
 
2822
2711
  #
2823
2712
 
2824
- @property
2825
- def exact_name(self) -> ta.Optional[str]:
2826
- return self._NAMES_BY_INT.get(self)
2713
+ _CACHE: ta.ClassVar[ta.MutableMapping[int, 'NamedLogLevel']] = {}
2714
+
2715
+ @ta.overload
2716
+ def __new__(cls, name: str, offset: int = 0, /) -> 'NamedLogLevel':
2717
+ ...
2718
+
2719
+ @ta.overload
2720
+ def __new__(cls, i: int, /) -> 'NamedLogLevel':
2721
+ ...
2827
2722
 
2828
- _effective_name: ta.Optional[str]
2723
+ def __new__(cls, x, offset=0, /):
2724
+ if isinstance(x, str):
2725
+ return cls(cls._INTS_BY_NAME[x.upper()] + offset)
2726
+ elif not offset and (c := cls._CACHE.get(x)) is not None:
2727
+ return c
2728
+ else:
2729
+ return super().__new__(cls, x + offset)
2730
+
2731
+ #
2732
+
2733
+ _name_and_offset: ta.Tuple[str, int]
2829
2734
 
2830
2735
  @property
2831
- def effective_name(self) -> ta.Optional[str]:
2736
+ def name_and_offset(self) -> ta.Tuple[str, int]:
2832
2737
  try:
2833
- return self._effective_name
2738
+ return self._name_and_offset
2834
2739
  except AttributeError:
2835
2740
  pass
2836
2741
 
2837
- if (n := self.exact_name) is None:
2742
+ if (n := self._NAMES_BY_INT.get(self)) is not None:
2743
+ t = (n, 0)
2744
+ else:
2838
2745
  for n, i in self._NAME_INT_PAIRS: # noqa
2839
2746
  if self >= i:
2747
+ t = (n, (self - i))
2840
2748
  break
2841
2749
  else:
2842
- n = None
2750
+ t = ('NOTSET', int(self))
2751
+
2752
+ self._name_and_offset = t
2753
+ return t
2754
+
2755
+ @property
2756
+ def exact_name(self) -> ta.Optional[str]:
2757
+ n, o = self.name_and_offset
2758
+ return n if not o else None
2843
2759
 
2844
- self._effective_name = n
2760
+ @property
2761
+ def effective_name(self) -> str:
2762
+ n, _ = self.name_and_offset
2845
2763
  return n
2846
2764
 
2847
2765
  #
2848
2766
 
2849
- def __repr__(self) -> str:
2850
- return f'{self.__class__.__name__}({int(self)})'
2851
-
2852
2767
  def __str__(self) -> str:
2853
- return self.exact_name or f'{self.effective_name or "INVALID"}:{int(self)}'
2768
+ return self.exact_name or f'{self.effective_name}{int(self):+}'
2769
+
2770
+ def __repr__(self) -> str:
2771
+ n, o = self.name_and_offset
2772
+ return f'{self.__class__.__name__}({n!r}{f", {int(o)}" if o else ""})'
2854
2773
 
2855
2774
  #
2856
2775
 
@@ -2870,6 +2789,9 @@ NamedLogLevel.DEBUG = NamedLogLevel(logging.DEBUG)
2870
2789
  NamedLogLevel.NOTSET = NamedLogLevel(logging.NOTSET)
2871
2790
 
2872
2791
 
2792
+ NamedLogLevel._CACHE.update({i: NamedLogLevel(i) for i in NamedLogLevel._NAMES_BY_INT}) # noqa
2793
+
2794
+
2873
2795
  ########################################
2874
2796
  # ../../../../../omlish/logs/std/filters.py
2875
2797
 
@@ -4907,74 +4829,362 @@ def check_lite_runtime_version() -> None:
4907
4829
 
4908
4830
 
4909
4831
  ########################################
4910
- # ../../../../../omlish/logs/callers.py
4832
+ # ../../../../../omlish/logs/infos.py
4833
+ """
4834
+ TODO:
4835
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
4836
+ """
4911
4837
 
4912
4838
 
4913
4839
  ##
4914
4840
 
4915
4841
 
4916
- @logging_context_info
4842
+ def logging_context_info(cls):
4843
+ return cls
4844
+
4845
+
4917
4846
  @ta.final
4918
- class LoggingCaller(ta.NamedTuple):
4919
- file_path: str
4920
- line_no: int
4921
- name: str
4922
- stack_info: ta.Optional[str]
4847
+ class LoggingContextInfos:
4848
+ def __new__(cls, *args, **kwargs): # noqa
4849
+ raise TypeError
4923
4850
 
4924
- @classmethod
4925
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
4926
- file_path = os.path.normcase(frame.f_code.co_filename)
4851
+ #
4927
4852
 
4928
- # Yes, really.
4929
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
4930
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
4931
- if 'importlib' in file_path and '_bootstrap' in file_path:
4932
- return True
4853
+ @logging_context_info
4854
+ @ta.final
4855
+ class Name(ta.NamedTuple):
4856
+ name: str
4933
4857
 
4934
- return False
4858
+ @logging_context_info
4859
+ @ta.final
4860
+ class Level(ta.NamedTuple):
4861
+ level: NamedLogLevel
4862
+ name: str
4935
4863
 
4936
- @classmethod
4937
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
4938
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
4864
+ @classmethod
4865
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
4866
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
4867
+ return cls(
4868
+ level=nl,
4869
+ name=logging.getLevelName(nl),
4870
+ )
4939
4871
 
4940
- while f is not None:
4941
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
4942
- # stack_offset management.
4943
- if hasattr(f, 'f_code'):
4944
- return f
4872
+ @logging_context_info
4873
+ @ta.final
4874
+ class Msg(ta.NamedTuple):
4875
+ msg: str
4876
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
4877
+
4878
+ @classmethod
4879
+ def build(
4880
+ cls,
4881
+ msg: ta.Union[str, tuple, LoggingMsgFn],
4882
+ *args: ta.Any,
4883
+ ) -> 'LoggingContextInfos.Msg':
4884
+ s: str
4885
+ a: ta.Any
4886
+
4887
+ if callable(msg):
4888
+ if args:
4889
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
4890
+ x = msg()
4891
+ if isinstance(x, str):
4892
+ s, a = x, ()
4893
+ elif isinstance(x, tuple):
4894
+ if x:
4895
+ s, a = x[0], x[1:]
4896
+ else:
4897
+ s, a = '', ()
4898
+ else:
4899
+ raise TypeError(x)
4945
4900
 
4946
- f = f.f_back
4901
+ elif isinstance(msg, tuple):
4902
+ if args:
4903
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
4904
+ if msg:
4905
+ s, a = msg[0], msg[1:]
4906
+ else:
4907
+ s, a = '', ()
4947
4908
 
4948
- return None
4909
+ elif isinstance(msg, str):
4910
+ s, a = msg, args
4911
+
4912
+ else:
4913
+ raise TypeError(msg)
4914
+
4915
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
4916
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
4917
+ a = a[0]
4918
+
4919
+ return cls(
4920
+ msg=s,
4921
+ args=a,
4922
+ )
4923
+
4924
+ @logging_context_info
4925
+ @ta.final
4926
+ class Extra(ta.NamedTuple):
4927
+ extra: ta.Mapping[ta.Any, ta.Any]
4928
+
4929
+ @logging_context_info
4930
+ @ta.final
4931
+ class Time(ta.NamedTuple):
4932
+ ns: int
4933
+ secs: float
4934
+ msecs: float
4935
+ relative_secs: float
4936
+
4937
+ @classmethod
4938
+ def get_std_start_ns(cls) -> int:
4939
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
4940
+
4941
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
4942
+ # an int.
4943
+ #
4944
+ # See:
4945
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
4946
+ #
4947
+ if isinstance(x, float):
4948
+ return int(x * 1e9)
4949
+ else:
4950
+ return x
4951
+
4952
+ @classmethod
4953
+ def build(
4954
+ cls,
4955
+ ns: int,
4956
+ *,
4957
+ start_ns: ta.Optional[int] = None,
4958
+ ) -> 'LoggingContextInfos.Time':
4959
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
4960
+ secs = ns / 1e9 # ns to float seconds
4961
+
4962
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
4963
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
4964
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
4965
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
4966
+
4967
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
4968
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
4969
+ # ns -> sec conversion can round up, e.g:
4970
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
4971
+ msecs = 0.0
4972
+
4973
+ if start_ns is None:
4974
+ start_ns = cls.get_std_start_ns()
4975
+ relative_secs = (ns - start_ns) / 1e6
4976
+
4977
+ return cls(
4978
+ ns=ns,
4979
+ secs=secs,
4980
+ msecs=msecs,
4981
+ relative_secs=relative_secs,
4982
+ )
4983
+
4984
+ @logging_context_info
4985
+ @ta.final
4986
+ class Exc(ta.NamedTuple):
4987
+ info: LoggingExcInfo
4988
+ info_tuple: LoggingExcInfoTuple
4989
+
4990
+ @classmethod
4991
+ def build(
4992
+ cls,
4993
+ arg: LoggingExcInfoArg = False,
4994
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
4995
+ if arg is True:
4996
+ sys_exc_info = sys.exc_info()
4997
+ if sys_exc_info[0] is not None:
4998
+ arg = sys_exc_info
4999
+ else:
5000
+ arg = None
5001
+ elif arg is False:
5002
+ arg = None
5003
+ if arg is None:
5004
+ return None
5005
+
5006
+ info: LoggingExcInfo = arg
5007
+ if isinstance(info, BaseException):
5008
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
5009
+ else:
5010
+ info_tuple = info
5011
+
5012
+ return cls(
5013
+ info=info,
5014
+ info_tuple=info_tuple,
5015
+ )
5016
+
5017
+ @logging_context_info
5018
+ @ta.final
5019
+ class Caller(ta.NamedTuple):
5020
+ file_path: str
5021
+ line_no: int
5022
+ func_name: str
5023
+ stack_info: ta.Optional[str]
5024
+
5025
+ @classmethod
5026
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
5027
+ file_path = os.path.normcase(frame.f_code.co_filename)
5028
+
5029
+ # Yes, really.
5030
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
5031
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
5032
+ if 'importlib' in file_path and '_bootstrap' in file_path:
5033
+ return True
5034
+
5035
+ return False
5036
+
5037
+ @classmethod
5038
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
5039
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
5040
+
5041
+ while f is not None:
5042
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
5043
+ # manual stack_offset management.
5044
+ if hasattr(f, 'f_code'):
5045
+ return f
5046
+
5047
+ f = f.f_back
4949
5048
 
4950
- @classmethod
4951
- def find(
4952
- cls,
4953
- ofs: int = 0,
4954
- *,
4955
- stack_info: bool = False,
4956
- ) -> ta.Optional['LoggingCaller']:
4957
- if (f := cls.find_frame(ofs + 1)) is None:
4958
5049
  return None
4959
5050
 
4960
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
4961
- sinfo = None
4962
- if stack_info:
4963
- sio = io.StringIO()
4964
- traceback.print_stack(f, file=sio)
4965
- sinfo = sio.getvalue()
4966
- sio.close()
4967
- if sinfo[-1] == '\n':
4968
- sinfo = sinfo[:-1]
5051
+ @classmethod
5052
+ def build(
5053
+ cls,
5054
+ stack_offset: int = 0,
5055
+ *,
5056
+ stack_info: bool = False,
5057
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
5058
+ if (f := cls.find_frame(stack_offset + 1)) is None:
5059
+ return None
4969
5060
 
4970
- return cls(
4971
- f.f_code.co_filename,
4972
- f.f_lineno or 0,
4973
- f.f_code.co_name,
4974
- sinfo,
5061
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5062
+ sinfo = None
5063
+ if stack_info:
5064
+ sio = io.StringIO()
5065
+ traceback.print_stack(f, file=sio)
5066
+ sinfo = sio.getvalue()
5067
+ sio.close()
5068
+ if sinfo[-1] == '\n':
5069
+ sinfo = sinfo[:-1]
5070
+
5071
+ return cls(
5072
+ file_path=f.f_code.co_filename,
5073
+ line_no=f.f_lineno or 0,
5074
+ func_name=f.f_code.co_name,
5075
+ stack_info=sinfo,
5076
+ )
5077
+
5078
+ @logging_context_info
5079
+ @ta.final
5080
+ class SourceFile(ta.NamedTuple):
5081
+ file_name: str
5082
+ module: str
5083
+
5084
+ @classmethod
5085
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
5086
+ if caller_file_path is None:
5087
+ return None
5088
+
5089
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
5090
+ try:
5091
+ file_name = os.path.basename(caller_file_path)
5092
+ module = os.path.splitext(file_name)[0]
5093
+ except (TypeError, ValueError, AttributeError):
5094
+ return None
5095
+
5096
+ return cls(
5097
+ file_name=file_name,
5098
+ module=module,
5099
+ )
5100
+
5101
+ @logging_context_info
5102
+ @ta.final
5103
+ class Thread(ta.NamedTuple):
5104
+ ident: int
5105
+ native_id: ta.Optional[int]
5106
+ name: str
5107
+
5108
+ @classmethod
5109
+ def build(cls) -> 'LoggingContextInfos.Thread':
5110
+ return cls(
5111
+ ident=threading.get_ident(),
5112
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
5113
+ name=threading.current_thread().name,
5114
+ )
5115
+
5116
+ @logging_context_info
5117
+ @ta.final
5118
+ class Process(ta.NamedTuple):
5119
+ pid: int
5120
+
5121
+ @classmethod
5122
+ def build(cls) -> 'LoggingContextInfos.Process':
5123
+ return cls(
5124
+ pid=os.getpid(),
5125
+ )
5126
+
5127
+ @logging_context_info
5128
+ @ta.final
5129
+ class Multiprocessing(ta.NamedTuple):
5130
+ process_name: str
5131
+
5132
+ @classmethod
5133
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
5134
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
5135
+ if (mp := sys.modules.get('multiprocessing')) is None:
5136
+ return None
5137
+
5138
+ return cls(
5139
+ process_name=mp.current_process().name,
5140
+ )
5141
+
5142
+ @logging_context_info
5143
+ @ta.final
5144
+ class AsyncioTask(ta.NamedTuple):
5145
+ name: str
5146
+
5147
+ @classmethod
5148
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
5149
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
5150
+ if (asyncio := sys.modules.get('asyncio')) is None:
5151
+ return None
5152
+
5153
+ try:
5154
+ task = asyncio.current_task()
5155
+ except Exception: # noqa
5156
+ return None
5157
+
5158
+ if task is None:
5159
+ return None
5160
+
5161
+ return cls(
5162
+ name=task.get_name(), # Always non-None
5163
+ )
5164
+
5165
+
5166
+ ##
5167
+
5168
+
5169
+ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5170
+ pass
5171
+
5172
+
5173
+ def _check_logging_start_time() -> None:
5174
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
5175
+ import warnings # noqa
5176
+
5177
+ warnings.warn(
5178
+ f'Unexpected logging start time detected: '
5179
+ f'get_std_start_ns={x}, '
5180
+ f'time.time()={t}',
5181
+ UnexpectedLoggingStartTimeWarning,
4975
5182
  )
4976
5183
 
4977
5184
 
5185
+ _check_logging_start_time()
5186
+
5187
+
4978
5188
  ########################################
4979
5189
  # ../../../../../omlish/logs/std/json.py
4980
5190
  """
@@ -5032,91 +5242,6 @@ class JsonLoggingFormatter(logging.Formatter):
5032
5242
  return self._json_dumps(dct)
5033
5243
 
5034
5244
 
5035
- ########################################
5036
- # ../../../../../omlish/logs/times.py
5037
-
5038
-
5039
- ##
5040
-
5041
-
5042
- @logging_context_info
5043
- @ta.final
5044
- class LoggingTimeFields(ta.NamedTuple):
5045
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5046
-
5047
- created: float
5048
- msecs: float
5049
- relative_created: float
5050
-
5051
- @classmethod
5052
- def get_std_start_time_ns(cls) -> int:
5053
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5054
-
5055
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5056
- # int.
5057
- #
5058
- # See:
5059
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5060
- #
5061
- if isinstance(x, float):
5062
- return int(x * 1e9)
5063
- else:
5064
- return x
5065
-
5066
- @classmethod
5067
- def build(
5068
- cls,
5069
- time_ns: int,
5070
- *,
5071
- start_time_ns: ta.Optional[int] = None,
5072
- ) -> 'LoggingTimeFields':
5073
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5074
- created = time_ns / 1e9 # ns to float seconds
5075
-
5076
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5077
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5078
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5079
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5080
-
5081
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5082
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5083
- # ns -> sec conversion can round up, e.g:
5084
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5085
- msecs = 0.0
5086
-
5087
- if start_time_ns is None:
5088
- start_time_ns = cls.get_std_start_time_ns()
5089
- relative_created = (time_ns - start_time_ns) / 1e6
5090
-
5091
- return cls(
5092
- created,
5093
- msecs,
5094
- relative_created,
5095
- )
5096
-
5097
-
5098
- ##
5099
-
5100
-
5101
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5102
- pass
5103
-
5104
-
5105
- def _check_logging_start_time() -> None:
5106
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5107
- import warnings # noqa
5108
-
5109
- warnings.warn(
5110
- f'Unexpected logging start time detected: '
5111
- f'get_std_start_time_ns={x}, '
5112
- f'time.time()={t}',
5113
- UnexpectedLoggingStartTimeWarning,
5114
- )
5115
-
5116
-
5117
- _check_logging_start_time()
5118
-
5119
-
5120
5245
  ########################################
5121
5246
  # ../../logs.py
5122
5247
  """
@@ -5328,68 +5453,36 @@ def load_config_file_obj(
5328
5453
 
5329
5454
 
5330
5455
  class LoggingContext(Abstract):
5331
- @property
5332
- @abc.abstractmethod
5333
- def level(self) -> NamedLogLevel:
5334
- raise NotImplementedError
5335
-
5336
- #
5337
-
5338
- @property
5339
- @abc.abstractmethod
5340
- def time_ns(self) -> int:
5341
- raise NotImplementedError
5342
-
5343
- @property
5344
5456
  @abc.abstractmethod
5345
- def times(self) -> LoggingTimeFields:
5457
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5346
5458
  raise NotImplementedError
5347
5459
 
5348
- #
5460
+ @ta.final
5461
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5462
+ return self.get_info(ty)
5349
5463
 
5350
- @property
5351
- @abc.abstractmethod
5352
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
5353
- raise NotImplementedError
5464
+ @ta.final
5465
+ def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
5466
+ if (info := self.get_info(ty)) is None:
5467
+ raise TypeError(f'LoggingContextInfo absent: {ty}')
5468
+ return info
5354
5469
 
5355
- @property
5356
- @abc.abstractmethod
5357
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
5358
- raise NotImplementedError
5470
+ ##
5359
5471
 
5360
- #
5361
5472
 
5473
+ class CaptureLoggingContext(LoggingContext, Abstract):
5362
5474
  @abc.abstractmethod
5363
- def caller(self) -> ta.Optional[LoggingCaller]:
5364
- raise NotImplementedError
5475
+ def set_basic(
5476
+ self,
5477
+ name: str,
5365
5478
 
5366
- @abc.abstractmethod
5367
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
5479
+ msg: ta.Union[str, tuple, LoggingMsgFn],
5480
+ args: tuple,
5481
+ ) -> 'CaptureLoggingContext':
5368
5482
  raise NotImplementedError
5369
5483
 
5370
5484
  #
5371
5485
 
5372
- @abc.abstractmethod
5373
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
5374
- raise NotImplementedError
5375
-
5376
- @abc.abstractmethod
5377
- def process(self) -> ta.Optional[LoggingProcessInfo]:
5378
- raise NotImplementedError
5379
-
5380
- @abc.abstractmethod
5381
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
5382
- raise NotImplementedError
5383
-
5384
- @abc.abstractmethod
5385
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
5386
- raise NotImplementedError
5387
-
5388
-
5389
- ##
5390
-
5391
-
5392
- class CaptureLoggingContext(LoggingContext, Abstract):
5393
5486
  class AlreadyCapturedError(Exception):
5394
5487
  pass
5395
5488
 
@@ -5420,80 +5513,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
5420
5513
 
5421
5514
  exc_info: LoggingExcInfoArg = False,
5422
5515
 
5423
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
5516
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
5424
5517
  stack_offset: int = 0,
5425
5518
  stack_info: bool = False,
5426
5519
  ) -> None:
5427
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
5428
-
5429
- #
5520
+ # TODO: Name, Msg, Extra
5430
5521
 
5431
5522
  if time_ns is None:
5432
5523
  time_ns = time.time_ns()
5433
- self._time_ns: int = time_ns
5434
-
5435
- #
5436
-
5437
- if exc_info is True:
5438
- sys_exc_info = sys.exc_info()
5439
- if sys_exc_info[0] is not None:
5440
- exc_info = sys_exc_info
5441
- else:
5442
- exc_info = None
5443
- elif exc_info is False:
5444
- exc_info = None
5445
-
5446
- if exc_info is not None:
5447
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
5448
- if isinstance(exc_info, BaseException):
5449
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
5450
- else:
5451
- self._exc_info_tuple = exc_info
5452
5524
 
5453
- #
5525
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
5526
+ self._set_info(
5527
+ LoggingContextInfos.Level.build(level),
5528
+ LoggingContextInfos.Time.build(time_ns),
5529
+ LoggingContextInfos.Exc.build(exc_info),
5530
+ )
5454
5531
 
5455
5532
  if caller is not CaptureLoggingContextImpl.NOT_SET:
5456
- self._caller = caller # type: ignore[assignment]
5533
+ self._infos[LoggingContextInfos.Caller] = caller
5457
5534
  else:
5458
5535
  self._stack_offset = stack_offset
5459
5536
  self._stack_info = stack_info
5460
5537
 
5461
- ##
5462
-
5463
- @property
5464
- def level(self) -> NamedLogLevel:
5465
- return self._level
5466
-
5467
- #
5468
-
5469
- @property
5470
- def time_ns(self) -> int:
5471
- return self._time_ns
5472
-
5473
- _times: LoggingTimeFields
5474
-
5475
- @property
5476
- def times(self) -> LoggingTimeFields:
5477
- try:
5478
- return self._times
5479
- except AttributeError:
5480
- pass
5538
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
5539
+ for info in infos:
5540
+ if info is not None:
5541
+ self._infos[type(info)] = info
5542
+ return self
5481
5543
 
5482
- times = self._times = LoggingTimeFields.build(self.time_ns)
5483
- return times
5544
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5545
+ return self._infos.get(ty)
5484
5546
 
5485
- #
5486
-
5487
- _exc_info: ta.Optional[LoggingExcInfo] = None
5488
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
5547
+ ##
5489
5548
 
5490
- @property
5491
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
5492
- return self._exc_info
5549
+ def set_basic(
5550
+ self,
5551
+ name: str,
5493
5552
 
5494
- @property
5495
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
5496
- return self._exc_info_tuple
5553
+ msg: ta.Union[str, tuple, LoggingMsgFn],
5554
+ args: tuple,
5555
+ ) -> 'CaptureLoggingContextImpl':
5556
+ return self._set_info(
5557
+ LoggingContextInfos.Name(name),
5558
+ LoggingContextInfos.Msg.build(msg, *args),
5559
+ )
5497
5560
 
5498
5561
  ##
5499
5562
 
@@ -5507,74 +5570,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
5507
5570
 
5508
5571
  _has_captured: bool = False
5509
5572
 
5510
- _caller: ta.Optional[LoggingCaller]
5511
- _source_file: ta.Optional[LoggingSourceFileInfo]
5512
-
5513
- _thread: ta.Optional[LoggingThreadInfo]
5514
- _process: ta.Optional[LoggingProcessInfo]
5515
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
5516
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
5517
-
5518
5573
  def capture(self) -> None:
5519
5574
  if self._has_captured:
5520
5575
  raise CaptureLoggingContextImpl.AlreadyCapturedError
5521
5576
  self._has_captured = True
5522
5577
 
5523
- if not hasattr(self, '_caller'):
5524
- self._caller = LoggingCaller.find(
5578
+ if LoggingContextInfos.Caller not in self._infos:
5579
+ self._set_info(LoggingContextInfos.Caller.build(
5525
5580
  self._stack_offset + 1,
5526
5581
  stack_info=self._stack_info,
5527
- )
5528
-
5529
- if (caller := self._caller) is not None:
5530
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
5531
- else:
5532
- self._source_file = None
5533
-
5534
- self._thread = LoggingThreadInfo.build()
5535
- self._process = LoggingProcessInfo.build()
5536
- self._multiprocessing = LoggingMultiprocessingInfo.build()
5537
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
5538
-
5539
- #
5540
-
5541
- def caller(self) -> ta.Optional[LoggingCaller]:
5542
- try:
5543
- return self._caller
5544
- except AttributeError:
5545
- raise CaptureLoggingContext.NotCapturedError from None
5546
-
5547
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
5548
- try:
5549
- return self._source_file
5550
- except AttributeError:
5551
- raise CaptureLoggingContext.NotCapturedError from None
5552
-
5553
- #
5554
-
5555
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
5556
- try:
5557
- return self._thread
5558
- except AttributeError:
5559
- raise CaptureLoggingContext.NotCapturedError from None
5560
-
5561
- def process(self) -> ta.Optional[LoggingProcessInfo]:
5562
- try:
5563
- return self._process
5564
- except AttributeError:
5565
- raise CaptureLoggingContext.NotCapturedError from None
5582
+ ))
5566
5583
 
5567
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
5568
- try:
5569
- return self._multiprocessing
5570
- except AttributeError:
5571
- raise CaptureLoggingContext.NotCapturedError from None
5584
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
5585
+ self._set_info(LoggingContextInfos.SourceFile.build(
5586
+ caller.file_path,
5587
+ ))
5572
5588
 
5573
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
5574
- try:
5575
- return self._asyncio_task
5576
- except AttributeError:
5577
- raise CaptureLoggingContext.NotCapturedError from None
5589
+ self._set_info(
5590
+ LoggingContextInfos.Thread.build(),
5591
+ LoggingContextInfos.Process.build(),
5592
+ LoggingContextInfos.Multiprocessing.build(),
5593
+ LoggingContextInfos.AsyncioTask.build(),
5594
+ )
5578
5595
 
5579
5596
 
5580
5597
  ########################################
@@ -5734,7 +5751,6 @@ def subprocess_maybe_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
5734
5751
 
5735
5752
 
5736
5753
  class AnyLogger(Abstract, ta.Generic[T]):
5737
- @ta.final
5738
5754
  def is_enabled_for(self, level: LogLevel) -> bool:
5739
5755
  return level >= self.get_effective_level()
5740
5756
 
@@ -5880,36 +5896,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
5880
5896
 
5881
5897
  ##
5882
5898
 
5883
- @classmethod
5884
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
5885
- if callable(msg):
5886
- if args:
5887
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
5888
- x = msg()
5889
- if isinstance(x, str):
5890
- return x, ()
5891
- elif isinstance(x, tuple):
5892
- if x:
5893
- return x[0], x[1:]
5894
- else:
5895
- return '', ()
5896
- else:
5897
- raise TypeError(x)
5898
-
5899
- elif isinstance(msg, tuple):
5900
- if args:
5901
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
5902
- if msg:
5903
- return msg[0], msg[1:]
5904
- else:
5905
- return '', ()
5906
-
5907
- elif isinstance(msg, str):
5908
- return msg, args
5909
-
5910
- else:
5911
- raise TypeError(msg)
5912
-
5913
5899
  @abc.abstractmethod
5914
5900
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
5915
5901
  raise NotImplementedError
@@ -5933,7 +5919,7 @@ class AsyncLogger(AnyLogger[ta.Awaitable[None]], Abstract):
5933
5919
  class AnyNopLogger(AnyLogger[T], Abstract):
5934
5920
  @ta.final
5935
5921
  def get_effective_level(self) -> LogLevel:
5936
- return 999
5922
+ return -999
5937
5923
 
5938
5924
 
5939
5925
  @ta.final
@@ -5950,137 +5936,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
5950
5936
 
5951
5937
  ########################################
5952
5938
  # ../../../../../omlish/logs/std/records.py
5939
+ """
5940
+ TODO:
5941
+ - TypedDict?
5942
+ """
5953
5943
 
5954
5944
 
5955
5945
  ##
5956
5946
 
5957
5947
 
5958
- # Ref:
5959
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
5960
- #
5961
- # LogRecord:
5962
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
5963
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
5964
- #
5965
- # LogRecord.__init__ args:
5966
- # - name: str
5967
- # - level: int
5968
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
5969
- # - lineno: int - May be 0.
5970
- # - msg: str
5971
- # - args: tuple | dict | 1-tuple[dict]
5972
- # - exc_info: LoggingExcInfoTuple | None
5973
- # - func: str | None = None -> funcName
5974
- # - sinfo: str | None = None -> stack_info
5975
- #
5976
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
5977
- # Name of the logger used to log the call. Unmodified by ctor.
5978
- name=str,
5948
+ class LoggingContextInfoRecordAdapters:
5949
+ # Ref:
5950
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
5951
+ #
5952
+ # LogRecord:
5953
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
5954
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
5955
+ #
5979
5956
 
5980
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
5981
- # (see Using arbitrary objects as messages). Unmodified by ctor.
5982
- msg=str,
5957
+ def __new__(cls, *args, **kwargs): # noqa
5958
+ raise TypeError
5983
5959
 
5984
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
5985
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
5986
- # the mapping, but is otherwise unmodified.
5987
- args=ta.Union[tuple, dict],
5960
+ class Adapter(Abstract, ta.Generic[T]):
5961
+ @property
5962
+ @abc.abstractmethod
5963
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
5964
+ raise NotImplementedError
5988
5965
 
5989
- #
5966
+ #
5990
5967
 
5991
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
5992
- # `getLevelName(level)`.
5993
- levelname=str,
5968
+ @ta.final
5969
+ class NOT_SET: # noqa
5970
+ def __new__(cls, *args, **kwargs): # noqa
5971
+ raise TypeError
5994
5972
 
5995
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
5996
- levelno=int,
5973
+ class RecordAttr(ta.NamedTuple):
5974
+ name: str
5975
+ type: ta.Any
5976
+ default: ta.Any
5997
5977
 
5998
- #
5978
+ # @abc.abstractmethod
5979
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
5999
5980
 
6000
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
6001
- # to "(unknown file)" by Logger.findCaller / Logger._log.
6002
- pathname=str,
5981
+ @property
5982
+ @abc.abstractmethod
5983
+ def _record_attrs(self) -> ta.Union[
5984
+ ta.Mapping[str, ta.Any],
5985
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
5986
+ ]:
5987
+ raise NotImplementedError
6003
5988
 
6004
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
6005
- filename=str,
5989
+ #
6006
5990
 
6007
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
6008
- # "Unknown module".
6009
- module=str,
5991
+ @abc.abstractmethod
5992
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
5993
+ raise NotImplementedError
6010
5994
 
6011
- #
5995
+ #
6012
5996
 
6013
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
6014
- exc_info=ta.Optional[LoggingExcInfoTuple],
5997
+ @abc.abstractmethod
5998
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
5999
+ raise NotImplementedError
6015
6000
 
6016
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
6017
- exc_text=ta.Optional[str],
6001
+ #
6018
6002
 
6019
- #
6003
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6004
+ super().__init_subclass__(**kwargs)
6020
6005
 
6021
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
6022
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
6023
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
6024
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
6025
- stack_info=ta.Optional[str],
6006
+ if Abstract in cls.__bases__:
6007
+ return
6026
6008
 
6027
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
6028
- # Logger.findCaller / Logger._log.
6029
- lineno=int,
6009
+ if 'record_attrs' in cls.__dict__:
6010
+ raise TypeError(cls)
6011
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
6012
+ raise TypeError(ra)
6013
+
6014
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
6015
+ for n, v in ra.items():
6016
+ if not n or not isinstance(n, str) or n in rd:
6017
+ raise AttributeError(n)
6018
+ if isinstance(v, tuple):
6019
+ t, d = v
6020
+ else:
6021
+ t, d = v, cls.NOT_SET
6022
+ rd[n] = cls.RecordAttr(
6023
+ name=n,
6024
+ type=t,
6025
+ default=d,
6026
+ )
6027
+ cls.record_attrs = rd
6030
6028
 
6031
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
6032
- # "(unknown function)" by Logger.findCaller / Logger._log.
6033
- funcName=str,
6029
+ class RequiredAdapter(Adapter[T], Abstract):
6030
+ @property
6031
+ @abc.abstractmethod
6032
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
6033
+ raise NotImplementedError
6034
6034
 
6035
- #
6035
+ #
6036
6036
 
6037
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
6038
- #
6039
- # See:
6040
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6041
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6042
- #
6043
- created=float,
6037
+ @ta.final
6038
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6039
+ if (info := ctx.get_info(self.info_cls)) is not None:
6040
+ return self._info_to_record(info)
6041
+ else:
6042
+ raise TypeError # FIXME: fallback?
6044
6043
 
6045
- # Millisecond portion of the time when the LogRecord was created.
6046
- msecs=float,
6044
+ @abc.abstractmethod
6045
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
6046
+ raise NotImplementedError
6047
6047
 
6048
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
6049
- relativeCreated=float,
6048
+ #
6050
6049
 
6051
- #
6050
+ @abc.abstractmethod
6051
+ def record_to_info(self, rec: logging.LogRecord) -> T:
6052
+ raise NotImplementedError
6052
6053
 
6053
- # Thread ID if available, and `logging.logThreads` is truthy.
6054
- thread=ta.Optional[int],
6054
+ #
6055
6055
 
6056
- # Thread name if available, and `logging.logThreads` is truthy.
6057
- threadName=ta.Optional[str],
6056
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6057
+ super().__init_subclass__(**kwargs)
6058
6058
 
6059
- #
6059
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
6060
+ raise TypeError(cls.record_attrs)
6061
+
6062
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
6063
+ @property
6064
+ @abc.abstractmethod
6065
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
6066
+ raise NotImplementedError
6067
+
6068
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
6069
+
6070
+ #
6071
+
6072
+ @ta.final
6073
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6074
+ if (info := ctx.get_info(self.info_cls)) is not None:
6075
+ return self._info_to_record(info)
6076
+ else:
6077
+ return self.record_defaults
6078
+
6079
+ @abc.abstractmethod
6080
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
6081
+ raise NotImplementedError
6082
+
6083
+ #
6084
+
6085
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6086
+ super().__init_subclass__(**kwargs)
6087
+
6088
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
6089
+ if any(d is cls.NOT_SET for d in dd.values()):
6090
+ raise TypeError(cls.record_attrs)
6091
+ cls.record_defaults = dd
6060
6092
 
6061
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
6062
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
6063
- # as 'MainProcess'.
6064
6093
  #
6065
- # As noted by stdlib:
6094
+
6095
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
6096
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
6097
+
6098
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6099
+ # Name of the logger used to log the call. Unmodified by ctor.
6100
+ name=str,
6101
+ )
6102
+
6103
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
6104
+ return dict(
6105
+ name=info.name,
6106
+ )
6107
+
6108
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
6109
+ return LoggingContextInfos.Name(
6110
+ name=rec.name,
6111
+ )
6112
+
6113
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
6114
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
6115
+
6116
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6117
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
6118
+ # `getLevelName(level)`.
6119
+ levelname=str,
6120
+
6121
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
6122
+ levelno=int,
6123
+ )
6124
+
6125
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
6126
+ return dict(
6127
+ levelname=info.name,
6128
+ levelno=int(info.level),
6129
+ )
6130
+
6131
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
6132
+ return LoggingContextInfos.Level.build(rec.levelno)
6133
+
6134
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
6135
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
6136
+
6137
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6138
+ # The format string passed in the original logging call. Merged with args to produce message, or an
6139
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
6140
+ msg=str,
6141
+
6142
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
6143
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
6144
+ # Mapping into just the mapping, but is otherwise unmodified.
6145
+ args=ta.Union[tuple, dict, None],
6146
+ )
6147
+
6148
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
6149
+ return dict(
6150
+ msg=info.msg,
6151
+ args=info.args,
6152
+ )
6153
+
6154
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
6155
+ return LoggingContextInfos.Msg(
6156
+ msg=rec.msg,
6157
+ args=rec.args,
6158
+ )
6159
+
6160
+ # FIXME: handled specially - all unknown attrs on LogRecord
6161
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
6162
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
6066
6163
  #
6067
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
6068
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
6164
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
6165
+ # # FIXME:
6166
+ # # if extra is not None:
6167
+ # # for key in extra:
6168
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
6169
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
6170
+ # # rv.__dict__[key] = extra[key]
6171
+ # return dict()
6069
6172
  #
6070
- processName=ta.Optional[str],
6173
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
6174
+ # return None
6175
+
6176
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
6177
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
6178
+
6179
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6180
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
6181
+ # `time.time()`.
6182
+ #
6183
+ # See:
6184
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6185
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6186
+ #
6187
+ created=float,
6188
+
6189
+ # Millisecond portion of the time when the LogRecord was created.
6190
+ msecs=float,
6191
+
6192
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
6193
+ relativeCreated=float,
6194
+ )
6071
6195
 
6072
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
6073
- # None.
6074
- process=ta.Optional[int],
6196
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
6197
+ return dict(
6198
+ created=info.secs,
6199
+ msecs=info.msecs,
6200
+ relativeCreated=info.relative_secs,
6201
+ )
6075
6202
 
6076
- #
6203
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
6204
+ return LoggingContextInfos.Time.build(
6205
+ int(rec.created * 1e9),
6206
+ )
6077
6207
 
6078
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
6079
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
6080
- taskName=ta.Optional[str],
6081
- )
6208
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
6209
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
6210
+
6211
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6212
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
6213
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
6214
+
6215
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
6216
+ exc_text=(ta.Optional[str], None),
6217
+ )
6218
+
6219
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
6220
+ return dict(
6221
+ exc_info=info.info_tuple,
6222
+ exc_text=None,
6223
+ )
6224
+
6225
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
6226
+ # FIXME:
6227
+ # error: Argument 1 to "build" of "Exc" has incompatible type
6228
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
6229
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
6230
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
6231
+
6232
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
6233
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
6234
+
6235
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
6236
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
6237
+
6238
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
6239
+
6240
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6241
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
6242
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
6243
+ pathname=(str, _UNKNOWN_PATH_NAME),
6244
+
6245
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
6246
+ # y Logger.findCaller / Logger._log.
6247
+ lineno=(int, 0),
6248
+
6249
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
6250
+ # "(unknown function)" by Logger.findCaller / Logger._log.
6251
+ funcName=(str, _UNKNOWN_FUNC_NAME),
6252
+
6253
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
6254
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
6255
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
6256
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
6257
+ # stripped of exactly one trailing `\n` if present.
6258
+ stack_info=(ta.Optional[str], None),
6259
+ )
6260
+
6261
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
6262
+ if (sinfo := caller.stack_info) is not None:
6263
+ stack_info: ta.Optional[str] = '\n'.join([
6264
+ self._STACK_INFO_PREFIX,
6265
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
6266
+ ])
6267
+ else:
6268
+ stack_info = None
6269
+
6270
+ return dict(
6271
+ pathname=caller.file_path,
6272
+
6273
+ lineno=caller.line_no,
6274
+ funcName=caller.func_name,
6275
+
6276
+ stack_info=stack_info,
6277
+ )
6278
+
6279
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
6280
+ # FIXME: piecemeal?
6281
+ # FIXME: strip _STACK_INFO_PREFIX
6282
+ raise NotImplementedError
6283
+
6284
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
6285
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
6286
+
6287
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6288
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
6289
+ # pathname.
6290
+ filename=str,
6291
+
6292
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
6293
+ # "Unknown module".
6294
+ module=str,
6295
+ )
6296
+
6297
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
6298
+
6299
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6300
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
6301
+ return dict(
6302
+ filename=info.file_name,
6303
+ module=info.module,
6304
+ )
6305
+
6306
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
6307
+ return dict(
6308
+ filename=caller.file_path,
6309
+ module=self._UNKNOWN_MODULE,
6310
+ )
6311
+
6312
+ return dict(
6313
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
6314
+ module=self._UNKNOWN_MODULE,
6315
+ )
6316
+
6317
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
6318
+ if not (
6319
+ rec.module is None or
6320
+ rec.module == self._UNKNOWN_MODULE
6321
+ ):
6322
+ return LoggingContextInfos.SourceFile(
6323
+ file_name=rec.filename,
6324
+ module=rec.module, # FIXME: piecemeal?
6325
+ )
6326
+
6327
+ return None
6328
+
6329
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
6330
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
6331
+
6332
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6333
+ # Thread ID if available, and `logging.logThreads` is truthy.
6334
+ thread=(ta.Optional[int], None),
6335
+
6336
+ # Thread name if available, and `logging.logThreads` is truthy.
6337
+ threadName=(ta.Optional[str], None),
6338
+ )
6339
+
6340
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
6341
+ if logging.logThreads:
6342
+ return dict(
6343
+ thread=info.ident,
6344
+ threadName=info.name,
6345
+ )
6346
+
6347
+ return self.record_defaults
6348
+
6349
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
6350
+ if (
6351
+ (ident := rec.thread) is not None and
6352
+ (name := rec.threadName) is not None
6353
+ ):
6354
+ return LoggingContextInfos.Thread(
6355
+ ident=ident,
6356
+ native_id=None,
6357
+ name=name,
6358
+ )
6359
+
6360
+ return None
6082
6361
 
6083
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
6362
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
6363
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
6364
+
6365
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6366
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
6367
+ # otherwise None.
6368
+ process=(ta.Optional[int], None),
6369
+ )
6370
+
6371
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
6372
+ if logging.logProcesses:
6373
+ return dict(
6374
+ process=info.pid,
6375
+ )
6376
+
6377
+ return self.record_defaults
6378
+
6379
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
6380
+ if (
6381
+ (pid := rec.process) is not None
6382
+ ):
6383
+ return LoggingContextInfos.Process(
6384
+ pid=pid,
6385
+ )
6386
+
6387
+ return None
6388
+
6389
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
6390
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
6391
+
6392
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6393
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
6394
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
6395
+ # remains as 'MainProcess'.
6396
+ #
6397
+ # As noted by stdlib:
6398
+ #
6399
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
6400
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
6401
+ #
6402
+ processName=(ta.Optional[str], None),
6403
+ )
6404
+
6405
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
6406
+ if logging.logMultiprocessing:
6407
+ return dict(
6408
+ processName=info.process_name,
6409
+ )
6410
+
6411
+ return self.record_defaults
6412
+
6413
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
6414
+ if (
6415
+ (process_name := rec.processName) is not None
6416
+ ):
6417
+ return LoggingContextInfos.Multiprocessing(
6418
+ process_name=process_name,
6419
+ )
6420
+
6421
+ return None
6422
+
6423
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
6424
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
6425
+
6426
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
6427
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
6428
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
6429
+ taskName=(ta.Optional[str], None),
6430
+ )
6431
+
6432
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
6433
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
6434
+ return dict(
6435
+ taskName=info.name,
6436
+ )
6437
+
6438
+ return self.record_defaults
6439
+
6440
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
6441
+ if (
6442
+ (name := getattr(rec, 'taskName', None)) is not None
6443
+ ):
6444
+ return LoggingContextInfos.AsyncioTask(
6445
+ name=name,
6446
+ )
6447
+
6448
+ return None
6449
+
6450
+
6451
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
6452
+ LoggingContextInfoRecordAdapters.Name(),
6453
+ LoggingContextInfoRecordAdapters.Level(),
6454
+ LoggingContextInfoRecordAdapters.Msg(),
6455
+ LoggingContextInfoRecordAdapters.Time(),
6456
+ LoggingContextInfoRecordAdapters.Exc(),
6457
+ LoggingContextInfoRecordAdapters.Caller(),
6458
+ LoggingContextInfoRecordAdapters.SourceFile(),
6459
+ LoggingContextInfoRecordAdapters.Thread(),
6460
+ LoggingContextInfoRecordAdapters.Process(),
6461
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
6462
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
6463
+ ]
6464
+
6465
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
6466
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
6467
+ }
6468
+
6469
+
6470
+ ##
6471
+
6472
+
6473
+ KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
6474
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
6475
+ )
6084
6476
 
6085
6477
 
6086
6478
  # Formatter:
@@ -6104,14 +6496,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6104
6496
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
6105
6497
 
6106
6498
 
6107
- ##
6108
-
6109
-
6110
6499
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
6111
6500
  pass
6112
6501
 
6113
6502
 
6114
6503
  def _check_std_logging_record_attrs() -> None:
6504
+ if (
6505
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
6506
+ len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
6507
+ ):
6508
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
6509
+
6115
6510
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
6116
6511
 
6117
6512
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -6130,120 +6525,24 @@ _check_std_logging_record_attrs()
6130
6525
 
6131
6526
 
6132
6527
  class LoggingContextLogRecord(logging.LogRecord):
6133
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
6134
-
6135
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
6136
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
6137
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
6138
-
6139
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
6140
-
6141
- def __init__( # noqa
6142
- self,
6143
- # name,
6144
- # level,
6145
- # pathname,
6146
- # lineno,
6147
- # msg,
6148
- # args,
6149
- # exc_info,
6150
- # func=None,
6151
- # sinfo=None,
6152
- # **kwargs,
6153
- *,
6154
- name: str,
6155
- msg: str,
6156
- args: ta.Union[tuple, dict],
6157
-
6158
- _logging_context: LoggingContext,
6159
- ) -> None:
6160
- ctx = _logging_context
6161
-
6162
- self.name: str = name
6163
-
6164
- self.msg: str = msg
6165
-
6166
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
6167
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
6168
- args = args[0] # type: ignore[assignment]
6169
- self.args: ta.Union[tuple, dict] = args
6170
-
6171
- self.levelname: str = logging.getLevelName(ctx.level)
6172
- self.levelno: int = ctx.level
6173
-
6174
- if (caller := ctx.caller()) is not None:
6175
- self.pathname: str = caller.file_path
6176
- else:
6177
- self.pathname = self._UNKNOWN_PATH_NAME
6178
-
6179
- if (src_file := ctx.source_file()) is not None:
6180
- self.filename: str = src_file.file_name
6181
- self.module: str = src_file.module
6182
- else:
6183
- self.filename = self.pathname
6184
- self.module = self._UNKNOWN_MODULE
6185
-
6186
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
6187
- self.exc_text: ta.Optional[str] = None
6188
-
6189
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
6190
- if caller is not None:
6191
- if (sinfo := caller.stack_info) is not None:
6192
- self.stack_info: ta.Optional[str] = '\n'.join([
6193
- self._STACK_INFO_PREFIX,
6194
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
6195
- ])
6196
- else:
6197
- self.stack_info = None
6198
-
6199
- self.lineno: int = caller.line_no
6200
- self.funcName: str = caller.name
6201
-
6202
- else:
6203
- self.stack_info = None
6204
-
6205
- self.lineno = 0
6206
- self.funcName = self._UNKNOWN_FUNC_NAME
6207
-
6208
- times = ctx.times
6209
- self.created: float = times.created
6210
- self.msecs: float = times.msecs
6211
- self.relativeCreated: float = times.relative_created
6212
-
6213
- if logging.logThreads:
6214
- thread = check.not_none(ctx.thread())
6215
- self.thread: ta.Optional[int] = thread.ident
6216
- self.threadName: ta.Optional[str] = thread.name
6217
- else:
6218
- self.thread = None
6219
- self.threadName = None
6220
-
6221
- if logging.logProcesses:
6222
- process = check.not_none(ctx.process())
6223
- self.process: ta.Optional[int] = process.pid
6224
- else:
6225
- self.process = None
6226
-
6227
- if logging.logMultiprocessing:
6228
- if (mp := ctx.multiprocessing()) is not None:
6229
- self.processName: ta.Optional[str] = mp.process_name
6230
- else:
6231
- self.processName = None
6232
- else:
6233
- self.processName = None
6234
-
6235
- # Absent <3.12
6236
- if getattr(logging, 'logAsyncioTasks', None):
6237
- if (at := ctx.asyncio_task()) is not None:
6238
- self.taskName: ta.Optional[str] = at.name
6239
- else:
6240
- self.taskName = None
6241
- else:
6242
- self.taskName = None
6528
+ # LogRecord.__init__ args:
6529
+ # - name: str
6530
+ # - level: int
6531
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
6532
+ # - lineno: int - May be 0.
6533
+ # - msg: str
6534
+ # - args: tuple | dict | 1-tuple[dict]
6535
+ # - exc_info: LoggingExcInfoTuple | None
6536
+ # - func: str | None = None -> funcName
6537
+ # - sinfo: str | None = None -> stack_info
6538
+
6539
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
6540
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
6541
+ self.__dict__.update(ad.context_to_record(_logging_context))
6243
6542
 
6244
6543
 
6245
6544
  ########################################
6246
- # ../../../../../omlish/logs/std/adapters.py
6545
+ # ../../../../../omlish/logs/std/loggers.py
6247
6546
 
6248
6547
 
6249
6548
  ##
@@ -6259,25 +6558,27 @@ class StdLogger(Logger):
6259
6558
  def std(self) -> logging.Logger:
6260
6559
  return self._std
6261
6560
 
6561
+ def is_enabled_for(self, level: LogLevel) -> bool:
6562
+ return self._std.isEnabledFor(level)
6563
+
6262
6564
  def get_effective_level(self) -> LogLevel:
6263
6565
  return self._std.getEffectiveLevel()
6264
6566
 
6265
6567
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
6266
- if not self.is_enabled_for(ctx.level):
6568
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
6267
6569
  return
6268
6570
 
6269
- ctx.capture()
6270
-
6271
- ms, args = self._prepare_msg_args(msg, *args)
6272
-
6273
- rec = LoggingContextLogRecord(
6571
+ ctx.set_basic(
6274
6572
  name=self._std.name,
6275
- msg=ms,
6276
- args=args,
6277
6573
 
6278
- _logging_context=ctx,
6574
+ msg=msg,
6575
+ args=args,
6279
6576
  )
6280
6577
 
6578
+ ctx.capture()
6579
+
6580
+ rec = LoggingContextLogRecord(_logging_context=ctx)
6581
+
6281
6582
  self._std.handle(rec)
6282
6583
 
6283
6584