omlish 0.0.0.dev4__py3-none-any.whl → 0.0.0.dev5__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.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/__about__.py +1 -1
- omlish/__init__.py +1 -1
- omlish/asyncs/__init__.py +1 -4
- omlish/asyncs/anyio.py +66 -0
- omlish/asyncs/flavors.py +27 -1
- omlish/asyncs/trio_asyncio.py +24 -18
- omlish/c3.py +1 -1
- omlish/cached.py +1 -2
- omlish/collections/__init__.py +4 -1
- omlish/collections/cache/impl.py +1 -1
- omlish/collections/indexed.py +1 -1
- omlish/collections/utils.py +38 -6
- omlish/configs/__init__.py +5 -0
- omlish/configs/classes.py +53 -0
- omlish/configs/dotenv.py +586 -0
- omlish/configs/props.py +589 -49
- omlish/dataclasses/impl/api.py +1 -1
- omlish/dataclasses/impl/as_.py +1 -1
- omlish/dataclasses/impl/fields.py +1 -0
- omlish/dataclasses/impl/init.py +1 -1
- omlish/dataclasses/impl/main.py +1 -0
- omlish/dataclasses/impl/metaclass.py +6 -1
- omlish/dataclasses/impl/order.py +1 -1
- omlish/dataclasses/impl/reflect.py +15 -2
- omlish/defs.py +1 -1
- omlish/diag/procfs.py +29 -1
- omlish/diag/procstats.py +32 -0
- omlish/diag/replserver/console.py +3 -3
- omlish/diag/replserver/server.py +6 -5
- omlish/diag/threads.py +86 -0
- omlish/docker.py +19 -0
- omlish/fnpairs.py +26 -18
- omlish/graphs/dags.py +113 -0
- omlish/graphs/domination.py +268 -0
- omlish/graphs/trees.py +2 -2
- omlish/http/__init__.py +25 -0
- omlish/http/asgi.py +131 -0
- omlish/http/consts.py +31 -4
- omlish/http/cookies.py +194 -0
- omlish/http/dates.py +70 -0
- omlish/http/encodings.py +6 -0
- omlish/http/json.py +273 -0
- omlish/http/sessions.py +197 -0
- omlish/inject/__init__.py +8 -2
- omlish/inject/bindings.py +3 -3
- omlish/inject/exceptions.py +3 -3
- omlish/inject/impl/elements.py +33 -24
- omlish/inject/impl/injector.py +1 -0
- omlish/inject/impl/multis.py +74 -0
- omlish/inject/impl/providers.py +19 -39
- omlish/inject/{proxy.py → impl/proxy.py} +2 -2
- omlish/inject/impl/scopes.py +1 -0
- omlish/inject/injector.py +1 -0
- omlish/inject/keys.py +3 -9
- omlish/inject/multis.py +70 -0
- omlish/inject/providers.py +23 -23
- omlish/inject/scopes.py +7 -3
- omlish/inject/types.py +0 -8
- omlish/iterators.py +13 -0
- omlish/json.py +2 -1
- omlish/lang/__init__.py +4 -0
- omlish/lang/classes/restrict.py +1 -1
- omlish/lang/classes/virtual.py +2 -2
- omlish/lang/contextmanagers.py +64 -0
- omlish/lang/datetimes.py +6 -5
- omlish/lang/functions.py +10 -0
- omlish/lang/imports.py +11 -2
- omlish/lang/typing.py +1 -0
- omlish/logs/utils.py +1 -1
- omlish/marshal/datetimes.py +1 -1
- omlish/reflect.py +8 -2
- omlish/sync.py +70 -0
- omlish/term.py +6 -1
- omlish/testing/pytest/__init__.py +5 -0
- omlish/testing/pytest/helpers.py +0 -24
- omlish/testing/pytest/inject/harness.py +1 -1
- omlish/testing/pytest/marks.py +48 -0
- omlish/testing/pytest/plugins/__init__.py +2 -0
- omlish/testing/pytest/plugins/managermarks.py +60 -0
- omlish/testing/testing.py +10 -0
- omlish/text/delimit.py +4 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +86 -69
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
omlish/lang/contextmanagers.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import abc
|
|
1
2
|
import contextlib
|
|
2
3
|
import contextvars
|
|
3
4
|
import functools
|
|
@@ -50,6 +51,69 @@ NOP_CONTEXT_MANAGER = NopContextManager()
|
|
|
50
51
|
##
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
class ContextManager(abc.ABC, ta.Generic[T]):
|
|
55
|
+
|
|
56
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
|
57
|
+
super().__init_subclass__(**kwargs)
|
|
58
|
+
|
|
59
|
+
if not hasattr(cls.__contextmanager__, '_is_contextmanager'):
|
|
60
|
+
cls.__contextmanager__ = contextlib.contextmanager(cls.__contextmanager__) # type: ignore # noqa
|
|
61
|
+
cls.__contextmanager__._is_contextmanager = True # type: ignore # noqa
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def __contextmanager__(self) -> ta.Iterable[T]:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
__contextmanager__._is_contextmanager = True # type: ignore # noqa
|
|
68
|
+
|
|
69
|
+
_contextmanager: ta.Any
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> T:
|
|
72
|
+
self._contextmanager = self.__contextmanager__()
|
|
73
|
+
return self._contextmanager.__enter__() # type: ignore
|
|
74
|
+
|
|
75
|
+
def __exit__(
|
|
76
|
+
self,
|
|
77
|
+
exc_type: type[BaseException] | None,
|
|
78
|
+
exc_val: BaseException | None,
|
|
79
|
+
exc_tb: types.TracebackType | None,
|
|
80
|
+
) -> None:
|
|
81
|
+
return self._contextmanager.__exit__(exc_type, exc_val, exc_tb)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AsyncContextManager(abc.ABC, ta.Generic[T]):
|
|
85
|
+
|
|
86
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
|
87
|
+
super().__init_subclass__(**kwargs)
|
|
88
|
+
|
|
89
|
+
if not hasattr(cls.__asynccontextmanager__, '_is_asynccontextmanager'):
|
|
90
|
+
cls.__asynccontextmanager__ = contextlib.asynccontextmanager(cls.__asynccontextmanager__) # type: ignore # noqa
|
|
91
|
+
cls.__asynccontextmanager__._is_asynccontextmanager = True # type: ignore # noqa
|
|
92
|
+
|
|
93
|
+
@abc.abstractmethod
|
|
94
|
+
def __asynccontextmanager__(self) -> ta.AsyncIterator[T]:
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
__asynccontextmanager__._is_asynccontextmanager = True # type: ignore # noqa
|
|
98
|
+
|
|
99
|
+
_asynccontextmanager: ta.Any
|
|
100
|
+
|
|
101
|
+
async def __aenter__(self) -> T:
|
|
102
|
+
self._asynccontextmanager = self.__asynccontextmanager__()
|
|
103
|
+
return await self._asynccontextmanager.__aenter__() # type: ignore
|
|
104
|
+
|
|
105
|
+
async def __aexit__(
|
|
106
|
+
self,
|
|
107
|
+
exc_type: type[BaseException] | None,
|
|
108
|
+
exc_val: BaseException | None,
|
|
109
|
+
exc_tb: types.TracebackType | None,
|
|
110
|
+
) -> None:
|
|
111
|
+
return await self._asynccontextmanager.__aexit__(exc_type, exc_val, exc_tb)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
|
|
116
|
+
|
|
53
117
|
@contextlib.contextmanager
|
|
54
118
|
def defer(fn: ta.Callable) -> ta.Generator[ta.Callable, None, None]:
|
|
55
119
|
try:
|
omlish/lang/datetimes.py
CHANGED
|
@@ -18,17 +18,18 @@ def months_ago(date: datetime.date, num: int) -> datetime.date:
|
|
|
18
18
|
return datetime.date(ago_year, ago_month, 1)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def parse_date(s: str) -> datetime.date:
|
|
21
|
+
def parse_date(s: str, tz: datetime.timezone | None = None) -> datetime.date:
|
|
22
|
+
|
|
22
23
|
if s.lower() in ['today', 'now']:
|
|
23
|
-
return datetime.
|
|
24
|
+
return datetime.datetime.now(tz=tz)
|
|
24
25
|
elif s.lower() == 'yesterday':
|
|
25
|
-
return datetime.
|
|
26
|
+
return datetime.datetime.now(tz=tz) - datetime.timedelta(days=1)
|
|
26
27
|
elif s.lower().endswith(' days ago'):
|
|
27
28
|
num = int(s.split(' ', 1)[0])
|
|
28
|
-
return datetime.
|
|
29
|
+
return datetime.datetime.now(tz=tz) - datetime.timedelta(days=num)
|
|
29
30
|
elif s.lower().endswith(' months ago'):
|
|
30
31
|
months = int(s.split(' ', 1)[0])
|
|
31
|
-
return months_ago(datetime.
|
|
32
|
+
return months_ago(datetime.datetime.now(tz=tz), months)
|
|
32
33
|
else:
|
|
33
34
|
return datetime.date(*map(int, s.split('-', 3)))
|
|
34
35
|
|
omlish/lang/functions.py
CHANGED
|
@@ -148,6 +148,16 @@ def void(*args: ta.Any, **kwargs: ta.Any) -> ta.NoReturn:
|
|
|
148
148
|
##
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def as_async(fn: ta.Callable[P, T]) -> ta.Callable[P, ta.Awaitable[T]]:
|
|
152
|
+
@functools.wraps(fn)
|
|
153
|
+
async def inner(*args, **kwargs):
|
|
154
|
+
return fn(*args, **kwargs)
|
|
155
|
+
return inner
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
##
|
|
159
|
+
|
|
160
|
+
|
|
151
161
|
_MISSING = object()
|
|
152
162
|
|
|
153
163
|
|
omlish/lang/imports.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import functools
|
|
3
|
-
import importlib.
|
|
3
|
+
import importlib.util
|
|
4
4
|
import sys
|
|
5
5
|
import types
|
|
6
6
|
import typing as ta
|
|
@@ -11,6 +11,13 @@ from .cached import cached_function
|
|
|
11
11
|
##
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def can_import(name: str, package: str | None = None) -> bool:
|
|
15
|
+
return importlib.util.find_spec(name, package) is not None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
|
|
20
|
+
|
|
14
21
|
def lazy_import(name: str, package: str | None = None) -> ta.Callable[[], ta.Any]:
|
|
15
22
|
return cached_function(functools.partial(importlib.import_module, name, package=package))
|
|
16
23
|
|
|
@@ -66,6 +73,8 @@ def yield_importable(
|
|
|
66
73
|
filter: ta.Callable[[str], bool] | None = None, # noqa
|
|
67
74
|
include_special: bool = False,
|
|
68
75
|
) -> ta.Iterator[str]:
|
|
76
|
+
from importlib import resources
|
|
77
|
+
|
|
69
78
|
def rec(cur):
|
|
70
79
|
if cur.split('.')[-1] == '__pycache__':
|
|
71
80
|
return
|
|
@@ -83,7 +92,7 @@ def yield_importable(
|
|
|
83
92
|
if getattr(module, '__file__', None) is None:
|
|
84
93
|
return
|
|
85
94
|
|
|
86
|
-
for file in
|
|
95
|
+
for file in resources.files(cur).iterdir():
|
|
87
96
|
if file.is_file() and file.name.endswith('.py'):
|
|
88
97
|
if not (include_special or file.name not in SPECIAL_IMPORTABLE):
|
|
89
98
|
continue
|
omlish/lang/typing.py
CHANGED
omlish/logs/utils.py
CHANGED
omlish/marshal/datetimes.py
CHANGED
omlish/reflect.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
TODO:
|
|
3
|
+
- callables..
|
|
3
4
|
- uniform collection isinstance - items() for mappings, iter() for other
|
|
4
5
|
- also check instance type in isinstance not just items lol
|
|
5
6
|
- ta.Generic in mro causing trouble - omit? no longer 1:1
|
|
@@ -7,12 +8,13 @@ TODO:
|
|
|
7
8
|
- cache __hash__ in Generic/Union
|
|
8
9
|
"""
|
|
9
10
|
import collections.abc
|
|
10
|
-
import typing as ta
|
|
11
11
|
import types
|
|
12
|
+
import typing as ta
|
|
12
13
|
|
|
13
14
|
from . import c3
|
|
14
15
|
from . import lang
|
|
15
16
|
|
|
17
|
+
|
|
16
18
|
if ta.TYPE_CHECKING:
|
|
17
19
|
from .collections import cache
|
|
18
20
|
else:
|
|
@@ -25,6 +27,7 @@ _NONE_TYPE_FROZENSET: frozenset['Type'] = frozenset([_NoneType])
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
_GenericAlias = ta._GenericAlias # type: ignore # noqa
|
|
30
|
+
# _CallableGenericAlias = ta._CallableGenericAlias # type: ignore # noqa
|
|
28
31
|
_SpecialGenericAlias = ta._SpecialGenericAlias # type: ignore # noqa
|
|
29
32
|
_UnionGenericAlias = ta._UnionGenericAlias # type: ignore # noqa
|
|
30
33
|
|
|
@@ -155,7 +158,10 @@ def type_(obj: ta.Any) -> Type:
|
|
|
155
158
|
if isinstance(obj, ta.NewType): # noqa
|
|
156
159
|
return NewType(oty)
|
|
157
160
|
|
|
158
|
-
if
|
|
161
|
+
if (
|
|
162
|
+
oty is _GenericAlias or
|
|
163
|
+
oty is ta.GenericAlias # type: ignore # noqa
|
|
164
|
+
):
|
|
159
165
|
origin = ta.get_origin(obj)
|
|
160
166
|
args = ta.get_args(obj)
|
|
161
167
|
if origin is ta.Generic:
|
omlish/sync.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- sync (lol) w/ asyncs.anyio
|
|
4
|
+
- atomics
|
|
5
|
+
"""
|
|
6
|
+
import threading
|
|
7
|
+
import typing as ta
|
|
8
|
+
|
|
9
|
+
from . import lang
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = ta.TypeVar('T')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Once:
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self._done = False
|
|
19
|
+
self._lock = threading.Lock()
|
|
20
|
+
|
|
21
|
+
def do(self, fn: ta.Callable[[], None]) -> bool:
|
|
22
|
+
if self._done:
|
|
23
|
+
return False
|
|
24
|
+
with self._lock:
|
|
25
|
+
if self._done:
|
|
26
|
+
return False # type: ignore
|
|
27
|
+
try:
|
|
28
|
+
fn()
|
|
29
|
+
finally:
|
|
30
|
+
self._done = True
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Lazy(ta.Generic[T]):
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
super().__init__()
|
|
37
|
+
self._once = Once()
|
|
38
|
+
self._v: lang.Maybe[T] = lang.empty()
|
|
39
|
+
|
|
40
|
+
def peek(self) -> lang.Maybe[T]:
|
|
41
|
+
return self._v
|
|
42
|
+
|
|
43
|
+
def set(self, v: T) -> None:
|
|
44
|
+
self._v = lang.just(v)
|
|
45
|
+
|
|
46
|
+
def get(self, fn: ta.Callable[[], T]) -> T:
|
|
47
|
+
def do():
|
|
48
|
+
self._v = lang.just(fn())
|
|
49
|
+
self._once.do(do)
|
|
50
|
+
return self._v.must()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LazyFn(ta.Generic[T]):
|
|
54
|
+
def __init__(self, fn: ta.Callable[[], T]) -> None:
|
|
55
|
+
super().__init__()
|
|
56
|
+
self._fn = fn
|
|
57
|
+
self._once = Once()
|
|
58
|
+
self._v: lang.Maybe[T] = lang.empty()
|
|
59
|
+
|
|
60
|
+
def peek(self) -> lang.Maybe[T]:
|
|
61
|
+
return self._v
|
|
62
|
+
|
|
63
|
+
def set(self, v: T) -> None:
|
|
64
|
+
self._v = lang.just(v)
|
|
65
|
+
|
|
66
|
+
def get(self) -> T:
|
|
67
|
+
def do():
|
|
68
|
+
self._v = lang.just(self._fn())
|
|
69
|
+
self._once.do(do)
|
|
70
|
+
return self._v.must()
|
omlish/term.py
CHANGED
|
@@ -213,8 +213,13 @@ def main() -> None:
|
|
|
213
213
|
|
|
214
214
|
sys.stdout.write(SGR(SGRs.RESET))
|
|
215
215
|
sys.stdout.write(BG8(15) + ' ')
|
|
216
|
-
for i in
|
|
216
|
+
for i in range(20):
|
|
217
217
|
sys.stdout.write(BG8(i) + ' ')
|
|
218
|
+
sys.stdout.write('\n')
|
|
219
|
+
for i in range(256):
|
|
220
|
+
if i % 12 == 0:
|
|
221
|
+
sys.stdout.write('\n')
|
|
222
|
+
sys.stdout.write(BG8(i + 16) + ' ')
|
|
218
223
|
sys.stdout.write(SGR(SGRs.RESET) + '\n')
|
|
219
224
|
|
|
220
225
|
|
omlish/testing/pytest/helpers.py
CHANGED
|
@@ -1,28 +1,4 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
-
import shutil
|
|
3
|
-
import sys
|
|
4
|
-
import typing as ta
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from ..testing import can_import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def skip_if_cant_import(module: str, *args, **kwargs):
|
|
12
|
-
return pytest.mark.skipif(not can_import(module, *args, **kwargs), reason=f'requires import {module}')
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def skip_if_not_on_path(exe: str):
|
|
16
|
-
return pytest.mark.skipif(shutil.which(exe) is None, reason=f'requires exe on path {exe}')
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def skip_if_python_version_less_than(num: ta.Sequence[int]):
|
|
20
|
-
return pytest.mark.skipif(sys.version_info < tuple(num), reason=f'python version {tuple(sys.version_info)} < {tuple(num)}') # noqa
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def skip_if_not_single():
|
|
24
|
-
# [resolve_collection_argument(a) for a in session.config.args]
|
|
25
|
-
raise NotImplementedError
|
|
26
2
|
|
|
27
3
|
|
|
28
4
|
@contextlib.contextmanager
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
import sysconfig
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from ... import lang # noqa
|
|
9
|
+
from ..testing import can_import
|
|
10
|
+
from .plugins.managermarks import ManagerMark # noqa
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if ta.TYPE_CHECKING:
|
|
14
|
+
import asyncio
|
|
15
|
+
else:
|
|
16
|
+
asyncio = lang.proxy_import('asyncio')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def skip_if_cant_import(module: str, *args, **kwargs):
|
|
20
|
+
return pytest.mark.skipif(not can_import(module, *args, **kwargs), reason=f'requires import {module}')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def skip_if_not_on_path(exe: str):
|
|
24
|
+
return pytest.mark.skipif(shutil.which(exe) is None, reason=f'requires exe on path {exe}')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def skip_if_python_version_less_than(num: ta.Sequence[int]):
|
|
28
|
+
return pytest.mark.skipif(sys.version_info < tuple(num), reason=f'python version {tuple(sys.version_info)} < {tuple(num)}') # noqa
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def skip_if_not_single():
|
|
32
|
+
# FIXME
|
|
33
|
+
# [resolve_collection_argument(a) for a in session.config.args]
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def skip_if_nogil():
|
|
38
|
+
return pytest.mark.skipif(sysconfig.get_config_var('Py_GIL_DISABLED'), reason='requires gil build')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class drain_asyncio(ManagerMark): # noqa
|
|
42
|
+
def __call__(self, item: pytest.Function) -> ta.Iterator[None]:
|
|
43
|
+
loop = asyncio.get_event_loop()
|
|
44
|
+
try:
|
|
45
|
+
yield
|
|
46
|
+
finally:
|
|
47
|
+
while loop._ready or loop._scheduled: # type: ignore # noqa
|
|
48
|
+
loop._run_once() # type: ignore # noqa
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import contextlib
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from .... import lang
|
|
8
|
+
from ._registry import register
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ManagerMark(lang.Abstract):
|
|
12
|
+
def __init_subclass__(cls, **kwargs):
|
|
13
|
+
super().__init_subclass__(**kwargs)
|
|
14
|
+
|
|
15
|
+
@abc.abstractmethod
|
|
16
|
+
def __call__(self, item: pytest.Function) -> ta.Iterator[None]:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _deep_subclasses(cls):
|
|
21
|
+
ret = set()
|
|
22
|
+
|
|
23
|
+
def rec(cur):
|
|
24
|
+
for nxt in cur.__subclasses__():
|
|
25
|
+
if nxt not in ret:
|
|
26
|
+
ret.add(nxt)
|
|
27
|
+
rec(nxt)
|
|
28
|
+
|
|
29
|
+
rec(cls)
|
|
30
|
+
return ret
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@register
|
|
34
|
+
class ManagerMarksPlugin:
|
|
35
|
+
|
|
36
|
+
@lang.cached_function
|
|
37
|
+
def mark_classes(self) -> ta.Mapping[str, type[ManagerMark]]:
|
|
38
|
+
return {
|
|
39
|
+
cls.__name__: cls
|
|
40
|
+
for cls in _deep_subclasses(ManagerMark)
|
|
41
|
+
if not lang.is_abstract_class(cls)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def pytest_configure(self, config):
|
|
45
|
+
for n in self.mark_classes():
|
|
46
|
+
config.addinivalue_line(
|
|
47
|
+
'markers',
|
|
48
|
+
f'{n}: mark to manage {n}',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
52
|
+
def pytest_runtest_call(self, item):
|
|
53
|
+
with contextlib.ExitStack() as es:
|
|
54
|
+
for n, cls in self.mark_classes().items():
|
|
55
|
+
if (m := item.get_closest_marker(n)) is None:
|
|
56
|
+
continue
|
|
57
|
+
inst = cls(*m.args, **m.kwargs)
|
|
58
|
+
es.enter_context(contextlib.contextmanager(inst)(item))
|
|
59
|
+
|
|
60
|
+
yield
|
omlish/testing/testing.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import importlib
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
import threading
|
|
5
6
|
import time
|
|
6
7
|
import traceback
|
|
@@ -100,3 +101,12 @@ def xfail(fn):
|
|
|
100
101
|
traceback.print_exc()
|
|
101
102
|
|
|
102
103
|
return inner
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def raise_in_thread(thr: threading.Thread, exc: BaseException | type[BaseException]) -> None:
|
|
107
|
+
if sys.implementation.name != 'cpython':
|
|
108
|
+
raise RuntimeError(sys.implementation.name)
|
|
109
|
+
|
|
110
|
+
# https://github.com/python/cpython/blob/37ba7531a59a0a2b240a86f7e2adfb1b1cd8ac0c/Lib/test/test_threading.py#L182
|
|
111
|
+
import ctypes as ct
|
|
112
|
+
ct.pythonapi.PyThreadState_SetAsyncExc(ct.c_ulong(thr.ident), ct.py_object(exc)) # type: ignore
|
omlish/text/delimit.py
CHANGED