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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
omlish/logs/infos.py CHANGED
@@ -1,120 +1,377 @@
1
- # ruff: noqa: UP045
1
+ # ruff: noqa: UP006 UP007 UP045
2
2
  # @omlish-lite
3
+ """
4
+ TODO:
5
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
6
+ """
7
+ import collections.abc
8
+ import io
9
+ import logging
3
10
  import os.path
4
11
  import sys
5
12
  import threading
13
+ import time
14
+ import traceback
15
+ import types
6
16
  import typing as ta
7
17
 
18
+ from .levels import NamedLogLevel
19
+ from .warnings import LoggingSetupWarning
8
20
 
9
- ##
10
-
11
-
12
- def logging_context_info(cls):
13
- return cls
14
21
 
22
+ LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
15
23
 
16
- ##
24
+ LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
25
+ LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
26
+ LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
17
27
 
18
-
19
- @logging_context_info
20
- @ta.final
21
- class LoggingSourceFileInfo(ta.NamedTuple):
22
- file_name: str
23
- module: str
24
-
25
- @classmethod
26
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
27
- if file_path is None:
28
- return None
29
-
30
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
31
- try:
32
- file_name = os.path.basename(file_path)
33
- module = os.path.splitext(file_name)[0]
34
- except (TypeError, ValueError, AttributeError):
35
- return None
36
-
37
- return cls(
38
- file_name=file_name,
39
- module=module,
40
- )
28
+ LoggingContextInfo = ta.Any # ta.TypeAlias
41
29
 
42
30
 
43
31
  ##
44
32
 
45
33
 
46
- @logging_context_info
47
- @ta.final
48
- class LoggingThreadInfo(ta.NamedTuple):
49
- ident: int
50
- native_id: ta.Optional[int]
51
- name: str
52
-
53
- @classmethod
54
- def build(cls) -> 'LoggingThreadInfo':
55
- return cls(
56
- ident=threading.get_ident(),
57
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
58
- name=threading.current_thread().name,
59
- )
60
-
61
-
62
- ##
63
-
64
-
65
- @logging_context_info
66
- @ta.final
67
- class LoggingProcessInfo(ta.NamedTuple):
68
- pid: int
69
-
70
- @classmethod
71
- def build(cls) -> 'LoggingProcessInfo':
72
- return cls(
73
- pid=os.getpid(),
74
- )
75
-
76
-
77
- ##
34
+ def logging_context_info(cls):
35
+ return cls
78
36
 
79
37
 
80
- @logging_context_info
81
38
  @ta.final
82
- class LoggingMultiprocessingInfo(ta.NamedTuple):
83
- process_name: str
39
+ class LoggingContextInfos:
40
+ def __new__(cls, *args, **kwargs): # noqa
41
+ raise TypeError
42
+
43
+ #
44
+
45
+ @logging_context_info
46
+ @ta.final
47
+ class Name(ta.NamedTuple):
48
+ name: str
49
+
50
+ @logging_context_info
51
+ @ta.final
52
+ class Level(ta.NamedTuple):
53
+ level: NamedLogLevel
54
+ name: str
55
+
56
+ @classmethod
57
+ def build(cls, level: int) -> 'LoggingContextInfos.Level':
58
+ nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
59
+ return cls(
60
+ level=nl,
61
+ name=logging.getLevelName(nl),
62
+ )
63
+
64
+ @logging_context_info
65
+ @ta.final
66
+ class Msg(ta.NamedTuple):
67
+ msg: str
68
+ args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
69
+
70
+ @classmethod
71
+ def build(
72
+ cls,
73
+ msg: ta.Union[str, tuple, LoggingMsgFn],
74
+ *args: ta.Any,
75
+ ) -> 'LoggingContextInfos.Msg':
76
+ s: str
77
+ a: ta.Any
78
+
79
+ if callable(msg):
80
+ if args:
81
+ raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
82
+ x = msg()
83
+ if isinstance(x, str):
84
+ s, a = x, ()
85
+ elif isinstance(x, tuple):
86
+ if x:
87
+ s, a = x[0], x[1:]
88
+ else:
89
+ s, a = '', ()
90
+ else:
91
+ raise TypeError(x)
92
+
93
+ elif isinstance(msg, tuple):
94
+ if args:
95
+ raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
96
+ if msg:
97
+ s, a = msg[0], msg[1:]
98
+ else:
99
+ s, a = '', ()
100
+
101
+ elif isinstance(msg, str):
102
+ s, a = msg, args
103
+
104
+ else:
105
+ raise TypeError(msg)
106
+
107
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
108
+ if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
109
+ a = a[0]
110
+
111
+ return cls(
112
+ msg=s,
113
+ args=a,
114
+ )
115
+
116
+ @logging_context_info
117
+ @ta.final
118
+ class Extra(ta.NamedTuple):
119
+ extra: ta.Mapping[ta.Any, ta.Any]
120
+
121
+ @logging_context_info
122
+ @ta.final
123
+ class Time(ta.NamedTuple):
124
+ ns: int
125
+ secs: float
126
+ msecs: float
127
+ relative_secs: float
128
+
129
+ @classmethod
130
+ def get_std_start_ns(cls) -> int:
131
+ x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
132
+
133
+ # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
134
+ # an int.
135
+ #
136
+ # See:
137
+ # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
138
+ #
139
+ if isinstance(x, float):
140
+ return int(x * 1e9)
141
+ else:
142
+ return x
143
+
144
+ @classmethod
145
+ def build(
146
+ cls,
147
+ ns: int,
148
+ *,
149
+ start_ns: ta.Optional[int] = None,
150
+ ) -> 'LoggingContextInfos.Time':
151
+ # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
152
+ secs = ns / 1e9 # ns to float seconds
153
+
154
+ # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
155
+ # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
156
+ # Convert to float by adding 0.0 for historical reasons. See gh-89047
157
+ msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
158
+
159
+ # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
160
+ if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
161
+ # ns -> sec conversion can round up, e.g:
162
+ # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
163
+ msecs = 0.0
164
+
165
+ if start_ns is None:
166
+ start_ns = cls.get_std_start_ns()
167
+ relative_secs = (ns - start_ns) / 1e6
168
+
169
+ return cls(
170
+ ns=ns,
171
+ secs=secs,
172
+ msecs=msecs,
173
+ relative_secs=relative_secs,
174
+ )
175
+
176
+ @logging_context_info
177
+ @ta.final
178
+ class Exc(ta.NamedTuple):
179
+ info: LoggingExcInfo
180
+ info_tuple: LoggingExcInfoTuple
181
+
182
+ @classmethod
183
+ def build(
184
+ cls,
185
+ arg: LoggingExcInfoArg = False,
186
+ ) -> ta.Optional['LoggingContextInfos.Exc']:
187
+ if arg is True:
188
+ sys_exc_info = sys.exc_info()
189
+ if sys_exc_info[0] is not None:
190
+ arg = sys_exc_info
191
+ else:
192
+ arg = None
193
+ elif arg is False:
194
+ arg = None
195
+ if arg is None:
196
+ return None
197
+
198
+ info: LoggingExcInfo = arg
199
+ if isinstance(info, BaseException):
200
+ info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
201
+ else:
202
+ info_tuple = info
203
+
204
+ return cls(
205
+ info=info,
206
+ info_tuple=info_tuple,
207
+ )
208
+
209
+ @logging_context_info
210
+ @ta.final
211
+ class Caller(ta.NamedTuple):
212
+ file_path: str
213
+ line_no: int
214
+ func_name: str
215
+ stack_info: ta.Optional[str]
216
+
217
+ @classmethod
218
+ def is_internal_frame(cls, frame: types.FrameType) -> bool:
219
+ file_path = os.path.normcase(frame.f_code.co_filename)
220
+
221
+ # Yes, really.
222
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
223
+ # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
224
+ if 'importlib' in file_path and '_bootstrap' in file_path:
225
+ return True
226
+
227
+ return False
228
+
229
+ @classmethod
230
+ def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
231
+ f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
232
+
233
+ while f is not None:
234
+ # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
235
+ # manual stack_offset management.
236
+ if hasattr(f, 'f_code'):
237
+ return f
238
+
239
+ f = f.f_back
84
240
 
85
- @classmethod
86
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
87
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
88
- if (mp := sys.modules.get('multiprocessing')) is None:
89
241
  return None
90
242
 
91
- return cls(
92
- process_name=mp.current_process().name,
93
- )
243
+ @classmethod
244
+ def build(
245
+ cls,
246
+ stack_offset: int = 0,
247
+ *,
248
+ stack_info: bool = False,
249
+ ) -> ta.Optional['LoggingContextInfos.Caller']:
250
+ if (f := cls.find_frame(stack_offset + 1)) is None:
251
+ return None
252
+
253
+ # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
254
+ sinfo = None
255
+ if stack_info:
256
+ sio = io.StringIO()
257
+ traceback.print_stack(f, file=sio)
258
+ sinfo = sio.getvalue()
259
+ sio.close()
260
+ if sinfo[-1] == '\n':
261
+ sinfo = sinfo[:-1]
262
+
263
+ return cls(
264
+ file_path=f.f_code.co_filename,
265
+ line_no=f.f_lineno or 0,
266
+ func_name=f.f_code.co_name,
267
+ stack_info=sinfo,
268
+ )
269
+
270
+ @logging_context_info
271
+ @ta.final
272
+ class SourceFile(ta.NamedTuple):
273
+ file_name: str
274
+ module: str
275
+
276
+ @classmethod
277
+ def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
278
+ if caller_file_path is None:
279
+ return None
280
+
281
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
282
+ try:
283
+ file_name = os.path.basename(caller_file_path)
284
+ module = os.path.splitext(file_name)[0]
285
+ except (TypeError, ValueError, AttributeError):
286
+ return None
287
+
288
+ return cls(
289
+ file_name=file_name,
290
+ module=module,
291
+ )
292
+
293
+ @logging_context_info
294
+ @ta.final
295
+ class Thread(ta.NamedTuple):
296
+ ident: int
297
+ native_id: ta.Optional[int]
298
+ name: str
299
+
300
+ @classmethod
301
+ def build(cls) -> 'LoggingContextInfos.Thread':
302
+ return cls(
303
+ ident=threading.get_ident(),
304
+ native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
305
+ name=threading.current_thread().name,
306
+ )
307
+
308
+ @logging_context_info
309
+ @ta.final
310
+ class Process(ta.NamedTuple):
311
+ pid: int
312
+
313
+ @classmethod
314
+ def build(cls) -> 'LoggingContextInfos.Process':
315
+ return cls(
316
+ pid=os.getpid(),
317
+ )
318
+
319
+ @logging_context_info
320
+ @ta.final
321
+ class Multiprocessing(ta.NamedTuple):
322
+ process_name: str
323
+
324
+ @classmethod
325
+ def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
326
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
327
+ if (mp := sys.modules.get('multiprocessing')) is None:
328
+ return None
329
+
330
+ return cls(
331
+ process_name=mp.current_process().name,
332
+ )
333
+
334
+ @logging_context_info
335
+ @ta.final
336
+ class AsyncioTask(ta.NamedTuple):
337
+ name: str
338
+
339
+ @classmethod
340
+ def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
341
+ # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
342
+ if (asyncio := sys.modules.get('asyncio')) is None:
343
+ return None
344
+
345
+ try:
346
+ task = asyncio.current_task()
347
+ except Exception: # noqa
348
+ return None
349
+
350
+ if task is None:
351
+ return None
352
+
353
+ return cls(
354
+ name=task.get_name(), # Always non-None
355
+ )
94
356
 
95
357
 
96
358
  ##
97
359
 
98
360
 
99
- @logging_context_info
100
- @ta.final
101
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
102
- name: str
361
+ class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
362
+ pass
103
363
 
104
- @classmethod
105
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
106
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
107
- if (asyncio := sys.modules.get('asyncio')) is None:
108
- return None
109
364
 
110
- try:
111
- task = asyncio.current_task()
112
- except Exception: # noqa
113
- return None
365
+ def _check_logging_start_time() -> None:
366
+ if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
367
+ import warnings # noqa
114
368
 
115
- if task is None:
116
- return None
117
-
118
- return cls(
119
- name=task.get_name(), # Always non-None
369
+ warnings.warn(
370
+ f'Unexpected logging start time detected: '
371
+ f'get_std_start_ns={x}, '
372
+ f'time.time()={t}',
373
+ UnexpectedLoggingStartTimeWarning,
120
374
  )
375
+
376
+
377
+ _check_logging_start_time()
@@ -6,6 +6,7 @@ import typing as ta
6
6
  from ..base import Logger
7
7
  from ..base import LoggingMsgFn
8
8
  from ..contexts import CaptureLoggingContext
9
+ from ..infos import LoggingContextInfos
9
10
  from ..levels import LogLevel
10
11
  from .records import LoggingContextLogRecord
11
12
 
@@ -30,19 +31,18 @@ class StdLogger(Logger):
30
31
  return self._std.getEffectiveLevel()
31
32
 
32
33
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
33
- if not self.is_enabled_for(ctx.level):
34
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
34
35
  return
35
36
 
36
- ctx.capture()
37
-
38
- ms, args = self._prepare_msg_args(msg, *args)
39
-
40
- rec = LoggingContextLogRecord(
37
+ ctx.set_basic(
41
38
  name=self._std.name,
42
- msg=ms,
43
- args=args,
44
39
 
45
- _logging_context=ctx,
40
+ msg=msg,
41
+ args=args,
46
42
  )
47
43
 
44
+ ctx.capture()
45
+
46
+ rec = LoggingContextLogRecord(_logging_context=ctx)
47
+
48
48
  self._std.handle(rec)