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.
@@ -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(reversed(type(obj).__mro__), key=lambda _ty: _ty.__dict__.get('__repr_priority__', 0)) # noqa
68
- for attr in ty.__dict__.get('__repr_attrs__', [])]
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 f'{type(obj).__name__}@{hex(id(obj))[2:]}({", ".join(f"{attr}={getattr(obj, attr)!r}" for attr in attrs)})'
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(cls_dct, '__hash__', '__eq__', '__ne__', wrapper=abc.abstractmethod)
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)-16s'),
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
- NOISY_LOGGERS: set[str] = {
12
- 'boto3.resources.action',
13
- 'datadog.dogstatsd',
14
- 'elasticsearch',
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, 'FilterConfig'] = dc.field(default_factory=dict)
29
- formatters: dict[str, 'FormatterConfig'] = dc.field(default_factory=dict)
30
- handlers: dict[str, 'HandlerConfig'] = dc.field(default_factory=dict)
31
- loggers: dict[str, 'LoggerConfig'] = dc.field(default_factory=dict)
32
- root: ta.Optional['LoggerConfig'] = None
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
- for noisy_logger in NOISY_LOGGERS:
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 addhooks(pluginmanager):
19
- present_types = {type(p) for p in pluginmanager.get_plugins()}
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
- pluginmanager.register(plugin()) # noqa
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev18
3
+ Version: 0.0.0.dev19
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause