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.
@@ -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')
@@ -2689,124 +2691,6 @@ def format_num_bytes(num_bytes: int) -> str:
2689
2691
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
2690
2692
 
2691
2693
 
2692
- ########################################
2693
- # ../../../../../omlish/logs/infos.py
2694
-
2695
-
2696
- ##
2697
-
2698
-
2699
- def logging_context_info(cls):
2700
- return cls
2701
-
2702
-
2703
- ##
2704
-
2705
-
2706
- @logging_context_info
2707
- @ta.final
2708
- class LoggingSourceFileInfo(ta.NamedTuple):
2709
- file_name: str
2710
- module: str
2711
-
2712
- @classmethod
2713
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
2714
- if file_path is None:
2715
- return None
2716
-
2717
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
2718
- try:
2719
- file_name = os.path.basename(file_path)
2720
- module = os.path.splitext(file_name)[0]
2721
- except (TypeError, ValueError, AttributeError):
2722
- return None
2723
-
2724
- return cls(
2725
- file_name=file_name,
2726
- module=module,
2727
- )
2728
-
2729
-
2730
- ##
2731
-
2732
-
2733
- @logging_context_info
2734
- @ta.final
2735
- class LoggingThreadInfo(ta.NamedTuple):
2736
- ident: int
2737
- native_id: ta.Optional[int]
2738
- name: str
2739
-
2740
- @classmethod
2741
- def build(cls) -> 'LoggingThreadInfo':
2742
- return cls(
2743
- ident=threading.get_ident(),
2744
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
2745
- name=threading.current_thread().name,
2746
- )
2747
-
2748
-
2749
- ##
2750
-
2751
-
2752
- @logging_context_info
2753
- @ta.final
2754
- class LoggingProcessInfo(ta.NamedTuple):
2755
- pid: int
2756
-
2757
- @classmethod
2758
- def build(cls) -> 'LoggingProcessInfo':
2759
- return cls(
2760
- pid=os.getpid(),
2761
- )
2762
-
2763
-
2764
- ##
2765
-
2766
-
2767
- @logging_context_info
2768
- @ta.final
2769
- class LoggingMultiprocessingInfo(ta.NamedTuple):
2770
- process_name: str
2771
-
2772
- @classmethod
2773
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
2774
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
2775
- if (mp := sys.modules.get('multiprocessing')) is None:
2776
- return None
2777
-
2778
- return cls(
2779
- process_name=mp.current_process().name,
2780
- )
2781
-
2782
-
2783
- ##
2784
-
2785
-
2786
- @logging_context_info
2787
- @ta.final
2788
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
2789
- name: str
2790
-
2791
- @classmethod
2792
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
2793
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
2794
- if (asyncio := sys.modules.get('asyncio')) is None:
2795
- return None
2796
-
2797
- try:
2798
- task = asyncio.current_task()
2799
- except Exception: # noqa
2800
- return None
2801
-
2802
- if task is None:
2803
- return None
2804
-
2805
- return cls(
2806
- name=task.get_name(), # Always non-None
2807
- )
2808
-
2809
-
2810
2694
  ########################################
2811
2695
  # ../../../../../omlish/logs/levels.py
2812
2696
 
@@ -4945,74 +4829,362 @@ def check_lite_runtime_version() -> None:
4945
4829
 
4946
4830
 
4947
4831
  ########################################
4948
- # ../../../../../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
+ """
4949
4837
 
4950
4838
 
4951
4839
  ##
4952
4840
 
4953
4841
 
4954
- @logging_context_info
4842
+ def logging_context_info(cls):
4843
+ return cls
4844
+
4845
+
4955
4846
  @ta.final
4956
- class LoggingCaller(ta.NamedTuple):
4957
- file_path: str
4958
- line_no: int
4959
- name: str
4960
- stack_info: ta.Optional[str]
4847
+ class LoggingContextInfos:
4848
+ def __new__(cls, *args, **kwargs): # noqa
4849
+ raise TypeError
4961
4850
 
4962
- @classmethod
4963
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
4964
- file_path = os.path.normcase(frame.f_code.co_filename)
4851
+ #
4965
4852
 
4966
- # Yes, really.
4967
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
4968
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
4969
- if 'importlib' in file_path and '_bootstrap' in file_path:
4970
- return True
4853
+ @logging_context_info
4854
+ @ta.final
4855
+ class Name(ta.NamedTuple):
4856
+ name: str
4971
4857
 
4972
- return False
4858
+ @logging_context_info
4859
+ @ta.final
4860
+ class Level(ta.NamedTuple):
4861
+ level: NamedLogLevel
4862
+ name: str
4973
4863
 
4974
- @classmethod
4975
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
4976
- 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
+ )
4871
+
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]
4977
4877
 
4978
- while f is not None:
4979
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
4980
- # stack_offset management.
4981
- if hasattr(f, 'f_code'):
4982
- return f
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)
4983
4900
 
4984
- 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 = '', ()
4985
4908
 
4986
- 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
4987
5048
 
4988
- @classmethod
4989
- def find(
4990
- cls,
4991
- ofs: int = 0,
4992
- *,
4993
- stack_info: bool = False,
4994
- ) -> ta.Optional['LoggingCaller']:
4995
- if (f := cls.find_frame(ofs + 1)) is None:
4996
5049
  return None
4997
5050
 
4998
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
4999
- sinfo = None
5000
- if stack_info:
5001
- sio = io.StringIO()
5002
- traceback.print_stack(f, file=sio)
5003
- sinfo = sio.getvalue()
5004
- sio.close()
5005
- if sinfo[-1] == '\n':
5006
- 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
5060
+
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
+ )
5007
5077
 
5008
- return cls(
5009
- file_path=f.f_code.co_filename,
5010
- line_no=f.f_lineno or 0,
5011
- name=f.f_code.co_name,
5012
- stack_info=sinfo,
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,
5013
5182
  )
5014
5183
 
5015
5184
 
5185
+ _check_logging_start_time()
5186
+
5187
+
5016
5188
  ########################################
5017
5189
  # ../../../../../omlish/logs/std/json.py
5018
5190
  """
@@ -5071,131 +5243,46 @@ class JsonLoggingFormatter(logging.Formatter):
5071
5243
 
5072
5244
 
5073
5245
  ########################################
5074
- # ../../../../../omlish/logs/times.py
5246
+ # ../../logs.py
5247
+ """
5248
+ https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html :
5249
+ - The maximum batch size is 1,048,576 bytes. This size is calculated as the sum of all event messages in UTF-8, plus 26
5250
+ bytes for each log event.
5251
+ - None of the log events in the batch can be more than 2 hours in the future.
5252
+ - None of the log events in the batch can be more than 14 days in the past. Also, none of the log events can be from
5253
+ earlier than the retention period of the log group.
5254
+ - The log events in the batch must be in chronological order by their timestamp. The timestamp is the time that the
5255
+ event occurred, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. (In AWS Tools for PowerShell
5256
+ and the AWS SDK for .NET, the timestamp is specified in .NET format: yyyy-mm-ddThh:mm:ss. For example,
5257
+ 2017-09-15T13:45:30.)
5258
+ - A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails.
5259
+ - Each log event can be no larger than 256 KB.
5260
+ - The maximum number of log events in a batch is 10,000.
5261
+ """
5075
5262
 
5076
5263
 
5077
5264
  ##
5078
5265
 
5079
5266
 
5080
- @logging_context_info
5081
- @ta.final
5082
- class LoggingTimeFields(ta.NamedTuple):
5083
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5267
+ @dc.dataclass(frozen=True)
5268
+ class AwsLogEvent(AwsDataclass):
5269
+ message: str
5270
+ timestamp: int # milliseconds UTC
5084
5271
 
5085
- created: float
5086
- msecs: float
5087
- relative_created: float
5088
5272
 
5089
- @classmethod
5090
- def get_std_start_time_ns(cls) -> int:
5091
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5273
+ @dc.dataclass(frozen=True)
5274
+ class AwsPutLogEventsRequest(AwsDataclass):
5275
+ log_group_name: str
5276
+ log_stream_name: str
5277
+ log_events: ta.Sequence[AwsLogEvent]
5278
+ sequence_token: ta.Optional[str] = None
5092
5279
 
5093
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5094
- # int.
5095
- #
5096
- # See:
5097
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5098
- #
5099
- if isinstance(x, float):
5100
- return int(x * 1e9)
5101
- else:
5102
- return x
5103
5280
 
5104
- @classmethod
5105
- def build(
5106
- cls,
5107
- time_ns: int,
5108
- *,
5109
- start_time_ns: ta.Optional[int] = None,
5110
- ) -> 'LoggingTimeFields':
5111
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5112
- created = time_ns / 1e9 # ns to float seconds
5113
-
5114
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5115
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5116
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5117
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5118
-
5119
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5120
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5121
- # ns -> sec conversion can round up, e.g:
5122
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5123
- msecs = 0.0
5124
-
5125
- if start_time_ns is None:
5126
- start_time_ns = cls.get_std_start_time_ns()
5127
- relative_created = (time_ns - start_time_ns) / 1e6
5128
-
5129
- return cls(
5130
- created=created,
5131
- msecs=msecs,
5132
- relative_created=relative_created,
5133
- )
5134
-
5135
-
5136
- ##
5137
-
5138
-
5139
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5140
- pass
5141
-
5142
-
5143
- def _check_logging_start_time() -> None:
5144
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5145
- import warnings # noqa
5146
-
5147
- warnings.warn(
5148
- f'Unexpected logging start time detected: '
5149
- f'get_std_start_time_ns={x}, '
5150
- f'time.time()={t}',
5151
- UnexpectedLoggingStartTimeWarning,
5152
- )
5153
-
5154
-
5155
- _check_logging_start_time()
5156
-
5157
-
5158
- ########################################
5159
- # ../../logs.py
5160
- """
5161
- https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html :
5162
- - The maximum batch size is 1,048,576 bytes. This size is calculated as the sum of all event messages in UTF-8, plus 26
5163
- bytes for each log event.
5164
- - None of the log events in the batch can be more than 2 hours in the future.
5165
- - None of the log events in the batch can be more than 14 days in the past. Also, none of the log events can be from
5166
- earlier than the retention period of the log group.
5167
- - The log events in the batch must be in chronological order by their timestamp. The timestamp is the time that the
5168
- event occurred, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. (In AWS Tools for PowerShell
5169
- and the AWS SDK for .NET, the timestamp is specified in .NET format: yyyy-mm-ddThh:mm:ss. For example,
5170
- 2017-09-15T13:45:30.)
5171
- - A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails.
5172
- - Each log event can be no larger than 256 KB.
5173
- - The maximum number of log events in a batch is 10,000.
5174
- """
5175
-
5176
-
5177
- ##
5178
-
5179
-
5180
- @dc.dataclass(frozen=True)
5181
- class AwsLogEvent(AwsDataclass):
5182
- message: str
5183
- timestamp: int # milliseconds UTC
5184
-
5185
-
5186
- @dc.dataclass(frozen=True)
5187
- class AwsPutLogEventsRequest(AwsDataclass):
5188
- log_group_name: str
5189
- log_stream_name: str
5190
- log_events: ta.Sequence[AwsLogEvent]
5191
- sequence_token: ta.Optional[str] = None
5192
-
5193
-
5194
- @dc.dataclass(frozen=True)
5195
- class AwsRejectedLogEventsInfo(AwsDataclass):
5196
- expired_log_event_end_index: ta.Optional[int] = None
5197
- too_new_log_event_start_index: ta.Optional[int] = None
5198
- too_old_log_event_end_index: ta.Optional[int] = None
5281
+ @dc.dataclass(frozen=True)
5282
+ class AwsRejectedLogEventsInfo(AwsDataclass):
5283
+ expired_log_event_end_index: ta.Optional[int] = None
5284
+ too_new_log_event_start_index: ta.Optional[int] = None
5285
+ too_old_log_event_end_index: ta.Optional[int] = None
5199
5286
 
5200
5287
 
5201
5288
  @dc.dataclass(frozen=True)
@@ -5366,68 +5453,46 @@ def load_config_file_obj(
5366
5453
 
5367
5454
 
5368
5455
  class LoggingContext(Abstract):
5369
- @property
5370
- @abc.abstractmethod
5371
- def level(self) -> NamedLogLevel:
5372
- raise NotImplementedError
5373
-
5374
- #
5375
-
5376
- @property
5377
5456
  @abc.abstractmethod
5378
- def time_ns(self) -> int:
5457
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5379
5458
  raise NotImplementedError
5380
5459
 
5381
- @property
5382
- @abc.abstractmethod
5383
- def times(self) -> LoggingTimeFields:
5384
- raise NotImplementedError
5385
-
5386
- #
5387
-
5388
- @property
5389
- @abc.abstractmethod
5390
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
5391
- raise NotImplementedError
5460
+ @ta.final
5461
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5462
+ return self.get_info(ty)
5392
5463
 
5393
- @property
5394
- @abc.abstractmethod
5395
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
5396
- 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
5397
5469
 
5398
- #
5399
5470
 
5400
- @abc.abstractmethod
5401
- def caller(self) -> ta.Optional[LoggingCaller]:
5402
- raise NotImplementedError
5471
+ @ta.final
5472
+ class SimpleLoggingContext(LoggingContext):
5473
+ def __init__(self, *infos: LoggingContextInfo) -> None:
5474
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {type(i): i for i in infos}
5403
5475
 
5404
- @abc.abstractmethod
5405
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
5406
- raise NotImplementedError
5476
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5477
+ return self._infos.get(ty)
5407
5478
 
5408
- #
5409
5479
 
5410
- @abc.abstractmethod
5411
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
5412
- raise NotImplementedError
5480
+ ##
5413
5481
 
5414
- @abc.abstractmethod
5415
- def process(self) -> ta.Optional[LoggingProcessInfo]:
5416
- raise NotImplementedError
5417
5482
 
5483
+ class CaptureLoggingContext(LoggingContext, Abstract):
5418
5484
  @abc.abstractmethod
5419
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
5420
- raise NotImplementedError
5485
+ def set_basic(
5486
+ self,
5487
+ name: str,
5421
5488
 
5422
- @abc.abstractmethod
5423
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
5489
+ msg: ta.Union[str, tuple, LoggingMsgFn],
5490
+ args: tuple,
5491
+ ) -> 'CaptureLoggingContext':
5424
5492
  raise NotImplementedError
5425
5493
 
5494
+ #
5426
5495
 
5427
- ##
5428
-
5429
-
5430
- class CaptureLoggingContext(LoggingContext, Abstract):
5431
5496
  class AlreadyCapturedError(Exception):
5432
5497
  pass
5433
5498
 
@@ -5458,80 +5523,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
5458
5523
 
5459
5524
  exc_info: LoggingExcInfoArg = False,
5460
5525
 
5461
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
5526
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
5462
5527
  stack_offset: int = 0,
5463
5528
  stack_info: bool = False,
5464
5529
  ) -> None:
5465
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
5466
-
5467
- #
5530
+ # TODO: Name, Msg, Extra
5468
5531
 
5469
5532
  if time_ns is None:
5470
5533
  time_ns = time.time_ns()
5471
- self._time_ns: int = time_ns
5472
-
5473
- #
5474
5534
 
5475
- if exc_info is True:
5476
- sys_exc_info = sys.exc_info()
5477
- if sys_exc_info[0] is not None:
5478
- exc_info = sys_exc_info
5479
- else:
5480
- exc_info = None
5481
- elif exc_info is False:
5482
- exc_info = None
5483
-
5484
- if exc_info is not None:
5485
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
5486
- if isinstance(exc_info, BaseException):
5487
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
5488
- else:
5489
- self._exc_info_tuple = exc_info
5490
-
5491
- #
5535
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
5536
+ self._set_info(
5537
+ LoggingContextInfos.Level.build(level),
5538
+ LoggingContextInfos.Time.build(time_ns),
5539
+ LoggingContextInfos.Exc.build(exc_info),
5540
+ )
5492
5541
 
5493
5542
  if caller is not CaptureLoggingContextImpl.NOT_SET:
5494
- self._caller = caller # type: ignore[assignment]
5543
+ self._infos[LoggingContextInfos.Caller] = caller
5495
5544
  else:
5496
5545
  self._stack_offset = stack_offset
5497
5546
  self._stack_info = stack_info
5498
5547
 
5499
- ##
5500
-
5501
- @property
5502
- def level(self) -> NamedLogLevel:
5503
- return self._level
5504
-
5505
- #
5506
-
5507
- @property
5508
- def time_ns(self) -> int:
5509
- return self._time_ns
5510
-
5511
- _times: LoggingTimeFields
5512
-
5513
- @property
5514
- def times(self) -> LoggingTimeFields:
5515
- try:
5516
- return self._times
5517
- except AttributeError:
5518
- pass
5519
-
5520
- times = self._times = LoggingTimeFields.build(self.time_ns)
5521
- return times
5548
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
5549
+ for info in infos:
5550
+ if info is not None:
5551
+ self._infos[type(info)] = info
5552
+ return self
5522
5553
 
5523
- #
5554
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5555
+ return self._infos.get(ty)
5524
5556
 
5525
- _exc_info: ta.Optional[LoggingExcInfo] = None
5526
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
5557
+ ##
5527
5558
 
5528
- @property
5529
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
5530
- return self._exc_info
5559
+ def set_basic(
5560
+ self,
5561
+ name: str,
5531
5562
 
5532
- @property
5533
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
5534
- return self._exc_info_tuple
5563
+ msg: ta.Union[str, tuple, LoggingMsgFn],
5564
+ args: tuple,
5565
+ ) -> 'CaptureLoggingContextImpl':
5566
+ return self._set_info(
5567
+ LoggingContextInfos.Name(name),
5568
+ LoggingContextInfos.Msg.build(msg, *args),
5569
+ )
5535
5570
 
5536
5571
  ##
5537
5572
 
@@ -5545,74 +5580,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
5545
5580
 
5546
5581
  _has_captured: bool = False
5547
5582
 
5548
- _caller: ta.Optional[LoggingCaller]
5549
- _source_file: ta.Optional[LoggingSourceFileInfo]
5550
-
5551
- _thread: ta.Optional[LoggingThreadInfo]
5552
- _process: ta.Optional[LoggingProcessInfo]
5553
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
5554
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
5555
-
5556
5583
  def capture(self) -> None:
5557
5584
  if self._has_captured:
5558
5585
  raise CaptureLoggingContextImpl.AlreadyCapturedError
5559
5586
  self._has_captured = True
5560
5587
 
5561
- if not hasattr(self, '_caller'):
5562
- self._caller = LoggingCaller.find(
5588
+ if LoggingContextInfos.Caller not in self._infos:
5589
+ self._set_info(LoggingContextInfos.Caller.build(
5563
5590
  self._stack_offset + 1,
5564
5591
  stack_info=self._stack_info,
5565
- )
5566
-
5567
- if (caller := self._caller) is not None:
5568
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
5569
- else:
5570
- self._source_file = None
5571
-
5572
- self._thread = LoggingThreadInfo.build()
5573
- self._process = LoggingProcessInfo.build()
5574
- self._multiprocessing = LoggingMultiprocessingInfo.build()
5575
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
5576
-
5577
- #
5578
-
5579
- def caller(self) -> ta.Optional[LoggingCaller]:
5580
- try:
5581
- return self._caller
5582
- except AttributeError:
5583
- raise CaptureLoggingContext.NotCapturedError from None
5584
-
5585
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
5586
- try:
5587
- return self._source_file
5588
- except AttributeError:
5589
- raise CaptureLoggingContext.NotCapturedError from None
5590
-
5591
- #
5592
-
5593
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
5594
- try:
5595
- return self._thread
5596
- except AttributeError:
5597
- raise CaptureLoggingContext.NotCapturedError from None
5598
-
5599
- def process(self) -> ta.Optional[LoggingProcessInfo]:
5600
- try:
5601
- return self._process
5602
- except AttributeError:
5603
- raise CaptureLoggingContext.NotCapturedError from None
5592
+ ))
5604
5593
 
5605
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
5606
- try:
5607
- return self._multiprocessing
5608
- except AttributeError:
5609
- raise CaptureLoggingContext.NotCapturedError from None
5594
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
5595
+ self._set_info(LoggingContextInfos.SourceFile.build(
5596
+ caller.file_path,
5597
+ ))
5610
5598
 
5611
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
5612
- try:
5613
- return self._asyncio_task
5614
- except AttributeError:
5615
- raise CaptureLoggingContext.NotCapturedError from None
5599
+ self._set_info(
5600
+ LoggingContextInfos.Thread.build(),
5601
+ LoggingContextInfos.Process.build(),
5602
+ LoggingContextInfos.Multiprocessing.build(),
5603
+ LoggingContextInfos.AsyncioTask.build(),
5604
+ )
5616
5605
 
5617
5606
 
5618
5607
  ########################################
@@ -5689,10 +5678,14 @@ def _locking_logging_module_lock() -> ta.Iterator[None]:
5689
5678
  def configure_standard_logging(
5690
5679
  level: ta.Union[int, str] = logging.INFO,
5691
5680
  *,
5692
- json: bool = False,
5693
5681
  target: ta.Optional[logging.Logger] = None,
5682
+
5694
5683
  force: bool = False,
5684
+
5695
5685
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
5686
+
5687
+ formatter: ta.Optional[logging.Formatter] = None, # noqa
5688
+ json: bool = False,
5696
5689
  ) -> ta.Optional[StandardConfiguredLoggingHandler]:
5697
5690
  with _locking_logging_module_lock():
5698
5691
  if target is None:
@@ -5713,11 +5706,11 @@ def configure_standard_logging(
5713
5706
 
5714
5707
  #
5715
5708
 
5716
- formatter: logging.Formatter
5717
- if json:
5718
- formatter = JsonLoggingFormatter()
5719
- else:
5720
- formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
5709
+ if formatter is None:
5710
+ if json:
5711
+ formatter = JsonLoggingFormatter()
5712
+ else:
5713
+ formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS)) # noqa
5721
5714
  handler.setFormatter(formatter)
5722
5715
 
5723
5716
  #
@@ -5917,36 +5910,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
5917
5910
 
5918
5911
  ##
5919
5912
 
5920
- @classmethod
5921
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
5922
- if callable(msg):
5923
- if args:
5924
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
5925
- x = msg()
5926
- if isinstance(x, str):
5927
- return x, ()
5928
- elif isinstance(x, tuple):
5929
- if x:
5930
- return x[0], x[1:]
5931
- else:
5932
- return '', ()
5933
- else:
5934
- raise TypeError(x)
5935
-
5936
- elif isinstance(msg, tuple):
5937
- if args:
5938
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
5939
- if msg:
5940
- return msg[0], msg[1:]
5941
- else:
5942
- return '', ()
5943
-
5944
- elif isinstance(msg, str):
5945
- return msg, args
5946
-
5947
- else:
5948
- raise TypeError(msg)
5949
-
5950
5913
  @abc.abstractmethod
5951
5914
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
5952
5915
  raise NotImplementedError
@@ -5987,144 +5950,560 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
5987
5950
 
5988
5951
  ########################################
5989
5952
  # ../../../../../omlish/logs/std/records.py
5953
+ """
5954
+ TODO:
5955
+ - TypedDict?
5956
+ """
5990
5957
 
5991
5958
 
5992
5959
  ##
5993
5960
 
5994
5961
 
5995
- # Ref:
5996
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
5997
- #
5998
- # LogRecord:
5999
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
6000
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
6001
- #
6002
- # LogRecord.__init__ args:
6003
- # - name: str
6004
- # - level: int
6005
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
6006
- # - lineno: int - May be 0.
6007
- # - msg: str
6008
- # - args: tuple | dict | 1-tuple[dict]
6009
- # - exc_info: LoggingExcInfoTuple | None
6010
- # - func: str | None = None -> funcName
6011
- # - sinfo: str | None = None -> stack_info
6012
- #
6013
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6014
- # Name of the logger used to log the call. Unmodified by ctor.
6015
- name=str,
5962
+ class LoggingContextInfoRecordAdapters:
5963
+ # Ref:
5964
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
5965
+ #
5966
+ # LogRecord:
5967
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
5968
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
5969
+ #
6016
5970
 
6017
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
6018
- # (see Using arbitrary objects as messages). Unmodified by ctor.
6019
- msg=str,
5971
+ def __new__(cls, *args, **kwargs): # noqa
5972
+ raise TypeError
6020
5973
 
6021
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
6022
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
6023
- # the mapping, but is otherwise unmodified.
6024
- args=ta.Union[tuple, dict],
5974
+ class Adapter(Abstract, ta.Generic[T]):
5975
+ @property
5976
+ @abc.abstractmethod
5977
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
5978
+ raise NotImplementedError
6025
5979
 
6026
- #
5980
+ #
6027
5981
 
6028
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
6029
- # `getLevelName(level)`.
6030
- levelname=str,
5982
+ @ta.final
5983
+ class NOT_SET: # noqa
5984
+ def __new__(cls, *args, **kwargs): # noqa
5985
+ raise TypeError
6031
5986
 
6032
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
6033
- levelno=int,
5987
+ class RecordAttr(ta.NamedTuple):
5988
+ name: str
5989
+ type: ta.Any
5990
+ default: ta.Any
6034
5991
 
6035
- #
5992
+ # @abc.abstractmethod
5993
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
6036
5994
 
6037
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
6038
- # to "(unknown file)" by Logger.findCaller / Logger._log.
6039
- pathname=str,
5995
+ @property
5996
+ @abc.abstractmethod
5997
+ def _record_attrs(self) -> ta.Union[
5998
+ ta.Mapping[str, ta.Any],
5999
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
6000
+ ]:
6001
+ raise NotImplementedError
6040
6002
 
6041
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
6042
- filename=str,
6003
+ #
6043
6004
 
6044
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
6045
- # "Unknown module".
6046
- module=str,
6005
+ @abc.abstractmethod
6006
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6007
+ raise NotImplementedError
6047
6008
 
6048
- #
6009
+ #
6049
6010
 
6050
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
6051
- exc_info=ta.Optional[LoggingExcInfoTuple],
6011
+ @abc.abstractmethod
6012
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
6013
+ raise NotImplementedError
6052
6014
 
6053
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
6054
- exc_text=ta.Optional[str],
6015
+ #
6055
6016
 
6056
- #
6017
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6018
+ super().__init_subclass__(**kwargs)
6057
6019
 
6058
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
6059
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
6060
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
6061
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
6062
- stack_info=ta.Optional[str],
6020
+ if Abstract in cls.__bases__:
6021
+ return
6063
6022
 
6064
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
6065
- # Logger.findCaller / Logger._log.
6066
- lineno=int,
6023
+ if 'record_attrs' in cls.__dict__:
6024
+ raise TypeError(cls)
6025
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
6026
+ raise TypeError(ra)
6027
+
6028
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
6029
+ for n, v in ra.items():
6030
+ if not n or not isinstance(n, str) or n in rd:
6031
+ raise AttributeError(n)
6032
+ if isinstance(v, tuple):
6033
+ t, d = v
6034
+ else:
6035
+ t, d = v, cls.NOT_SET
6036
+ rd[n] = cls.RecordAttr(
6037
+ name=n,
6038
+ type=t,
6039
+ default=d,
6040
+ )
6041
+ cls.record_attrs = rd
6067
6042
 
6068
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
6069
- # "(unknown function)" by Logger.findCaller / Logger._log.
6070
- funcName=str,
6043
+ class RequiredAdapter(Adapter[T], Abstract):
6044
+ @property
6045
+ @abc.abstractmethod
6046
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
6047
+ raise NotImplementedError
6071
6048
 
6072
- #
6049
+ #
6073
6050
 
6074
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
6075
- #
6076
- # See:
6077
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6078
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6079
- #
6080
- created=float,
6051
+ @ta.final
6052
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6053
+ if (info := ctx.get_info(self.info_cls)) is not None:
6054
+ return self._info_to_record(info)
6055
+ else:
6056
+ raise TypeError # FIXME: fallback?
6081
6057
 
6082
- # Millisecond portion of the time when the LogRecord was created.
6083
- msecs=float,
6058
+ @abc.abstractmethod
6059
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
6060
+ raise NotImplementedError
6084
6061
 
6085
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
6086
- relativeCreated=float,
6062
+ #
6087
6063
 
6088
- #
6064
+ @abc.abstractmethod
6065
+ def record_to_info(self, rec: logging.LogRecord) -> T:
6066
+ raise NotImplementedError
6089
6067
 
6090
- # Thread ID if available, and `logging.logThreads` is truthy.
6091
- thread=ta.Optional[int],
6068
+ #
6092
6069
 
6093
- # Thread name if available, and `logging.logThreads` is truthy.
6094
- threadName=ta.Optional[str],
6070
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6071
+ super().__init_subclass__(**kwargs)
6095
6072
 
6096
- #
6073
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
6074
+ raise TypeError(cls.record_attrs)
6075
+
6076
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
6077
+ @property
6078
+ @abc.abstractmethod
6079
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
6080
+ raise NotImplementedError
6081
+
6082
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
6083
+
6084
+ #
6085
+
6086
+ @ta.final
6087
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6088
+ if (info := ctx.get_info(self.info_cls)) is not None:
6089
+ return self._info_to_record(info)
6090
+ else:
6091
+ return self.record_defaults
6092
+
6093
+ @abc.abstractmethod
6094
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
6095
+ raise NotImplementedError
6096
+
6097
+ #
6098
+
6099
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6100
+ super().__init_subclass__(**kwargs)
6101
+
6102
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
6103
+ if any(d is cls.NOT_SET for d in dd.values()):
6104
+ raise TypeError(cls.record_attrs)
6105
+ cls.record_defaults = dd
6097
6106
 
6098
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
6099
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
6100
- # as 'MainProcess'.
6101
6107
  #
6102
- # As noted by stdlib:
6108
+
6109
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
6110
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
6111
+
6112
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6113
+ # Name of the logger used to log the call. Unmodified by ctor.
6114
+ name=str,
6115
+ )
6116
+
6117
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
6118
+ return dict(
6119
+ name=info.name,
6120
+ )
6121
+
6122
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
6123
+ return LoggingContextInfos.Name(
6124
+ name=rec.name,
6125
+ )
6126
+
6127
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
6128
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
6129
+
6130
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6131
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
6132
+ # `getLevelName(level)`.
6133
+ levelname=str,
6134
+
6135
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
6136
+ levelno=int,
6137
+ )
6138
+
6139
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
6140
+ return dict(
6141
+ levelname=info.name,
6142
+ levelno=int(info.level),
6143
+ )
6144
+
6145
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
6146
+ return LoggingContextInfos.Level.build(rec.levelno)
6147
+
6148
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
6149
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
6150
+
6151
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6152
+ # The format string passed in the original logging call. Merged with args to produce message, or an
6153
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
6154
+ msg=str,
6155
+
6156
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
6157
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
6158
+ # Mapping into just the mapping, but is otherwise unmodified.
6159
+ args=ta.Union[tuple, dict, None],
6160
+ )
6161
+
6162
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
6163
+ return dict(
6164
+ msg=info.msg,
6165
+ args=info.args,
6166
+ )
6167
+
6168
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
6169
+ return LoggingContextInfos.Msg(
6170
+ msg=rec.msg,
6171
+ args=rec.args,
6172
+ )
6173
+
6174
+ # FIXME: handled specially - all unknown attrs on LogRecord
6175
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
6176
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
6103
6177
  #
6104
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
6105
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
6178
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
6179
+ # # FIXME:
6180
+ # # if extra is not None:
6181
+ # # for key in extra:
6182
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
6183
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
6184
+ # # rv.__dict__[key] = extra[key]
6185
+ # return dict()
6106
6186
  #
6107
- processName=ta.Optional[str],
6187
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
6188
+ # return None
6189
+
6190
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
6191
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
6192
+
6193
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6194
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
6195
+ # `time.time()`.
6196
+ #
6197
+ # See:
6198
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6199
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6200
+ #
6201
+ created=float,
6202
+
6203
+ # Millisecond portion of the time when the LogRecord was created.
6204
+ msecs=float,
6205
+
6206
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
6207
+ relativeCreated=float,
6208
+ )
6108
6209
 
6109
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
6110
- # None.
6111
- process=ta.Optional[int],
6210
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
6211
+ return dict(
6212
+ created=info.secs,
6213
+ msecs=info.msecs,
6214
+ relativeCreated=info.relative_secs,
6215
+ )
6112
6216
 
6113
- #
6217
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
6218
+ return LoggingContextInfos.Time.build(
6219
+ int(rec.created * 1e9),
6220
+ )
6114
6221
 
6115
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
6116
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
6117
- taskName=ta.Optional[str],
6118
- )
6222
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
6223
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
6224
+
6225
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6226
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
6227
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
6228
+
6229
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
6230
+ exc_text=(ta.Optional[str], None),
6231
+ )
6232
+
6233
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
6234
+ return dict(
6235
+ exc_info=info.info_tuple,
6236
+ exc_text=None,
6237
+ )
6238
+
6239
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
6240
+ # FIXME:
6241
+ # error: Argument 1 to "build" of "Exc" has incompatible type
6242
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
6243
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
6244
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
6245
+
6246
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
6247
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
6248
+
6249
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
6250
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
6251
+
6252
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
6253
+
6254
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6255
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
6256
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
6257
+ pathname=(str, _UNKNOWN_PATH_NAME),
6258
+
6259
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
6260
+ # y Logger.findCaller / Logger._log.
6261
+ lineno=(int, 0),
6262
+
6263
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
6264
+ # "(unknown function)" by Logger.findCaller / Logger._log.
6265
+ funcName=(str, _UNKNOWN_FUNC_NAME),
6266
+
6267
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
6268
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
6269
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
6270
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
6271
+ # stripped of exactly one trailing `\n` if present.
6272
+ stack_info=(ta.Optional[str], None),
6273
+ )
6274
+
6275
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
6276
+ if (sinfo := caller.stack_info) is not None:
6277
+ stack_info: ta.Optional[str] = '\n'.join([
6278
+ self._STACK_INFO_PREFIX,
6279
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
6280
+ ])
6281
+ else:
6282
+ stack_info = None
6283
+
6284
+ return dict(
6285
+ pathname=caller.file_path,
6286
+
6287
+ lineno=caller.line_no,
6288
+ funcName=caller.func_name,
6289
+
6290
+ stack_info=stack_info,
6291
+ )
6292
+
6293
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
6294
+ # FIXME: piecemeal?
6295
+ if (
6296
+ rec.pathname != self._UNKNOWN_PATH_NAME and
6297
+ rec.lineno != 0 and
6298
+ rec.funcName != self._UNKNOWN_FUNC_NAME
6299
+ ):
6300
+ if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
6301
+ sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
6302
+ return LoggingContextInfos.Caller(
6303
+ file_path=rec.pathname,
6304
+
6305
+ line_no=rec.lineno,
6306
+ func_name=rec.funcName,
6307
+
6308
+ stack_info=sinfo,
6309
+ )
6310
+
6311
+ return None
6312
+
6313
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
6314
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
6315
+
6316
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
6317
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
6318
+ # pathname.
6319
+ filename=str,
6320
+
6321
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
6322
+ # "Unknown module".
6323
+ module=str,
6324
+ )
6325
+
6326
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
6327
+
6328
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
6329
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
6330
+ return dict(
6331
+ filename=info.file_name,
6332
+ module=info.module,
6333
+ )
6334
+
6335
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
6336
+ return dict(
6337
+ filename=caller.file_path,
6338
+ module=self._UNKNOWN_MODULE,
6339
+ )
6119
6340
 
6120
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
6341
+ return dict(
6342
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
6343
+ module=self._UNKNOWN_MODULE,
6344
+ )
6345
+
6346
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
6347
+ if (
6348
+ rec.module is not None and
6349
+ rec.module != self._UNKNOWN_MODULE
6350
+ ):
6351
+ return LoggingContextInfos.SourceFile(
6352
+ file_name=rec.filename,
6353
+ module=rec.module, # FIXME: piecemeal?
6354
+ )
6355
+
6356
+ return None
6357
+
6358
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
6359
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
6360
+
6361
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6362
+ # Thread ID if available, and `logging.logThreads` is truthy.
6363
+ thread=(ta.Optional[int], None),
6364
+
6365
+ # Thread name if available, and `logging.logThreads` is truthy.
6366
+ threadName=(ta.Optional[str], None),
6367
+ )
6368
+
6369
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
6370
+ if logging.logThreads:
6371
+ return dict(
6372
+ thread=info.ident,
6373
+ threadName=info.name,
6374
+ )
6375
+
6376
+ return self.record_defaults
6377
+
6378
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
6379
+ if (
6380
+ (ident := rec.thread) is not None and
6381
+ (name := rec.threadName) is not None
6382
+ ):
6383
+ return LoggingContextInfos.Thread(
6384
+ ident=ident,
6385
+ native_id=None,
6386
+ name=name,
6387
+ )
6388
+
6389
+ return None
6390
+
6391
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
6392
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
6393
+
6394
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6395
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
6396
+ # otherwise None.
6397
+ process=(ta.Optional[int], None),
6398
+ )
6399
+
6400
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
6401
+ if logging.logProcesses:
6402
+ return dict(
6403
+ process=info.pid,
6404
+ )
6405
+
6406
+ return self.record_defaults
6407
+
6408
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
6409
+ if (
6410
+ (pid := rec.process) is not None
6411
+ ):
6412
+ return LoggingContextInfos.Process(
6413
+ pid=pid,
6414
+ )
6415
+
6416
+ return None
6417
+
6418
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
6419
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
6420
+
6421
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
6422
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
6423
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
6424
+ # remains as 'MainProcess'.
6425
+ #
6426
+ # As noted by stdlib:
6427
+ #
6428
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
6429
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
6430
+ #
6431
+ processName=(ta.Optional[str], None),
6432
+ )
6433
+
6434
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
6435
+ if logging.logMultiprocessing:
6436
+ return dict(
6437
+ processName=info.process_name,
6438
+ )
6439
+
6440
+ return self.record_defaults
6441
+
6442
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
6443
+ if (
6444
+ (process_name := rec.processName) is not None
6445
+ ):
6446
+ return LoggingContextInfos.Multiprocessing(
6447
+ process_name=process_name,
6448
+ )
6449
+
6450
+ return None
6451
+
6452
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
6453
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
6454
+
6455
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
6456
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
6457
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
6458
+ taskName=(ta.Optional[str], None),
6459
+ )
6460
+
6461
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
6462
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
6463
+ return dict(
6464
+ taskName=info.name,
6465
+ )
6466
+
6467
+ return self.record_defaults
6468
+
6469
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
6470
+ if (
6471
+ (name := getattr(rec, 'taskName', None)) is not None
6472
+ ):
6473
+ return LoggingContextInfos.AsyncioTask(
6474
+ name=name,
6475
+ )
6476
+
6477
+ return None
6478
+
6479
+
6480
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
6481
+ LoggingContextInfoRecordAdapters.Name(),
6482
+ LoggingContextInfoRecordAdapters.Level(),
6483
+ LoggingContextInfoRecordAdapters.Msg(),
6484
+ LoggingContextInfoRecordAdapters.Time(),
6485
+ LoggingContextInfoRecordAdapters.Exc(),
6486
+ LoggingContextInfoRecordAdapters.Caller(),
6487
+ LoggingContextInfoRecordAdapters.SourceFile(),
6488
+ LoggingContextInfoRecordAdapters.Thread(),
6489
+ LoggingContextInfoRecordAdapters.Process(),
6490
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
6491
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
6492
+ ]
6493
+
6494
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
6495
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
6496
+ }
6497
+
6498
+
6499
+ ##
6121
6500
 
6122
6501
 
6123
6502
  # Formatter:
6124
6503
  # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
6125
6504
  # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
6126
6505
  #
6127
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6506
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6128
6507
  # The logged message, computed as msg % args. Set to `record.getMessage()`.
6129
6508
  message=str,
6130
6509
 
@@ -6138,20 +6517,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6138
6517
  exc_text=ta.Optional[str],
6139
6518
  )
6140
6519
 
6141
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
6142
-
6143
6520
 
6144
6521
  ##
6145
6522
 
6146
6523
 
6524
+ _KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
6525
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
6526
+ )
6527
+
6528
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
6529
+
6530
+
6147
6531
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
6148
6532
  pass
6149
6533
 
6150
6534
 
6151
6535
  def _check_std_logging_record_attrs() -> None:
6536
+ if (
6537
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
6538
+ len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
6539
+ ):
6540
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
6541
+
6152
6542
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
6153
6543
 
6154
- if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
6544
+ if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
6155
6545
  import warnings # noqa
6156
6546
 
6157
6547
  warnings.warn(
@@ -6167,116 +6557,43 @@ _check_std_logging_record_attrs()
6167
6557
 
6168
6558
 
6169
6559
  class LoggingContextLogRecord(logging.LogRecord):
6170
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
6560
+ # LogRecord.__init__ args:
6561
+ # - name: str
6562
+ # - level: int
6563
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
6564
+ # - lineno: int - May be 0.
6565
+ # - msg: str
6566
+ # - args: tuple | dict | 1-tuple[dict]
6567
+ # - exc_info: LoggingExcInfoTuple | None
6568
+ # - func: str | None = None -> funcName
6569
+ # - sinfo: str | None = None -> stack_info
6171
6570
 
6172
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
6173
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
6174
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
6571
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
6572
+ self._logging_context = _logging_context
6175
6573
 
6176
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
6574
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
6575
+ self.__dict__.update(ad.context_to_record(_logging_context))
6177
6576
 
6178
- def __init__( # noqa
6179
- self,
6180
- # name,
6181
- # level,
6182
- # pathname,
6183
- # lineno,
6184
- # msg,
6185
- # args,
6186
- # exc_info,
6187
- # func=None,
6188
- # sinfo=None,
6189
- # **kwargs,
6190
- *,
6191
- name: str,
6192
- msg: str,
6193
- args: ta.Union[tuple, dict],
6194
6577
 
6195
- _logging_context: LoggingContext,
6196
- ) -> None:
6197
- ctx = _logging_context
6198
-
6199
- self.name: str = name
6200
-
6201
- self.msg: str = msg
6202
-
6203
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
6204
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
6205
- args = args[0] # type: ignore[assignment]
6206
- self.args: ta.Union[tuple, dict] = args
6207
-
6208
- self.levelname: str = logging.getLevelName(ctx.level)
6209
- self.levelno: int = ctx.level
6210
-
6211
- if (caller := ctx.caller()) is not None:
6212
- self.pathname: str = caller.file_path
6213
- else:
6214
- self.pathname = self._UNKNOWN_PATH_NAME
6215
-
6216
- if (src_file := ctx.source_file()) is not None:
6217
- self.filename: str = src_file.file_name
6218
- self.module: str = src_file.module
6219
- else:
6220
- self.filename = self.pathname
6221
- self.module = self._UNKNOWN_MODULE
6222
-
6223
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
6224
- self.exc_text: ta.Optional[str] = None
6225
-
6226
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
6227
- if caller is not None:
6228
- if (sinfo := caller.stack_info) is not None:
6229
- self.stack_info: ta.Optional[str] = '\n'.join([
6230
- self._STACK_INFO_PREFIX,
6231
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
6232
- ])
6233
- else:
6234
- self.stack_info = None
6235
-
6236
- self.lineno: int = caller.line_no
6237
- self.funcName: str = caller.name
6238
-
6239
- else:
6240
- self.stack_info = None
6241
-
6242
- self.lineno = 0
6243
- self.funcName = self._UNKNOWN_FUNC_NAME
6578
+ ##
6244
6579
 
6245
- times = ctx.times
6246
- self.created: float = times.created
6247
- self.msecs: float = times.msecs
6248
- self.relativeCreated: float = times.relative_created
6249
6580
 
6250
- if logging.logThreads:
6251
- thread = check.not_none(ctx.thread())
6252
- self.thread: ta.Optional[int] = thread.ident
6253
- self.threadName: ta.Optional[str] = thread.name
6254
- else:
6255
- self.thread = None
6256
- self.threadName = None
6581
+ @ta.final
6582
+ class LogRecordLoggingContext(LoggingContext):
6583
+ def __init__(self, rec: logging.LogRecord) -> None:
6584
+ if isinstance(rec, LoggingContextLogRecord):
6585
+ raise TypeError(rec)
6257
6586
 
6258
- if logging.logProcesses:
6259
- process = check.not_none(ctx.process())
6260
- self.process: ta.Optional[int] = process.pid
6261
- else:
6262
- self.process = None
6587
+ self._rec = rec
6263
6588
 
6264
- if logging.logMultiprocessing:
6265
- if (mp := ctx.multiprocessing()) is not None:
6266
- self.processName: ta.Optional[str] = mp.process_name
6267
- else:
6268
- self.processName = None
6269
- else:
6270
- self.processName = None
6589
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
6590
+ type(info): info
6591
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
6592
+ if (info := ad.record_to_info(rec)) is not None
6593
+ }
6271
6594
 
6272
- # Absent <3.12
6273
- if getattr(logging, 'logAsyncioTasks', None):
6274
- if (at := ctx.asyncio_task()) is not None:
6275
- self.taskName: ta.Optional[str] = at.name
6276
- else:
6277
- self.taskName = None
6278
- else:
6279
- self.taskName = None
6595
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
6596
+ return self._infos.get(ty)
6280
6597
 
6281
6598
 
6282
6599
  ########################################
@@ -6303,21 +6620,20 @@ class StdLogger(Logger):
6303
6620
  return self._std.getEffectiveLevel()
6304
6621
 
6305
6622
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
6306
- if not self.is_enabled_for(ctx.level):
6623
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
6307
6624
  return
6308
6625
 
6309
- ctx.capture()
6310
-
6311
- ms, args = self._prepare_msg_args(msg, *args)
6312
-
6313
- rec = LoggingContextLogRecord(
6626
+ ctx.set_basic(
6314
6627
  name=self._std.name,
6315
- msg=ms,
6316
- args=args,
6317
6628
 
6318
- _logging_context=ctx,
6629
+ msg=msg,
6630
+ args=args,
6319
6631
  )
6320
6632
 
6633
+ ctx.capture()
6634
+
6635
+ rec = LoggingContextLogRecord(_logging_context=ctx)
6636
+
6321
6637
  self._std.handle(rec)
6322
6638
 
6323
6639