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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
omdev/scripts/ci.py CHANGED
@@ -126,6 +126,13 @@ U = ta.TypeVar('U')
126
126
  # ../../omlish/lite/timeouts.py
127
127
  TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.DEFAULT'], ta.Iterable['TimeoutLike'], float, None] # ta.TypeAlias
128
128
 
129
+ # ../../omlish/logs/infos.py
130
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
131
+ LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
132
+ LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
133
+ LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
134
+ LoggingContextInfo = ta.Any # ta.TypeAlias
135
+
129
136
  # ../../omlish/sockets/bind.py
130
137
  SocketBinderT = ta.TypeVar('SocketBinderT', bound='SocketBinder')
131
138
  SocketBinderConfigT = ta.TypeVar('SocketBinderConfigT', bound='SocketBinder.Config')
@@ -149,9 +156,7 @@ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
149
156
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
150
157
 
151
158
  # ../../omlish/logs/contexts.py
152
- LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
153
- LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
154
- LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
159
+ LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
155
160
 
156
161
  # ../../omlish/sockets/server/handlers.py
157
162
  SocketServerHandler = ta.Callable[['SocketAndAddress'], None] # ta.TypeAlias
@@ -162,9 +167,6 @@ DataServerTargetT = ta.TypeVar('DataServerTargetT', bound='DataServerTarget')
162
167
  # ../../omlish/http/coro/server/server.py
163
168
  CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
164
169
 
165
- # ../../omlish/logs/base.py
166
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
167
-
168
170
  # ../../omlish/subprocesses/base.py
169
171
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
170
172
 
@@ -1800,124 +1802,6 @@ def format_num_bytes(num_bytes: int) -> str:
1800
1802
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
1801
1803
 
1802
1804
 
1803
- ########################################
1804
- # ../../../omlish/logs/infos.py
1805
-
1806
-
1807
- ##
1808
-
1809
-
1810
- def logging_context_info(cls):
1811
- return cls
1812
-
1813
-
1814
- ##
1815
-
1816
-
1817
- @logging_context_info
1818
- @ta.final
1819
- class LoggingSourceFileInfo(ta.NamedTuple):
1820
- file_name: str
1821
- module: str
1822
-
1823
- @classmethod
1824
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
1825
- if file_path is None:
1826
- return None
1827
-
1828
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
1829
- try:
1830
- file_name = os.path.basename(file_path)
1831
- module = os.path.splitext(file_name)[0]
1832
- except (TypeError, ValueError, AttributeError):
1833
- return None
1834
-
1835
- return cls(
1836
- file_name=file_name,
1837
- module=module,
1838
- )
1839
-
1840
-
1841
- ##
1842
-
1843
-
1844
- @logging_context_info
1845
- @ta.final
1846
- class LoggingThreadInfo(ta.NamedTuple):
1847
- ident: int
1848
- native_id: ta.Optional[int]
1849
- name: str
1850
-
1851
- @classmethod
1852
- def build(cls) -> 'LoggingThreadInfo':
1853
- return cls(
1854
- ident=threading.get_ident(),
1855
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
1856
- name=threading.current_thread().name,
1857
- )
1858
-
1859
-
1860
- ##
1861
-
1862
-
1863
- @logging_context_info
1864
- @ta.final
1865
- class LoggingProcessInfo(ta.NamedTuple):
1866
- pid: int
1867
-
1868
- @classmethod
1869
- def build(cls) -> 'LoggingProcessInfo':
1870
- return cls(
1871
- pid=os.getpid(),
1872
- )
1873
-
1874
-
1875
- ##
1876
-
1877
-
1878
- @logging_context_info
1879
- @ta.final
1880
- class LoggingMultiprocessingInfo(ta.NamedTuple):
1881
- process_name: str
1882
-
1883
- @classmethod
1884
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
1885
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
1886
- if (mp := sys.modules.get('multiprocessing')) is None:
1887
- return None
1888
-
1889
- return cls(
1890
- process_name=mp.current_process().name,
1891
- )
1892
-
1893
-
1894
- ##
1895
-
1896
-
1897
- @logging_context_info
1898
- @ta.final
1899
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
1900
- name: str
1901
-
1902
- @classmethod
1903
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
1904
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
1905
- if (asyncio := sys.modules.get('asyncio')) is None:
1906
- return None
1907
-
1908
- try:
1909
- task = asyncio.current_task()
1910
- except Exception: # noqa
1911
- return None
1912
-
1913
- if task is None:
1914
- return None
1915
-
1916
- return cls(
1917
- name=task.get_name(), # Always non-None
1918
- )
1919
-
1920
-
1921
1805
  ########################################
1922
1806
  # ../../../omlish/logs/levels.py
1923
1807
 
@@ -4980,74 +4864,362 @@ class PredicateTimeout(Timeout):
4980
4864
 
4981
4865
 
4982
4866
  ########################################
4983
- # ../../../omlish/logs/callers.py
4867
+ # ../../../omlish/logs/infos.py
4868
+ """
4869
+ TODO:
4870
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
4871
+ """
4984
4872
 
4985
4873
 
4986
4874
  ##
4987
4875
 
4988
4876
 
4989
- @logging_context_info
4877
+ def logging_context_info(cls):
4878
+ return cls
4879
+
4880
+
4990
4881
  @ta.final
4991
- class LoggingCaller(ta.NamedTuple):
4992
- file_path: str
4993
- line_no: int
4994
- name: str
4995
- stack_info: ta.Optional[str]
4882
+ class LoggingContextInfos:
4883
+ def __new__(cls, *args, **kwargs): # noqa
4884
+ raise TypeError
4996
4885
 
4997
- @classmethod
4998
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
4999
- file_path = os.path.normcase(frame.f_code.co_filename)
4886
+ #
5000
4887
 
5001
- # Yes, really.
5002
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
5003
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
5004
- if 'importlib' in file_path and '_bootstrap' in file_path:
5005
- return True
4888
+ @logging_context_info
4889
+ @ta.final
4890
+ class Name(ta.NamedTuple):
4891
+ name: str
5006
4892
 
5007
- return False
4893
+ @logging_context_info
4894
+ @ta.final
4895
+ class Level(ta.NamedTuple):
4896
+ level: NamedLogLevel
4897
+ name: str
5008
4898
 
5009
- @classmethod
5010
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
5011
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
4899
+ @classmethod
4900
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
4901
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
4902
+ return cls(
4903
+ level=nl,
4904
+ name=logging.getLevelName(nl),
4905
+ )
4906
+
4907
+ @logging_context_info
4908
+ @ta.final
4909
+ class Msg(ta.NamedTuple):
4910
+ msg: str
4911
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
5012
4912
 
5013
- while f is not None:
5014
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
5015
- # stack_offset management.
5016
- if hasattr(f, 'f_code'):
5017
- return f
4913
+ @classmethod
4914
+ def build(
4915
+ cls,
4916
+ msg: ta.Union[str, tuple, LoggingMsgFn],
4917
+ *args: ta.Any,
4918
+ ) -> 'LoggingContextInfos.Msg':
4919
+ s: str
4920
+ a: ta.Any
4921
+
4922
+ if callable(msg):
4923
+ if args:
4924
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
4925
+ x = msg()
4926
+ if isinstance(x, str):
4927
+ s, a = x, ()
4928
+ elif isinstance(x, tuple):
4929
+ if x:
4930
+ s, a = x[0], x[1:]
4931
+ else:
4932
+ s, a = '', ()
4933
+ else:
4934
+ raise TypeError(x)
5018
4935
 
5019
- f = f.f_back
4936
+ elif isinstance(msg, tuple):
4937
+ if args:
4938
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
4939
+ if msg:
4940
+ s, a = msg[0], msg[1:]
4941
+ else:
4942
+ s, a = '', ()
5020
4943
 
5021
- return None
4944
+ elif isinstance(msg, str):
4945
+ s, a = msg, args
4946
+
4947
+ else:
4948
+ raise TypeError(msg)
4949
+
4950
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
4951
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
4952
+ a = a[0]
4953
+
4954
+ return cls(
4955
+ msg=s,
4956
+ args=a,
4957
+ )
4958
+
4959
+ @logging_context_info
4960
+ @ta.final
4961
+ class Extra(ta.NamedTuple):
4962
+ extra: ta.Mapping[ta.Any, ta.Any]
4963
+
4964
+ @logging_context_info
4965
+ @ta.final
4966
+ class Time(ta.NamedTuple):
4967
+ ns: int
4968
+ secs: float
4969
+ msecs: float
4970
+ relative_secs: float
4971
+
4972
+ @classmethod
4973
+ def get_std_start_ns(cls) -> int:
4974
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
4975
+
4976
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
4977
+ # an int.
4978
+ #
4979
+ # See:
4980
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
4981
+ #
4982
+ if isinstance(x, float):
4983
+ return int(x * 1e9)
4984
+ else:
4985
+ return x
4986
+
4987
+ @classmethod
4988
+ def build(
4989
+ cls,
4990
+ ns: int,
4991
+ *,
4992
+ start_ns: ta.Optional[int] = None,
4993
+ ) -> 'LoggingContextInfos.Time':
4994
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
4995
+ secs = ns / 1e9 # ns to float seconds
4996
+
4997
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
4998
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
4999
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
5000
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
5001
+
5002
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5003
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
5004
+ # ns -> sec conversion can round up, e.g:
5005
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5006
+ msecs = 0.0
5007
+
5008
+ if start_ns is None:
5009
+ start_ns = cls.get_std_start_ns()
5010
+ relative_secs = (ns - start_ns) / 1e6
5011
+
5012
+ return cls(
5013
+ ns=ns,
5014
+ secs=secs,
5015
+ msecs=msecs,
5016
+ relative_secs=relative_secs,
5017
+ )
5018
+
5019
+ @logging_context_info
5020
+ @ta.final
5021
+ class Exc(ta.NamedTuple):
5022
+ info: LoggingExcInfo
5023
+ info_tuple: LoggingExcInfoTuple
5024
+
5025
+ @classmethod
5026
+ def build(
5027
+ cls,
5028
+ arg: LoggingExcInfoArg = False,
5029
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
5030
+ if arg is True:
5031
+ sys_exc_info = sys.exc_info()
5032
+ if sys_exc_info[0] is not None:
5033
+ arg = sys_exc_info
5034
+ else:
5035
+ arg = None
5036
+ elif arg is False:
5037
+ arg = None
5038
+ if arg is None:
5039
+ return None
5040
+
5041
+ info: LoggingExcInfo = arg
5042
+ if isinstance(info, BaseException):
5043
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
5044
+ else:
5045
+ info_tuple = info
5046
+
5047
+ return cls(
5048
+ info=info,
5049
+ info_tuple=info_tuple,
5050
+ )
5051
+
5052
+ @logging_context_info
5053
+ @ta.final
5054
+ class Caller(ta.NamedTuple):
5055
+ file_path: str
5056
+ line_no: int
5057
+ func_name: str
5058
+ stack_info: ta.Optional[str]
5059
+
5060
+ @classmethod
5061
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
5062
+ file_path = os.path.normcase(frame.f_code.co_filename)
5063
+
5064
+ # Yes, really.
5065
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
5066
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
5067
+ if 'importlib' in file_path and '_bootstrap' in file_path:
5068
+ return True
5069
+
5070
+ return False
5071
+
5072
+ @classmethod
5073
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
5074
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
5075
+
5076
+ while f is not None:
5077
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
5078
+ # manual stack_offset management.
5079
+ if hasattr(f, 'f_code'):
5080
+ return f
5081
+
5082
+ f = f.f_back
5022
5083
 
5023
- @classmethod
5024
- def find(
5025
- cls,
5026
- ofs: int = 0,
5027
- *,
5028
- stack_info: bool = False,
5029
- ) -> ta.Optional['LoggingCaller']:
5030
- if (f := cls.find_frame(ofs + 1)) is None:
5031
5084
  return None
5032
5085
 
5033
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5034
- sinfo = None
5035
- if stack_info:
5036
- sio = io.StringIO()
5037
- traceback.print_stack(f, file=sio)
5038
- sinfo = sio.getvalue()
5039
- sio.close()
5040
- if sinfo[-1] == '\n':
5041
- sinfo = sinfo[:-1]
5086
+ @classmethod
5087
+ def build(
5088
+ cls,
5089
+ stack_offset: int = 0,
5090
+ *,
5091
+ stack_info: bool = False,
5092
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
5093
+ if (f := cls.find_frame(stack_offset + 1)) is None:
5094
+ return None
5095
+
5096
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5097
+ sinfo = None
5098
+ if stack_info:
5099
+ sio = io.StringIO()
5100
+ traceback.print_stack(f, file=sio)
5101
+ sinfo = sio.getvalue()
5102
+ sio.close()
5103
+ if sinfo[-1] == '\n':
5104
+ sinfo = sinfo[:-1]
5042
5105
 
5043
- return cls(
5044
- file_path=f.f_code.co_filename,
5045
- line_no=f.f_lineno or 0,
5046
- name=f.f_code.co_name,
5047
- stack_info=sinfo,
5106
+ return cls(
5107
+ file_path=f.f_code.co_filename,
5108
+ line_no=f.f_lineno or 0,
5109
+ func_name=f.f_code.co_name,
5110
+ stack_info=sinfo,
5111
+ )
5112
+
5113
+ @logging_context_info
5114
+ @ta.final
5115
+ class SourceFile(ta.NamedTuple):
5116
+ file_name: str
5117
+ module: str
5118
+
5119
+ @classmethod
5120
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
5121
+ if caller_file_path is None:
5122
+ return None
5123
+
5124
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
5125
+ try:
5126
+ file_name = os.path.basename(caller_file_path)
5127
+ module = os.path.splitext(file_name)[0]
5128
+ except (TypeError, ValueError, AttributeError):
5129
+ return None
5130
+
5131
+ return cls(
5132
+ file_name=file_name,
5133
+ module=module,
5134
+ )
5135
+
5136
+ @logging_context_info
5137
+ @ta.final
5138
+ class Thread(ta.NamedTuple):
5139
+ ident: int
5140
+ native_id: ta.Optional[int]
5141
+ name: str
5142
+
5143
+ @classmethod
5144
+ def build(cls) -> 'LoggingContextInfos.Thread':
5145
+ return cls(
5146
+ ident=threading.get_ident(),
5147
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
5148
+ name=threading.current_thread().name,
5149
+ )
5150
+
5151
+ @logging_context_info
5152
+ @ta.final
5153
+ class Process(ta.NamedTuple):
5154
+ pid: int
5155
+
5156
+ @classmethod
5157
+ def build(cls) -> 'LoggingContextInfos.Process':
5158
+ return cls(
5159
+ pid=os.getpid(),
5160
+ )
5161
+
5162
+ @logging_context_info
5163
+ @ta.final
5164
+ class Multiprocessing(ta.NamedTuple):
5165
+ process_name: str
5166
+
5167
+ @classmethod
5168
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
5169
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
5170
+ if (mp := sys.modules.get('multiprocessing')) is None:
5171
+ return None
5172
+
5173
+ return cls(
5174
+ process_name=mp.current_process().name,
5175
+ )
5176
+
5177
+ @logging_context_info
5178
+ @ta.final
5179
+ class AsyncioTask(ta.NamedTuple):
5180
+ name: str
5181
+
5182
+ @classmethod
5183
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
5184
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
5185
+ if (asyncio := sys.modules.get('asyncio')) is None:
5186
+ return None
5187
+
5188
+ try:
5189
+ task = asyncio.current_task()
5190
+ except Exception: # noqa
5191
+ return None
5192
+
5193
+ if task is None:
5194
+ return None
5195
+
5196
+ return cls(
5197
+ name=task.get_name(), # Always non-None
5198
+ )
5199
+
5200
+
5201
+ ##
5202
+
5203
+
5204
+ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5205
+ pass
5206
+
5207
+
5208
+ def _check_logging_start_time() -> None:
5209
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
5210
+ import warnings # noqa
5211
+
5212
+ warnings.warn(
5213
+ f'Unexpected logging start time detected: '
5214
+ f'get_std_start_ns={x}, '
5215
+ f'time.time()={t}',
5216
+ UnexpectedLoggingStartTimeWarning,
5048
5217
  )
5049
5218
 
5050
5219
 
5220
+ _check_logging_start_time()
5221
+
5222
+
5051
5223
  ########################################
5052
5224
  # ../../../omlish/logs/protocols.py
5053
5225
 
@@ -5138,110 +5310,25 @@ class JsonLoggingFormatter(logging.Formatter):
5138
5310
 
5139
5311
 
5140
5312
  ########################################
5141
- # ../../../omlish/logs/times.py
5313
+ # ../../../omlish/os/temp.py
5142
5314
 
5143
5315
 
5144
5316
  ##
5145
5317
 
5146
5318
 
5147
- @logging_context_info
5148
- @ta.final
5149
- class LoggingTimeFields(ta.NamedTuple):
5150
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5319
+ def make_temp_file(**kwargs: ta.Any) -> str:
5320
+ file_fd, file = tempfile.mkstemp(**kwargs)
5321
+ os.close(file_fd)
5322
+ return file
5151
5323
 
5152
- created: float
5153
- msecs: float
5154
- relative_created: float
5155
5324
 
5156
- @classmethod
5157
- def get_std_start_time_ns(cls) -> int:
5158
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5159
-
5160
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5161
- # int.
5162
- #
5163
- # See:
5164
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5165
- #
5166
- if isinstance(x, float):
5167
- return int(x * 1e9)
5168
- else:
5169
- return x
5170
-
5171
- @classmethod
5172
- def build(
5173
- cls,
5174
- time_ns: int,
5175
- *,
5176
- start_time_ns: ta.Optional[int] = None,
5177
- ) -> 'LoggingTimeFields':
5178
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5179
- created = time_ns / 1e9 # ns to float seconds
5180
-
5181
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5182
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5183
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5184
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5185
-
5186
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5187
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5188
- # ns -> sec conversion can round up, e.g:
5189
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5190
- msecs = 0.0
5191
-
5192
- if start_time_ns is None:
5193
- start_time_ns = cls.get_std_start_time_ns()
5194
- relative_created = (time_ns - start_time_ns) / 1e6
5195
-
5196
- return cls(
5197
- created=created,
5198
- msecs=msecs,
5199
- relative_created=relative_created,
5200
- )
5201
-
5202
-
5203
- ##
5204
-
5205
-
5206
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5207
- pass
5208
-
5209
-
5210
- def _check_logging_start_time() -> None:
5211
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5212
- import warnings # noqa
5213
-
5214
- warnings.warn(
5215
- f'Unexpected logging start time detected: '
5216
- f'get_std_start_time_ns={x}, '
5217
- f'time.time()={t}',
5218
- UnexpectedLoggingStartTimeWarning,
5219
- )
5220
-
5221
-
5222
- _check_logging_start_time()
5223
-
5224
-
5225
- ########################################
5226
- # ../../../omlish/os/temp.py
5227
-
5228
-
5229
- ##
5230
-
5231
-
5232
- def make_temp_file(**kwargs: ta.Any) -> str:
5233
- file_fd, file = tempfile.mkstemp(**kwargs)
5234
- os.close(file_fd)
5235
- return file
5236
-
5237
-
5238
- @contextlib.contextmanager
5239
- def temp_file_context(**kwargs: ta.Any) -> ta.Iterator[str]:
5240
- path = make_temp_file(**kwargs)
5241
- try:
5242
- yield path
5243
- finally:
5244
- unlink_if_exists(path)
5325
+ @contextlib.contextmanager
5326
+ def temp_file_context(**kwargs: ta.Any) -> ta.Iterator[str]:
5327
+ path = make_temp_file(**kwargs)
5328
+ try:
5329
+ yield path
5330
+ finally:
5331
+ unlink_if_exists(path)
5245
5332
 
5246
5333
 
5247
5334
  @contextlib.contextmanager
@@ -7552,68 +7639,36 @@ inj = InjectionApi()
7552
7639
 
7553
7640
 
7554
7641
  class LoggingContext(Abstract):
7555
- @property
7556
7642
  @abc.abstractmethod
7557
- def level(self) -> NamedLogLevel:
7643
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7558
7644
  raise NotImplementedError
7559
7645
 
7560
- #
7561
-
7562
- @property
7563
- @abc.abstractmethod
7564
- def time_ns(self) -> int:
7565
- raise NotImplementedError
7566
-
7567
- @property
7568
- @abc.abstractmethod
7569
- def times(self) -> LoggingTimeFields:
7570
- raise NotImplementedError
7571
-
7572
- #
7646
+ @ta.final
7647
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7648
+ return self.get_info(ty)
7573
7649
 
7574
- @property
7575
- @abc.abstractmethod
7576
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7577
- raise NotImplementedError
7650
+ @ta.final
7651
+ def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
7652
+ if (info := self.get_info(ty)) is None:
7653
+ raise TypeError(f'LoggingContextInfo absent: {ty}')
7654
+ return info
7578
7655
 
7579
- @property
7580
- @abc.abstractmethod
7581
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7582
- raise NotImplementedError
7656
+ ##
7583
7657
 
7584
- #
7585
7658
 
7659
+ class CaptureLoggingContext(LoggingContext, Abstract):
7586
7660
  @abc.abstractmethod
7587
- def caller(self) -> ta.Optional[LoggingCaller]:
7588
- raise NotImplementedError
7661
+ def set_basic(
7662
+ self,
7663
+ name: str,
7589
7664
 
7590
- @abc.abstractmethod
7591
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7665
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7666
+ args: tuple,
7667
+ ) -> 'CaptureLoggingContext':
7592
7668
  raise NotImplementedError
7593
7669
 
7594
7670
  #
7595
7671
 
7596
- @abc.abstractmethod
7597
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7598
- raise NotImplementedError
7599
-
7600
- @abc.abstractmethod
7601
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7602
- raise NotImplementedError
7603
-
7604
- @abc.abstractmethod
7605
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7606
- raise NotImplementedError
7607
-
7608
- @abc.abstractmethod
7609
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7610
- raise NotImplementedError
7611
-
7612
-
7613
- ##
7614
-
7615
-
7616
- class CaptureLoggingContext(LoggingContext, Abstract):
7617
7672
  class AlreadyCapturedError(Exception):
7618
7673
  pass
7619
7674
 
@@ -7644,80 +7699,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7644
7699
 
7645
7700
  exc_info: LoggingExcInfoArg = False,
7646
7701
 
7647
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
7702
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
7648
7703
  stack_offset: int = 0,
7649
7704
  stack_info: bool = False,
7650
7705
  ) -> None:
7651
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7652
-
7653
- #
7706
+ # TODO: Name, Msg, Extra
7654
7707
 
7655
7708
  if time_ns is None:
7656
7709
  time_ns = time.time_ns()
7657
- self._time_ns: int = time_ns
7658
7710
 
7659
- #
7660
-
7661
- if exc_info is True:
7662
- sys_exc_info = sys.exc_info()
7663
- if sys_exc_info[0] is not None:
7664
- exc_info = sys_exc_info
7665
- else:
7666
- exc_info = None
7667
- elif exc_info is False:
7668
- exc_info = None
7669
-
7670
- if exc_info is not None:
7671
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
7672
- if isinstance(exc_info, BaseException):
7673
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
7674
- else:
7675
- self._exc_info_tuple = exc_info
7676
-
7677
- #
7711
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
7712
+ self._set_info(
7713
+ LoggingContextInfos.Level.build(level),
7714
+ LoggingContextInfos.Time.build(time_ns),
7715
+ LoggingContextInfos.Exc.build(exc_info),
7716
+ )
7678
7717
 
7679
7718
  if caller is not CaptureLoggingContextImpl.NOT_SET:
7680
- self._caller = caller # type: ignore[assignment]
7719
+ self._infos[LoggingContextInfos.Caller] = caller
7681
7720
  else:
7682
7721
  self._stack_offset = stack_offset
7683
7722
  self._stack_info = stack_info
7684
7723
 
7685
- ##
7686
-
7687
- @property
7688
- def level(self) -> NamedLogLevel:
7689
- return self._level
7690
-
7691
- #
7692
-
7693
- @property
7694
- def time_ns(self) -> int:
7695
- return self._time_ns
7696
-
7697
- _times: LoggingTimeFields
7698
-
7699
- @property
7700
- def times(self) -> LoggingTimeFields:
7701
- try:
7702
- return self._times
7703
- except AttributeError:
7704
- pass
7705
-
7706
- times = self._times = LoggingTimeFields.build(self.time_ns)
7707
- return times
7724
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
7725
+ for info in infos:
7726
+ if info is not None:
7727
+ self._infos[type(info)] = info
7728
+ return self
7708
7729
 
7709
- #
7730
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7731
+ return self._infos.get(ty)
7710
7732
 
7711
- _exc_info: ta.Optional[LoggingExcInfo] = None
7712
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
7733
+ ##
7713
7734
 
7714
- @property
7715
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7716
- return self._exc_info
7735
+ def set_basic(
7736
+ self,
7737
+ name: str,
7717
7738
 
7718
- @property
7719
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7720
- return self._exc_info_tuple
7739
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7740
+ args: tuple,
7741
+ ) -> 'CaptureLoggingContextImpl':
7742
+ return self._set_info(
7743
+ LoggingContextInfos.Name(name),
7744
+ LoggingContextInfos.Msg.build(msg, *args),
7745
+ )
7721
7746
 
7722
7747
  ##
7723
7748
 
@@ -7731,74 +7756,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7731
7756
 
7732
7757
  _has_captured: bool = False
7733
7758
 
7734
- _caller: ta.Optional[LoggingCaller]
7735
- _source_file: ta.Optional[LoggingSourceFileInfo]
7736
-
7737
- _thread: ta.Optional[LoggingThreadInfo]
7738
- _process: ta.Optional[LoggingProcessInfo]
7739
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
7740
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
7741
-
7742
7759
  def capture(self) -> None:
7743
7760
  if self._has_captured:
7744
7761
  raise CaptureLoggingContextImpl.AlreadyCapturedError
7745
7762
  self._has_captured = True
7746
7763
 
7747
- if not hasattr(self, '_caller'):
7748
- self._caller = LoggingCaller.find(
7764
+ if LoggingContextInfos.Caller not in self._infos:
7765
+ self._set_info(LoggingContextInfos.Caller.build(
7749
7766
  self._stack_offset + 1,
7750
7767
  stack_info=self._stack_info,
7751
- )
7752
-
7753
- if (caller := self._caller) is not None:
7754
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
7755
- else:
7756
- self._source_file = None
7757
-
7758
- self._thread = LoggingThreadInfo.build()
7759
- self._process = LoggingProcessInfo.build()
7760
- self._multiprocessing = LoggingMultiprocessingInfo.build()
7761
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
7762
-
7763
- #
7764
-
7765
- def caller(self) -> ta.Optional[LoggingCaller]:
7766
- try:
7767
- return self._caller
7768
- except AttributeError:
7769
- raise CaptureLoggingContext.NotCapturedError from None
7770
-
7771
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7772
- try:
7773
- return self._source_file
7774
- except AttributeError:
7775
- raise CaptureLoggingContext.NotCapturedError from None
7776
-
7777
- #
7778
-
7779
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7780
- try:
7781
- return self._thread
7782
- except AttributeError:
7783
- raise CaptureLoggingContext.NotCapturedError from None
7784
-
7785
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7786
- try:
7787
- return self._process
7788
- except AttributeError:
7789
- raise CaptureLoggingContext.NotCapturedError from None
7768
+ ))
7790
7769
 
7791
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7792
- try:
7793
- return self._multiprocessing
7794
- except AttributeError:
7795
- raise CaptureLoggingContext.NotCapturedError from None
7770
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
7771
+ self._set_info(LoggingContextInfos.SourceFile.build(
7772
+ caller.file_path,
7773
+ ))
7796
7774
 
7797
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7798
- try:
7799
- return self._asyncio_task
7800
- except AttributeError:
7801
- raise CaptureLoggingContext.NotCapturedError from None
7775
+ self._set_info(
7776
+ LoggingContextInfos.Thread.build(),
7777
+ LoggingContextInfos.Process.build(),
7778
+ LoggingContextInfos.Multiprocessing.build(),
7779
+ LoggingContextInfos.AsyncioTask.build(),
7780
+ )
7802
7781
 
7803
7782
 
7804
7783
  ########################################
@@ -9615,36 +9594,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
9615
9594
 
9616
9595
  ##
9617
9596
 
9618
- @classmethod
9619
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
9620
- if callable(msg):
9621
- if args:
9622
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
9623
- x = msg()
9624
- if isinstance(x, str):
9625
- return x, ()
9626
- elif isinstance(x, tuple):
9627
- if x:
9628
- return x[0], x[1:]
9629
- else:
9630
- return '', ()
9631
- else:
9632
- raise TypeError(x)
9633
-
9634
- elif isinstance(msg, tuple):
9635
- if args:
9636
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
9637
- if msg:
9638
- return msg[0], msg[1:]
9639
- else:
9640
- return '', ()
9641
-
9642
- elif isinstance(msg, str):
9643
- return msg, args
9644
-
9645
- else:
9646
- raise TypeError(msg)
9647
-
9648
9597
  @abc.abstractmethod
9649
9598
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
9650
9599
  raise NotImplementedError
@@ -9685,137 +9634,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
9685
9634
 
9686
9635
  ########################################
9687
9636
  # ../../../omlish/logs/std/records.py
9637
+ """
9638
+ TODO:
9639
+ - TypedDict?
9640
+ """
9688
9641
 
9689
9642
 
9690
9643
  ##
9691
9644
 
9692
9645
 
9693
- # Ref:
9694
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9695
- #
9696
- # LogRecord:
9697
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
9698
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9699
- #
9700
- # LogRecord.__init__ args:
9701
- # - name: str
9702
- # - level: int
9703
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
9704
- # - lineno: int - May be 0.
9705
- # - msg: str
9706
- # - args: tuple | dict | 1-tuple[dict]
9707
- # - exc_info: LoggingExcInfoTuple | None
9708
- # - func: str | None = None -> funcName
9709
- # - sinfo: str | None = None -> stack_info
9710
- #
9711
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9712
- # Name of the logger used to log the call. Unmodified by ctor.
9713
- name=str,
9646
+ class LoggingContextInfoRecordAdapters:
9647
+ # Ref:
9648
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9649
+ #
9650
+ # LogRecord:
9651
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
9652
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9653
+ #
9714
9654
 
9715
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
9716
- # (see Using arbitrary objects as messages). Unmodified by ctor.
9717
- msg=str,
9655
+ def __new__(cls, *args, **kwargs): # noqa
9656
+ raise TypeError
9718
9657
 
9719
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
9720
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
9721
- # the mapping, but is otherwise unmodified.
9722
- args=ta.Union[tuple, dict],
9658
+ class Adapter(Abstract, ta.Generic[T]):
9659
+ @property
9660
+ @abc.abstractmethod
9661
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
9662
+ raise NotImplementedError
9723
9663
 
9724
- #
9664
+ #
9725
9665
 
9726
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9727
- # `getLevelName(level)`.
9728
- levelname=str,
9666
+ @ta.final
9667
+ class NOT_SET: # noqa
9668
+ def __new__(cls, *args, **kwargs): # noqa
9669
+ raise TypeError
9729
9670
 
9730
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9731
- levelno=int,
9671
+ class RecordAttr(ta.NamedTuple):
9672
+ name: str
9673
+ type: ta.Any
9674
+ default: ta.Any
9732
9675
 
9733
- #
9676
+ # @abc.abstractmethod
9677
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
9734
9678
 
9735
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
9736
- # to "(unknown file)" by Logger.findCaller / Logger._log.
9737
- pathname=str,
9679
+ @property
9680
+ @abc.abstractmethod
9681
+ def _record_attrs(self) -> ta.Union[
9682
+ ta.Mapping[str, ta.Any],
9683
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
9684
+ ]:
9685
+ raise NotImplementedError
9738
9686
 
9739
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
9740
- filename=str,
9687
+ #
9741
9688
 
9742
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
9743
- # "Unknown module".
9744
- module=str,
9689
+ @abc.abstractmethod
9690
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9691
+ raise NotImplementedError
9745
9692
 
9746
- #
9693
+ #
9747
9694
 
9748
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9749
- exc_info=ta.Optional[LoggingExcInfoTuple],
9695
+ @abc.abstractmethod
9696
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
9697
+ raise NotImplementedError
9750
9698
 
9751
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9752
- exc_text=ta.Optional[str],
9699
+ #
9753
9700
 
9754
- #
9701
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9702
+ super().__init_subclass__(**kwargs)
9755
9703
 
9756
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
9757
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
9758
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
9759
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
9760
- stack_info=ta.Optional[str],
9704
+ if Abstract in cls.__bases__:
9705
+ return
9761
9706
 
9762
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
9763
- # Logger.findCaller / Logger._log.
9764
- lineno=int,
9707
+ if 'record_attrs' in cls.__dict__:
9708
+ raise TypeError(cls)
9709
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
9710
+ raise TypeError(ra)
9711
+
9712
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
9713
+ for n, v in ra.items():
9714
+ if not n or not isinstance(n, str) or n in rd:
9715
+ raise AttributeError(n)
9716
+ if isinstance(v, tuple):
9717
+ t, d = v
9718
+ else:
9719
+ t, d = v, cls.NOT_SET
9720
+ rd[n] = cls.RecordAttr(
9721
+ name=n,
9722
+ type=t,
9723
+ default=d,
9724
+ )
9725
+ cls.record_attrs = rd
9765
9726
 
9766
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
9767
- # "(unknown function)" by Logger.findCaller / Logger._log.
9768
- funcName=str,
9727
+ class RequiredAdapter(Adapter[T], Abstract):
9728
+ @property
9729
+ @abc.abstractmethod
9730
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
9731
+ raise NotImplementedError
9769
9732
 
9770
- #
9733
+ #
9771
9734
 
9772
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
9773
- #
9774
- # See:
9775
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
9776
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
9777
- #
9778
- created=float,
9735
+ @ta.final
9736
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9737
+ if (info := ctx.get_info(self.info_cls)) is not None:
9738
+ return self._info_to_record(info)
9739
+ else:
9740
+ raise TypeError # FIXME: fallback?
9779
9741
 
9780
- # Millisecond portion of the time when the LogRecord was created.
9781
- msecs=float,
9742
+ @abc.abstractmethod
9743
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9744
+ raise NotImplementedError
9782
9745
 
9783
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9784
- relativeCreated=float,
9746
+ #
9785
9747
 
9786
- #
9748
+ @abc.abstractmethod
9749
+ def record_to_info(self, rec: logging.LogRecord) -> T:
9750
+ raise NotImplementedError
9787
9751
 
9788
- # Thread ID if available, and `logging.logThreads` is truthy.
9789
- thread=ta.Optional[int],
9752
+ #
9790
9753
 
9791
- # Thread name if available, and `logging.logThreads` is truthy.
9792
- threadName=ta.Optional[str],
9754
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9755
+ super().__init_subclass__(**kwargs)
9793
9756
 
9794
- #
9757
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
9758
+ raise TypeError(cls.record_attrs)
9759
+
9760
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
9761
+ @property
9762
+ @abc.abstractmethod
9763
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
9764
+ raise NotImplementedError
9765
+
9766
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
9767
+
9768
+ #
9769
+
9770
+ @ta.final
9771
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9772
+ if (info := ctx.get_info(self.info_cls)) is not None:
9773
+ return self._info_to_record(info)
9774
+ else:
9775
+ return self.record_defaults
9776
+
9777
+ @abc.abstractmethod
9778
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9779
+ raise NotImplementedError
9780
+
9781
+ #
9782
+
9783
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9784
+ super().__init_subclass__(**kwargs)
9785
+
9786
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
9787
+ if any(d is cls.NOT_SET for d in dd.values()):
9788
+ raise TypeError(cls.record_attrs)
9789
+ cls.record_defaults = dd
9795
9790
 
9796
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
9797
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
9798
- # as 'MainProcess'.
9799
9791
  #
9800
- # As noted by stdlib:
9792
+
9793
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
9794
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
9795
+
9796
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9797
+ # Name of the logger used to log the call. Unmodified by ctor.
9798
+ name=str,
9799
+ )
9800
+
9801
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
9802
+ return dict(
9803
+ name=info.name,
9804
+ )
9805
+
9806
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
9807
+ return LoggingContextInfos.Name(
9808
+ name=rec.name,
9809
+ )
9810
+
9811
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
9812
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
9813
+
9814
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9815
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9816
+ # `getLevelName(level)`.
9817
+ levelname=str,
9818
+
9819
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9820
+ levelno=int,
9821
+ )
9822
+
9823
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
9824
+ return dict(
9825
+ levelname=info.name,
9826
+ levelno=int(info.level),
9827
+ )
9828
+
9829
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
9830
+ return LoggingContextInfos.Level.build(rec.levelno)
9831
+
9832
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
9833
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
9834
+
9835
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9836
+ # The format string passed in the original logging call. Merged with args to produce message, or an
9837
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
9838
+ msg=str,
9839
+
9840
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
9841
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
9842
+ # Mapping into just the mapping, but is otherwise unmodified.
9843
+ args=ta.Union[tuple, dict, None],
9844
+ )
9845
+
9846
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
9847
+ return dict(
9848
+ msg=info.msg,
9849
+ args=info.args,
9850
+ )
9851
+
9852
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
9853
+ return LoggingContextInfos.Msg(
9854
+ msg=rec.msg,
9855
+ args=rec.args,
9856
+ )
9857
+
9858
+ # FIXME: handled specially - all unknown attrs on LogRecord
9859
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
9860
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
9801
9861
  #
9802
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
9803
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
9862
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
9863
+ # # FIXME:
9864
+ # # if extra is not None:
9865
+ # # for key in extra:
9866
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
9867
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
9868
+ # # rv.__dict__[key] = extra[key]
9869
+ # return dict()
9804
9870
  #
9805
- processName=ta.Optional[str],
9871
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
9872
+ # return None
9806
9873
 
9807
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
9808
- # None.
9809
- process=ta.Optional[int],
9874
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
9875
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
9810
9876
 
9811
- #
9877
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9878
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
9879
+ # `time.time()`.
9880
+ #
9881
+ # See:
9882
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
9883
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
9884
+ #
9885
+ created=float,
9812
9886
 
9813
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
9814
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
9815
- taskName=ta.Optional[str],
9816
- )
9887
+ # Millisecond portion of the time when the LogRecord was created.
9888
+ msecs=float,
9889
+
9890
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9891
+ relativeCreated=float,
9892
+ )
9893
+
9894
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
9895
+ return dict(
9896
+ created=info.secs,
9897
+ msecs=info.msecs,
9898
+ relativeCreated=info.relative_secs,
9899
+ )
9900
+
9901
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
9902
+ return LoggingContextInfos.Time.build(
9903
+ int(rec.created * 1e9),
9904
+ )
9905
+
9906
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
9907
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
9908
+
9909
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
9910
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9911
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
9912
+
9913
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9914
+ exc_text=(ta.Optional[str], None),
9915
+ )
9916
+
9917
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
9918
+ return dict(
9919
+ exc_info=info.info_tuple,
9920
+ exc_text=None,
9921
+ )
9922
+
9923
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
9924
+ # FIXME:
9925
+ # error: Argument 1 to "build" of "Exc" has incompatible type
9926
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
9927
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
9928
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
9929
+
9930
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
9931
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
9932
+
9933
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
9934
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
9935
+
9936
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
9937
+
9938
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
9939
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
9940
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
9941
+ pathname=(str, _UNKNOWN_PATH_NAME),
9942
+
9943
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
9944
+ # y Logger.findCaller / Logger._log.
9945
+ lineno=(int, 0),
9946
+
9947
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
9948
+ # "(unknown function)" by Logger.findCaller / Logger._log.
9949
+ funcName=(str, _UNKNOWN_FUNC_NAME),
9950
+
9951
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
9952
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
9953
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
9954
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
9955
+ # stripped of exactly one trailing `\n` if present.
9956
+ stack_info=(ta.Optional[str], None),
9957
+ )
9958
+
9959
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
9960
+ if (sinfo := caller.stack_info) is not None:
9961
+ stack_info: ta.Optional[str] = '\n'.join([
9962
+ self._STACK_INFO_PREFIX,
9963
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
9964
+ ])
9965
+ else:
9966
+ stack_info = None
9967
+
9968
+ return dict(
9969
+ pathname=caller.file_path,
9970
+
9971
+ lineno=caller.line_no,
9972
+ funcName=caller.func_name,
9973
+
9974
+ stack_info=stack_info,
9975
+ )
9976
+
9977
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
9978
+ # FIXME: piecemeal?
9979
+ # FIXME: strip _STACK_INFO_PREFIX
9980
+ raise NotImplementedError
9981
+
9982
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
9983
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
9984
+
9985
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9986
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
9987
+ # pathname.
9988
+ filename=str,
9989
+
9990
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
9991
+ # "Unknown module".
9992
+ module=str,
9993
+ )
9994
+
9995
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
9996
+
9997
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9998
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
9999
+ return dict(
10000
+ filename=info.file_name,
10001
+ module=info.module,
10002
+ )
10003
+
10004
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
10005
+ return dict(
10006
+ filename=caller.file_path,
10007
+ module=self._UNKNOWN_MODULE,
10008
+ )
10009
+
10010
+ return dict(
10011
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
10012
+ module=self._UNKNOWN_MODULE,
10013
+ )
10014
+
10015
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
10016
+ if not (
10017
+ rec.module is None or
10018
+ rec.module == self._UNKNOWN_MODULE
10019
+ ):
10020
+ return LoggingContextInfos.SourceFile(
10021
+ file_name=rec.filename,
10022
+ module=rec.module, # FIXME: piecemeal?
10023
+ )
10024
+
10025
+ return None
10026
+
10027
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
10028
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
10029
+
10030
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10031
+ # Thread ID if available, and `logging.logThreads` is truthy.
10032
+ thread=(ta.Optional[int], None),
10033
+
10034
+ # Thread name if available, and `logging.logThreads` is truthy.
10035
+ threadName=(ta.Optional[str], None),
10036
+ )
10037
+
10038
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
10039
+ if logging.logThreads:
10040
+ return dict(
10041
+ thread=info.ident,
10042
+ threadName=info.name,
10043
+ )
10044
+
10045
+ return self.record_defaults
10046
+
10047
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
10048
+ if (
10049
+ (ident := rec.thread) is not None and
10050
+ (name := rec.threadName) is not None
10051
+ ):
10052
+ return LoggingContextInfos.Thread(
10053
+ ident=ident,
10054
+ native_id=None,
10055
+ name=name,
10056
+ )
10057
+
10058
+ return None
10059
+
10060
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
10061
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
9817
10062
 
9818
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
10063
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10064
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
10065
+ # otherwise None.
10066
+ process=(ta.Optional[int], None),
10067
+ )
10068
+
10069
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
10070
+ if logging.logProcesses:
10071
+ return dict(
10072
+ process=info.pid,
10073
+ )
10074
+
10075
+ return self.record_defaults
10076
+
10077
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
10078
+ if (
10079
+ (pid := rec.process) is not None
10080
+ ):
10081
+ return LoggingContextInfos.Process(
10082
+ pid=pid,
10083
+ )
10084
+
10085
+ return None
10086
+
10087
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
10088
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
10089
+
10090
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10091
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
10092
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
10093
+ # remains as 'MainProcess'.
10094
+ #
10095
+ # As noted by stdlib:
10096
+ #
10097
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
10098
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
10099
+ #
10100
+ processName=(ta.Optional[str], None),
10101
+ )
10102
+
10103
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
10104
+ if logging.logMultiprocessing:
10105
+ return dict(
10106
+ processName=info.process_name,
10107
+ )
10108
+
10109
+ return self.record_defaults
10110
+
10111
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
10112
+ if (
10113
+ (process_name := rec.processName) is not None
10114
+ ):
10115
+ return LoggingContextInfos.Multiprocessing(
10116
+ process_name=process_name,
10117
+ )
10118
+
10119
+ return None
10120
+
10121
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
10122
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
10123
+
10124
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
10125
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
10126
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
10127
+ taskName=(ta.Optional[str], None),
10128
+ )
10129
+
10130
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
10131
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
10132
+ return dict(
10133
+ taskName=info.name,
10134
+ )
10135
+
10136
+ return self.record_defaults
10137
+
10138
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
10139
+ if (
10140
+ (name := getattr(rec, 'taskName', None)) is not None
10141
+ ):
10142
+ return LoggingContextInfos.AsyncioTask(
10143
+ name=name,
10144
+ )
10145
+
10146
+ return None
10147
+
10148
+
10149
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
10150
+ LoggingContextInfoRecordAdapters.Name(),
10151
+ LoggingContextInfoRecordAdapters.Level(),
10152
+ LoggingContextInfoRecordAdapters.Msg(),
10153
+ LoggingContextInfoRecordAdapters.Time(),
10154
+ LoggingContextInfoRecordAdapters.Exc(),
10155
+ LoggingContextInfoRecordAdapters.Caller(),
10156
+ LoggingContextInfoRecordAdapters.SourceFile(),
10157
+ LoggingContextInfoRecordAdapters.Thread(),
10158
+ LoggingContextInfoRecordAdapters.Process(),
10159
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
10160
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
10161
+ ]
10162
+
10163
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
10164
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
10165
+ }
10166
+
10167
+
10168
+ ##
10169
+
10170
+
10171
+ KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
10172
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
10173
+ )
9819
10174
 
9820
10175
 
9821
10176
  # Formatter:
@@ -9839,14 +10194,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9839
10194
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
9840
10195
 
9841
10196
 
9842
- ##
9843
-
9844
-
9845
10197
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
9846
10198
  pass
9847
10199
 
9848
10200
 
9849
10201
  def _check_std_logging_record_attrs() -> None:
10202
+ if (
10203
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
10204
+ len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
10205
+ ):
10206
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
10207
+
9850
10208
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
9851
10209
 
9852
10210
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -9865,116 +10223,20 @@ _check_std_logging_record_attrs()
9865
10223
 
9866
10224
 
9867
10225
  class LoggingContextLogRecord(logging.LogRecord):
9868
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
9869
-
9870
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
9871
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
9872
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
9873
-
9874
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
9875
-
9876
- def __init__( # noqa
9877
- self,
9878
- # name,
9879
- # level,
9880
- # pathname,
9881
- # lineno,
9882
- # msg,
9883
- # args,
9884
- # exc_info,
9885
- # func=None,
9886
- # sinfo=None,
9887
- # **kwargs,
9888
- *,
9889
- name: str,
9890
- msg: str,
9891
- args: ta.Union[tuple, dict],
9892
-
9893
- _logging_context: LoggingContext,
9894
- ) -> None:
9895
- ctx = _logging_context
9896
-
9897
- self.name: str = name
9898
-
9899
- self.msg: str = msg
9900
-
9901
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
9902
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
9903
- args = args[0] # type: ignore[assignment]
9904
- self.args: ta.Union[tuple, dict] = args
9905
-
9906
- self.levelname: str = logging.getLevelName(ctx.level)
9907
- self.levelno: int = ctx.level
9908
-
9909
- if (caller := ctx.caller()) is not None:
9910
- self.pathname: str = caller.file_path
9911
- else:
9912
- self.pathname = self._UNKNOWN_PATH_NAME
9913
-
9914
- if (src_file := ctx.source_file()) is not None:
9915
- self.filename: str = src_file.file_name
9916
- self.module: str = src_file.module
9917
- else:
9918
- self.filename = self.pathname
9919
- self.module = self._UNKNOWN_MODULE
9920
-
9921
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
9922
- self.exc_text: ta.Optional[str] = None
9923
-
9924
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
9925
- if caller is not None:
9926
- if (sinfo := caller.stack_info) is not None:
9927
- self.stack_info: ta.Optional[str] = '\n'.join([
9928
- self._STACK_INFO_PREFIX,
9929
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
9930
- ])
9931
- else:
9932
- self.stack_info = None
9933
-
9934
- self.lineno: int = caller.line_no
9935
- self.funcName: str = caller.name
9936
-
9937
- else:
9938
- self.stack_info = None
9939
-
9940
- self.lineno = 0
9941
- self.funcName = self._UNKNOWN_FUNC_NAME
9942
-
9943
- times = ctx.times
9944
- self.created: float = times.created
9945
- self.msecs: float = times.msecs
9946
- self.relativeCreated: float = times.relative_created
9947
-
9948
- if logging.logThreads:
9949
- thread = check.not_none(ctx.thread())
9950
- self.thread: ta.Optional[int] = thread.ident
9951
- self.threadName: ta.Optional[str] = thread.name
9952
- else:
9953
- self.thread = None
9954
- self.threadName = None
9955
-
9956
- if logging.logProcesses:
9957
- process = check.not_none(ctx.process())
9958
- self.process: ta.Optional[int] = process.pid
9959
- else:
9960
- self.process = None
9961
-
9962
- if logging.logMultiprocessing:
9963
- if (mp := ctx.multiprocessing()) is not None:
9964
- self.processName: ta.Optional[str] = mp.process_name
9965
- else:
9966
- self.processName = None
9967
- else:
9968
- self.processName = None
9969
-
9970
- # Absent <3.12
9971
- if getattr(logging, 'logAsyncioTasks', None):
9972
- if (at := ctx.asyncio_task()) is not None:
9973
- self.taskName: ta.Optional[str] = at.name
9974
- else:
9975
- self.taskName = None
9976
- else:
9977
- self.taskName = None
10226
+ # LogRecord.__init__ args:
10227
+ # - name: str
10228
+ # - level: int
10229
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
10230
+ # - lineno: int - May be 0.
10231
+ # - msg: str
10232
+ # - args: tuple | dict | 1-tuple[dict]
10233
+ # - exc_info: LoggingExcInfoTuple | None
10234
+ # - func: str | None = None -> funcName
10235
+ # - sinfo: str | None = None -> stack_info
10236
+
10237
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
10238
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
10239
+ self.__dict__.update(ad.context_to_record(_logging_context))
9978
10240
 
9979
10241
 
9980
10242
  ########################################
@@ -11194,21 +11456,20 @@ class StdLogger(Logger):
11194
11456
  return self._std.getEffectiveLevel()
11195
11457
 
11196
11458
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
11197
- if not self.is_enabled_for(ctx.level):
11459
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
11198
11460
  return
11199
11461
 
11200
- ctx.capture()
11201
-
11202
- ms, args = self._prepare_msg_args(msg, *args)
11203
-
11204
- rec = LoggingContextLogRecord(
11462
+ ctx.set_basic(
11205
11463
  name=self._std.name,
11206
- msg=ms,
11207
- args=args,
11208
11464
 
11209
- _logging_context=ctx,
11465
+ msg=msg,
11466
+ args=args,
11210
11467
  )
11211
11468
 
11469
+ ctx.capture()
11470
+
11471
+ rec = LoggingContextLogRecord(_logging_context=ctx)
11472
+
11212
11473
  self._std.handle(rec)
11213
11474
 
11214
11475