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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
1837
- 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
- threading.get_ident(),
1855
- threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
1856
- 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
- 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
- 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
- task.get_name(), # Always non-None
1918
- )
1919
-
1920
-
1921
1805
  ########################################
1922
1806
  # ../../../omlish/logs/levels.py
1923
1807
 
@@ -1937,36 +1821,66 @@ class NamedLogLevel(int):
1937
1821
 
1938
1822
  #
1939
1823
 
1940
- @property
1941
- def exact_name(self) -> ta.Optional[str]:
1942
- return self._NAMES_BY_INT.get(self)
1824
+ _CACHE: ta.ClassVar[ta.MutableMapping[int, 'NamedLogLevel']] = {}
1825
+
1826
+ @ta.overload
1827
+ def __new__(cls, name: str, offset: int = 0, /) -> 'NamedLogLevel':
1828
+ ...
1829
+
1830
+ @ta.overload
1831
+ def __new__(cls, i: int, /) -> 'NamedLogLevel':
1832
+ ...
1943
1833
 
1944
- _effective_name: ta.Optional[str]
1834
+ def __new__(cls, x, offset=0, /):
1835
+ if isinstance(x, str):
1836
+ return cls(cls._INTS_BY_NAME[x.upper()] + offset)
1837
+ elif not offset and (c := cls._CACHE.get(x)) is not None:
1838
+ return c
1839
+ else:
1840
+ return super().__new__(cls, x + offset)
1841
+
1842
+ #
1843
+
1844
+ _name_and_offset: ta.Tuple[str, int]
1945
1845
 
1946
1846
  @property
1947
- def effective_name(self) -> ta.Optional[str]:
1847
+ def name_and_offset(self) -> ta.Tuple[str, int]:
1948
1848
  try:
1949
- return self._effective_name
1849
+ return self._name_and_offset
1950
1850
  except AttributeError:
1951
1851
  pass
1952
1852
 
1953
- if (n := self.exact_name) is None:
1853
+ if (n := self._NAMES_BY_INT.get(self)) is not None:
1854
+ t = (n, 0)
1855
+ else:
1954
1856
  for n, i in self._NAME_INT_PAIRS: # noqa
1955
1857
  if self >= i:
1858
+ t = (n, (self - i))
1956
1859
  break
1957
1860
  else:
1958
- n = None
1861
+ t = ('NOTSET', int(self))
1862
+
1863
+ self._name_and_offset = t
1864
+ return t
1865
+
1866
+ @property
1867
+ def exact_name(self) -> ta.Optional[str]:
1868
+ n, o = self.name_and_offset
1869
+ return n if not o else None
1959
1870
 
1960
- self._effective_name = n
1871
+ @property
1872
+ def effective_name(self) -> str:
1873
+ n, _ = self.name_and_offset
1961
1874
  return n
1962
1875
 
1963
1876
  #
1964
1877
 
1965
- def __repr__(self) -> str:
1966
- return f'{self.__class__.__name__}({int(self)})'
1967
-
1968
1878
  def __str__(self) -> str:
1969
- return self.exact_name or f'{self.effective_name or "INVALID"}:{int(self)}'
1879
+ return self.exact_name or f'{self.effective_name}{int(self):+}'
1880
+
1881
+ def __repr__(self) -> str:
1882
+ n, o = self.name_and_offset
1883
+ return f'{self.__class__.__name__}({n!r}{f", {int(o)}" if o else ""})'
1970
1884
 
1971
1885
  #
1972
1886
 
@@ -1986,6 +1900,9 @@ NamedLogLevel.DEBUG = NamedLogLevel(logging.DEBUG)
1986
1900
  NamedLogLevel.NOTSET = NamedLogLevel(logging.NOTSET)
1987
1901
 
1988
1902
 
1903
+ NamedLogLevel._CACHE.update({i: NamedLogLevel(i) for i in NamedLogLevel._NAMES_BY_INT}) # noqa
1904
+
1905
+
1989
1906
  ########################################
1990
1907
  # ../../../omlish/logs/std/filters.py
1991
1908
 
@@ -4947,223 +4864,338 @@ class PredicateTimeout(Timeout):
4947
4864
 
4948
4865
 
4949
4866
  ########################################
4950
- # ../../../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
+ """
4951
4872
 
4952
4873
 
4953
4874
  ##
4954
4875
 
4955
4876
 
4956
- @logging_context_info
4957
- @ta.final
4958
- class LoggingCaller(ta.NamedTuple):
4959
- file_path: str
4960
- line_no: int
4961
- name: str
4962
- stack_info: ta.Optional[str]
4877
+ def logging_context_info(cls):
4878
+ return cls
4963
4879
 
4964
- @classmethod
4965
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
4966
- file_path = os.path.normcase(frame.f_code.co_filename)
4967
4880
 
4968
- # Yes, really.
4969
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
4970
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
4971
- if 'importlib' in file_path and '_bootstrap' in file_path:
4972
- return True
4881
+ @ta.final
4882
+ class LoggingContextInfos:
4883
+ def __new__(cls, *args, **kwargs): # noqa
4884
+ raise TypeError
4973
4885
 
4974
- return False
4886
+ #
4975
4887
 
4976
- @classmethod
4977
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
4978
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
4888
+ @logging_context_info
4889
+ @ta.final
4890
+ class Name(ta.NamedTuple):
4891
+ name: str
4979
4892
 
4980
- while f is not None:
4981
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
4982
- # stack_offset management.
4983
- if hasattr(f, 'f_code'):
4984
- return f
4893
+ @logging_context_info
4894
+ @ta.final
4895
+ class Level(ta.NamedTuple):
4896
+ level: NamedLogLevel
4897
+ name: str
4985
4898
 
4986
- f = f.f_back
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
+ )
4987
4906
 
4988
- return None
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]
4989
4912
 
4990
- @classmethod
4991
- def find(
4992
- cls,
4993
- ofs: int = 0,
4994
- *,
4995
- stack_info: bool = False,
4996
- ) -> ta.Optional['LoggingCaller']:
4997
- if (f := cls.find_frame(ofs + 1)) is None:
4998
- return None
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)
4999
4935
 
5000
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
5001
- sinfo = None
5002
- if stack_info:
5003
- sio = io.StringIO()
5004
- traceback.print_stack(f, file=sio)
5005
- sinfo = sio.getvalue()
5006
- sio.close()
5007
- if sinfo[-1] == '\n':
5008
- sinfo = sinfo[:-1]
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 = '', ()
5009
4943
 
5010
- return cls(
5011
- f.f_code.co_filename,
5012
- f.f_lineno or 0,
5013
- f.f_code.co_name,
5014
- sinfo,
5015
- )
4944
+ elif isinstance(msg, str):
4945
+ s, a = msg, args
5016
4946
 
4947
+ else:
4948
+ raise TypeError(msg)
5017
4949
 
5018
- ########################################
5019
- # ../../../omlish/logs/protocols.py
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]
5020
4953
 
4954
+ return cls(
4955
+ msg=s,
4956
+ args=a,
4957
+ )
5021
4958
 
5022
- ##
4959
+ @logging_context_info
4960
+ @ta.final
4961
+ class Extra(ta.NamedTuple):
4962
+ extra: ta.Mapping[ta.Any, ta.Any]
5023
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
5024
4971
 
5025
- class LoggerLike(ta.Protocol):
5026
- """Satisfied by both our Logger and stdlib logging.Logger."""
4972
+ @classmethod
4973
+ def get_std_start_ns(cls) -> int:
4974
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5027
4975
 
5028
- def isEnabledFor(self, level: LogLevel) -> bool: ... # noqa
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
5029
4986
 
5030
- def getEffectiveLevel(self) -> LogLevel: ... # noqa
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
5031
5011
 
5032
- #
5012
+ return cls(
5013
+ ns=ns,
5014
+ secs=secs,
5015
+ msecs=msecs,
5016
+ relative_secs=relative_secs,
5017
+ )
5033
5018
 
5034
- def log(self, level: LogLevel, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5019
+ @logging_context_info
5020
+ @ta.final
5021
+ class Exc(ta.NamedTuple):
5022
+ info: LoggingExcInfo
5023
+ info_tuple: LoggingExcInfoTuple
5035
5024
 
5036
- def debug(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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
5037
5040
 
5038
- def info(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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
5039
5046
 
5040
- def warning(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5047
+ return cls(
5048
+ info=info,
5049
+ info_tuple=info_tuple,
5050
+ )
5041
5051
 
5042
- def error(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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]
5043
5059
 
5044
- def exception(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5060
+ @classmethod
5061
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
5062
+ file_path = os.path.normcase(frame.f_code.co_filename)
5045
5063
 
5046
- def critical(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
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
5047
5069
 
5070
+ return False
5048
5071
 
5049
- ########################################
5050
- # ../../../omlish/logs/std/json.py
5051
- """
5052
- TODO:
5053
- - translate json keys
5054
- """
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
5055
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
5056
5081
 
5057
- ##
5082
+ f = f.f_back
5058
5083
 
5084
+ return None
5059
5085
 
5060
- class JsonLoggingFormatter(logging.Formatter):
5061
- KEYS: ta.Mapping[str, bool] = {
5062
- 'name': False,
5063
- 'msg': False,
5064
- 'args': False,
5065
- 'levelname': False,
5066
- 'levelno': False,
5067
- 'pathname': False,
5068
- 'filename': False,
5069
- 'module': False,
5070
- 'exc_info': True,
5071
- 'exc_text': True,
5072
- 'stack_info': True,
5073
- 'lineno': False,
5074
- 'funcName': False,
5075
- 'created': False,
5076
- 'msecs': False,
5077
- 'relativeCreated': False,
5078
- 'thread': False,
5079
- 'threadName': False,
5080
- 'processName': False,
5081
- 'process': False,
5082
- }
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
5083
5095
 
5084
- def __init__(
5085
- self,
5086
- *args: ta.Any,
5087
- json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
5088
- **kwargs: ta.Any,
5089
- ) -> None:
5090
- super().__init__(*args, **kwargs)
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]
5091
5105
 
5092
- if json_dumps is None:
5093
- json_dumps = json_dumps_compact
5094
- self._json_dumps = json_dumps
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
+ )
5095
5112
 
5096
- def format(self, record: logging.LogRecord) -> str:
5097
- dct = {
5098
- k: v
5099
- for k, o in self.KEYS.items()
5100
- for v in [getattr(record, k)]
5101
- if not (o and v is None)
5102
- }
5103
- return self._json_dumps(dct)
5113
+ @logging_context_info
5114
+ @ta.final
5115
+ class SourceFile(ta.NamedTuple):
5116
+ file_name: str
5117
+ module: str
5104
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
5105
5123
 
5106
- ########################################
5107
- # ../../../omlish/logs/times.py
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
5108
5130
 
5131
+ return cls(
5132
+ file_name=file_name,
5133
+ module=module,
5134
+ )
5109
5135
 
5110
- ##
5136
+ @logging_context_info
5137
+ @ta.final
5138
+ class Thread(ta.NamedTuple):
5139
+ ident: int
5140
+ native_id: ta.Optional[int]
5141
+ name: str
5111
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
+ )
5112
5150
 
5113
- @logging_context_info
5114
- @ta.final
5115
- class LoggingTimeFields(ta.NamedTuple):
5116
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5151
+ @logging_context_info
5152
+ @ta.final
5153
+ class Process(ta.NamedTuple):
5154
+ pid: int
5117
5155
 
5118
- created: float
5119
- msecs: float
5120
- relative_created: float
5156
+ @classmethod
5157
+ def build(cls) -> 'LoggingContextInfos.Process':
5158
+ return cls(
5159
+ pid=os.getpid(),
5160
+ )
5121
5161
 
5122
- @classmethod
5123
- def get_std_start_time_ns(cls) -> int:
5124
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5162
+ @logging_context_info
5163
+ @ta.final
5164
+ class Multiprocessing(ta.NamedTuple):
5165
+ process_name: str
5125
5166
 
5126
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5127
- # int.
5128
- #
5129
- # See:
5130
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5131
- #
5132
- if isinstance(x, float):
5133
- return int(x * 1e9)
5134
- else:
5135
- return x
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
5136
5172
 
5137
- @classmethod
5138
- def build(
5139
- cls,
5140
- time_ns: int,
5141
- *,
5142
- start_time_ns: ta.Optional[int] = None,
5143
- ) -> 'LoggingTimeFields':
5144
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5145
- created = time_ns / 1e9 # ns to float seconds
5146
-
5147
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5148
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5149
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5150
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5151
-
5152
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5153
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5154
- # ns -> sec conversion can round up, e.g:
5155
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5156
- msecs = 0.0
5157
-
5158
- if start_time_ns is None:
5159
- start_time_ns = cls.get_std_start_time_ns()
5160
- relative_created = (time_ns - start_time_ns) / 1e6
5173
+ return cls(
5174
+ process_name=mp.current_process().name,
5175
+ )
5161
5176
 
5162
- return cls(
5163
- created,
5164
- msecs,
5165
- relative_created,
5166
- )
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
+ )
5167
5199
 
5168
5200
 
5169
5201
  ##
@@ -5174,12 +5206,12 @@ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5174
5206
 
5175
5207
 
5176
5208
  def _check_logging_start_time() -> None:
5177
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5209
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
5178
5210
  import warnings # noqa
5179
5211
 
5180
5212
  warnings.warn(
5181
5213
  f'Unexpected logging start time detected: '
5182
- f'get_std_start_time_ns={x}, '
5214
+ f'get_std_start_ns={x}, '
5183
5215
  f'time.time()={t}',
5184
5216
  UnexpectedLoggingStartTimeWarning,
5185
5217
  )
@@ -5188,6 +5220,95 @@ def _check_logging_start_time() -> None:
5188
5220
  _check_logging_start_time()
5189
5221
 
5190
5222
 
5223
+ ########################################
5224
+ # ../../../omlish/logs/protocols.py
5225
+
5226
+
5227
+ ##
5228
+
5229
+
5230
+ @ta.runtime_checkable
5231
+ class LoggerLike(ta.Protocol):
5232
+ """Satisfied by both our Logger and stdlib logging.Logger."""
5233
+
5234
+ def isEnabledFor(self, level: LogLevel) -> bool: ... # noqa
5235
+
5236
+ def getEffectiveLevel(self) -> LogLevel: ... # noqa
5237
+
5238
+ #
5239
+
5240
+ def log(self, level: LogLevel, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5241
+
5242
+ def debug(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5243
+
5244
+ def info(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5245
+
5246
+ def warning(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5247
+
5248
+ def error(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5249
+
5250
+ def exception(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5251
+
5252
+ def critical(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
5253
+
5254
+
5255
+ ########################################
5256
+ # ../../../omlish/logs/std/json.py
5257
+ """
5258
+ TODO:
5259
+ - translate json keys
5260
+ """
5261
+
5262
+
5263
+ ##
5264
+
5265
+
5266
+ class JsonLoggingFormatter(logging.Formatter):
5267
+ KEYS: ta.Mapping[str, bool] = {
5268
+ 'name': False,
5269
+ 'msg': False,
5270
+ 'args': False,
5271
+ 'levelname': False,
5272
+ 'levelno': False,
5273
+ 'pathname': False,
5274
+ 'filename': False,
5275
+ 'module': False,
5276
+ 'exc_info': True,
5277
+ 'exc_text': True,
5278
+ 'stack_info': True,
5279
+ 'lineno': False,
5280
+ 'funcName': False,
5281
+ 'created': False,
5282
+ 'msecs': False,
5283
+ 'relativeCreated': False,
5284
+ 'thread': False,
5285
+ 'threadName': False,
5286
+ 'processName': False,
5287
+ 'process': False,
5288
+ }
5289
+
5290
+ def __init__(
5291
+ self,
5292
+ *args: ta.Any,
5293
+ json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
5294
+ **kwargs: ta.Any,
5295
+ ) -> None:
5296
+ super().__init__(*args, **kwargs)
5297
+
5298
+ if json_dumps is None:
5299
+ json_dumps = json_dumps_compact
5300
+ self._json_dumps = json_dumps
5301
+
5302
+ def format(self, record: logging.LogRecord) -> str:
5303
+ dct = {
5304
+ k: v
5305
+ for k, o in self.KEYS.items()
5306
+ for v in [getattr(record, k)]
5307
+ if not (o and v is None)
5308
+ }
5309
+ return self._json_dumps(dct)
5310
+
5311
+
5191
5312
  ########################################
5192
5313
  # ../../../omlish/os/temp.py
5193
5314
 
@@ -7518,68 +7639,36 @@ inj = InjectionApi()
7518
7639
 
7519
7640
 
7520
7641
  class LoggingContext(Abstract):
7521
- @property
7522
7642
  @abc.abstractmethod
7523
- def level(self) -> NamedLogLevel:
7643
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7524
7644
  raise NotImplementedError
7525
7645
 
7526
- #
7527
-
7528
- @property
7529
- @abc.abstractmethod
7530
- def time_ns(self) -> int:
7531
- raise NotImplementedError
7532
-
7533
- @property
7534
- @abc.abstractmethod
7535
- def times(self) -> LoggingTimeFields:
7536
- raise NotImplementedError
7646
+ @ta.final
7647
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7648
+ return self.get_info(ty)
7537
7649
 
7538
- #
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
7539
7655
 
7540
- @property
7541
- @abc.abstractmethod
7542
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7543
- raise NotImplementedError
7656
+ ##
7544
7657
 
7545
- @property
7546
- @abc.abstractmethod
7547
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7548
- raise NotImplementedError
7549
-
7550
- #
7551
7658
 
7659
+ class CaptureLoggingContext(LoggingContext, Abstract):
7552
7660
  @abc.abstractmethod
7553
- def caller(self) -> ta.Optional[LoggingCaller]:
7554
- raise NotImplementedError
7661
+ def set_basic(
7662
+ self,
7663
+ name: str,
7555
7664
 
7556
- @abc.abstractmethod
7557
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7665
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7666
+ args: tuple,
7667
+ ) -> 'CaptureLoggingContext':
7558
7668
  raise NotImplementedError
7559
7669
 
7560
7670
  #
7561
7671
 
7562
- @abc.abstractmethod
7563
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7564
- raise NotImplementedError
7565
-
7566
- @abc.abstractmethod
7567
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7568
- raise NotImplementedError
7569
-
7570
- @abc.abstractmethod
7571
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7572
- raise NotImplementedError
7573
-
7574
- @abc.abstractmethod
7575
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7576
- raise NotImplementedError
7577
-
7578
-
7579
- ##
7580
-
7581
-
7582
- class CaptureLoggingContext(LoggingContext, Abstract):
7583
7672
  class AlreadyCapturedError(Exception):
7584
7673
  pass
7585
7674
 
@@ -7610,80 +7699,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7610
7699
 
7611
7700
  exc_info: LoggingExcInfoArg = False,
7612
7701
 
7613
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
7702
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
7614
7703
  stack_offset: int = 0,
7615
7704
  stack_info: bool = False,
7616
7705
  ) -> None:
7617
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7618
-
7619
- #
7706
+ # TODO: Name, Msg, Extra
7620
7707
 
7621
7708
  if time_ns is None:
7622
7709
  time_ns = time.time_ns()
7623
- self._time_ns: int = time_ns
7624
-
7625
- #
7626
7710
 
7627
- if exc_info is True:
7628
- sys_exc_info = sys.exc_info()
7629
- if sys_exc_info[0] is not None:
7630
- exc_info = sys_exc_info
7631
- else:
7632
- exc_info = None
7633
- elif exc_info is False:
7634
- exc_info = None
7635
-
7636
- if exc_info is not None:
7637
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
7638
- if isinstance(exc_info, BaseException):
7639
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
7640
- else:
7641
- self._exc_info_tuple = exc_info
7642
-
7643
- #
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
+ )
7644
7717
 
7645
7718
  if caller is not CaptureLoggingContextImpl.NOT_SET:
7646
- self._caller = caller # type: ignore[assignment]
7719
+ self._infos[LoggingContextInfos.Caller] = caller
7647
7720
  else:
7648
7721
  self._stack_offset = stack_offset
7649
7722
  self._stack_info = stack_info
7650
7723
 
7651
- ##
7652
-
7653
- @property
7654
- def level(self) -> NamedLogLevel:
7655
- return self._level
7656
-
7657
- #
7658
-
7659
- @property
7660
- def time_ns(self) -> int:
7661
- return self._time_ns
7662
-
7663
- _times: LoggingTimeFields
7664
-
7665
- @property
7666
- def times(self) -> LoggingTimeFields:
7667
- try:
7668
- return self._times
7669
- except AttributeError:
7670
- pass
7671
-
7672
- times = self._times = LoggingTimeFields.build(self.time_ns)
7673
- 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
7674
7729
 
7675
- #
7730
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
7731
+ return self._infos.get(ty)
7676
7732
 
7677
- _exc_info: ta.Optional[LoggingExcInfo] = None
7678
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
7733
+ ##
7679
7734
 
7680
- @property
7681
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
7682
- return self._exc_info
7735
+ def set_basic(
7736
+ self,
7737
+ name: str,
7683
7738
 
7684
- @property
7685
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
7686
- 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
+ )
7687
7746
 
7688
7747
  ##
7689
7748
 
@@ -7697,74 +7756,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
7697
7756
 
7698
7757
  _has_captured: bool = False
7699
7758
 
7700
- _caller: ta.Optional[LoggingCaller]
7701
- _source_file: ta.Optional[LoggingSourceFileInfo]
7702
-
7703
- _thread: ta.Optional[LoggingThreadInfo]
7704
- _process: ta.Optional[LoggingProcessInfo]
7705
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
7706
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
7707
-
7708
7759
  def capture(self) -> None:
7709
7760
  if self._has_captured:
7710
7761
  raise CaptureLoggingContextImpl.AlreadyCapturedError
7711
7762
  self._has_captured = True
7712
7763
 
7713
- if not hasattr(self, '_caller'):
7714
- self._caller = LoggingCaller.find(
7764
+ if LoggingContextInfos.Caller not in self._infos:
7765
+ self._set_info(LoggingContextInfos.Caller.build(
7715
7766
  self._stack_offset + 1,
7716
7767
  stack_info=self._stack_info,
7717
- )
7718
-
7719
- if (caller := self._caller) is not None:
7720
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
7721
- else:
7722
- self._source_file = None
7723
-
7724
- self._thread = LoggingThreadInfo.build()
7725
- self._process = LoggingProcessInfo.build()
7726
- self._multiprocessing = LoggingMultiprocessingInfo.build()
7727
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
7728
-
7729
- #
7730
-
7731
- def caller(self) -> ta.Optional[LoggingCaller]:
7732
- try:
7733
- return self._caller
7734
- except AttributeError:
7735
- raise CaptureLoggingContext.NotCapturedError from None
7736
-
7737
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
7738
- try:
7739
- return self._source_file
7740
- except AttributeError:
7741
- raise CaptureLoggingContext.NotCapturedError from None
7742
-
7743
- #
7744
-
7745
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
7746
- try:
7747
- return self._thread
7748
- except AttributeError:
7749
- raise CaptureLoggingContext.NotCapturedError from None
7750
-
7751
- def process(self) -> ta.Optional[LoggingProcessInfo]:
7752
- try:
7753
- return self._process
7754
- except AttributeError:
7755
- raise CaptureLoggingContext.NotCapturedError from None
7768
+ ))
7756
7769
 
7757
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
7758
- try:
7759
- return self._multiprocessing
7760
- except AttributeError:
7761
- 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
+ ))
7762
7774
 
7763
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
7764
- try:
7765
- return self._asyncio_task
7766
- except AttributeError:
7767
- 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
+ )
7768
7781
 
7769
7782
 
7770
7783
  ########################################
@@ -9436,7 +9449,6 @@ class CoroHttpServer:
9436
9449
 
9437
9450
 
9438
9451
  class AnyLogger(Abstract, ta.Generic[T]):
9439
- @ta.final
9440
9452
  def is_enabled_for(self, level: LogLevel) -> bool:
9441
9453
  return level >= self.get_effective_level()
9442
9454
 
@@ -9582,36 +9594,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
9582
9594
 
9583
9595
  ##
9584
9596
 
9585
- @classmethod
9586
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
9587
- if callable(msg):
9588
- if args:
9589
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
9590
- x = msg()
9591
- if isinstance(x, str):
9592
- return x, ()
9593
- elif isinstance(x, tuple):
9594
- if x:
9595
- return x[0], x[1:]
9596
- else:
9597
- return '', ()
9598
- else:
9599
- raise TypeError(x)
9600
-
9601
- elif isinstance(msg, tuple):
9602
- if args:
9603
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
9604
- if msg:
9605
- return msg[0], msg[1:]
9606
- else:
9607
- return '', ()
9608
-
9609
- elif isinstance(msg, str):
9610
- return msg, args
9611
-
9612
- else:
9613
- raise TypeError(msg)
9614
-
9615
9597
  @abc.abstractmethod
9616
9598
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
9617
9599
  raise NotImplementedError
@@ -9635,7 +9617,7 @@ class AsyncLogger(AnyLogger[ta.Awaitable[None]], Abstract):
9635
9617
  class AnyNopLogger(AnyLogger[T], Abstract):
9636
9618
  @ta.final
9637
9619
  def get_effective_level(self) -> LogLevel:
9638
- return 999
9620
+ return -999
9639
9621
 
9640
9622
 
9641
9623
  @ta.final
@@ -9652,137 +9634,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
9652
9634
 
9653
9635
  ########################################
9654
9636
  # ../../../omlish/logs/std/records.py
9637
+ """
9638
+ TODO:
9639
+ - TypedDict?
9640
+ """
9655
9641
 
9656
9642
 
9657
9643
  ##
9658
9644
 
9659
9645
 
9660
- # Ref:
9661
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
9662
- #
9663
- # LogRecord:
9664
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
9665
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
9666
- #
9667
- # LogRecord.__init__ args:
9668
- # - name: str
9669
- # - level: int
9670
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
9671
- # - lineno: int - May be 0.
9672
- # - msg: str
9673
- # - args: tuple | dict | 1-tuple[dict]
9674
- # - exc_info: LoggingExcInfoTuple | None
9675
- # - func: str | None = None -> funcName
9676
- # - sinfo: str | None = None -> stack_info
9677
- #
9678
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9679
- # Name of the logger used to log the call. Unmodified by ctor.
9680
- 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
+ #
9681
9654
 
9682
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
9683
- # (see Using arbitrary objects as messages). Unmodified by ctor.
9684
- msg=str,
9655
+ def __new__(cls, *args, **kwargs): # noqa
9656
+ raise TypeError
9685
9657
 
9686
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
9687
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
9688
- # the mapping, but is otherwise unmodified.
9689
- 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
9690
9663
 
9691
- #
9664
+ #
9692
9665
 
9693
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
9694
- # `getLevelName(level)`.
9695
- levelname=str,
9666
+ @ta.final
9667
+ class NOT_SET: # noqa
9668
+ def __new__(cls, *args, **kwargs): # noqa
9669
+ raise TypeError
9696
9670
 
9697
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
9698
- levelno=int,
9671
+ class RecordAttr(ta.NamedTuple):
9672
+ name: str
9673
+ type: ta.Any
9674
+ default: ta.Any
9699
9675
 
9700
- #
9676
+ # @abc.abstractmethod
9677
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
9701
9678
 
9702
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
9703
- # to "(unknown file)" by Logger.findCaller / Logger._log.
9704
- 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
9705
9686
 
9706
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
9707
- filename=str,
9687
+ #
9708
9688
 
9709
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
9710
- # "Unknown module".
9711
- module=str,
9689
+ @abc.abstractmethod
9690
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
9691
+ raise NotImplementedError
9712
9692
 
9713
- #
9693
+ #
9714
9694
 
9715
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
9716
- exc_info=ta.Optional[LoggingExcInfoTuple],
9695
+ @abc.abstractmethod
9696
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
9697
+ raise NotImplementedError
9717
9698
 
9718
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
9719
- exc_text=ta.Optional[str],
9699
+ #
9720
9700
 
9721
- #
9701
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9702
+ super().__init_subclass__(**kwargs)
9722
9703
 
9723
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
9724
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
9725
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
9726
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
9727
- stack_info=ta.Optional[str],
9704
+ if Abstract in cls.__bases__:
9705
+ return
9728
9706
 
9729
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
9730
- # Logger.findCaller / Logger._log.
9731
- 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
9732
9726
 
9733
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
9734
- # "(unknown function)" by Logger.findCaller / Logger._log.
9735
- 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
9736
9732
 
9737
- #
9733
+ #
9738
9734
 
9739
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
9740
- #
9741
- # See:
9742
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
9743
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
9744
- #
9745
- 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?
9741
+
9742
+ @abc.abstractmethod
9743
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
9744
+ raise NotImplementedError
9746
9745
 
9747
- # Millisecond portion of the time when the LogRecord was created.
9748
- msecs=float,
9746
+ #
9749
9747
 
9750
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
9751
- relativeCreated=float,
9748
+ @abc.abstractmethod
9749
+ def record_to_info(self, rec: logging.LogRecord) -> T:
9750
+ raise NotImplementedError
9752
9751
 
9753
- #
9752
+ #
9754
9753
 
9755
- # Thread ID if available, and `logging.logThreads` is truthy.
9756
- thread=ta.Optional[int],
9754
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9755
+ super().__init_subclass__(**kwargs)
9757
9756
 
9758
- # Thread name if available, and `logging.logThreads` is truthy.
9759
- threadName=ta.Optional[str],
9757
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
9758
+ raise TypeError(cls.record_attrs)
9760
9759
 
9761
- #
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
9762
9790
 
9763
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
9764
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
9765
- # as 'MainProcess'.
9766
9791
  #
9767
- # 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()
9768
9861
  #
9769
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
9770
- # 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()
9771
9870
  #
9772
- processName=ta.Optional[str],
9871
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
9872
+ # return None
9773
9873
 
9774
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
9775
- # None.
9776
- process=ta.Optional[int],
9874
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
9875
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
9777
9876
 
9778
- #
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,
9779
9886
 
9780
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
9781
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
9782
- taskName=ta.Optional[str],
9783
- )
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'
9784
9996
 
9785
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
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
10062
+
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
+ )
9786
10174
 
9787
10175
 
9788
10176
  # Formatter:
@@ -9806,14 +10194,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
9806
10194
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
9807
10195
 
9808
10196
 
9809
- ##
9810
-
9811
-
9812
10197
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
9813
10198
  pass
9814
10199
 
9815
10200
 
9816
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
+
9817
10208
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
9818
10209
 
9819
10210
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -9832,116 +10223,20 @@ _check_std_logging_record_attrs()
9832
10223
 
9833
10224
 
9834
10225
  class LoggingContextLogRecord(logging.LogRecord):
9835
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
9836
-
9837
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
9838
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
9839
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
9840
-
9841
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
9842
-
9843
- def __init__( # noqa
9844
- self,
9845
- # name,
9846
- # level,
9847
- # pathname,
9848
- # lineno,
9849
- # msg,
9850
- # args,
9851
- # exc_info,
9852
- # func=None,
9853
- # sinfo=None,
9854
- # **kwargs,
9855
- *,
9856
- name: str,
9857
- msg: str,
9858
- args: ta.Union[tuple, dict],
9859
-
9860
- _logging_context: LoggingContext,
9861
- ) -> None:
9862
- ctx = _logging_context
9863
-
9864
- self.name: str = name
9865
-
9866
- self.msg: str = msg
9867
-
9868
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
9869
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
9870
- args = args[0] # type: ignore[assignment]
9871
- self.args: ta.Union[tuple, dict] = args
9872
-
9873
- self.levelname: str = logging.getLevelName(ctx.level)
9874
- self.levelno: int = ctx.level
9875
-
9876
- if (caller := ctx.caller()) is not None:
9877
- self.pathname: str = caller.file_path
9878
- else:
9879
- self.pathname = self._UNKNOWN_PATH_NAME
9880
-
9881
- if (src_file := ctx.source_file()) is not None:
9882
- self.filename: str = src_file.file_name
9883
- self.module: str = src_file.module
9884
- else:
9885
- self.filename = self.pathname
9886
- self.module = self._UNKNOWN_MODULE
9887
-
9888
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
9889
- self.exc_text: ta.Optional[str] = None
9890
-
9891
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
9892
- if caller is not None:
9893
- if (sinfo := caller.stack_info) is not None:
9894
- self.stack_info: ta.Optional[str] = '\n'.join([
9895
- self._STACK_INFO_PREFIX,
9896
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
9897
- ])
9898
- else:
9899
- self.stack_info = None
9900
-
9901
- self.lineno: int = caller.line_no
9902
- self.funcName: str = caller.name
9903
-
9904
- else:
9905
- self.stack_info = None
9906
-
9907
- self.lineno = 0
9908
- self.funcName = self._UNKNOWN_FUNC_NAME
9909
-
9910
- times = ctx.times
9911
- self.created: float = times.created
9912
- self.msecs: float = times.msecs
9913
- self.relativeCreated: float = times.relative_created
9914
-
9915
- if logging.logThreads:
9916
- thread = check.not_none(ctx.thread())
9917
- self.thread: ta.Optional[int] = thread.ident
9918
- self.threadName: ta.Optional[str] = thread.name
9919
- else:
9920
- self.thread = None
9921
- self.threadName = None
9922
-
9923
- if logging.logProcesses:
9924
- process = check.not_none(ctx.process())
9925
- self.process: ta.Optional[int] = process.pid
9926
- else:
9927
- self.process = None
9928
-
9929
- if logging.logMultiprocessing:
9930
- if (mp := ctx.multiprocessing()) is not None:
9931
- self.processName: ta.Optional[str] = mp.process_name
9932
- else:
9933
- self.processName = None
9934
- else:
9935
- self.processName = None
9936
-
9937
- # Absent <3.12
9938
- if getattr(logging, 'logAsyncioTasks', None):
9939
- if (at := ctx.asyncio_task()) is not None:
9940
- self.taskName: ta.Optional[str] = at.name
9941
- else:
9942
- self.taskName = None
9943
- else:
9944
- 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))
9945
10240
 
9946
10241
 
9947
10242
  ########################################
@@ -11138,7 +11433,7 @@ class CoroHttpServerSocketHandler(SocketHandler_):
11138
11433
 
11139
11434
 
11140
11435
  ########################################
11141
- # ../../../omlish/logs/std/adapters.py
11436
+ # ../../../omlish/logs/std/loggers.py
11142
11437
 
11143
11438
 
11144
11439
  ##
@@ -11154,25 +11449,27 @@ class StdLogger(Logger):
11154
11449
  def std(self) -> logging.Logger:
11155
11450
  return self._std
11156
11451
 
11452
+ def is_enabled_for(self, level: LogLevel) -> bool:
11453
+ return self._std.isEnabledFor(level)
11454
+
11157
11455
  def get_effective_level(self) -> LogLevel:
11158
11456
  return self._std.getEffectiveLevel()
11159
11457
 
11160
11458
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
11161
- if not self.is_enabled_for(ctx.level):
11459
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
11162
11460
  return
11163
11461
 
11164
- ctx.capture()
11165
-
11166
- ms, args = self._prepare_msg_args(msg, *args)
11167
-
11168
- rec = LoggingContextLogRecord(
11462
+ ctx.set_basic(
11169
11463
  name=self._std.name,
11170
- msg=ms,
11171
- args=args,
11172
11464
 
11173
- _logging_context=ctx,
11465
+ msg=msg,
11466
+ args=args,
11174
11467
  )
11175
11468
 
11469
+ ctx.capture()
11470
+
11471
+ rec = LoggingContextLogRecord(_logging_context=ctx)
11472
+
11176
11473
  self._std.handle(rec)
11177
11474
 
11178
11475