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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ominfra/scripts/manage.py CHANGED
@@ -133,6 +133,13 @@ U = ta.TypeVar('U')
133
133
  # ../../omlish/lite/timeouts.py
134
134
  TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.DEFAULT'], ta.Iterable['TimeoutLike'], float, None] # ta.TypeAlias
135
135
 
136
+ # ../../omlish/logs/infos.py
137
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
138
+ LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
139
+ LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
140
+ LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
141
+ LoggingContextInfo = ta.Any # ta.TypeAlias
142
+
136
143
  # ../../omlish/os/atomics.py
137
144
  AtomicPathSwapKind = ta.Literal['dir', 'file']
138
145
  AtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
@@ -151,16 +158,11 @@ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
151
158
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
152
159
 
153
160
  # ../../omlish/logs/contexts.py
154
- LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
155
- LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
156
- LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
161
+ LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
157
162
 
158
163
  # deploy/specs.py
159
164
  KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
160
165
 
161
- # ../../omlish/logs/base.py
162
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
163
-
164
166
  # ../../omlish/subprocesses/base.py
165
167
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
166
168
 
@@ -2779,8 +2781,6 @@ class AttrOps(ta.Generic[T]):
2779
2781
  self._eq = _eq
2780
2782
  return _eq
2781
2783
 
2782
- #
2783
-
2784
2784
  @property
2785
2785
  def hash_eq(self) -> ta.Tuple[
2786
2786
  ta.Callable[[T], int],
@@ -2788,6 +2788,8 @@ class AttrOps(ta.Generic[T]):
2788
2788
  ]:
2789
2789
  return (self.hash, self.eq)
2790
2790
 
2791
+ #
2792
+
2791
2793
  @property
2792
2794
  def repr_hash_eq(self) -> ta.Tuple[
2793
2795
  ta.Callable[[T], str],
@@ -2798,20 +2800,25 @@ class AttrOps(ta.Generic[T]):
2798
2800
 
2799
2801
  #
2800
2802
 
2803
+ class NOT_SET: # noqa
2804
+ def __new__(cls, *args, **kwargs): # noqa
2805
+ raise TypeError
2806
+
2801
2807
  def install(
2802
2808
  self,
2803
2809
  locals_dct: ta.MutableMapping[str, ta.Any],
2804
2810
  *,
2805
- all: bool = False, # noqa
2806
- repr: bool = False, # noqa
2807
- hash: bool = False, # noqa
2808
- eq: bool = False,
2811
+ repr: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
2812
+ hash: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
2813
+ eq: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET,
2809
2814
  ) -> 'AttrOps[T]':
2810
- if repr or all:
2815
+ if all(a is self.NOT_SET for a in (repr, hash, eq)):
2816
+ repr = hash = eq = True # noqa
2817
+ if repr:
2811
2818
  locals_dct.update(__repr__=self.repr)
2812
- if hash or all:
2819
+ if hash:
2813
2820
  locals_dct.update(__hash__=self.hash)
2814
- if eq or all:
2821
+ if eq:
2815
2822
  locals_dct.update(__eq__=self.eq)
2816
2823
  return self
2817
2824
 
@@ -3986,124 +3993,6 @@ def typing_annotations_attr() -> str:
3986
3993
  return _TYPING_ANNOTATIONS_ATTR
3987
3994
 
3988
3995
 
3989
- ########################################
3990
- # ../../../omlish/logs/infos.py
3991
-
3992
-
3993
- ##
3994
-
3995
-
3996
- def logging_context_info(cls):
3997
- return cls
3998
-
3999
-
4000
- ##
4001
-
4002
-
4003
- @logging_context_info
4004
- @ta.final
4005
- class LoggingSourceFileInfo(ta.NamedTuple):
4006
- file_name: str
4007
- module: str
4008
-
4009
- @classmethod
4010
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
4011
- if file_path is None:
4012
- return None
4013
-
4014
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
4015
- try:
4016
- file_name = os.path.basename(file_path)
4017
- module = os.path.splitext(file_name)[0]
4018
- except (TypeError, ValueError, AttributeError):
4019
- return None
4020
-
4021
- return cls(
4022
- file_name,
4023
- module,
4024
- )
4025
-
4026
-
4027
- ##
4028
-
4029
-
4030
- @logging_context_info
4031
- @ta.final
4032
- class LoggingThreadInfo(ta.NamedTuple):
4033
- ident: int
4034
- native_id: ta.Optional[int]
4035
- name: str
4036
-
4037
- @classmethod
4038
- def build(cls) -> 'LoggingThreadInfo':
4039
- return cls(
4040
- threading.get_ident(),
4041
- threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
4042
- threading.current_thread().name,
4043
- )
4044
-
4045
-
4046
- ##
4047
-
4048
-
4049
- @logging_context_info
4050
- @ta.final
4051
- class LoggingProcessInfo(ta.NamedTuple):
4052
- pid: int
4053
-
4054
- @classmethod
4055
- def build(cls) -> 'LoggingProcessInfo':
4056
- return cls(
4057
- os.getpid(),
4058
- )
4059
-
4060
-
4061
- ##
4062
-
4063
-
4064
- @logging_context_info
4065
- @ta.final
4066
- class LoggingMultiprocessingInfo(ta.NamedTuple):
4067
- process_name: str
4068
-
4069
- @classmethod
4070
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
4071
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
4072
- if (mp := sys.modules.get('multiprocessing')) is None:
4073
- return None
4074
-
4075
- return cls(
4076
- mp.current_process().name,
4077
- )
4078
-
4079
-
4080
- ##
4081
-
4082
-
4083
- @logging_context_info
4084
- @ta.final
4085
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
4086
- name: str
4087
-
4088
- @classmethod
4089
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
4090
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
4091
- if (asyncio := sys.modules.get('asyncio')) is None:
4092
- return None
4093
-
4094
- try:
4095
- task = asyncio.current_task()
4096
- except Exception: # noqa
4097
- return None
4098
-
4099
- if task is None:
4100
- return None
4101
-
4102
- return cls(
4103
- task.get_name(), # Always non-None
4104
- )
4105
-
4106
-
4107
3996
  ########################################
4108
3997
  # ../../../omlish/logs/levels.py
4109
3998
 
@@ -4123,36 +4012,66 @@ class NamedLogLevel(int):
4123
4012
 
4124
4013
  #
4125
4014
 
4126
- @property
4127
- def exact_name(self) -> ta.Optional[str]:
4128
- return self._NAMES_BY_INT.get(self)
4015
+ _CACHE: ta.ClassVar[ta.MutableMapping[int, 'NamedLogLevel']] = {}
4016
+
4017
+ @ta.overload
4018
+ def __new__(cls, name: str, offset: int = 0, /) -> 'NamedLogLevel':
4019
+ ...
4020
+
4021
+ @ta.overload
4022
+ def __new__(cls, i: int, /) -> 'NamedLogLevel':
4023
+ ...
4129
4024
 
4130
- _effective_name: ta.Optional[str]
4025
+ def __new__(cls, x, offset=0, /):
4026
+ if isinstance(x, str):
4027
+ return cls(cls._INTS_BY_NAME[x.upper()] + offset)
4028
+ elif not offset and (c := cls._CACHE.get(x)) is not None:
4029
+ return c
4030
+ else:
4031
+ return super().__new__(cls, x + offset)
4032
+
4033
+ #
4034
+
4035
+ _name_and_offset: ta.Tuple[str, int]
4131
4036
 
4132
4037
  @property
4133
- def effective_name(self) -> ta.Optional[str]:
4038
+ def name_and_offset(self) -> ta.Tuple[str, int]:
4134
4039
  try:
4135
- return self._effective_name
4040
+ return self._name_and_offset
4136
4041
  except AttributeError:
4137
4042
  pass
4138
4043
 
4139
- if (n := self.exact_name) is None:
4044
+ if (n := self._NAMES_BY_INT.get(self)) is not None:
4045
+ t = (n, 0)
4046
+ else:
4140
4047
  for n, i in self._NAME_INT_PAIRS: # noqa
4141
4048
  if self >= i:
4049
+ t = (n, (self - i))
4142
4050
  break
4143
4051
  else:
4144
- n = None
4052
+ t = ('NOTSET', int(self))
4053
+
4054
+ self._name_and_offset = t
4055
+ return t
4056
+
4057
+ @property
4058
+ def exact_name(self) -> ta.Optional[str]:
4059
+ n, o = self.name_and_offset
4060
+ return n if not o else None
4145
4061
 
4146
- self._effective_name = n
4062
+ @property
4063
+ def effective_name(self) -> str:
4064
+ n, _ = self.name_and_offset
4147
4065
  return n
4148
4066
 
4149
4067
  #
4150
4068
 
4151
- def __repr__(self) -> str:
4152
- return f'{self.__class__.__name__}({int(self)})'
4153
-
4154
4069
  def __str__(self) -> str:
4155
- return self.exact_name or f'{self.effective_name or "INVALID"}:{int(self)}'
4070
+ return self.exact_name or f'{self.effective_name}{int(self):+}'
4071
+
4072
+ def __repr__(self) -> str:
4073
+ n, o = self.name_and_offset
4074
+ return f'{self.__class__.__name__}({n!r}{f", {int(o)}" if o else ""})'
4156
4075
 
4157
4076
  #
4158
4077
 
@@ -4172,6 +4091,9 @@ NamedLogLevel.DEBUG = NamedLogLevel(logging.DEBUG)
4172
4091
  NamedLogLevel.NOTSET = NamedLogLevel(logging.NOTSET)
4173
4092
 
4174
4093
 
4094
+ NamedLogLevel._CACHE.update({i: NamedLogLevel(i) for i in NamedLogLevel._NAMES_BY_INT}) # noqa
4095
+
4096
+
4175
4097
  ########################################
4176
4098
  # ../../../omlish/logs/std/filters.py
4177
4099
 
@@ -7585,223 +7507,338 @@ class PredicateTimeout(Timeout):
7585
7507
 
7586
7508
 
7587
7509
  ########################################
7588
- # ../../../omlish/logs/callers.py
7510
+ # ../../../omlish/logs/infos.py
7511
+ """
7512
+ TODO:
7513
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
7514
+ """
7589
7515
 
7590
7516
 
7591
7517
  ##
7592
7518
 
7593
7519
 
7594
- @logging_context_info
7595
- @ta.final
7596
- class LoggingCaller(ta.NamedTuple):
7597
- file_path: str
7598
- line_no: int
7599
- name: str
7600
- stack_info: ta.Optional[str]
7520
+ def logging_context_info(cls):
7521
+ return cls
7601
7522
 
7602
- @classmethod
7603
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
7604
- file_path = os.path.normcase(frame.f_code.co_filename)
7605
7523
 
7606
- # Yes, really.
7607
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
7608
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
7609
- if 'importlib' in file_path and '_bootstrap' in file_path:
7610
- return True
7524
+ @ta.final
7525
+ class LoggingContextInfos:
7526
+ def __new__(cls, *args, **kwargs): # noqa
7527
+ raise TypeError
7611
7528
 
7612
- return False
7529
+ #
7613
7530
 
7614
- @classmethod
7615
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
7616
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
7531
+ @logging_context_info
7532
+ @ta.final
7533
+ class Name(ta.NamedTuple):
7534
+ name: str
7617
7535
 
7618
- while f is not None:
7619
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
7620
- # stack_offset management.
7621
- if hasattr(f, 'f_code'):
7622
- return f
7536
+ @logging_context_info
7537
+ @ta.final
7538
+ class Level(ta.NamedTuple):
7539
+ level: NamedLogLevel
7540
+ name: str
7623
7541
 
7624
- f = f.f_back
7542
+ @classmethod
7543
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
7544
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
7545
+ return cls(
7546
+ level=nl,
7547
+ name=logging.getLevelName(nl),
7548
+ )
7625
7549
 
7626
- return None
7550
+ @logging_context_info
7551
+ @ta.final
7552
+ class Msg(ta.NamedTuple):
7553
+ msg: str
7554
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
7627
7555
 
7628
- @classmethod
7629
- def find(
7630
- cls,
7631
- ofs: int = 0,
7632
- *,
7633
- stack_info: bool = False,
7634
- ) -> ta.Optional['LoggingCaller']:
7635
- if (f := cls.find_frame(ofs + 1)) is None:
7636
- return None
7556
+ @classmethod
7557
+ def build(
7558
+ cls,
7559
+ msg: ta.Union[str, tuple, LoggingMsgFn],
7560
+ *args: ta.Any,
7561
+ ) -> 'LoggingContextInfos.Msg':
7562
+ s: str
7563
+ a: ta.Any
7564
+
7565
+ if callable(msg):
7566
+ if args:
7567
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
7568
+ x = msg()
7569
+ if isinstance(x, str):
7570
+ s, a = x, ()
7571
+ elif isinstance(x, tuple):
7572
+ if x:
7573
+ s, a = x[0], x[1:]
7574
+ else:
7575
+ s, a = '', ()
7576
+ else:
7577
+ raise TypeError(x)
7637
7578
 
7638
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
7639
- sinfo = None
7640
- if stack_info:
7641
- sio = io.StringIO()
7642
- traceback.print_stack(f, file=sio)
7643
- sinfo = sio.getvalue()
7644
- sio.close()
7645
- if sinfo[-1] == '\n':
7646
- sinfo = sinfo[:-1]
7579
+ elif isinstance(msg, tuple):
7580
+ if args:
7581
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
7582
+ if msg:
7583
+ s, a = msg[0], msg[1:]
7584
+ else:
7585
+ s, a = '', ()
7647
7586
 
7648
- return cls(
7649
- f.f_code.co_filename,
7650
- f.f_lineno or 0,
7651
- f.f_code.co_name,
7652
- sinfo,
7653
- )
7587
+ elif isinstance(msg, str):
7588
+ s, a = msg, args
7654
7589
 
7590
+ else:
7591
+ raise TypeError(msg)
7655
7592
 
7656
- ########################################
7657
- # ../../../omlish/logs/protocols.py
7593
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
7594
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
7595
+ a = a[0]
7658
7596
 
7597
+ return cls(
7598
+ msg=s,
7599
+ args=a,
7600
+ )
7659
7601
 
7660
- ##
7602
+ @logging_context_info
7603
+ @ta.final
7604
+ class Extra(ta.NamedTuple):
7605
+ extra: ta.Mapping[ta.Any, ta.Any]
7661
7606
 
7607
+ @logging_context_info
7608
+ @ta.final
7609
+ class Time(ta.NamedTuple):
7610
+ ns: int
7611
+ secs: float
7612
+ msecs: float
7613
+ relative_secs: float
7662
7614
 
7663
- class LoggerLike(ta.Protocol):
7664
- """Satisfied by both our Logger and stdlib logging.Logger."""
7615
+ @classmethod
7616
+ def get_std_start_ns(cls) -> int:
7617
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
7665
7618
 
7666
- def isEnabledFor(self, level: LogLevel) -> bool: ... # noqa
7619
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
7620
+ # an int.
7621
+ #
7622
+ # See:
7623
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7624
+ #
7625
+ if isinstance(x, float):
7626
+ return int(x * 1e9)
7627
+ else:
7628
+ return x
7667
7629
 
7668
- def getEffectiveLevel(self) -> LogLevel: ... # noqa
7630
+ @classmethod
7631
+ def build(
7632
+ cls,
7633
+ ns: int,
7634
+ *,
7635
+ start_ns: ta.Optional[int] = None,
7636
+ ) -> 'LoggingContextInfos.Time':
7637
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7638
+ secs = ns / 1e9 # ns to float seconds
7639
+
7640
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
7641
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
7642
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
7643
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
7644
+
7645
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
7646
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
7647
+ # ns -> sec conversion can round up, e.g:
7648
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
7649
+ msecs = 0.0
7650
+
7651
+ if start_ns is None:
7652
+ start_ns = cls.get_std_start_ns()
7653
+ relative_secs = (ns - start_ns) / 1e6
7654
+
7655
+ return cls(
7656
+ ns=ns,
7657
+ secs=secs,
7658
+ msecs=msecs,
7659
+ relative_secs=relative_secs,
7660
+ )
7669
7661
 
7670
- #
7662
+ @logging_context_info
7663
+ @ta.final
7664
+ class Exc(ta.NamedTuple):
7665
+ info: LoggingExcInfo
7666
+ info_tuple: LoggingExcInfoTuple
7671
7667
 
7672
- def log(self, level: LogLevel, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7668
+ @classmethod
7669
+ def build(
7670
+ cls,
7671
+ arg: LoggingExcInfoArg = False,
7672
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
7673
+ if arg is True:
7674
+ sys_exc_info = sys.exc_info()
7675
+ if sys_exc_info[0] is not None:
7676
+ arg = sys_exc_info
7677
+ else:
7678
+ arg = None
7679
+ elif arg is False:
7680
+ arg = None
7681
+ if arg is None:
7682
+ return None
7673
7683
 
7674
- def debug(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7684
+ info: LoggingExcInfo = arg
7685
+ if isinstance(info, BaseException):
7686
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
7687
+ else:
7688
+ info_tuple = info
7675
7689
 
7676
- def info(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7690
+ return cls(
7691
+ info=info,
7692
+ info_tuple=info_tuple,
7693
+ )
7677
7694
 
7678
- def warning(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7695
+ @logging_context_info
7696
+ @ta.final
7697
+ class Caller(ta.NamedTuple):
7698
+ file_path: str
7699
+ line_no: int
7700
+ func_name: str
7701
+ stack_info: ta.Optional[str]
7679
7702
 
7680
- def error(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7703
+ @classmethod
7704
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
7705
+ file_path = os.path.normcase(frame.f_code.co_filename)
7681
7706
 
7682
- def exception(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7707
+ # Yes, really.
7708
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
7709
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
7710
+ if 'importlib' in file_path and '_bootstrap' in file_path:
7711
+ return True
7683
7712
 
7684
- def critical(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7713
+ return False
7685
7714
 
7715
+ @classmethod
7716
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
7717
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
7686
7718
 
7687
- ########################################
7688
- # ../../../omlish/logs/std/json.py
7689
- """
7690
- TODO:
7691
- - translate json keys
7692
- """
7719
+ while f is not None:
7720
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
7721
+ # manual stack_offset management.
7722
+ if hasattr(f, 'f_code'):
7723
+ return f
7693
7724
 
7725
+ f = f.f_back
7694
7726
 
7695
- ##
7727
+ return None
7696
7728
 
7729
+ @classmethod
7730
+ def build(
7731
+ cls,
7732
+ stack_offset: int = 0,
7733
+ *,
7734
+ stack_info: bool = False,
7735
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
7736
+ if (f := cls.find_frame(stack_offset + 1)) is None:
7737
+ return None
7697
7738
 
7698
- class JsonLoggingFormatter(logging.Formatter):
7699
- KEYS: ta.Mapping[str, bool] = {
7700
- 'name': False,
7701
- 'msg': False,
7702
- 'args': False,
7703
- 'levelname': False,
7704
- 'levelno': False,
7705
- 'pathname': False,
7706
- 'filename': False,
7707
- 'module': False,
7708
- 'exc_info': True,
7709
- 'exc_text': True,
7710
- 'stack_info': True,
7711
- 'lineno': False,
7712
- 'funcName': False,
7713
- 'created': False,
7714
- 'msecs': False,
7715
- 'relativeCreated': False,
7716
- 'thread': False,
7717
- 'threadName': False,
7718
- 'processName': False,
7719
- 'process': False,
7720
- }
7739
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
7740
+ sinfo = None
7741
+ if stack_info:
7742
+ sio = io.StringIO()
7743
+ traceback.print_stack(f, file=sio)
7744
+ sinfo = sio.getvalue()
7745
+ sio.close()
7746
+ if sinfo[-1] == '\n':
7747
+ sinfo = sinfo[:-1]
7748
+
7749
+ return cls(
7750
+ file_path=f.f_code.co_filename,
7751
+ line_no=f.f_lineno or 0,
7752
+ func_name=f.f_code.co_name,
7753
+ stack_info=sinfo,
7754
+ )
7721
7755
 
7722
- def __init__(
7723
- self,
7724
- *args: ta.Any,
7725
- json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
7726
- **kwargs: ta.Any,
7727
- ) -> None:
7728
- super().__init__(*args, **kwargs)
7756
+ @logging_context_info
7757
+ @ta.final
7758
+ class SourceFile(ta.NamedTuple):
7759
+ file_name: str
7760
+ module: str
7729
7761
 
7730
- if json_dumps is None:
7731
- json_dumps = json_dumps_compact
7732
- self._json_dumps = json_dumps
7762
+ @classmethod
7763
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
7764
+ if caller_file_path is None:
7765
+ return None
7733
7766
 
7734
- def format(self, record: logging.LogRecord) -> str:
7735
- dct = {
7736
- k: v
7737
- for k, o in self.KEYS.items()
7738
- for v in [getattr(record, k)]
7739
- if not (o and v is None)
7740
- }
7741
- return self._json_dumps(dct)
7767
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
7768
+ try:
7769
+ file_name = os.path.basename(caller_file_path)
7770
+ module = os.path.splitext(file_name)[0]
7771
+ except (TypeError, ValueError, AttributeError):
7772
+ return None
7742
7773
 
7774
+ return cls(
7775
+ file_name=file_name,
7776
+ module=module,
7777
+ )
7743
7778
 
7744
- ########################################
7745
- # ../../../omlish/logs/times.py
7779
+ @logging_context_info
7780
+ @ta.final
7781
+ class Thread(ta.NamedTuple):
7782
+ ident: int
7783
+ native_id: ta.Optional[int]
7784
+ name: str
7746
7785
 
7786
+ @classmethod
7787
+ def build(cls) -> 'LoggingContextInfos.Thread':
7788
+ return cls(
7789
+ ident=threading.get_ident(),
7790
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
7791
+ name=threading.current_thread().name,
7792
+ )
7747
7793
 
7748
- ##
7794
+ @logging_context_info
7795
+ @ta.final
7796
+ class Process(ta.NamedTuple):
7797
+ pid: int
7749
7798
 
7799
+ @classmethod
7800
+ def build(cls) -> 'LoggingContextInfos.Process':
7801
+ return cls(
7802
+ pid=os.getpid(),
7803
+ )
7750
7804
 
7751
- @logging_context_info
7752
- @ta.final
7753
- class LoggingTimeFields(ta.NamedTuple):
7754
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
7805
+ @logging_context_info
7806
+ @ta.final
7807
+ class Multiprocessing(ta.NamedTuple):
7808
+ process_name: str
7755
7809
 
7756
- created: float
7757
- msecs: float
7758
- relative_created: float
7810
+ @classmethod
7811
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
7812
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
7813
+ if (mp := sys.modules.get('multiprocessing')) is None:
7814
+ return None
7759
7815
 
7760
- @classmethod
7761
- def get_std_start_time_ns(cls) -> int:
7762
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
7816
+ return cls(
7817
+ process_name=mp.current_process().name,
7818
+ )
7763
7819
 
7764
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
7765
- # int.
7766
- #
7767
- # See:
7768
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7769
- #
7770
- if isinstance(x, float):
7771
- return int(x * 1e9)
7772
- else:
7773
- return x
7820
+ @logging_context_info
7821
+ @ta.final
7822
+ class AsyncioTask(ta.NamedTuple):
7823
+ name: str
7774
7824
 
7775
- @classmethod
7776
- def build(
7777
- cls,
7778
- time_ns: int,
7779
- *,
7780
- start_time_ns: ta.Optional[int] = None,
7781
- ) -> 'LoggingTimeFields':
7782
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
7783
- created = time_ns / 1e9 # ns to float seconds
7784
-
7785
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
7786
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
7787
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
7788
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
7789
-
7790
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
7791
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
7792
- # ns -> sec conversion can round up, e.g:
7793
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
7794
- msecs = 0.0
7795
-
7796
- if start_time_ns is None:
7797
- start_time_ns = cls.get_std_start_time_ns()
7798
- relative_created = (time_ns - start_time_ns) / 1e6
7825
+ @classmethod
7826
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
7827
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
7828
+ if (asyncio := sys.modules.get('asyncio')) is None:
7829
+ return None
7799
7830
 
7800
- return cls(
7801
- created,
7802
- msecs,
7803
- relative_created,
7804
- )
7831
+ try:
7832
+ task = asyncio.current_task()
7833
+ except Exception: # noqa
7834
+ return None
7835
+
7836
+ if task is None:
7837
+ return None
7838
+
7839
+ return cls(
7840
+ name=task.get_name(), # Always non-None
7841
+ )
7805
7842
 
7806
7843
 
7807
7844
  ##
@@ -7812,12 +7849,12 @@ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
7812
7849
 
7813
7850
 
7814
7851
  def _check_logging_start_time() -> None:
7815
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
7852
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
7816
7853
  import warnings # noqa
7817
7854
 
7818
7855
  warnings.warn(
7819
7856
  f'Unexpected logging start time detected: '
7820
- f'get_std_start_time_ns={x}, '
7857
+ f'get_std_start_ns={x}, '
7821
7858
  f'time.time()={t}',
7822
7859
  UnexpectedLoggingStartTimeWarning,
7823
7860
  )
@@ -7826,6 +7863,95 @@ def _check_logging_start_time() -> None:
7826
7863
  _check_logging_start_time()
7827
7864
 
7828
7865
 
7866
+ ########################################
7867
+ # ../../../omlish/logs/protocols.py
7868
+
7869
+
7870
+ ##
7871
+
7872
+
7873
+ @ta.runtime_checkable
7874
+ class LoggerLike(ta.Protocol):
7875
+ """Satisfied by both our Logger and stdlib logging.Logger."""
7876
+
7877
+ def isEnabledFor(self, level: LogLevel) -> bool: ... # noqa
7878
+
7879
+ def getEffectiveLevel(self) -> LogLevel: ... # noqa
7880
+
7881
+ #
7882
+
7883
+ def log(self, level: LogLevel, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7884
+
7885
+ def debug(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7886
+
7887
+ def info(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7888
+
7889
+ def warning(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7890
+
7891
+ def error(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7892
+
7893
+ def exception(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7894
+
7895
+ def critical(self, msg: str, /, *args: ta.Any, **kwargs: ta.Any) -> None: ... # noqa
7896
+
7897
+
7898
+ ########################################
7899
+ # ../../../omlish/logs/std/json.py
7900
+ """
7901
+ TODO:
7902
+ - translate json keys
7903
+ """
7904
+
7905
+
7906
+ ##
7907
+
7908
+
7909
+ class JsonLoggingFormatter(logging.Formatter):
7910
+ KEYS: ta.Mapping[str, bool] = {
7911
+ 'name': False,
7912
+ 'msg': False,
7913
+ 'args': False,
7914
+ 'levelname': False,
7915
+ 'levelno': False,
7916
+ 'pathname': False,
7917
+ 'filename': False,
7918
+ 'module': False,
7919
+ 'exc_info': True,
7920
+ 'exc_text': True,
7921
+ 'stack_info': True,
7922
+ 'lineno': False,
7923
+ 'funcName': False,
7924
+ 'created': False,
7925
+ 'msecs': False,
7926
+ 'relativeCreated': False,
7927
+ 'thread': False,
7928
+ 'threadName': False,
7929
+ 'processName': False,
7930
+ 'process': False,
7931
+ }
7932
+
7933
+ def __init__(
7934
+ self,
7935
+ *args: ta.Any,
7936
+ json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
7937
+ **kwargs: ta.Any,
7938
+ ) -> None:
7939
+ super().__init__(*args, **kwargs)
7940
+
7941
+ if json_dumps is None:
7942
+ json_dumps = json_dumps_compact
7943
+ self._json_dumps = json_dumps
7944
+
7945
+ def format(self, record: logging.LogRecord) -> str:
7946
+ dct = {
7947
+ k: v
7948
+ for k, o in self.KEYS.items()
7949
+ for v in [getattr(record, k)]
7950
+ if not (o and v is None)
7951
+ }
7952
+ return self._json_dumps(dct)
7953
+
7954
+
7829
7955
  ########################################
7830
7956
  # ../../../omlish/os/atomics.py
7831
7957
 
@@ -9965,68 +10091,36 @@ inj = InjectionApi()
9965
10091
 
9966
10092
 
9967
10093
  class LoggingContext(Abstract):
9968
- @property
9969
10094
  @abc.abstractmethod
9970
- def level(self) -> NamedLogLevel:
10095
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
9971
10096
  raise NotImplementedError
9972
10097
 
9973
- #
9974
-
9975
- @property
9976
- @abc.abstractmethod
9977
- def time_ns(self) -> int:
9978
- raise NotImplementedError
9979
-
9980
- @property
9981
- @abc.abstractmethod
9982
- def times(self) -> LoggingTimeFields:
9983
- raise NotImplementedError
9984
-
9985
- #
10098
+ @ta.final
10099
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10100
+ return self.get_info(ty)
9986
10101
 
9987
- @property
9988
- @abc.abstractmethod
9989
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
9990
- raise NotImplementedError
10102
+ @ta.final
10103
+ def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
10104
+ if (info := self.get_info(ty)) is None:
10105
+ raise TypeError(f'LoggingContextInfo absent: {ty}')
10106
+ return info
9991
10107
 
9992
- @property
9993
- @abc.abstractmethod
9994
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
9995
- raise NotImplementedError
10108
+ ##
9996
10109
 
9997
- #
9998
10110
 
10111
+ class CaptureLoggingContext(LoggingContext, Abstract):
9999
10112
  @abc.abstractmethod
10000
- def caller(self) -> ta.Optional[LoggingCaller]:
10001
- raise NotImplementedError
10113
+ def set_basic(
10114
+ self,
10115
+ name: str,
10002
10116
 
10003
- @abc.abstractmethod
10004
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
10117
+ msg: ta.Union[str, tuple, LoggingMsgFn],
10118
+ args: tuple,
10119
+ ) -> 'CaptureLoggingContext':
10005
10120
  raise NotImplementedError
10006
10121
 
10007
10122
  #
10008
10123
 
10009
- @abc.abstractmethod
10010
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
10011
- raise NotImplementedError
10012
-
10013
- @abc.abstractmethod
10014
- def process(self) -> ta.Optional[LoggingProcessInfo]:
10015
- raise NotImplementedError
10016
-
10017
- @abc.abstractmethod
10018
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
10019
- raise NotImplementedError
10020
-
10021
- @abc.abstractmethod
10022
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
10023
- raise NotImplementedError
10024
-
10025
-
10026
- ##
10027
-
10028
-
10029
- class CaptureLoggingContext(LoggingContext, Abstract):
10030
10124
  class AlreadyCapturedError(Exception):
10031
10125
  pass
10032
10126
 
@@ -10057,80 +10151,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
10057
10151
 
10058
10152
  exc_info: LoggingExcInfoArg = False,
10059
10153
 
10060
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
10154
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
10061
10155
  stack_offset: int = 0,
10062
10156
  stack_info: bool = False,
10063
10157
  ) -> None:
10064
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
10065
-
10066
- #
10158
+ # TODO: Name, Msg, Extra
10067
10159
 
10068
10160
  if time_ns is None:
10069
10161
  time_ns = time.time_ns()
10070
- self._time_ns: int = time_ns
10071
10162
 
10072
- #
10073
-
10074
- if exc_info is True:
10075
- sys_exc_info = sys.exc_info()
10076
- if sys_exc_info[0] is not None:
10077
- exc_info = sys_exc_info
10078
- else:
10079
- exc_info = None
10080
- elif exc_info is False:
10081
- exc_info = None
10082
-
10083
- if exc_info is not None:
10084
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
10085
- if isinstance(exc_info, BaseException):
10086
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
10087
- else:
10088
- self._exc_info_tuple = exc_info
10089
-
10090
- #
10163
+ self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
10164
+ self._set_info(
10165
+ LoggingContextInfos.Level.build(level),
10166
+ LoggingContextInfos.Time.build(time_ns),
10167
+ LoggingContextInfos.Exc.build(exc_info),
10168
+ )
10091
10169
 
10092
10170
  if caller is not CaptureLoggingContextImpl.NOT_SET:
10093
- self._caller = caller # type: ignore[assignment]
10171
+ self._infos[LoggingContextInfos.Caller] = caller
10094
10172
  else:
10095
10173
  self._stack_offset = stack_offset
10096
10174
  self._stack_info = stack_info
10097
10175
 
10098
- ##
10099
-
10100
- @property
10101
- def level(self) -> NamedLogLevel:
10102
- return self._level
10103
-
10104
- #
10105
-
10106
- @property
10107
- def time_ns(self) -> int:
10108
- return self._time_ns
10109
-
10110
- _times: LoggingTimeFields
10111
-
10112
- @property
10113
- def times(self) -> LoggingTimeFields:
10114
- try:
10115
- return self._times
10116
- except AttributeError:
10117
- pass
10118
-
10119
- times = self._times = LoggingTimeFields.build(self.time_ns)
10120
- return times
10176
+ def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
10177
+ for info in infos:
10178
+ if info is not None:
10179
+ self._infos[type(info)] = info
10180
+ return self
10121
10181
 
10122
- #
10182
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
10183
+ return self._infos.get(ty)
10123
10184
 
10124
- _exc_info: ta.Optional[LoggingExcInfo] = None
10125
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
10185
+ ##
10126
10186
 
10127
- @property
10128
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
10129
- return self._exc_info
10187
+ def set_basic(
10188
+ self,
10189
+ name: str,
10130
10190
 
10131
- @property
10132
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
10133
- return self._exc_info_tuple
10191
+ msg: ta.Union[str, tuple, LoggingMsgFn],
10192
+ args: tuple,
10193
+ ) -> 'CaptureLoggingContextImpl':
10194
+ return self._set_info(
10195
+ LoggingContextInfos.Name(name),
10196
+ LoggingContextInfos.Msg.build(msg, *args),
10197
+ )
10134
10198
 
10135
10199
  ##
10136
10200
 
@@ -10144,74 +10208,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
10144
10208
 
10145
10209
  _has_captured: bool = False
10146
10210
 
10147
- _caller: ta.Optional[LoggingCaller]
10148
- _source_file: ta.Optional[LoggingSourceFileInfo]
10149
-
10150
- _thread: ta.Optional[LoggingThreadInfo]
10151
- _process: ta.Optional[LoggingProcessInfo]
10152
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
10153
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
10154
-
10155
10211
  def capture(self) -> None:
10156
10212
  if self._has_captured:
10157
10213
  raise CaptureLoggingContextImpl.AlreadyCapturedError
10158
10214
  self._has_captured = True
10159
10215
 
10160
- if not hasattr(self, '_caller'):
10161
- self._caller = LoggingCaller.find(
10216
+ if LoggingContextInfos.Caller not in self._infos:
10217
+ self._set_info(LoggingContextInfos.Caller.build(
10162
10218
  self._stack_offset + 1,
10163
10219
  stack_info=self._stack_info,
10164
- )
10165
-
10166
- if (caller := self._caller) is not None:
10167
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
10168
- else:
10169
- self._source_file = None
10170
-
10171
- self._thread = LoggingThreadInfo.build()
10172
- self._process = LoggingProcessInfo.build()
10173
- self._multiprocessing = LoggingMultiprocessingInfo.build()
10174
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
10175
-
10176
- #
10177
-
10178
- def caller(self) -> ta.Optional[LoggingCaller]:
10179
- try:
10180
- return self._caller
10181
- except AttributeError:
10182
- raise CaptureLoggingContext.NotCapturedError from None
10183
-
10184
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
10185
- try:
10186
- return self._source_file
10187
- except AttributeError:
10188
- raise CaptureLoggingContext.NotCapturedError from None
10189
-
10190
- #
10191
-
10192
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
10193
- try:
10194
- return self._thread
10195
- except AttributeError:
10196
- raise CaptureLoggingContext.NotCapturedError from None
10197
-
10198
- def process(self) -> ta.Optional[LoggingProcessInfo]:
10199
- try:
10200
- return self._process
10201
- except AttributeError:
10202
- raise CaptureLoggingContext.NotCapturedError from None
10220
+ ))
10203
10221
 
10204
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
10205
- try:
10206
- return self._multiprocessing
10207
- except AttributeError:
10208
- raise CaptureLoggingContext.NotCapturedError from None
10222
+ if (caller := self[LoggingContextInfos.Caller]) is not None:
10223
+ self._set_info(LoggingContextInfos.SourceFile.build(
10224
+ caller.file_path,
10225
+ ))
10209
10226
 
10210
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
10211
- try:
10212
- return self._asyncio_task
10213
- except AttributeError:
10214
- raise CaptureLoggingContext.NotCapturedError from None
10227
+ self._set_info(
10228
+ LoggingContextInfos.Thread.build(),
10229
+ LoggingContextInfos.Process.build(),
10230
+ LoggingContextInfos.Multiprocessing.build(),
10231
+ LoggingContextInfos.AsyncioTask.build(),
10232
+ )
10215
10233
 
10216
10234
 
10217
10235
  ########################################
@@ -10970,7 +10988,6 @@ class DeploySpec(DeploySpecKeyed[DeployKey]):
10970
10988
 
10971
10989
 
10972
10990
  class AnyLogger(Abstract, ta.Generic[T]):
10973
- @ta.final
10974
10991
  def is_enabled_for(self, level: LogLevel) -> bool:
10975
10992
  return level >= self.get_effective_level()
10976
10993
 
@@ -11116,36 +11133,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
11116
11133
 
11117
11134
  ##
11118
11135
 
11119
- @classmethod
11120
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
11121
- if callable(msg):
11122
- if args:
11123
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
11124
- x = msg()
11125
- if isinstance(x, str):
11126
- return x, ()
11127
- elif isinstance(x, tuple):
11128
- if x:
11129
- return x[0], x[1:]
11130
- else:
11131
- return '', ()
11132
- else:
11133
- raise TypeError(x)
11134
-
11135
- elif isinstance(msg, tuple):
11136
- if args:
11137
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
11138
- if msg:
11139
- return msg[0], msg[1:]
11140
- else:
11141
- return '', ()
11142
-
11143
- elif isinstance(msg, str):
11144
- return msg, args
11145
-
11146
- else:
11147
- raise TypeError(msg)
11148
-
11149
11136
  @abc.abstractmethod
11150
11137
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
11151
11138
  raise NotImplementedError
@@ -11169,7 +11156,7 @@ class AsyncLogger(AnyLogger[ta.Awaitable[None]], Abstract):
11169
11156
  class AnyNopLogger(AnyLogger[T], Abstract):
11170
11157
  @ta.final
11171
11158
  def get_effective_level(self) -> LogLevel:
11172
- return 999
11159
+ return -999
11173
11160
 
11174
11161
 
11175
11162
  @ta.final
@@ -11186,137 +11173,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
11186
11173
 
11187
11174
  ########################################
11188
11175
  # ../../../omlish/logs/std/records.py
11176
+ """
11177
+ TODO:
11178
+ - TypedDict?
11179
+ """
11189
11180
 
11190
11181
 
11191
11182
  ##
11192
11183
 
11193
11184
 
11194
- # Ref:
11195
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
11196
- #
11197
- # LogRecord:
11198
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
11199
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
11200
- #
11201
- # LogRecord.__init__ args:
11202
- # - name: str
11203
- # - level: int
11204
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
11205
- # - lineno: int - May be 0.
11206
- # - msg: str
11207
- # - args: tuple | dict | 1-tuple[dict]
11208
- # - exc_info: LoggingExcInfoTuple | None
11209
- # - func: str | None = None -> funcName
11210
- # - sinfo: str | None = None -> stack_info
11211
- #
11212
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11213
- # Name of the logger used to log the call. Unmodified by ctor.
11214
- name=str,
11185
+ class LoggingContextInfoRecordAdapters:
11186
+ # Ref:
11187
+ # - https://docs.python.org/3/library/logging.html#logrecord-attributes
11188
+ #
11189
+ # LogRecord:
11190
+ # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
11191
+ # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
11192
+ #
11215
11193
 
11216
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
11217
- # (see Using arbitrary objects as messages). Unmodified by ctor.
11218
- msg=str,
11194
+ def __new__(cls, *args, **kwargs): # noqa
11195
+ raise TypeError
11219
11196
 
11220
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
11221
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
11222
- # the mapping, but is otherwise unmodified.
11223
- args=ta.Union[tuple, dict],
11197
+ class Adapter(Abstract, ta.Generic[T]):
11198
+ @property
11199
+ @abc.abstractmethod
11200
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
11201
+ raise NotImplementedError
11224
11202
 
11225
- #
11203
+ #
11226
11204
 
11227
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
11228
- # `getLevelName(level)`.
11229
- levelname=str,
11205
+ @ta.final
11206
+ class NOT_SET: # noqa
11207
+ def __new__(cls, *args, **kwargs): # noqa
11208
+ raise TypeError
11230
11209
 
11231
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
11232
- levelno=int,
11210
+ class RecordAttr(ta.NamedTuple):
11211
+ name: str
11212
+ type: ta.Any
11213
+ default: ta.Any
11233
11214
 
11234
- #
11215
+ # @abc.abstractmethod
11216
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
11235
11217
 
11236
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
11237
- # to "(unknown file)" by Logger.findCaller / Logger._log.
11238
- pathname=str,
11218
+ @property
11219
+ @abc.abstractmethod
11220
+ def _record_attrs(self) -> ta.Union[
11221
+ ta.Mapping[str, ta.Any],
11222
+ ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
11223
+ ]:
11224
+ raise NotImplementedError
11239
11225
 
11240
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
11241
- filename=str,
11226
+ #
11242
11227
 
11243
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
11244
- # "Unknown module".
11245
- module=str,
11228
+ @abc.abstractmethod
11229
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11230
+ raise NotImplementedError
11246
11231
 
11247
- #
11232
+ #
11248
11233
 
11249
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
11250
- exc_info=ta.Optional[LoggingExcInfoTuple],
11234
+ @abc.abstractmethod
11235
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
11236
+ raise NotImplementedError
11251
11237
 
11252
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
11253
- exc_text=ta.Optional[str],
11238
+ #
11254
11239
 
11255
- #
11240
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11241
+ super().__init_subclass__(**kwargs)
11256
11242
 
11257
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
11258
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
11259
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
11260
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
11261
- stack_info=ta.Optional[str],
11243
+ if Abstract in cls.__bases__:
11244
+ return
11262
11245
 
11263
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
11264
- # Logger.findCaller / Logger._log.
11265
- lineno=int,
11246
+ if 'record_attrs' in cls.__dict__:
11247
+ raise TypeError(cls)
11248
+ if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
11249
+ raise TypeError(ra)
11250
+
11251
+ rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
11252
+ for n, v in ra.items():
11253
+ if not n or not isinstance(n, str) or n in rd:
11254
+ raise AttributeError(n)
11255
+ if isinstance(v, tuple):
11256
+ t, d = v
11257
+ else:
11258
+ t, d = v, cls.NOT_SET
11259
+ rd[n] = cls.RecordAttr(
11260
+ name=n,
11261
+ type=t,
11262
+ default=d,
11263
+ )
11264
+ cls.record_attrs = rd
11266
11265
 
11267
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
11268
- # "(unknown function)" by Logger.findCaller / Logger._log.
11269
- funcName=str,
11266
+ class RequiredAdapter(Adapter[T], Abstract):
11267
+ @property
11268
+ @abc.abstractmethod
11269
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
11270
+ raise NotImplementedError
11270
11271
 
11271
- #
11272
+ #
11272
11273
 
11273
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
11274
- #
11275
- # See:
11276
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
11277
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
11278
- #
11279
- created=float,
11274
+ @ta.final
11275
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11276
+ if (info := ctx.get_info(self.info_cls)) is not None:
11277
+ return self._info_to_record(info)
11278
+ else:
11279
+ raise TypeError # FIXME: fallback?
11280
+
11281
+ @abc.abstractmethod
11282
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
11283
+ raise NotImplementedError
11280
11284
 
11281
- # Millisecond portion of the time when the LogRecord was created.
11282
- msecs=float,
11285
+ #
11283
11286
 
11284
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
11285
- relativeCreated=float,
11287
+ @abc.abstractmethod
11288
+ def record_to_info(self, rec: logging.LogRecord) -> T:
11289
+ raise NotImplementedError
11286
11290
 
11287
- #
11291
+ #
11288
11292
 
11289
- # Thread ID if available, and `logging.logThreads` is truthy.
11290
- thread=ta.Optional[int],
11293
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11294
+ super().__init_subclass__(**kwargs)
11291
11295
 
11292
- # Thread name if available, and `logging.logThreads` is truthy.
11293
- threadName=ta.Optional[str],
11296
+ if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
11297
+ raise TypeError(cls.record_attrs)
11294
11298
 
11295
- #
11299
+ class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
11300
+ @property
11301
+ @abc.abstractmethod
11302
+ def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
11303
+ raise NotImplementedError
11304
+
11305
+ record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
11306
+
11307
+ #
11308
+
11309
+ @ta.final
11310
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11311
+ if (info := ctx.get_info(self.info_cls)) is not None:
11312
+ return self._info_to_record(info)
11313
+ else:
11314
+ return self.record_defaults
11315
+
11316
+ @abc.abstractmethod
11317
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
11318
+ raise NotImplementedError
11319
+
11320
+ #
11321
+
11322
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
11323
+ super().__init_subclass__(**kwargs)
11324
+
11325
+ dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
11326
+ if any(d is cls.NOT_SET for d in dd.values()):
11327
+ raise TypeError(cls.record_attrs)
11328
+ cls.record_defaults = dd
11296
11329
 
11297
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
11298
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
11299
- # as 'MainProcess'.
11300
11330
  #
11301
- # As noted by stdlib:
11331
+
11332
+ class Name(RequiredAdapter[LoggingContextInfos.Name]):
11333
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
11334
+
11335
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11336
+ # Name of the logger used to log the call. Unmodified by ctor.
11337
+ name=str,
11338
+ )
11339
+
11340
+ def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
11341
+ return dict(
11342
+ name=info.name,
11343
+ )
11344
+
11345
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
11346
+ return LoggingContextInfos.Name(
11347
+ name=rec.name,
11348
+ )
11349
+
11350
+ class Level(RequiredAdapter[LoggingContextInfos.Level]):
11351
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
11352
+
11353
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11354
+ # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
11355
+ # `getLevelName(level)`.
11356
+ levelname=str,
11357
+
11358
+ # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
11359
+ levelno=int,
11360
+ )
11361
+
11362
+ def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
11363
+ return dict(
11364
+ levelname=info.name,
11365
+ levelno=int(info.level),
11366
+ )
11367
+
11368
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
11369
+ return LoggingContextInfos.Level.build(rec.levelno)
11370
+
11371
+ class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
11372
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
11373
+
11374
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11375
+ # The format string passed in the original logging call. Merged with args to produce message, or an
11376
+ # arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
11377
+ msg=str,
11378
+
11379
+ # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
11380
+ # (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
11381
+ # Mapping into just the mapping, but is otherwise unmodified.
11382
+ args=ta.Union[tuple, dict, None],
11383
+ )
11384
+
11385
+ def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
11386
+ return dict(
11387
+ msg=info.msg,
11388
+ args=info.args,
11389
+ )
11390
+
11391
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
11392
+ return LoggingContextInfos.Msg(
11393
+ msg=rec.msg,
11394
+ args=rec.args,
11395
+ )
11396
+
11397
+ # FIXME: handled specially - all unknown attrs on LogRecord
11398
+ # class Extra(Adapter[LoggingContextInfos.Extra]):
11399
+ # _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
11302
11400
  #
11303
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
11304
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
11401
+ # def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
11402
+ # # FIXME:
11403
+ # # if extra is not None:
11404
+ # # for key in extra:
11405
+ # # if (key in ["message", "asctime"]) or (key in rv.__dict__):
11406
+ # # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
11407
+ # # rv.__dict__[key] = extra[key]
11408
+ # return dict()
11305
11409
  #
11306
- processName=ta.Optional[str],
11410
+ # def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
11411
+ # return None
11307
11412
 
11308
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
11309
- # None.
11310
- process=ta.Optional[int],
11413
+ class Time(RequiredAdapter[LoggingContextInfos.Time]):
11414
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
11311
11415
 
11312
- #
11416
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11417
+ # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
11418
+ # `time.time()`.
11419
+ #
11420
+ # See:
11421
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
11422
+ # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
11423
+ #
11424
+ created=float,
11313
11425
 
11314
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
11315
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
11316
- taskName=ta.Optional[str],
11317
- )
11426
+ # Millisecond portion of the time when the LogRecord was created.
11427
+ msecs=float,
11428
+
11429
+ # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
11430
+ relativeCreated=float,
11431
+ )
11432
+
11433
+ def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
11434
+ return dict(
11435
+ created=info.secs,
11436
+ msecs=info.msecs,
11437
+ relativeCreated=info.relative_secs,
11438
+ )
11439
+
11440
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
11441
+ return LoggingContextInfos.Time.build(
11442
+ int(rec.created * 1e9),
11443
+ )
11444
+
11445
+ class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
11446
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
11447
+
11448
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11449
+ # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
11450
+ exc_info=(ta.Optional[LoggingExcInfoTuple], None),
11451
+
11452
+ # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
11453
+ exc_text=(ta.Optional[str], None),
11454
+ )
11455
+
11456
+ def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
11457
+ return dict(
11458
+ exc_info=info.info_tuple,
11459
+ exc_text=None,
11460
+ )
11461
+
11462
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
11463
+ # FIXME:
11464
+ # error: Argument 1 to "build" of "Exc" has incompatible type
11465
+ # "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
11466
+ # "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
11467
+ return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
11468
+
11469
+ class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
11470
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
11471
+
11472
+ _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
11473
+ _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
11474
+
11475
+ _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
11318
11476
 
11319
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
11477
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11478
+ # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
11479
+ # default to "(unknown file)" by Logger.findCaller / Logger._log.
11480
+ pathname=(str, _UNKNOWN_PATH_NAME),
11481
+
11482
+ # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
11483
+ # y Logger.findCaller / Logger._log.
11484
+ lineno=(int, 0),
11485
+
11486
+ # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
11487
+ # "(unknown function)" by Logger.findCaller / Logger._log.
11488
+ funcName=(str, _UNKNOWN_FUNC_NAME),
11489
+
11490
+ # Stack frame information (where available) from the bottom of the stack in the current thread, up to and
11491
+ # including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
11492
+ # to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
11493
+ # `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
11494
+ # stripped of exactly one trailing `\n` if present.
11495
+ stack_info=(ta.Optional[str], None),
11496
+ )
11497
+
11498
+ def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
11499
+ if (sinfo := caller.stack_info) is not None:
11500
+ stack_info: ta.Optional[str] = '\n'.join([
11501
+ self._STACK_INFO_PREFIX,
11502
+ sinfo[1:] if sinfo.endswith('\n') else sinfo,
11503
+ ])
11504
+ else:
11505
+ stack_info = None
11506
+
11507
+ return dict(
11508
+ pathname=caller.file_path,
11509
+
11510
+ lineno=caller.line_no,
11511
+ funcName=caller.func_name,
11512
+
11513
+ stack_info=stack_info,
11514
+ )
11515
+
11516
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
11517
+ # FIXME: piecemeal?
11518
+ # FIXME: strip _STACK_INFO_PREFIX
11519
+ raise NotImplementedError
11520
+
11521
+ class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
11522
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
11523
+
11524
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
11525
+ # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
11526
+ # pathname.
11527
+ filename=str,
11528
+
11529
+ # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
11530
+ # "Unknown module".
11531
+ module=str,
11532
+ )
11533
+
11534
+ _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
11535
+
11536
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
11537
+ if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
11538
+ return dict(
11539
+ filename=info.file_name,
11540
+ module=info.module,
11541
+ )
11542
+
11543
+ if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
11544
+ return dict(
11545
+ filename=caller.file_path,
11546
+ module=self._UNKNOWN_MODULE,
11547
+ )
11548
+
11549
+ return dict(
11550
+ filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
11551
+ module=self._UNKNOWN_MODULE,
11552
+ )
11553
+
11554
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
11555
+ if not (
11556
+ rec.module is None or
11557
+ rec.module == self._UNKNOWN_MODULE
11558
+ ):
11559
+ return LoggingContextInfos.SourceFile(
11560
+ file_name=rec.filename,
11561
+ module=rec.module, # FIXME: piecemeal?
11562
+ )
11563
+
11564
+ return None
11565
+
11566
+ class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
11567
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
11568
+
11569
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11570
+ # Thread ID if available, and `logging.logThreads` is truthy.
11571
+ thread=(ta.Optional[int], None),
11572
+
11573
+ # Thread name if available, and `logging.logThreads` is truthy.
11574
+ threadName=(ta.Optional[str], None),
11575
+ )
11576
+
11577
+ def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
11578
+ if logging.logThreads:
11579
+ return dict(
11580
+ thread=info.ident,
11581
+ threadName=info.name,
11582
+ )
11583
+
11584
+ return self.record_defaults
11585
+
11586
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
11587
+ if (
11588
+ (ident := rec.thread) is not None and
11589
+ (name := rec.threadName) is not None
11590
+ ):
11591
+ return LoggingContextInfos.Thread(
11592
+ ident=ident,
11593
+ native_id=None,
11594
+ name=name,
11595
+ )
11596
+
11597
+ return None
11598
+
11599
+ class Process(OptionalAdapter[LoggingContextInfos.Process]):
11600
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
11601
+
11602
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11603
+ # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
11604
+ # otherwise None.
11605
+ process=(ta.Optional[int], None),
11606
+ )
11607
+
11608
+ def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
11609
+ if logging.logProcesses:
11610
+ return dict(
11611
+ process=info.pid,
11612
+ )
11613
+
11614
+ return self.record_defaults
11615
+
11616
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
11617
+ if (
11618
+ (pid := rec.process) is not None
11619
+ ):
11620
+ return LoggingContextInfos.Process(
11621
+ pid=pid,
11622
+ )
11623
+
11624
+ return None
11625
+
11626
+ class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
11627
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
11628
+
11629
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
11630
+ # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
11631
+ # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
11632
+ # remains as 'MainProcess'.
11633
+ #
11634
+ # As noted by stdlib:
11635
+ #
11636
+ # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
11637
+ # third-party code to run when multiprocessing calls import. See issue 8200 for an example
11638
+ #
11639
+ processName=(ta.Optional[str], None),
11640
+ )
11641
+
11642
+ def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
11643
+ if logging.logMultiprocessing:
11644
+ return dict(
11645
+ processName=info.process_name,
11646
+ )
11647
+
11648
+ return self.record_defaults
11649
+
11650
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
11651
+ if (
11652
+ (process_name := rec.processName) is not None
11653
+ ):
11654
+ return LoggingContextInfos.Multiprocessing(
11655
+ process_name=process_name,
11656
+ )
11657
+
11658
+ return None
11659
+
11660
+ class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
11661
+ info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
11662
+
11663
+ _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
11664
+ # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
11665
+ # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
11666
+ taskName=(ta.Optional[str], None),
11667
+ )
11668
+
11669
+ def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
11670
+ if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
11671
+ return dict(
11672
+ taskName=info.name,
11673
+ )
11674
+
11675
+ return self.record_defaults
11676
+
11677
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
11678
+ if (
11679
+ (name := getattr(rec, 'taskName', None)) is not None
11680
+ ):
11681
+ return LoggingContextInfos.AsyncioTask(
11682
+ name=name,
11683
+ )
11684
+
11685
+ return None
11686
+
11687
+
11688
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
11689
+ LoggingContextInfoRecordAdapters.Name(),
11690
+ LoggingContextInfoRecordAdapters.Level(),
11691
+ LoggingContextInfoRecordAdapters.Msg(),
11692
+ LoggingContextInfoRecordAdapters.Time(),
11693
+ LoggingContextInfoRecordAdapters.Exc(),
11694
+ LoggingContextInfoRecordAdapters.Caller(),
11695
+ LoggingContextInfoRecordAdapters.SourceFile(),
11696
+ LoggingContextInfoRecordAdapters.Thread(),
11697
+ LoggingContextInfoRecordAdapters.Process(),
11698
+ LoggingContextInfoRecordAdapters.Multiprocessing(),
11699
+ LoggingContextInfoRecordAdapters.AsyncioTask(),
11700
+ ]
11701
+
11702
+ _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
11703
+ ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
11704
+ }
11705
+
11706
+
11707
+ ##
11708
+
11709
+
11710
+ KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
11711
+ a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
11712
+ )
11320
11713
 
11321
11714
 
11322
11715
  # Formatter:
@@ -11340,14 +11733,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
11340
11733
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
11341
11734
 
11342
11735
 
11343
- ##
11344
-
11345
-
11346
11736
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
11347
11737
  pass
11348
11738
 
11349
11739
 
11350
11740
  def _check_std_logging_record_attrs() -> None:
11741
+ if (
11742
+ len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
11743
+ len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
11744
+ ):
11745
+ raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
11746
+
11351
11747
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
11352
11748
 
11353
11749
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -11366,116 +11762,20 @@ _check_std_logging_record_attrs()
11366
11762
 
11367
11763
 
11368
11764
  class LoggingContextLogRecord(logging.LogRecord):
11369
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
11370
-
11371
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
11372
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
11373
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
11374
-
11375
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
11376
-
11377
- def __init__( # noqa
11378
- self,
11379
- # name,
11380
- # level,
11381
- # pathname,
11382
- # lineno,
11383
- # msg,
11384
- # args,
11385
- # exc_info,
11386
- # func=None,
11387
- # sinfo=None,
11388
- # **kwargs,
11389
- *,
11390
- name: str,
11391
- msg: str,
11392
- args: ta.Union[tuple, dict],
11393
-
11394
- _logging_context: LoggingContext,
11395
- ) -> None:
11396
- ctx = _logging_context
11397
-
11398
- self.name: str = name
11399
-
11400
- self.msg: str = msg
11401
-
11402
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
11403
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
11404
- args = args[0] # type: ignore[assignment]
11405
- self.args: ta.Union[tuple, dict] = args
11406
-
11407
- self.levelname: str = logging.getLevelName(ctx.level)
11408
- self.levelno: int = ctx.level
11409
-
11410
- if (caller := ctx.caller()) is not None:
11411
- self.pathname: str = caller.file_path
11412
- else:
11413
- self.pathname = self._UNKNOWN_PATH_NAME
11414
-
11415
- if (src_file := ctx.source_file()) is not None:
11416
- self.filename: str = src_file.file_name
11417
- self.module: str = src_file.module
11418
- else:
11419
- self.filename = self.pathname
11420
- self.module = self._UNKNOWN_MODULE
11421
-
11422
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
11423
- self.exc_text: ta.Optional[str] = None
11424
-
11425
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
11426
- if caller is not None:
11427
- if (sinfo := caller.stack_info) is not None:
11428
- self.stack_info: ta.Optional[str] = '\n'.join([
11429
- self._STACK_INFO_PREFIX,
11430
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
11431
- ])
11432
- else:
11433
- self.stack_info = None
11434
-
11435
- self.lineno: int = caller.line_no
11436
- self.funcName: str = caller.name
11437
-
11438
- else:
11439
- self.stack_info = None
11440
-
11441
- self.lineno = 0
11442
- self.funcName = self._UNKNOWN_FUNC_NAME
11443
-
11444
- times = ctx.times
11445
- self.created: float = times.created
11446
- self.msecs: float = times.msecs
11447
- self.relativeCreated: float = times.relative_created
11448
-
11449
- if logging.logThreads:
11450
- thread = check.not_none(ctx.thread())
11451
- self.thread: ta.Optional[int] = thread.ident
11452
- self.threadName: ta.Optional[str] = thread.name
11453
- else:
11454
- self.thread = None
11455
- self.threadName = None
11456
-
11457
- if logging.logProcesses:
11458
- process = check.not_none(ctx.process())
11459
- self.process: ta.Optional[int] = process.pid
11460
- else:
11461
- self.process = None
11462
-
11463
- if logging.logMultiprocessing:
11464
- if (mp := ctx.multiprocessing()) is not None:
11465
- self.processName: ta.Optional[str] = mp.process_name
11466
- else:
11467
- self.processName = None
11468
- else:
11469
- self.processName = None
11470
-
11471
- # Absent <3.12
11472
- if getattr(logging, 'logAsyncioTasks', None):
11473
- if (at := ctx.asyncio_task()) is not None:
11474
- self.taskName: ta.Optional[str] = at.name
11475
- else:
11476
- self.taskName = None
11477
- else:
11478
- self.taskName = None
11765
+ # LogRecord.__init__ args:
11766
+ # - name: str
11767
+ # - level: int
11768
+ # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
11769
+ # - lineno: int - May be 0.
11770
+ # - msg: str
11771
+ # - args: tuple | dict | 1-tuple[dict]
11772
+ # - exc_info: LoggingExcInfoTuple | None
11773
+ # - func: str | None = None -> funcName
11774
+ # - sinfo: str | None = None -> stack_info
11775
+
11776
+ def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
11777
+ for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
11778
+ self.__dict__.update(ad.context_to_record(_logging_context))
11479
11779
 
11480
11780
 
11481
11781
  ########################################
@@ -12091,7 +12391,7 @@ class SingleDirDeployPathOwner(DeployPathOwner, Abstract):
12091
12391
 
12092
12392
 
12093
12393
  ########################################
12094
- # ../../../omlish/logs/std/adapters.py
12394
+ # ../../../omlish/logs/std/loggers.py
12095
12395
 
12096
12396
 
12097
12397
  ##
@@ -12107,25 +12407,27 @@ class StdLogger(Logger):
12107
12407
  def std(self) -> logging.Logger:
12108
12408
  return self._std
12109
12409
 
12410
+ def is_enabled_for(self, level: LogLevel) -> bool:
12411
+ return self._std.isEnabledFor(level)
12412
+
12110
12413
  def get_effective_level(self) -> LogLevel:
12111
12414
  return self._std.getEffectiveLevel()
12112
12415
 
12113
12416
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
12114
- if not self.is_enabled_for(ctx.level):
12417
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
12115
12418
  return
12116
12419
 
12117
- ctx.capture()
12118
-
12119
- ms, args = self._prepare_msg_args(msg, *args)
12120
-
12121
- rec = LoggingContextLogRecord(
12420
+ ctx.set_basic(
12122
12421
  name=self._std.name,
12123
- msg=ms,
12124
- args=args,
12125
12422
 
12126
- _logging_context=ctx,
12423
+ msg=msg,
12424
+ args=args,
12127
12425
  )
12128
12426
 
12427
+ ctx.capture()
12428
+
12429
+ rec = LoggingContextLogRecord(_logging_context=ctx)
12430
+
12129
12431
  self._std.handle(rec)
12130
12432
 
12131
12433