omdev 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.
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,46 @@ 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
7646
+ @ta.final
7647
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7648
+ return self.get_info(ty)
7571
7649
 
7572
- #
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
7573
7655
 
7574
- @property
7575
- @abc.abstractmethod
7576
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7577
- raise NotImplementedError
7578
7656
 
7579
- @property
7580
- @abc.abstractmethod
7581
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7582
- raise NotImplementedError
7657
+ @ta.final
7658
+ class SimpleLoggingContext(LoggingContext):
7659
+ def __init__(self, *infos: LoggingContextInfo) -> None:
7660
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {type(i): i for i in infos}
7583
7661
 
7584
- #
7662
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7663
+ return self._infos.get(ty)
7585
7664
 
7586
- @abc.abstractmethod
7587
- def caller(self) -> ta.Optional[LoggingCaller]:
7588
- raise NotImplementedError
7589
-
7590
- @abc.abstractmethod
7591
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7592
- raise NotImplementedError
7593
7665
 
7594
- #
7666
+ ##
7595
7667
 
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
7668
 
7669
+ class CaptureLoggingContext(LoggingContext, Abstract):
7604
7670
  @abc.abstractmethod
7605
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7606
- raise NotImplementedError
7671
+ def set_basic(
7672
+ self,
7673
+ name: str,
7607
7674
 
7608
- @abc.abstractmethod
7609
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7675
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7676
+ args: tuple,
7677
+ ) -> 'CaptureLoggingContext':
7610
7678
  raise NotImplementedError
7611
7679
 
7680
+ #
7612
7681
 
7613
- ##
7614
-
7615
-
7616
- class CaptureLoggingContext(LoggingContext, Abstract):
7617
7682
  class AlreadyCapturedError(Exception):
7618
7683
  pass
7619
7684
 
@@ -7644,80 +7709,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7644
7709
 
7645
7710
  exc_info: LoggingExcInfoArg = False,
7646
7711
 
7647
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
7712
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
7648
7713
  stack_offset: int = 0,
7649
7714
  stack_info: bool = False,
7650
7715
  ) -> None:
7651
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7652
-
7653
- #
7716
+ # TODO: Name, Msg, Extra
7654
7717
 
7655
7718
  if time_ns is None:
7656
7719
  time_ns = time.time_ns()
7657
- self._time_ns: int = time_ns
7658
7720
 
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
- #
7721
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
7722
+ self._set_info(
7723
+ LoggingContextInfos.Level.build(level),
7724
+ LoggingContextInfos.Time.build(time_ns),
7725
+ LoggingContextInfos.Exc.build(exc_info),
7726
+ )
7678
7727
 
7679
7728
  if caller is not CaptureLoggingContextImpl.NOT_SET:
7680
- self._caller = caller # type: ignore[assignment]
7729
+ self._infos[LoggingContextInfos.Caller] = caller
7681
7730
  else:
7682
7731
  self._stack_offset = stack_offset
7683
7732
  self._stack_info = stack_info
7684
7733
 
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
7734
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
7735
+ for info in infos:
7736
+ if info is not None:
7737
+ self._infos[type(info)] = info
7738
+ return self
7708
7739
 
7709
- #
7740
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7741
+ return self._infos.get(ty)
7710
7742
 
7711
- _exc_info: ta.Optional[LoggingExcInfo] = None
7712
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
7743
+ ##
7713
7744
 
7714
- @property
7715
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7716
- return self._exc_info
7745
+ def set_basic(
7746
+ self,
7747
+ name: str,
7717
7748
 
7718
- @property
7719
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7720
- return self._exc_info_tuple
7749
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7750
+ args: tuple,
7751
+ ) -> 'CaptureLoggingContextImpl':
7752
+ return self._set_info(
7753
+ LoggingContextInfos.Name(name),
7754
+ LoggingContextInfos.Msg.build(msg, *args),
7755
+ )
7721
7756
 
7722
7757
  ##
7723
7758
 
@@ -7731,74 +7766,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7731
7766
 
7732
7767
  _has_captured: bool = False
7733
7768
 
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
7769
  def capture(self) -> None:
7743
7770
  if self._has_captured:
7744
7771
  raise CaptureLoggingContextImpl.AlreadyCapturedError
7745
7772
  self._has_captured = True
7746
7773
 
7747
- if not hasattr(self, '_caller'):
7748
- self._caller = LoggingCaller.find(
7774
+ if LoggingContextInfos.Caller not in self._infos:
7775
+ self._set_info(LoggingContextInfos.Caller.build(
7749
7776
  self._stack_offset + 1,
7750
7777
  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
7778
+ ))
7790
7779
 
7791
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7792
- try:
7793
- return self._multiprocessing
7794
- except AttributeError:
7795
- raise CaptureLoggingContext.NotCapturedError from None
7780
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
7781
+ self._set_info(LoggingContextInfos.SourceFile.build(
7782
+ caller.file_path,
7783
+ ))
7796
7784
 
7797
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7798
- try:
7799
- return self._asyncio_task
7800
- except AttributeError:
7801
- raise CaptureLoggingContext.NotCapturedError from None
7785
+ self._set_info(
7786
+ LoggingContextInfos.Thread.build(),
7787
+ LoggingContextInfos.Process.build(),
7788
+ LoggingContextInfos.Multiprocessing.build(),
7789
+ LoggingContextInfos.AsyncioTask.build(),
7790
+ )
7802
7791
 
7803
7792
 
7804
7793
  ########################################
@@ -7875,10 +7864,14 @@ def _locking_logging_module_lock() -> ta.Iterator[None]:
7875
7864
  def configure_standard_logging(
7876
7865
  level: ta.Union[int, str] = logging.INFO,
7877
7866
  *,
7878
- json: bool = False,
7879
7867
  target: ta.Optional[logging.Logger] = None,
7868
+
7880
7869
  force: bool = False,
7870
+
7881
7871
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
7872
+
7873
+ formatter: ta.Optional[logging.Formatter] = None, # noqa
7874
+ json: bool = False,
7882
7875
  ) -> ta.Optional[StandardConfiguredLoggingHandler]:
7883
7876
  with _locking_logging_module_lock():
7884
7877
  if target is None:
@@ -7899,11 +7892,11 @@ def configure_standard_logging(
7899
7892
 
7900
7893
  #
7901
7894
 
7902
- formatter: logging.Formatter
7903
- if json:
7904
- formatter = JsonLoggingFormatter()
7905
- else:
7906
- formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
7895
+ if formatter is None:
7896
+ if json:
7897
+ formatter = JsonLoggingFormatter()
7898
+ else:
7899
+ formatter = StandardLoggingFormatter(StandardLoggingFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS)) # noqa
7907
7900
  handler.setFormatter(formatter)
7908
7901
 
7909
7902
  #
@@ -9615,36 +9608,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
9615
9608
 
9616
9609
  ##
9617
9610
 
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
9611
  @abc.abstractmethod
9649
9612
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
9650
9613
  raise NotImplementedError
@@ -9685,144 +9648,560 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
9685
9648
 
9686
9649
  ########################################
9687
9650
  # ../../../omlish/logs/std/records.py
9651
+ """
9652
+ TODO:
9653
+ - TypedDict?
9654
+ """
9688
9655
 
9689
9656
 
9690
9657
  ##
9691
9658
 
9692
9659
 
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,
9660
+ class LoggingContextInfoRecordAdapters:
9661
+ # Ref:
9662
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9663
+ #
9664
+ # LogRecord:
9665
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
9666
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9667
+ #
9714
9668
 
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,
9669
+ def __new__(cls, *args, **kwargs): # noqa
9670
+ raise TypeError
9718
9671
 
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],
9672
+ class Adapter(Abstract, ta.Generic[T]):
9673
+ @property
9674
+ @abc.abstractmethod
9675
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
9676
+ raise NotImplementedError
9723
9677
 
9724
- #
9678
+ #
9725
9679
 
9726
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9727
- # `getLevelName(level)`.
9728
- levelname=str,
9680
+ @ta.final
9681
+ class NOT_SET: # noqa
9682
+ def __new__(cls, *args, **kwargs): # noqa
9683
+ raise TypeError
9729
9684
 
9730
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9731
- levelno=int,
9685
+ class RecordAttr(ta.NamedTuple):
9686
+ name: str
9687
+ type: ta.Any
9688
+ default: ta.Any
9732
9689
 
9733
- #
9690
+ # @abc.abstractmethod
9691
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
9734
9692
 
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,
9693
+ @property
9694
+ @abc.abstractmethod
9695
+ def _record_attrs(self) -> ta.Union[
9696
+ ta.Mapping[str, ta.Any],
9697
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
9698
+ ]:
9699
+ raise NotImplementedError
9738
9700
 
9739
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
9740
- filename=str,
9701
+ #
9741
9702
 
9742
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
9743
- # "Unknown module".
9744
- module=str,
9703
+ @abc.abstractmethod
9704
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9705
+ raise NotImplementedError
9745
9706
 
9746
- #
9707
+ #
9747
9708
 
9748
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9749
- exc_info=ta.Optional[LoggingExcInfoTuple],
9709
+ @abc.abstractmethod
9710
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
9711
+ raise NotImplementedError
9750
9712
 
9751
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9752
- exc_text=ta.Optional[str],
9713
+ #
9753
9714
 
9754
- #
9715
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9716
+ super().__init_subclass__(**kwargs)
9755
9717
 
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],
9718
+ if Abstract in cls.__bases__:
9719
+ return
9761
9720
 
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,
9721
+ if 'record_attrs' in cls.__dict__:
9722
+ raise TypeError(cls)
9723
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
9724
+ raise TypeError(ra)
9725
+
9726
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
9727
+ for n, v in ra.items():
9728
+ if not n or not isinstance(n, str) or n in rd:
9729
+ raise AttributeError(n)
9730
+ if isinstance(v, tuple):
9731
+ t, d = v
9732
+ else:
9733
+ t, d = v, cls.NOT_SET
9734
+ rd[n] = cls.RecordAttr(
9735
+ name=n,
9736
+ type=t,
9737
+ default=d,
9738
+ )
9739
+ cls.record_attrs = rd
9765
9740
 
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,
9741
+ class RequiredAdapter(Adapter[T], Abstract):
9742
+ @property
9743
+ @abc.abstractmethod
9744
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
9745
+ raise NotImplementedError
9769
9746
 
9770
- #
9747
+ #
9771
9748
 
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,
9749
+ @ta.final
9750
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9751
+ if (info := ctx.get_info(self.info_cls)) is not None:
9752
+ return self._info_to_record(info)
9753
+ else:
9754
+ raise TypeError # FIXME: fallback?
9779
9755
 
9780
- # Millisecond portion of the time when the LogRecord was created.
9781
- msecs=float,
9756
+ @abc.abstractmethod
9757
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9758
+ raise NotImplementedError
9782
9759
 
9783
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9784
- relativeCreated=float,
9760
+ #
9785
9761
 
9786
- #
9762
+ @abc.abstractmethod
9763
+ def record_to_info(self, rec: logging.LogRecord) -> T:
9764
+ raise NotImplementedError
9787
9765
 
9788
- # Thread ID if available, and `logging.logThreads` is truthy.
9789
- thread=ta.Optional[int],
9766
+ #
9790
9767
 
9791
- # Thread name if available, and `logging.logThreads` is truthy.
9792
- threadName=ta.Optional[str],
9768
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9769
+ super().__init_subclass__(**kwargs)
9793
9770
 
9794
- #
9771
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
9772
+ raise TypeError(cls.record_attrs)
9773
+
9774
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
9775
+ @property
9776
+ @abc.abstractmethod
9777
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
9778
+ raise NotImplementedError
9779
+
9780
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
9781
+
9782
+ #
9783
+
9784
+ @ta.final
9785
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9786
+ if (info := ctx.get_info(self.info_cls)) is not None:
9787
+ return self._info_to_record(info)
9788
+ else:
9789
+ return self.record_defaults
9790
+
9791
+ @abc.abstractmethod
9792
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9793
+ raise NotImplementedError
9794
+
9795
+ #
9796
+
9797
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9798
+ super().__init_subclass__(**kwargs)
9799
+
9800
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
9801
+ if any(d is cls.NOT_SET for d in dd.values()):
9802
+ raise TypeError(cls.record_attrs)
9803
+ cls.record_defaults = dd
9795
9804
 
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
9805
  #
9800
- # As noted by stdlib:
9806
+
9807
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
9808
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
9809
+
9810
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9811
+ # Name of the logger used to log the call. Unmodified by ctor.
9812
+ name=str,
9813
+ )
9814
+
9815
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
9816
+ return dict(
9817
+ name=info.name,
9818
+ )
9819
+
9820
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
9821
+ return LoggingContextInfos.Name(
9822
+ name=rec.name,
9823
+ )
9824
+
9825
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
9826
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
9827
+
9828
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9829
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9830
+ # `getLevelName(level)`.
9831
+ levelname=str,
9832
+
9833
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9834
+ levelno=int,
9835
+ )
9836
+
9837
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
9838
+ return dict(
9839
+ levelname=info.name,
9840
+ levelno=int(info.level),
9841
+ )
9842
+
9843
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
9844
+ return LoggingContextInfos.Level.build(rec.levelno)
9845
+
9846
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
9847
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
9848
+
9849
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9850
+ # The format string passed in the original logging call. Merged with args to produce message, or an
9851
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
9852
+ msg=str,
9853
+
9854
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
9855
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
9856
+ # Mapping into just the mapping, but is otherwise unmodified.
9857
+ args=ta.Union[tuple, dict, None],
9858
+ )
9859
+
9860
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
9861
+ return dict(
9862
+ msg=info.msg,
9863
+ args=info.args,
9864
+ )
9865
+
9866
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
9867
+ return LoggingContextInfos.Msg(
9868
+ msg=rec.msg,
9869
+ args=rec.args,
9870
+ )
9871
+
9872
+ # FIXME: handled specially - all unknown attrs on LogRecord
9873
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
9874
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
9801
9875
  #
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
9876
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
9877
+ # # FIXME:
9878
+ # # if extra is not None:
9879
+ # # for key in extra:
9880
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
9881
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
9882
+ # # rv.__dict__[key] = extra[key]
9883
+ # return dict()
9804
9884
  #
9805
- processName=ta.Optional[str],
9885
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
9886
+ # return None
9806
9887
 
9807
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
9808
- # None.
9809
- process=ta.Optional[int],
9888
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
9889
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
9810
9890
 
9811
- #
9891
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
9892
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
9893
+ # `time.time()`.
9894
+ #
9895
+ # See:
9896
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
9897
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
9898
+ #
9899
+ created=float,
9812
9900
 
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
- )
9901
+ # Millisecond portion of the time when the LogRecord was created.
9902
+ msecs=float,
9903
+
9904
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9905
+ relativeCreated=float,
9906
+ )
9907
+
9908
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
9909
+ return dict(
9910
+ created=info.secs,
9911
+ msecs=info.msecs,
9912
+ relativeCreated=info.relative_secs,
9913
+ )
9914
+
9915
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
9916
+ return LoggingContextInfos.Time.build(
9917
+ int(rec.created * 1e9),
9918
+ )
9919
+
9920
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
9921
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
9922
+
9923
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
9924
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9925
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
9926
+
9927
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9928
+ exc_text=(ta.Optional[str], None),
9929
+ )
9930
+
9931
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
9932
+ return dict(
9933
+ exc_info=info.info_tuple,
9934
+ exc_text=None,
9935
+ )
9936
+
9937
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
9938
+ # FIXME:
9939
+ # error: Argument 1 to "build" of "Exc" has incompatible type
9940
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
9941
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
9942
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
9943
+
9944
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
9945
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
9946
+
9947
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
9948
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
9949
+
9950
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
9951
+
9952
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
9953
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
9954
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
9955
+ pathname=(str, _UNKNOWN_PATH_NAME),
9956
+
9957
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
9958
+ # y Logger.findCaller / Logger._log.
9959
+ lineno=(int, 0),
9960
+
9961
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
9962
+ # "(unknown function)" by Logger.findCaller / Logger._log.
9963
+ funcName=(str, _UNKNOWN_FUNC_NAME),
9964
+
9965
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
9966
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
9967
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
9968
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
9969
+ # stripped of exactly one trailing `\n` if present.
9970
+ stack_info=(ta.Optional[str], None),
9971
+ )
9972
+
9973
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
9974
+ if (sinfo := caller.stack_info) is not None:
9975
+ stack_info: ta.Optional[str] = '\n'.join([
9976
+ self._STACK_INFO_PREFIX,
9977
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
9978
+ ])
9979
+ else:
9980
+ stack_info = None
9981
+
9982
+ return dict(
9983
+ pathname=caller.file_path,
9984
+
9985
+ lineno=caller.line_no,
9986
+ funcName=caller.func_name,
9987
+
9988
+ stack_info=stack_info,
9989
+ )
9990
+
9991
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
9992
+ # FIXME: piecemeal?
9993
+ if (
9994
+ rec.pathname != self._UNKNOWN_PATH_NAME and
9995
+ rec.lineno != 0 and
9996
+ rec.funcName != self._UNKNOWN_FUNC_NAME
9997
+ ):
9998
+ if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
9999
+ sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
10000
+ return LoggingContextInfos.Caller(
10001
+ file_path=rec.pathname,
10002
+
10003
+ line_no=rec.lineno,
10004
+ func_name=rec.funcName,
10005
+
10006
+ stack_info=sinfo,
10007
+ )
10008
+
10009
+ return None
10010
+
10011
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
10012
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
10013
+
10014
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
10015
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
10016
+ # pathname.
10017
+ filename=str,
10018
+
10019
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
10020
+ # "Unknown module".
10021
+ module=str,
10022
+ )
10023
+
10024
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
10025
+
10026
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
10027
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
10028
+ return dict(
10029
+ filename=info.file_name,
10030
+ module=info.module,
10031
+ )
10032
+
10033
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
10034
+ return dict(
10035
+ filename=caller.file_path,
10036
+ module=self._UNKNOWN_MODULE,
10037
+ )
10038
+
10039
+ return dict(
10040
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
10041
+ module=self._UNKNOWN_MODULE,
10042
+ )
10043
+
10044
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
10045
+ if (
10046
+ rec.module is not None and
10047
+ rec.module != self._UNKNOWN_MODULE
10048
+ ):
10049
+ return LoggingContextInfos.SourceFile(
10050
+ file_name=rec.filename,
10051
+ module=rec.module, # FIXME: piecemeal?
10052
+ )
10053
+
10054
+ return None
10055
+
10056
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
10057
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
10058
+
10059
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10060
+ # Thread ID if available, and `logging.logThreads` is truthy.
10061
+ thread=(ta.Optional[int], None),
10062
+
10063
+ # Thread name if available, and `logging.logThreads` is truthy.
10064
+ threadName=(ta.Optional[str], None),
10065
+ )
10066
+
10067
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
10068
+ if logging.logThreads:
10069
+ return dict(
10070
+ thread=info.ident,
10071
+ threadName=info.name,
10072
+ )
10073
+
10074
+ return self.record_defaults
10075
+
10076
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
10077
+ if (
10078
+ (ident := rec.thread) is not None and
10079
+ (name := rec.threadName) is not None
10080
+ ):
10081
+ return LoggingContextInfos.Thread(
10082
+ ident=ident,
10083
+ native_id=None,
10084
+ name=name,
10085
+ )
10086
+
10087
+ return None
10088
+
10089
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
10090
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
10091
+
10092
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10093
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
10094
+ # otherwise None.
10095
+ process=(ta.Optional[int], None),
10096
+ )
10097
+
10098
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
10099
+ if logging.logProcesses:
10100
+ return dict(
10101
+ process=info.pid,
10102
+ )
10103
+
10104
+ return self.record_defaults
10105
+
10106
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
10107
+ if (
10108
+ (pid := rec.process) is not None
10109
+ ):
10110
+ return LoggingContextInfos.Process(
10111
+ pid=pid,
10112
+ )
10113
+
10114
+ return None
10115
+
10116
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
10117
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
10118
+
10119
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
10120
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
10121
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
10122
+ # remains as 'MainProcess'.
10123
+ #
10124
+ # As noted by stdlib:
10125
+ #
10126
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
10127
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
10128
+ #
10129
+ processName=(ta.Optional[str], None),
10130
+ )
10131
+
10132
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
10133
+ if logging.logMultiprocessing:
10134
+ return dict(
10135
+ processName=info.process_name,
10136
+ )
10137
+
10138
+ return self.record_defaults
10139
+
10140
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
10141
+ if (
10142
+ (process_name := rec.processName) is not None
10143
+ ):
10144
+ return LoggingContextInfos.Multiprocessing(
10145
+ process_name=process_name,
10146
+ )
10147
+
10148
+ return None
10149
+
10150
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
10151
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
10152
+
10153
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
10154
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
10155
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
10156
+ taskName=(ta.Optional[str], None),
10157
+ )
10158
+
10159
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
10160
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
10161
+ return dict(
10162
+ taskName=info.name,
10163
+ )
10164
+
10165
+ return self.record_defaults
10166
+
10167
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
10168
+ if (
10169
+ (name := getattr(rec, 'taskName', None)) is not None
10170
+ ):
10171
+ return LoggingContextInfos.AsyncioTask(
10172
+ name=name,
10173
+ )
10174
+
10175
+ return None
10176
+
10177
+
10178
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
10179
+ LoggingContextInfoRecordAdapters.Name(),
10180
+ LoggingContextInfoRecordAdapters.Level(),
10181
+ LoggingContextInfoRecordAdapters.Msg(),
10182
+ LoggingContextInfoRecordAdapters.Time(),
10183
+ LoggingContextInfoRecordAdapters.Exc(),
10184
+ LoggingContextInfoRecordAdapters.Caller(),
10185
+ LoggingContextInfoRecordAdapters.SourceFile(),
10186
+ LoggingContextInfoRecordAdapters.Thread(),
10187
+ LoggingContextInfoRecordAdapters.Process(),
10188
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
10189
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
10190
+ ]
9817
10191
 
9818
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
10192
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
10193
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
10194
+ }
10195
+
10196
+
10197
+ ##
9819
10198
 
9820
10199
 
9821
10200
  # Formatter:
9822
10201
  # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
9823
10202
  # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
9824
10203
  #
9825
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
10204
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9826
10205
  # The logged message, computed as msg % args. Set to `record.getMessage()`.
9827
10206
  message=str,
9828
10207
 
@@ -9836,20 +10215,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9836
10215
  exc_text=ta.Optional[str],
9837
10216
  )
9838
10217
 
9839
- KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
9840
-
9841
10218
 
9842
10219
  ##
9843
10220
 
9844
10221
 
10222
+ _KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
10223
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
10224
+ )
10225
+
10226
+ _KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
10227
+
10228
+
9845
10229
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
9846
10230
  pass
9847
10231
 
9848
10232
 
9849
10233
  def _check_std_logging_record_attrs() -> None:
10234
+ if (
10235
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
10236
+ len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
10237
+ ):
10238
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
10239
+
9850
10240
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
9851
10241
 
9852
- if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
10242
+ if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
9853
10243
  import warnings # noqa
9854
10244
 
9855
10245
  warnings.warn(
@@ -9865,116 +10255,43 @@ _check_std_logging_record_attrs()
9865
10255
 
9866
10256
 
9867
10257
  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
10258
+ # LogRecord.__init__ args:
10259
+ # - name: str
10260
+ # - level: int
10261
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
10262
+ # - lineno: int - May be 0.
10263
+ # - msg: str
10264
+ # - args: tuple | dict | 1-tuple[dict]
10265
+ # - exc_info: LoggingExcInfoTuple | None
10266
+ # - func: str | None = None -> funcName
10267
+ # - sinfo: str | None = None -> stack_info
9896
10268
 
9897
- self.name: str = name
10269
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
10270
+ self._logging_context = _logging_context
9898
10271
 
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
10272
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
10273
+ self.__dict__.update(ad.context_to_record(_logging_context))
9920
10274
 
9921
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
9922
- self.exc_text: ta.Optional[str] = None
9923
10275
 
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
10276
+ ##
9942
10277
 
9943
- times = ctx.times
9944
- self.created: float = times.created
9945
- self.msecs: float = times.msecs
9946
- self.relativeCreated: float = times.relative_created
9947
10278
 
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
10279
+ @ta.final
10280
+ class LogRecordLoggingContext(LoggingContext):
10281
+ def __init__(self, rec: logging.LogRecord) -> None:
10282
+ if isinstance(rec, LoggingContextLogRecord):
10283
+ raise TypeError(rec)
9955
10284
 
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
10285
+ self._rec = rec
9961
10286
 
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
10287
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
10288
+ type(info): info
10289
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
10290
+ if (info := ad.record_to_info(rec)) is not None
10291
+ }
9969
10292
 
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
10293
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10294
+ return self._infos.get(ty)
9978
10295
 
9979
10296
 
9980
10297
  ########################################
@@ -11194,21 +11511,20 @@ class StdLogger(Logger):
11194
11511
  return self._std.getEffectiveLevel()
11195
11512
 
11196
11513
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
11197
- if not self.is_enabled_for(ctx.level):
11514
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
11198
11515
  return
11199
11516
 
11200
- ctx.capture()
11201
-
11202
- ms, args = self._prepare_msg_args(msg, *args)
11203
-
11204
- rec = LoggingContextLogRecord(
11517
+ ctx.set_basic(
11205
11518
  name=self._std.name,
11206
- msg=ms,
11207
- args=args,
11208
11519
 
11209
- _logging_context=ctx,
11520
+ msg=msg,
11521
+ args=args,
11210
11522
  )
11211
11523
 
11524
+ ctx.capture()
11525
+
11526
+ rec = LoggingContextLogRecord(_logging_context=ctx)
11527
+
11212
11528
  self._std.handle(rec)
11213
11529
 
11214
11530