omlish 0.0.0.dev18__py3-none-any.whl → 0.0.0.dev19__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/__about__.py +2 -2
- omlish/bootstrap/__init__.py +3 -0
- omlish/bootstrap/__main__.py +4 -0
- omlish/bootstrap/base.py +38 -0
- omlish/bootstrap/diag.py +128 -0
- omlish/bootstrap/harness.py +81 -0
- omlish/bootstrap/main.py +183 -0
- omlish/bootstrap/sys.py +361 -0
- omlish/defs.py +29 -5
- omlish/lite/logs.py +1 -1
- omlish/logs/configs.py +11 -23
- omlish/logs/noisy.py +15 -0
- omlish/testing/pytest/plugins/__init__.py +8 -3
- omlish/testing/pytest/plugins/depskip.py +81 -0
- omlish/testing/pytest/plugins/utils.py +14 -0
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/RECORD +20 -11
- omlish/bootstrap.py +0 -746
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev18.dist-info → omlish-0.0.0.dev19.dist-info}/top_level.txt +0 -0
omlish/bootstrap/sys.py
ADDED
@@ -0,0 +1,361 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import contextlib
|
3
|
+
import dataclasses as dc
|
4
|
+
import enum
|
5
|
+
import faulthandler
|
6
|
+
import gc
|
7
|
+
import importlib
|
8
|
+
import logging
|
9
|
+
import os
|
10
|
+
import pwd
|
11
|
+
import resource
|
12
|
+
import signal
|
13
|
+
import sys
|
14
|
+
import typing as ta
|
15
|
+
|
16
|
+
from .. import lang
|
17
|
+
from .base import Bootstrap
|
18
|
+
from .base import ContextBootstrap
|
19
|
+
from .base import SimpleBootstrap
|
20
|
+
|
21
|
+
|
22
|
+
if ta.TYPE_CHECKING:
|
23
|
+
from .. import libc
|
24
|
+
from .. import logs
|
25
|
+
from .. import os as osu
|
26
|
+
from ..formats import dotenv
|
27
|
+
|
28
|
+
else:
|
29
|
+
libc = lang.proxy_import('..libc', __package__)
|
30
|
+
logs = lang.proxy_import('..logs', __package__)
|
31
|
+
osu = lang.proxy_import('..os', __package__)
|
32
|
+
dotenv = lang.proxy_import('.formats.dotenv', __package__)
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
class CwdBootstrap(ContextBootstrap['CwdBootstrap.Config']):
|
39
|
+
@dc.dataclass(frozen=True)
|
40
|
+
class Config(Bootstrap.Config):
|
41
|
+
path: ta.Optional[str] = None
|
42
|
+
|
43
|
+
@contextlib.contextmanager
|
44
|
+
def enter(self) -> ta.Iterator[None]:
|
45
|
+
if self._config.path is not None:
|
46
|
+
prev = os.getcwd()
|
47
|
+
os.chdir(self._config.path)
|
48
|
+
else:
|
49
|
+
prev = None
|
50
|
+
|
51
|
+
try:
|
52
|
+
yield
|
53
|
+
|
54
|
+
finally:
|
55
|
+
if prev is not None:
|
56
|
+
os.chdir(prev)
|
57
|
+
|
58
|
+
|
59
|
+
##
|
60
|
+
|
61
|
+
|
62
|
+
class SetuidBootstrap(SimpleBootstrap['SetuidBootstrap.Config']):
|
63
|
+
@dc.dataclass(frozen=True)
|
64
|
+
class Config(Bootstrap.Config):
|
65
|
+
user: ta.Optional[str] = None
|
66
|
+
|
67
|
+
def run(self) -> None:
|
68
|
+
if self._config.user is not None:
|
69
|
+
user = pwd.getpwnam(self._config.user)
|
70
|
+
os.setuid(user.pw_uid)
|
71
|
+
|
72
|
+
|
73
|
+
##
|
74
|
+
|
75
|
+
|
76
|
+
class GcDebugFlag(enum.Enum):
|
77
|
+
STATS = gc.DEBUG_STATS
|
78
|
+
COLLECTABLE = gc.DEBUG_COLLECTABLE
|
79
|
+
UNCOLLECTABLE = gc.DEBUG_UNCOLLECTABLE
|
80
|
+
SAVEALL = gc.DEBUG_SAVEALL
|
81
|
+
LEAK = gc.DEBUG_LEAK
|
82
|
+
|
83
|
+
|
84
|
+
class GcBootstrap(ContextBootstrap['GcBootstrap.Config']):
|
85
|
+
@dc.dataclass(frozen=True)
|
86
|
+
class Config(Bootstrap.Config):
|
87
|
+
disable: bool = False
|
88
|
+
debug: ta.Optional[int] = None
|
89
|
+
|
90
|
+
@contextlib.contextmanager
|
91
|
+
def enter(self) -> ta.Iterator[None]:
|
92
|
+
prev_enabled = gc.isenabled()
|
93
|
+
if self._config.disable:
|
94
|
+
gc.disable()
|
95
|
+
|
96
|
+
if self._config.debug is not None:
|
97
|
+
prev_debug = gc.get_debug()
|
98
|
+
gc.set_debug(self._config.debug)
|
99
|
+
else:
|
100
|
+
prev_debug = None
|
101
|
+
|
102
|
+
try:
|
103
|
+
yield
|
104
|
+
|
105
|
+
finally:
|
106
|
+
if prev_enabled:
|
107
|
+
gc.enable()
|
108
|
+
|
109
|
+
if prev_debug is not None:
|
110
|
+
gc.set_debug(prev_debug)
|
111
|
+
|
112
|
+
|
113
|
+
##
|
114
|
+
|
115
|
+
|
116
|
+
class NiceBootstrap(SimpleBootstrap['NiceBootstrap.Config']):
|
117
|
+
@dc.dataclass(frozen=True)
|
118
|
+
class Config(Bootstrap.Config):
|
119
|
+
nice: ta.Optional[int] = None
|
120
|
+
|
121
|
+
def run(self) -> None:
|
122
|
+
if self._config.nice is not None:
|
123
|
+
os.nice(self._config.nice)
|
124
|
+
|
125
|
+
|
126
|
+
##
|
127
|
+
|
128
|
+
|
129
|
+
class LogBootstrap(ContextBootstrap['LogBootstrap.Config']):
|
130
|
+
@dc.dataclass(frozen=True)
|
131
|
+
class Config(Bootstrap.Config):
|
132
|
+
level: ta.Union[str, int, None] = None
|
133
|
+
json: bool = False
|
134
|
+
|
135
|
+
@contextlib.contextmanager
|
136
|
+
def enter(self) -> ta.Iterator[None]:
|
137
|
+
if self._config.level is None:
|
138
|
+
yield
|
139
|
+
return
|
140
|
+
|
141
|
+
handler = logs.configure_standard_logging(
|
142
|
+
self._config.level,
|
143
|
+
json=self._config.json,
|
144
|
+
)
|
145
|
+
|
146
|
+
try:
|
147
|
+
yield
|
148
|
+
|
149
|
+
finally:
|
150
|
+
if handler is not None:
|
151
|
+
logging.root.removeHandler(handler)
|
152
|
+
|
153
|
+
|
154
|
+
##
|
155
|
+
|
156
|
+
|
157
|
+
class FaulthandlerBootstrap(ContextBootstrap['FaulthandlerBootstrap.Config']):
|
158
|
+
@dc.dataclass(frozen=True)
|
159
|
+
class Config(Bootstrap.Config):
|
160
|
+
enabled: ta.Optional[bool] = None
|
161
|
+
|
162
|
+
@contextlib.contextmanager
|
163
|
+
def enter(self) -> ta.Iterator[None]:
|
164
|
+
if self._config.enabled is None:
|
165
|
+
yield
|
166
|
+
return
|
167
|
+
|
168
|
+
prev = faulthandler.is_enabled()
|
169
|
+
if self._config.enabled:
|
170
|
+
faulthandler.enable()
|
171
|
+
else:
|
172
|
+
faulthandler.disable()
|
173
|
+
|
174
|
+
try:
|
175
|
+
yield
|
176
|
+
|
177
|
+
finally:
|
178
|
+
if prev:
|
179
|
+
faulthandler.enable()
|
180
|
+
else:
|
181
|
+
faulthandler.disable()
|
182
|
+
|
183
|
+
|
184
|
+
##
|
185
|
+
|
186
|
+
|
187
|
+
SIGNALS_BY_NAME = {
|
188
|
+
a[len('SIG'):]: v # noqa
|
189
|
+
for a in dir(signal)
|
190
|
+
if a.startswith('SIG')
|
191
|
+
and not a.startswith('SIG_')
|
192
|
+
and a == a.upper()
|
193
|
+
and isinstance((v := getattr(signal, a)), int)
|
194
|
+
}
|
195
|
+
|
196
|
+
|
197
|
+
class PrctlBootstrap(SimpleBootstrap['PrctlBootstrap.Config']):
|
198
|
+
@dc.dataclass(frozen=True)
|
199
|
+
class Config(Bootstrap.Config):
|
200
|
+
dumpable: bool = False
|
201
|
+
deathsig: ta.Union[int, str, None] = None
|
202
|
+
|
203
|
+
def run(self) -> None:
|
204
|
+
if self._config.dumpable:
|
205
|
+
libc.prctl(libc.PR_SET_DUMPABLE, 1, 0, 0, 0, 0)
|
206
|
+
|
207
|
+
if self._config.deathsig is not None:
|
208
|
+
if isinstance(self._config.deathsig, int):
|
209
|
+
sig = self._config.deathsig
|
210
|
+
else:
|
211
|
+
sig = SIGNALS_BY_NAME[self._config.deathsig.upper()]
|
212
|
+
libc.prctl(libc.PR_SET_PDEATHSIG, sig, 0, 0, 0, 0)
|
213
|
+
|
214
|
+
|
215
|
+
##
|
216
|
+
|
217
|
+
|
218
|
+
RLIMITS_BY_NAME = {
|
219
|
+
a[len('RLIMIT_'):]: v # noqa
|
220
|
+
for a in dir(resource)
|
221
|
+
if a.startswith('RLIMIT_')
|
222
|
+
and a == a.upper()
|
223
|
+
and isinstance((v := getattr(resource, a)), int)
|
224
|
+
}
|
225
|
+
|
226
|
+
|
227
|
+
class RlimitBootstrap(ContextBootstrap['RlimitBootstrap.Config']):
|
228
|
+
@dc.dataclass(frozen=True)
|
229
|
+
class Config(Bootstrap.Config):
|
230
|
+
limits: ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]] = None
|
231
|
+
|
232
|
+
@contextlib.contextmanager
|
233
|
+
def enter(self) -> ta.Iterator[None]:
|
234
|
+
if not self._config.limits:
|
235
|
+
yield
|
236
|
+
return
|
237
|
+
|
238
|
+
def or_infin(l: ta.Optional[int]) -> int:
|
239
|
+
return l if l is not None else resource.RLIM_INFINITY
|
240
|
+
|
241
|
+
prev = {}
|
242
|
+
for k, (s, h) in self._config.limits.items():
|
243
|
+
i = RLIMITS_BY_NAME[k.upper()]
|
244
|
+
prev[i] = resource.getrlimit(i)
|
245
|
+
resource.setrlimit(i, (or_infin(s), or_infin(h)))
|
246
|
+
|
247
|
+
try:
|
248
|
+
yield
|
249
|
+
|
250
|
+
finally:
|
251
|
+
for i, (s, h) in prev.items():
|
252
|
+
resource.setrlimit(i, (s, h))
|
253
|
+
|
254
|
+
|
255
|
+
##
|
256
|
+
|
257
|
+
|
258
|
+
class ImportBootstrap(SimpleBootstrap['ImportBootstrap.Config']):
|
259
|
+
@dc.dataclass(frozen=True)
|
260
|
+
class Config(Bootstrap.Config):
|
261
|
+
modules: ta.Optional[ta.Sequence[str]] = None
|
262
|
+
|
263
|
+
def run(self) -> None:
|
264
|
+
for m in self._config.modules or ():
|
265
|
+
importlib.import_module(m)
|
266
|
+
|
267
|
+
|
268
|
+
##
|
269
|
+
|
270
|
+
|
271
|
+
class EnvBootstrap(ContextBootstrap['EnvBootstrap.Config']):
|
272
|
+
@dc.dataclass(frozen=True)
|
273
|
+
class Config(Bootstrap.Config):
|
274
|
+
vars: ta.Optional[ta.Mapping[str, ta.Optional[str]]] = None
|
275
|
+
files: ta.Optional[ta.Sequence[str]] = None
|
276
|
+
|
277
|
+
@contextlib.contextmanager
|
278
|
+
def enter(self) -> ta.Iterator[None]:
|
279
|
+
if not (self._config.vars or self._config.files):
|
280
|
+
yield
|
281
|
+
return
|
282
|
+
|
283
|
+
new = dict(self._config.vars or {})
|
284
|
+
for f in self._config.files or ():
|
285
|
+
new.update(dotenv.dotenv_values(f, env=os.environ))
|
286
|
+
|
287
|
+
prev: ta.Dict[str, ta.Optional[str]] = {k: os.environ.get(k) for k in new}
|
288
|
+
|
289
|
+
def do(k: str, v: ta.Optional[str]) -> None:
|
290
|
+
if v is not None:
|
291
|
+
os.environ[k] = v
|
292
|
+
else:
|
293
|
+
del os.environ[k]
|
294
|
+
|
295
|
+
for k, v in new.items():
|
296
|
+
do(k, v)
|
297
|
+
|
298
|
+
try:
|
299
|
+
yield
|
300
|
+
|
301
|
+
finally:
|
302
|
+
for k, v in prev.items():
|
303
|
+
do(k, v)
|
304
|
+
|
305
|
+
|
306
|
+
##
|
307
|
+
|
308
|
+
|
309
|
+
class PidfileBootstrap(ContextBootstrap['PidfileBootstrap.Config']):
|
310
|
+
@dc.dataclass(frozen=True)
|
311
|
+
class Config(Bootstrap.Config):
|
312
|
+
path: ta.Optional[str] = None
|
313
|
+
|
314
|
+
@contextlib.contextmanager
|
315
|
+
def enter(self) -> ta.Iterator[None]:
|
316
|
+
if self._config.path is None:
|
317
|
+
yield
|
318
|
+
return
|
319
|
+
|
320
|
+
with osu.Pidfile(self._config.path) as pf:
|
321
|
+
pf.write()
|
322
|
+
yield
|
323
|
+
|
324
|
+
|
325
|
+
##
|
326
|
+
|
327
|
+
|
328
|
+
class FdsBootstrap(SimpleBootstrap['FdsBootstrap.Config']):
|
329
|
+
@dc.dataclass(frozen=True)
|
330
|
+
class Config(Bootstrap.Config):
|
331
|
+
redirects: ta.Optional[ta.Mapping[int, ta.Union[int, str, None]]] = None
|
332
|
+
|
333
|
+
def run(self) -> None:
|
334
|
+
for dst, src in (self._config.redirects or {}).items():
|
335
|
+
if src is None:
|
336
|
+
src = '/dev/null'
|
337
|
+
if isinstance(src, int):
|
338
|
+
os.dup2(src, dst)
|
339
|
+
elif isinstance(src, str):
|
340
|
+
sfd = os.open(src, os.O_RDWR)
|
341
|
+
os.dup2(sfd, dst)
|
342
|
+
os.close(sfd)
|
343
|
+
else:
|
344
|
+
raise TypeError(src)
|
345
|
+
|
346
|
+
|
347
|
+
##
|
348
|
+
|
349
|
+
|
350
|
+
class PrintPidBootstrap(SimpleBootstrap['PrintPidBootstrap.Config']):
|
351
|
+
@dc.dataclass(frozen=True)
|
352
|
+
class Config(Bootstrap.Config):
|
353
|
+
enable: bool = False
|
354
|
+
pause: bool = False
|
355
|
+
|
356
|
+
def run(self) -> None:
|
357
|
+
if not (self._config.enable or self._config.pause):
|
358
|
+
return
|
359
|
+
print(str(os.getpid()), file=sys.stderr)
|
360
|
+
if self._config.pause:
|
361
|
+
input()
|
omlish/defs.py
CHANGED
@@ -5,7 +5,6 @@ remain @property's for type annotation, tool assistance, debugging, and otherwis
|
|
5
5
|
certain circumstances (the real-world alternative usually being simply not adding them).
|
6
6
|
"""
|
7
7
|
# ruff: noqa: ANN201
|
8
|
-
|
9
8
|
import abc
|
10
9
|
import functools
|
11
10
|
import operator
|
@@ -22,6 +21,7 @@ BASICS = {}
|
|
22
21
|
def _basic(fn):
|
23
22
|
if fn.__name__ in BASICS:
|
24
23
|
raise NameError(fn.__name__)
|
24
|
+
|
25
25
|
BASICS[fn.__name__] = fn
|
26
26
|
return fn
|
27
27
|
|
@@ -30,6 +30,7 @@ def _basic(fn):
|
|
30
30
|
def basic(cls_dct, *attrs, basics=None):
|
31
31
|
if basics is None:
|
32
32
|
basics = BASICS.keys()
|
33
|
+
|
33
34
|
for k in basics:
|
34
35
|
fn = BASICS[k]
|
35
36
|
fn(*attrs, cls_dct=cls_dct)
|
@@ -43,6 +44,7 @@ def _repr_guard(fn):
|
|
43
44
|
def inner(obj, *args, **kwargs):
|
44
45
|
try:
|
45
46
|
ids = _REPR_SEEN.ids
|
47
|
+
|
46
48
|
except AttributeError:
|
47
49
|
ids = _REPR_SEEN.ids = set()
|
48
50
|
try:
|
@@ -50,6 +52,7 @@ def _repr_guard(fn):
|
|
50
52
|
return fn(obj, *args, **kwargs)
|
51
53
|
finally:
|
52
54
|
del _REPR_SEEN.ids
|
55
|
+
|
53
56
|
else:
|
54
57
|
if id(obj) in ids:
|
55
58
|
return f'<seen:{type(obj).__name__}@{hex(id(obj))[2:]}>'
|
@@ -64,17 +67,27 @@ def build_attr_repr(obj, *, mro=False):
|
|
64
67
|
if mro:
|
65
68
|
attrs = [
|
66
69
|
attr
|
67
|
-
for ty in sorted(
|
68
|
-
|
70
|
+
for ty in sorted( # noqa
|
71
|
+
reversed(type(obj).__mro__),
|
72
|
+
key=lambda _ty: _ty.__dict__.get('__repr_priority__', 0),
|
73
|
+
)
|
74
|
+
for attr in ty.__dict__.get('__repr_attrs__', [])
|
75
|
+
]
|
76
|
+
|
69
77
|
else:
|
70
78
|
attrs = obj.__repr_attrs__
|
79
|
+
|
71
80
|
s = ', '.join(f'{a}={"<self>" if v is obj else _repr(v)}' for a in attrs for v in [getattr(obj, a)])
|
72
81
|
return f'{type(obj).__name__}@{hex(id(obj))[2:]}({s})'
|
73
82
|
|
74
83
|
|
75
84
|
@_repr_guard
|
76
85
|
def build_repr(obj, *attrs):
|
77
|
-
return
|
86
|
+
return (
|
87
|
+
f'{type(obj).__name__}'
|
88
|
+
f'@{hex(id(obj))[2:]}'
|
89
|
+
f'({", ".join(f"{attr}={getattr(obj, attr)!r}" for attr in attrs)})'
|
90
|
+
)
|
78
91
|
|
79
92
|
|
80
93
|
@_basic
|
@@ -86,6 +99,7 @@ def repr(cls_dct, *attrs, mro=False, priority=None): # noqa
|
|
86
99
|
cls_dct['__repr_attrs__'] = attrs
|
87
100
|
if priority is not None:
|
88
101
|
cls_dct['__repr_priority__'] = priority
|
102
|
+
|
89
103
|
cls_dct['__repr__'] = __repr__
|
90
104
|
|
91
105
|
|
@@ -141,9 +155,11 @@ def hash_eq(cls_dct, *attrs):
|
|
141
155
|
def __eq__(self, other): # noqa
|
142
156
|
if type(other) is not type(self):
|
143
157
|
return False
|
158
|
+
|
144
159
|
for attr in attrs: # noqa
|
145
160
|
if getattr(self, attr) != getattr(other, attr):
|
146
161
|
return False
|
162
|
+
|
147
163
|
return True
|
148
164
|
|
149
165
|
cls_dct['__eq__'] = __eq__
|
@@ -162,6 +178,7 @@ def not_implemented(cls_dct, *names, **kwargs):
|
|
162
178
|
wrapper = kwargs.pop('wrapper', lambda _: _)
|
163
179
|
if kwargs:
|
164
180
|
raise TypeError(kwargs)
|
181
|
+
|
165
182
|
ret = []
|
166
183
|
for name in names:
|
167
184
|
@wrapper
|
@@ -171,6 +188,7 @@ def not_implemented(cls_dct, *names, **kwargs):
|
|
171
188
|
not_implemented.__name__ = name
|
172
189
|
cls_dct[name] = not_implemented
|
173
190
|
ret.append(not_implemented)
|
191
|
+
|
174
192
|
return tuple(ret)
|
175
193
|
|
176
194
|
|
@@ -186,4 +204,10 @@ def abstract_property(cls_dct, *names):
|
|
186
204
|
|
187
205
|
@lang.cls_dct_fn()
|
188
206
|
def abstract_hash_eq(cls_dct):
|
189
|
-
return not_implemented(
|
207
|
+
return not_implemented(
|
208
|
+
cls_dct,
|
209
|
+
'__hash__',
|
210
|
+
'__eq__',
|
211
|
+
'__ne__',
|
212
|
+
wrapper=abc.abstractmethod,
|
213
|
+
)
|
omlish/lite/logs.py
CHANGED
@@ -69,7 +69,7 @@ class JsonLogFormatter(logging.Formatter):
|
|
69
69
|
STANDARD_LOG_FORMAT_PARTS = [
|
70
70
|
('asctime', '%(asctime)-15s'),
|
71
71
|
('process', 'pid=%(process)-6s'),
|
72
|
-
('thread', 'tid=%(thread)-
|
72
|
+
('thread', 'tid=%(thread)-10x'),
|
73
73
|
('levelname', '%(levelname)-8s'),
|
74
74
|
('name', '%(name)s'),
|
75
75
|
('separator', '::'),
|
omlish/logs/configs.py
CHANGED
@@ -3,21 +3,16 @@ import logging
|
|
3
3
|
import typing as ta
|
4
4
|
|
5
5
|
from ..lite.logs import configure_standard_logging as configure_lite_standard_logging
|
6
|
+
from .noisy import silence_noisy_loggers
|
6
7
|
|
7
8
|
|
8
9
|
##
|
9
10
|
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
'kazoo.client',
|
16
|
-
'requests.packages.urllib3.connectionpool',
|
17
|
-
}
|
18
|
-
|
19
|
-
|
20
|
-
##
|
12
|
+
FilterConfig = dict[str, ta.Any]
|
13
|
+
FormatterConfig = dict[str, ta.Any]
|
14
|
+
HandlerConfig = dict[str, ta.Any]
|
15
|
+
LoggerConfig = dict[str, ta.Any]
|
21
16
|
|
22
17
|
|
23
18
|
@dc.dataclass()
|
@@ -25,17 +20,11 @@ class DictConfig:
|
|
25
20
|
version: int = 1
|
26
21
|
incremental: bool = False
|
27
22
|
disable_existing_loggers: bool = False
|
28
|
-
filters: dict[str,
|
29
|
-
formatters: dict[str,
|
30
|
-
handlers: dict[str,
|
31
|
-
loggers: dict[str,
|
32
|
-
root:
|
33
|
-
|
34
|
-
|
35
|
-
FilterConfig = dict[str, ta.Any]
|
36
|
-
FormatterConfig = dict[str, ta.Any]
|
37
|
-
HandlerConfig = dict[str, ta.Any]
|
38
|
-
LoggerConfig = dict[str, ta.Any]
|
23
|
+
filters: dict[str, FilterConfig] = dc.field(default_factory=dict)
|
24
|
+
formatters: dict[str, FormatterConfig] = dc.field(default_factory=dict)
|
25
|
+
handlers: dict[str, HandlerConfig] = dc.field(default_factory=dict)
|
26
|
+
loggers: dict[str, LoggerConfig] = dc.field(default_factory=dict)
|
27
|
+
root: LoggerConfig | None = None
|
39
28
|
|
40
29
|
|
41
30
|
##
|
@@ -51,7 +40,6 @@ def configure_standard_logging(
|
|
51
40
|
json=json,
|
52
41
|
)
|
53
42
|
|
54
|
-
|
55
|
-
logging.getLogger(noisy_logger).setLevel(logging.WARNING)
|
43
|
+
silence_noisy_loggers()
|
56
44
|
|
57
45
|
return handler
|
omlish/logs/noisy.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
|
4
|
+
NOISY_LOGGERS: set[str] = {
|
5
|
+
'boto3.resources.action',
|
6
|
+
'datadog.dogstatsd',
|
7
|
+
'elasticsearch',
|
8
|
+
'kazoo.client',
|
9
|
+
'requests.packages.urllib3.connectionpool',
|
10
|
+
}
|
11
|
+
|
12
|
+
|
13
|
+
def silence_noisy_loggers() -> None:
|
14
|
+
for noisy_logger in NOISY_LOGGERS:
|
15
|
+
logging.getLogger(noisy_logger).setLevel(logging.WARNING)
|
@@ -1,5 +1,10 @@
|
|
1
|
+
# ruff: noqa: I001
|
2
|
+
# fmt: off
|
3
|
+
import pytest
|
4
|
+
|
1
5
|
from . import ( # noqa
|
2
6
|
asyncs,
|
7
|
+
depskip,
|
3
8
|
logging,
|
4
9
|
managermarks,
|
5
10
|
pydevd,
|
@@ -15,9 +20,9 @@ from ._registry import ( # noqa
|
|
15
20
|
)
|
16
21
|
|
17
22
|
|
18
|
-
def
|
19
|
-
present_types = {type(p) for p in
|
23
|
+
def add_hooks(pm: pytest.PytestPluginManager) -> None:
|
24
|
+
present_types = {type(p) for p in pm.get_plugins()}
|
20
25
|
|
21
26
|
for plugin in ALL:
|
22
27
|
if plugin not in present_types:
|
23
|
-
|
28
|
+
pm.register(plugin()) # noqa
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""
|
2
|
+
https://github.com/pytest-dev/pytest/blob/72c682ff9773ad2690711105a100423ebf7c7c15/src/_pytest/python.py#L494
|
3
|
+
|
4
|
+
--
|
5
|
+
|
6
|
+
https://github.com/pytest-dev/pytest-asyncio/blob/b1dc0c3e2e82750bdc6dbdf668d519aaa89c036c/pytest_asyncio/plugin.py#L657
|
7
|
+
"""
|
8
|
+
import dataclasses as dc
|
9
|
+
import re
|
10
|
+
import typing as ta
|
11
|
+
|
12
|
+
import pytest
|
13
|
+
|
14
|
+
from .... import check
|
15
|
+
from ._registry import register as register_plugin
|
16
|
+
from .utils import find_plugin
|
17
|
+
|
18
|
+
|
19
|
+
@register_plugin
|
20
|
+
class DepSkipPlugin:
|
21
|
+
@dc.dataclass(frozen=True)
|
22
|
+
class Entry:
|
23
|
+
file_pats: ta.Sequence[re.Pattern]
|
24
|
+
imp_pats: ta.Sequence[re.Pattern]
|
25
|
+
|
26
|
+
def __init__(self) -> None:
|
27
|
+
super().__init__()
|
28
|
+
|
29
|
+
self._entries: list[DepSkipPlugin.Entry] = []
|
30
|
+
|
31
|
+
def add_entry(self, e: Entry) -> None:
|
32
|
+
self._entries.append(e)
|
33
|
+
|
34
|
+
def should_skip(self, file_name: str, imp_name: str) -> bool:
|
35
|
+
for e in self._entries:
|
36
|
+
if (
|
37
|
+
any(fp.fullmatch(file_name) for fp in e.file_pats) and
|
38
|
+
any(ip.fullmatch(imp_name) for ip in e.imp_pats)
|
39
|
+
):
|
40
|
+
return True
|
41
|
+
return False
|
42
|
+
|
43
|
+
@pytest.hookimpl
|
44
|
+
def pytest_collectstart(self, collector: pytest.Collector) -> None:
|
45
|
+
if isinstance(collector, pytest.Module):
|
46
|
+
original_attr = f'__{self.__class__.__qualname__.replace(".", "__")}__original_getobj'
|
47
|
+
|
48
|
+
def _patched_getobj():
|
49
|
+
try:
|
50
|
+
return getattr(collector, original_attr)()
|
51
|
+
except pytest.Collector.CollectError as ce:
|
52
|
+
if (oe := ce.__cause__) and isinstance(oe, ImportError):
|
53
|
+
if (
|
54
|
+
(file_name := collector.nodeid) and
|
55
|
+
(imp_name := oe.name) and
|
56
|
+
self.should_skip(file_name, imp_name) # noqa
|
57
|
+
):
|
58
|
+
pytest.skip(
|
59
|
+
f'skipping {file_name} to missing optional dependency {imp_name}',
|
60
|
+
allow_module_level=True,
|
61
|
+
)
|
62
|
+
|
63
|
+
raise
|
64
|
+
|
65
|
+
setattr(collector, original_attr, collector._getobj) # noqa
|
66
|
+
collector._getobj = _patched_getobj # type: ignore # noqa
|
67
|
+
|
68
|
+
|
69
|
+
def register(
|
70
|
+
pm: pytest.PytestPluginManager,
|
71
|
+
file_pats: ta.Iterable[str],
|
72
|
+
imp_pats: ta.Iterable[str],
|
73
|
+
) -> None:
|
74
|
+
check.not_isinstance(file_pats, str)
|
75
|
+
check.not_isinstance(imp_pats, str)
|
76
|
+
|
77
|
+
pg = check.not_none(find_plugin(pm, DepSkipPlugin))
|
78
|
+
pg.add_entry(DepSkipPlugin.Entry(
|
79
|
+
[re.compile(fp) for fp in file_pats],
|
80
|
+
[re.compile(ip) for ip in imp_pats],
|
81
|
+
))
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
|
6
|
+
T = ta.TypeVar('T')
|
7
|
+
|
8
|
+
|
9
|
+
def find_plugin(pm: pytest.PytestPluginManager, ty: type[T]) -> T | None:
|
10
|
+
lst = [p for p in pm.get_plugins() if isinstance(p, ty)]
|
11
|
+
if not lst:
|
12
|
+
return None
|
13
|
+
[ret] = lst
|
14
|
+
return ret
|