omlish 0.0.0.dev3__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.

Files changed (91) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/__init__.py +8 -0
  3. omlish/asyncs/__init__.py +18 -0
  4. omlish/asyncs/anyio.py +66 -0
  5. omlish/asyncs/flavors.py +227 -0
  6. omlish/asyncs/trio_asyncio.py +47 -0
  7. omlish/c3.py +1 -1
  8. omlish/cached.py +1 -2
  9. omlish/collections/__init__.py +4 -1
  10. omlish/collections/cache/impl.py +1 -1
  11. omlish/collections/indexed.py +1 -1
  12. omlish/collections/utils.py +38 -6
  13. omlish/configs/__init__.py +5 -0
  14. omlish/configs/classes.py +53 -0
  15. omlish/configs/dotenv.py +586 -0
  16. omlish/configs/props.py +589 -49
  17. omlish/dataclasses/impl/api.py +1 -1
  18. omlish/dataclasses/impl/as_.py +1 -1
  19. omlish/dataclasses/impl/fields.py +1 -0
  20. omlish/dataclasses/impl/init.py +1 -1
  21. omlish/dataclasses/impl/main.py +1 -0
  22. omlish/dataclasses/impl/metaclass.py +6 -1
  23. omlish/dataclasses/impl/order.py +1 -1
  24. omlish/dataclasses/impl/reflect.py +15 -2
  25. omlish/defs.py +1 -1
  26. omlish/diag/procfs.py +29 -1
  27. omlish/diag/procstats.py +32 -0
  28. omlish/diag/replserver/console.py +3 -3
  29. omlish/diag/replserver/server.py +6 -5
  30. omlish/diag/threads.py +86 -0
  31. omlish/docker.py +19 -0
  32. omlish/dynamic.py +2 -2
  33. omlish/fnpairs.py +121 -24
  34. omlish/graphs/dags.py +113 -0
  35. omlish/graphs/domination.py +268 -0
  36. omlish/graphs/trees.py +2 -2
  37. omlish/http/__init__.py +25 -0
  38. omlish/http/asgi.py +131 -0
  39. omlish/http/consts.py +31 -4
  40. omlish/http/cookies.py +194 -0
  41. omlish/http/dates.py +70 -0
  42. omlish/http/encodings.py +6 -0
  43. omlish/http/json.py +273 -0
  44. omlish/http/sessions.py +197 -0
  45. omlish/inject/__init__.py +8 -2
  46. omlish/inject/bindings.py +3 -3
  47. omlish/inject/exceptions.py +3 -3
  48. omlish/inject/impl/elements.py +46 -25
  49. omlish/inject/impl/injector.py +8 -5
  50. omlish/inject/impl/multis.py +74 -0
  51. omlish/inject/impl/providers.py +19 -39
  52. omlish/inject/{proxy.py → impl/proxy.py} +2 -2
  53. omlish/inject/impl/scopes.py +4 -2
  54. omlish/inject/injector.py +1 -0
  55. omlish/inject/keys.py +3 -9
  56. omlish/inject/multis.py +70 -0
  57. omlish/inject/providers.py +23 -23
  58. omlish/inject/scopes.py +7 -3
  59. omlish/inject/types.py +0 -8
  60. omlish/iterators.py +13 -0
  61. omlish/json.py +138 -1
  62. omlish/lang/__init__.py +8 -0
  63. omlish/lang/classes/restrict.py +1 -1
  64. omlish/lang/classes/virtual.py +2 -2
  65. omlish/lang/contextmanagers.py +64 -0
  66. omlish/lang/datetimes.py +6 -5
  67. omlish/lang/functions.py +10 -0
  68. omlish/lang/imports.py +11 -2
  69. omlish/lang/sys.py +7 -0
  70. omlish/lang/typing.py +1 -0
  71. omlish/logs/utils.py +1 -1
  72. omlish/marshal/datetimes.py +1 -1
  73. omlish/reflect.py +8 -2
  74. omlish/sql/__init__.py +9 -0
  75. omlish/sql/asyncs.py +148 -0
  76. omlish/sync.py +70 -0
  77. omlish/term.py +6 -1
  78. omlish/testing/pydevd.py +2 -0
  79. omlish/testing/pytest/__init__.py +5 -0
  80. omlish/testing/pytest/helpers.py +0 -24
  81. omlish/testing/pytest/inject/harness.py +1 -1
  82. omlish/testing/pytest/marks.py +48 -0
  83. omlish/testing/pytest/plugins/__init__.py +2 -0
  84. omlish/testing/pytest/plugins/managermarks.py +60 -0
  85. omlish/testing/testing.py +10 -0
  86. omlish/text/delimit.py +4 -0
  87. {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +4 -1
  88. {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +91 -70
  89. {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
  90. {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
  91. {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
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 oty is _GenericAlias or oty is ta.GenericAlias: # type: ignore # noqa
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/sql/__init__.py CHANGED
@@ -0,0 +1,9 @@
1
+ from .asyncs import ( # noqa
2
+ AsyncConnection,
3
+ AsyncConnectionLike,
4
+ AsyncEngine,
5
+ AsyncEngineLike,
6
+ AsyncTransaction,
7
+ AsyncTransactionLike,
8
+ async_adapt,
9
+ )
omlish/sql/asyncs.py ADDED
@@ -0,0 +1,148 @@
1
+ """
2
+ TODO:
3
+ - Maysync impls?
4
+ - base Protocol so adapters and real sa impls can be used interchangeably (if in asyncio ctx)?
5
+ """
6
+ import contextlib
7
+ import typing as ta
8
+
9
+ import sqlalchemy as sa
10
+ import sqlalchemy.ext.asyncio as saa
11
+
12
+ from .. import asyncs as au
13
+
14
+
15
+ T = ta.TypeVar('T')
16
+ P = ta.ParamSpec('P')
17
+
18
+
19
+ AsyncEngineLike = ta.Union[saa.AsyncEngine, 'AsyncEngine']
20
+ AsyncConnectionLike = ta.Union[saa.AsyncConnection, 'AsyncConnection']
21
+ AsyncTransactionLike = ta.Union[saa.AsyncTransaction, 'AsyncTransaction']
22
+
23
+
24
+ class AsyncTransaction:
25
+ def __init__(self, underlying: saa.AsyncTransaction) -> None:
26
+ super().__init__()
27
+ self._underlying = underlying
28
+
29
+ @property
30
+ def underlying(self) -> saa.AsyncTransaction:
31
+ return self._underlying
32
+
33
+ ##
34
+
35
+ @au.mark_asyncio
36
+ async def close(self) -> None:
37
+ await au.from_asyncio(self._underlying.close)()
38
+
39
+ @au.mark_asyncio
40
+ async def rollback(self) -> None:
41
+ await au.from_asyncio(self._underlying.rollback)()
42
+
43
+ @au.mark_asyncio
44
+ async def commit(self) -> None:
45
+ await au.from_asyncio(self._underlying.commit)()
46
+
47
+
48
+ class AsyncConnection:
49
+ def __init__(self, underlying: saa.AsyncConnection) -> None:
50
+ super().__init__()
51
+ self._underlying = underlying
52
+
53
+ @property
54
+ def underlying(self) -> saa.AsyncConnection:
55
+ return self._underlying
56
+
57
+ ##
58
+
59
+ @contextlib.asynccontextmanager
60
+ @au.mark_asyncio
61
+ async def begin(self) -> ta.AsyncIterator[AsyncTransaction]:
62
+ async with au.from_asyncio_context(self._underlying.begin()) as u:
63
+ yield AsyncTransaction(u)
64
+
65
+ @au.mark_asyncio
66
+ async def execute(
67
+ self,
68
+ statement: ta.Any,
69
+ *args: ta.Any,
70
+ **kwargs: ta.Any,
71
+ ) -> sa.CursorResult[ta.Any]:
72
+ return await au.from_asyncio(self._underlying.execute)(statement, *args, **kwargs)
73
+
74
+ @au.mark_asyncio
75
+ async def run_sync(
76
+ self,
77
+ fn: ta.Callable[ta.Concatenate[sa.Connection, P], T],
78
+ *args: P.args,
79
+ **kwargs: P.kwargs,
80
+ ) -> T:
81
+ return await au.from_asyncio(self._underlying.run_sync)(fn, *args, **kwargs)
82
+
83
+
84
+ class AsyncEngine:
85
+ def __init__(self, underlying: saa.AsyncEngine) -> None:
86
+ super().__init__()
87
+ self._underlying = underlying
88
+
89
+ @property
90
+ def underlying(self) -> saa.AsyncEngine:
91
+ return self._underlying
92
+
93
+ ##
94
+
95
+ @contextlib.asynccontextmanager
96
+ @au.mark_asyncio
97
+ async def connect(self) -> ta.AsyncIterator[AsyncConnection]:
98
+ async with au.from_asyncio_context(self._underlying.connect()) as u:
99
+ yield AsyncConnection(u)
100
+
101
+ @au.mark_asyncio
102
+ async def dispose(self, close: bool = True) -> None:
103
+ await au.from_asyncio(self._underlying.dispose)(close)
104
+
105
+
106
+ ##
107
+
108
+
109
+ @ta.overload
110
+ def async_adapt(obj: AsyncEngine) -> AsyncEngine:
111
+ ...
112
+
113
+
114
+ @ta.overload
115
+ def async_adapt(obj: AsyncConnection) -> AsyncConnection:
116
+ ...
117
+
118
+
119
+ @ta.overload
120
+ def async_adapt(obj: AsyncTransaction) -> AsyncTransaction:
121
+ ...
122
+
123
+
124
+ @ta.overload
125
+ def async_adapt(obj: saa.AsyncEngine) -> AsyncEngine:
126
+ ...
127
+
128
+
129
+ @ta.overload
130
+ def async_adapt(obj: saa.AsyncConnection) -> AsyncConnection:
131
+ ...
132
+
133
+
134
+ @ta.overload
135
+ def async_adapt(obj: saa.AsyncTransaction) -> AsyncTransaction:
136
+ ...
137
+
138
+
139
+ def async_adapt(obj: ta.Any) -> ta.Any:
140
+ if isinstance(obj, (AsyncEngine, AsyncConnection, AsyncTransaction)):
141
+ return obj
142
+ if isinstance(obj, saa.AsyncTransaction):
143
+ return AsyncTransaction(obj)
144
+ if isinstance(obj, saa.AsyncConnection):
145
+ return AsyncConnection(obj)
146
+ if isinstance(obj, saa.AsyncEngine):
147
+ return AsyncEngine(obj)
148
+ raise TypeError(obj)
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 [196, 160, 124, 88, 52, 16]:
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/pydevd.py CHANGED
@@ -87,6 +87,8 @@ def silence_subprocess_check() -> None:
87
87
 
88
88
  @lang.cached_function
89
89
  def patch_for_trio_asyncio() -> None:
90
+ """Fix for `trio a callable object was expected by call_soon(), got Task`"""
91
+
90
92
  try:
91
93
  import pydevd_nest_asyncio # noqa
92
94
  except ImportError:
@@ -2,7 +2,12 @@ from . import plugins # noqa
2
2
 
3
3
  from .helpers import ( # noqa
4
4
  assert_raises_star,
5
+ )
6
+
7
+ from .marks import ( # noqa
8
+ drain_asyncio,
5
9
  skip_if_cant_import,
10
+ skip_if_nogil,
6
11
  skip_if_not_on_path,
7
12
  skip_if_python_version_less_than,
8
13
  )
@@ -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
@@ -4,10 +4,10 @@ import typing as ta
4
4
 
5
5
  import pytest
6
6
 
7
- from .. import plugins
8
7
  from .... import check
9
8
  from .... import inject as inj
10
9
  from .... import lang
10
+ from .. import plugins
11
11
 
12
12
 
13
13
  T = ta.TypeVar('T')
@@ -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
@@ -1,11 +1,13 @@
1
1
  from . import ( # noqa
2
2
  logging,
3
+ managermarks,
3
4
  pydevd,
4
5
  repeat,
5
6
  skips,
6
7
  spacing,
7
8
  switches,
8
9
  )
10
+
9
11
  from ._registry import ( # noqa
10
12
  ALL,
11
13
  register,
@@ -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
@@ -1,3 +1,7 @@
1
+ """
2
+ TODO:
3
+ - replace with codecs.StreamReader
4
+ """
1
5
  import io
2
6
  import typing as ta
3
7
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev3
3
+ Version: 0.0.0.dev5
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -24,6 +24,9 @@ Provides-Extra: sql
24
24
  Requires-Dist: sqlalchemy ; extra == 'sql'
25
25
  Provides-Extra: test
26
26
  Requires-Dist: pytest ; extra == 'test'
27
+ Provides-Extra: trio
28
+ Requires-Dist: trio ; extra == 'trio'
29
+ Requires-Dist: trio-asyncio ; extra == 'trio'
27
30
  Provides-Extra: wrapt
28
31
  Requires-Dist: wrapt ; extra == 'wrapt'
29
32
  Provides-Extra: yaml