omlish 0.0.0.dev5__py3-none-any.whl → 0.0.0.dev6__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 (100) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/asyncs/__init__.py +9 -0
  3. omlish/asyncs/anyio.py +83 -19
  4. omlish/asyncs/asyncio.py +23 -0
  5. omlish/asyncs/asyncs.py +9 -6
  6. omlish/asyncs/bridge.py +316 -0
  7. omlish/asyncs/trio_asyncio.py +7 -3
  8. omlish/collections/__init__.py +1 -0
  9. omlish/collections/identity.py +7 -0
  10. omlish/configs/strings.py +94 -0
  11. omlish/dataclasses/__init__.py +9 -0
  12. omlish/dataclasses/impl/copy.py +30 -0
  13. omlish/dataclasses/impl/exceptions.py +6 -0
  14. omlish/dataclasses/impl/fields.py +24 -25
  15. omlish/dataclasses/impl/init.py +4 -2
  16. omlish/dataclasses/impl/main.py +2 -0
  17. omlish/dataclasses/utils.py +44 -0
  18. omlish/diag/__init__.py +4 -0
  19. omlish/diag/procfs.py +2 -2
  20. omlish/{testing → diag}/pydevd.py +35 -0
  21. omlish/dispatch/_dispatch2.py +65 -0
  22. omlish/dispatch/_dispatch3.py +104 -0
  23. omlish/docker.py +1 -1
  24. omlish/fnpairs.py +11 -0
  25. omlish/http/asgi.py +2 -1
  26. omlish/http/collections.py +15 -0
  27. omlish/http/consts.py +16 -1
  28. omlish/http/sessions.py +10 -3
  29. omlish/inject/__init__.py +45 -17
  30. omlish/inject/binder.py +185 -5
  31. omlish/inject/bindings.py +3 -36
  32. omlish/inject/eagers.py +2 -8
  33. omlish/inject/elements.py +30 -9
  34. omlish/inject/exceptions.py +1 -1
  35. omlish/inject/impl/elements.py +37 -12
  36. omlish/inject/impl/injector.py +19 -2
  37. omlish/inject/impl/inspect.py +33 -5
  38. omlish/inject/impl/origins.py +75 -0
  39. omlish/inject/impl/{private.py → privates.py} +2 -2
  40. omlish/inject/impl/scopes.py +6 -2
  41. omlish/inject/injector.py +8 -4
  42. omlish/inject/inspect.py +18 -0
  43. omlish/inject/keys.py +8 -14
  44. omlish/inject/listeners.py +26 -0
  45. omlish/inject/managed.py +76 -10
  46. omlish/inject/multis.py +68 -18
  47. omlish/inject/origins.py +27 -0
  48. omlish/inject/overrides.py +5 -4
  49. omlish/inject/{private.py → privates.py} +6 -10
  50. omlish/inject/providers.py +12 -85
  51. omlish/inject/scopes.py +13 -6
  52. omlish/inject/types.py +3 -1
  53. omlish/lang/__init__.py +8 -2
  54. omlish/lang/cached.py +2 -2
  55. omlish/lang/classes/restrict.py +2 -1
  56. omlish/lang/classes/simple.py +18 -8
  57. omlish/lang/contextmanagers.py +12 -3
  58. omlish/lang/descriptors.py +131 -0
  59. omlish/lang/functions.py +8 -28
  60. omlish/lang/iterables.py +20 -1
  61. omlish/lang/typing.py +5 -0
  62. omlish/lifecycles/__init__.py +34 -0
  63. omlish/lifecycles/abstract.py +43 -0
  64. omlish/lifecycles/base.py +51 -0
  65. omlish/lifecycles/contextmanagers.py +74 -0
  66. omlish/lifecycles/controller.py +116 -0
  67. omlish/lifecycles/manager.py +161 -0
  68. omlish/lifecycles/states.py +43 -0
  69. omlish/lifecycles/transitions.py +64 -0
  70. omlish/logs/formatters.py +1 -1
  71. omlish/marshal/__init__.py +4 -0
  72. omlish/marshal/naming.py +4 -0
  73. omlish/marshal/objects.py +1 -0
  74. omlish/marshal/polymorphism.py +4 -4
  75. omlish/reflect.py +134 -19
  76. omlish/secrets/__init__.py +7 -0
  77. omlish/secrets/marshal.py +41 -0
  78. omlish/secrets/passwords.py +120 -0
  79. omlish/secrets/secrets.py +47 -0
  80. omlish/serde/__init__.py +0 -0
  81. omlish/{configs → serde}/dotenv.py +12 -24
  82. omlish/{json.py → serde/json.py} +2 -1
  83. omlish/serde/yaml.py +223 -0
  84. omlish/sql/dbs.py +1 -1
  85. omlish/sql/duckdb.py +136 -0
  86. omlish/sql/sqlean.py +17 -0
  87. omlish/term.py +1 -1
  88. omlish/testing/pytest/__init__.py +3 -2
  89. omlish/testing/pytest/inject/harness.py +3 -3
  90. omlish/testing/pytest/marks.py +4 -7
  91. omlish/testing/pytest/plugins/__init__.py +1 -0
  92. omlish/testing/pytest/plugins/asyncs.py +136 -0
  93. omlish/testing/pytest/plugins/pydevd.py +1 -1
  94. omlish/text/glyphsplit.py +92 -0
  95. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  96. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/RECORD +100 -72
  97. /omlish/{configs → serde}/props.py +0 -0
  98. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  99. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +0 -0
  100. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
omlish/serde/yaml.py ADDED
@@ -0,0 +1,223 @@
1
+ """
2
+ TODO:
3
+ - !! shared 'object format' node hierarchy
4
+ - fuse yaml and hocon - marks, *COMMENTS*, etc
5
+ - goal: perfect rewrites (comments, whitespace)
6
+ - or at least comments
7
+ - rename 'objects'? codecs/serde interplay still unresolved
8
+ """
9
+ import datetime
10
+ import types
11
+ import typing as ta
12
+
13
+ from .. import check
14
+ from .. import dataclasses as dc
15
+ from .. import lang
16
+
17
+
18
+ if ta.TYPE_CHECKING:
19
+ import yaml
20
+ import yaml.nodes as yaml_nodes
21
+ else:
22
+ yaml = lang.proxy_import('yaml')
23
+ yaml_nodes = lang.proxy_import('yaml.nodes')
24
+
25
+
26
+ T = ta.TypeVar('T')
27
+
28
+
29
+ @dc.dataclass(frozen=True)
30
+ class NodeWrapped(lang.Final, ta.Generic[T]):
31
+ value: T
32
+ node: 'yaml_nodes.Node'
33
+
34
+ def __post_init__(self) -> None:
35
+ if isinstance(self.value, NodeWrapped):
36
+ raise TypeError(self.value)
37
+ if not isinstance(self.node, yaml_nodes.Node):
38
+ raise TypeError(self.node)
39
+
40
+
41
+ class NodeUnwrapper:
42
+
43
+ seq_types: tuple[type, ...] = (
44
+ list,
45
+ set,
46
+ tuple,
47
+ )
48
+
49
+ def unwrap_seq(self, nw: NodeWrapped[T]) -> T:
50
+ return nw.value.__class__(map(self.unwrap, nw.value)) # type: ignore
51
+
52
+ map_types: tuple[type, ...] = (
53
+ dict,
54
+ )
55
+
56
+ def unwrap_map(self, nw: NodeWrapped[T]) -> T:
57
+ return nw.value.__class__({self.unwrap(k): self.unwrap(v) for k, v in nw.value.items()}) # type: ignore
58
+
59
+ scalar_types: tuple[type, ...] = (
60
+ bool,
61
+ bytes,
62
+ datetime.datetime,
63
+ float,
64
+ int,
65
+ str,
66
+ type(None),
67
+ )
68
+
69
+ def unwrap_scalar(self, nw: NodeWrapped[T]) -> T:
70
+ return nw.value
71
+
72
+ def unwrap_unknown(self, nw: NodeWrapped[T]) -> T:
73
+ raise TypeError(nw.value)
74
+
75
+ def unwrap(self, nw: NodeWrapped[T]) -> T:
76
+ check.isinstance(nw, NodeWrapped)
77
+ if isinstance(nw.value, self.seq_types):
78
+ return self.unwrap_seq(nw)
79
+ elif isinstance(nw.value, self.map_types):
80
+ return self.unwrap_map(nw)
81
+ elif isinstance(nw.value, self.scalar_types):
82
+ return self.unwrap_scalar(nw)
83
+ else:
84
+ return self.unwrap_unknown(nw)
85
+
86
+
87
+ def unwrap(nw: NodeWrapped[T]) -> T:
88
+ return NodeUnwrapper().unwrap(nw)
89
+
90
+
91
+ class NodeWrappingConstructorMixin:
92
+
93
+ def __init__(self, *args, **kwargs):
94
+ super().__init__(*args, **kwargs)
95
+
96
+ ctors = {
97
+ fn.__name__: fn for fn in [
98
+ self.__class__.construct_yaml_omap,
99
+ self.__class__.construct_yaml_pairs,
100
+ ]
101
+ }
102
+ self.yaml_constructors = {
103
+ tag: ctors.get(ctor.__name__, ctor)
104
+ for tag, ctor in self.yaml_constructors.items() # type: ignore # noqa
105
+ }
106
+
107
+ def construct_object(self, node, deep=False):
108
+ value = super().construct_object(node, deep=deep) # type: ignore
109
+ return NodeWrapped(value, node)
110
+
111
+ def __construct_yaml_pairs(self, node, fn):
112
+ omap = [] # type: ignore
113
+ gen = check.isinstance(fn(node), types.GeneratorType)
114
+ yield omap
115
+ uomap = next(gen)
116
+ lang.exhaust(gen)
117
+ for key, value in uomap: # type: ignore
118
+ omap.append(NodeWrapped((key, value), node))
119
+
120
+ def construct_yaml_omap(self, node):
121
+ return self.__construct_yaml_pairs(node, super().construct_yaml_omap) # type: ignore # noqa
122
+
123
+ def construct_yaml_pairs(self, node):
124
+ return self.__construct_yaml_pairs(node, super().construct_yaml_pairs) # type: ignore # noqa
125
+
126
+
127
+ class _cached_class_property: # noqa
128
+ def __init__(self, fn):
129
+ super().__init__()
130
+ self._fn = fn
131
+ self._attr = None
132
+
133
+ def __call__(self, *args, **kwargs):
134
+ raise TypeError
135
+
136
+ def __set_name__(self, owner, name):
137
+ self._attr = '_' + name
138
+
139
+ def __get__(self, instance, owner):
140
+ if owner is None:
141
+ if instance is None:
142
+ raise RuntimeError
143
+ owner = instance.__class__
144
+ try:
145
+ return owner.__dict__[self._attr]
146
+ except KeyError:
147
+ ret = self._fn(owner)
148
+ setattr(owner, self._attr, ret) # type: ignore
149
+ return ret
150
+
151
+
152
+ class WrappedLoaders(lang.Namespace):
153
+
154
+ @staticmethod
155
+ def _wrap(cls):
156
+ return type('NodeWrapping$' + cls.__name__, (NodeWrappingConstructorMixin, cls), {})
157
+
158
+ Base: type['yaml.BaseLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.BaseLoader)) # type: ignore
159
+
160
+ @classmethod
161
+ def base(cls, *args, **kwargs) -> 'yaml.BaseLoader':
162
+ return cls.Base(*args, **kwargs)
163
+
164
+ Full: type['yaml.FullLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.FullLoader)) # type: ignore
165
+
166
+ @classmethod
167
+ def full(cls, *args, **kwargs) -> 'yaml.FullLoader':
168
+ return cls.Full(*args, **kwargs)
169
+
170
+ Safe: type['yaml.SafeLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.SafeLoader)) # type: ignore
171
+
172
+ @classmethod
173
+ def safe(cls, *args, **kwargs) -> 'yaml.SafeLoader':
174
+ return cls.Safe(*args, **kwargs)
175
+
176
+ Unsafe: type['yaml.UnsafeLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.UnsafeLoader)) # type: ignore
177
+
178
+ @classmethod
179
+ def unsafe(cls, *args, **kwargs) -> 'yaml.UnsafeLoader':
180
+ return cls.Unsafe(*args, **kwargs)
181
+
182
+ CBase: type['yaml.CBaseLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.CBaseLoader)) # type: ignore
183
+
184
+ @classmethod
185
+ def cbase(cls, *args, **kwargs) -> 'yaml.CBaseLoader':
186
+ return cls.CBase(*args, **kwargs)
187
+
188
+ CFull: type['yaml.CFullLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.CFullLoader)) # type: ignore
189
+
190
+ @classmethod
191
+ def cfull(cls, *args, **kwargs) -> 'yaml.CFullLoader':
192
+ return cls.CFull(*args, **kwargs)
193
+
194
+ CSafe: type['yaml.CSafeLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.CSafeLoader)) # type: ignore
195
+
196
+ @classmethod
197
+ def csafe(cls, *args, **kwargs) -> 'yaml.CSafeLoader':
198
+ return cls.CSafe(*args, **kwargs)
199
+
200
+ CUnsafe: type['yaml.CUnsafeLoader'] = _cached_class_property(lambda cls: cls._wrap(yaml.CUnsafeLoader)) # type: ignore # noqa
201
+
202
+ @classmethod
203
+ def cunsafe(cls, *args, **kwargs) -> 'yaml.CUnsafeLoader':
204
+ return cls.CUnsafe(*args, **kwargs)
205
+
206
+
207
+ def load(stream, Loader): # noqa
208
+ with lang.disposing(Loader(stream)) as loader:
209
+ return loader.get_single_data()
210
+
211
+
212
+ def load_all(stream, Loader): # noqa
213
+ with lang.disposing(Loader(stream)) as loader:
214
+ while loader.check_data():
215
+ yield loader.get_data()
216
+
217
+
218
+ def full_load(stream): # noqa
219
+ return load(stream, yaml.FullLoader)
220
+
221
+
222
+ def full_load_all(stream): # noqa # noqa
223
+ return load_all(stream, yaml.FullLoader)
omlish/sql/dbs.py CHANGED
@@ -16,7 +16,7 @@ class DbType:
16
16
  default_port: int | None = None
17
17
 
18
18
 
19
- class DbTypes(lang.Namespace):
19
+ class DbTypes(lang.Namespace, lang.Final):
20
20
  MYSQL = DbType(
21
21
  name='mysql',
22
22
  dialect_name='mysql',
omlish/sql/duckdb.py ADDED
@@ -0,0 +1,136 @@
1
+ """
2
+ See:
3
+ - https://duckdb.org/docs/api/python/overview.html
4
+ - https://github.com/Mause/duckdb_engine/blob/0e3ea0107f81c66d50b444011d31fce22a9b902c/duckdb_engine/__init__.py
5
+ """
6
+ import typing as ta
7
+
8
+ import sqlalchemy as sa
9
+ from sqlalchemy.dialects import postgresql as sap
10
+
11
+ from .. import lang
12
+
13
+
14
+ if ta.TYPE_CHECKING:
15
+ import duckdb
16
+ else:
17
+ duckdb = lang.proxy_import('duckdb')
18
+
19
+
20
+ class ConnectionWrapper:
21
+ def __init__(self, c: 'duckdb.DuckDBPyConnection') -> None:
22
+ super().__init__()
23
+ self.__c = c
24
+ self.autocommit = None
25
+ self.closed = False
26
+
27
+ def cursor(self) -> 'CursorWrapper':
28
+ return CursorWrapper(self.__c, self)
29
+
30
+ def __getattr__(self, name: str) -> ta.Any:
31
+ return getattr(self.__c, name)
32
+
33
+ def close(self) -> None:
34
+ self.__c.close()
35
+ self.closed = True
36
+
37
+
38
+ def _is_transaction_context_message(e: Exception) -> bool:
39
+ return e.args[0] == 'TransactionContext Error: cannot rollback - no transaction is active'
40
+
41
+
42
+ class CursorWrapper:
43
+ def __init__(
44
+ self,
45
+ c: 'duckdb.DuckDBPyConnection',
46
+ connection_wrapper: 'ConnectionWrapper',
47
+ ) -> None:
48
+ super().__init__()
49
+ self.__c = c
50
+ self.__connection_wrapper = connection_wrapper
51
+
52
+ def executemany(
53
+ self,
54
+ statement: str,
55
+ parameters: ta.Sequence[ta.Mapping[str, ta.Any]] | None = None,
56
+ context: ta.Any | None = None,
57
+ ) -> None:
58
+ self.__c.executemany(statement, list(parameters) if parameters else [])
59
+
60
+ def execute(
61
+ self,
62
+ statement: str,
63
+ parameters: ta.Sequence[ta.Any] | None = None,
64
+ context: ta.Any | None = None,
65
+ ) -> None:
66
+ try:
67
+ if statement.lower() == 'commit': # this is largely for ipython-sql
68
+ self.__c.commit()
69
+ elif parameters is None:
70
+ self.__c.execute(statement)
71
+ else:
72
+ self.__c.execute(statement, parameters)
73
+ except RuntimeError as e:
74
+ if e.args[0].startswith('Not implemented Error'):
75
+ raise NotImplementedError(*e.args) from e
76
+ elif _is_transaction_context_message(e):
77
+ return
78
+ else:
79
+ raise
80
+
81
+ @property
82
+ def connection(self):
83
+ return self.__connection_wrapper
84
+
85
+ def close(self) -> None:
86
+ pass
87
+
88
+ def __getattr__(self, name: str) -> ta.Any:
89
+ return getattr(self.__c, name)
90
+
91
+ def fetchmany(self, size: int | None = None) -> list:
92
+ if size is None:
93
+ return self.__c.fetchmany()
94
+ else:
95
+ return self.__c.fetchmany(size)
96
+
97
+
98
+ class DuckdbDialect(sap.base.PGDialect):
99
+ name = 'postgres__duckdb'
100
+ driver = 'duckdb_engine'
101
+
102
+ supports_statement_cache = False
103
+
104
+ @classmethod
105
+ def import_dbapi(cls):
106
+ return duckdb
107
+
108
+ def connect(self, *cargs, **cparams):
109
+ conn = self.loaded_dbapi.connect(*cargs, **cparams)
110
+
111
+ return ConnectionWrapper(conn)
112
+
113
+ def do_rollback(self, connection) -> None:
114
+ try:
115
+ super().do_rollback(connection)
116
+ except duckdb.TransactionException as e:
117
+ if _is_transaction_context_message(e):
118
+ return
119
+ else:
120
+ raise
121
+
122
+ def do_begin(self, connection) -> None:
123
+ connection.begin()
124
+
125
+ def get_default_isolation_level(self, connection) -> ta.Any:
126
+ raise NotImplementedError
127
+
128
+ def _get_server_version_info(self, connection) -> tuple[int, int]:
129
+ connection.execute(sa.text('select version()')).fetchone()
130
+ return (8, 0)
131
+
132
+ def _set_backslash_escapes(self, connection):
133
+ pass
134
+
135
+
136
+ sa.dialects.registry.register(DuckdbDialect.name, __name__, DuckdbDialect.__name__)
omlish/sql/sqlean.py ADDED
@@ -0,0 +1,17 @@
1
+ import sqlalchemy as sa
2
+ from sqlalchemy.dialects import sqlite as sal
3
+
4
+
5
+ class SqleanDialect(sal.dialect): # type: ignore
6
+ name = 'sqlite__sqlean'
7
+ driver = 'sqlean_engine'
8
+
9
+ supports_statement_cache = True
10
+
11
+ @classmethod
12
+ def import_dbapi(cls):
13
+ from sqlean import dbapi2
14
+ return dbapi2
15
+
16
+
17
+ sa.dialects.registry.register(SqleanDialect.name, __name__, SqleanDialect.__name__)
omlish/term.py CHANGED
@@ -90,7 +90,7 @@ HIDE_CURSOR = ControlSequence(lambda: CSI + '?25l', 'Hide Cursor')
90
90
  SGR = ControlSequence(lambda n: CSI + _str_val(n) + 'm', 'Select Graphic Rendition')
91
91
 
92
92
 
93
- class SGRs(lang.Namespace):
93
+ class SGRs(lang.Namespace, lang.Final):
94
94
  RESET = 0
95
95
  NORMAL_COLOR_AND_INTENSITY = 22
96
96
 
@@ -1,5 +1,3 @@
1
- from . import plugins # noqa
2
-
3
1
  from .helpers import ( # noqa
4
2
  assert_raises_star,
5
3
  )
@@ -11,3 +9,6 @@ from .marks import ( # noqa
11
9
  skip_if_not_on_path,
12
10
  skip_if_python_version_less_than,
13
11
  )
12
+
13
+ # Imported for convenience in things that import this but not lang.
14
+ from ...lang import breakpoint_on_exception # noqa
@@ -24,7 +24,7 @@ class PytestScope(enum.Enum):
24
24
  FUNCTION = enum.auto()
25
25
 
26
26
 
27
- class Scopes(lang.Namespace):
27
+ class Scopes(lang.Namespace, lang.Final):
28
28
  Session = inj.SeededScope(PytestScope.SESSION)
29
29
  Package = inj.SeededScope(PytestScope.PACKAGE)
30
30
  Module = inj.SeededScope(PytestScope.MODULE)
@@ -50,7 +50,7 @@ class Harness:
50
50
  super().__init__()
51
51
  self._orig_es = es
52
52
  self._es = inj.as_elements(
53
- inj.as_binding(self),
53
+ inj.bind(self),
54
54
  *[
55
55
  inj.as_elements(
56
56
  inj.bind_scope(ss),
@@ -169,7 +169,7 @@ def bind(
169
169
  pts = scope if isinstance(scope, PytestScope) else PytestScope[check.isinstance(scope, str).upper()]
170
170
  check.isinstance(cls, type)
171
171
  register(inj.as_elements(
172
- inj.in_(cls, _SCOPES_BY_PYTEST_SCOPE[pts]),
172
+ inj.bind(cls, in_=_SCOPES_BY_PYTEST_SCOPE[pts]),
173
173
  ))
174
174
  return cls
175
175
  return inner
@@ -11,9 +11,10 @@ from .plugins.managermarks import ManagerMark # noqa
11
11
 
12
12
 
13
13
  if ta.TYPE_CHECKING:
14
- import asyncio
14
+ from ...asyncs import asyncio as aiu
15
+
15
16
  else:
16
- asyncio = lang.proxy_import('asyncio')
17
+ aiu = lang.proxy_import('...asyncs.asyncio', __package__)
17
18
 
18
19
 
19
20
  def skip_if_cant_import(module: str, *args, **kwargs):
@@ -40,9 +41,5 @@ def skip_if_nogil():
40
41
 
41
42
  class drain_asyncio(ManagerMark): # noqa
42
43
  def __call__(self, item: pytest.Function) -> ta.Iterator[None]:
43
- loop = asyncio.get_event_loop()
44
- try:
44
+ with aiu.draining_asyncio_tasks():
45
45
  yield
46
- finally:
47
- while loop._ready or loop._scheduled: # type: ignore # noqa
48
- loop._run_once() # type: ignore # noqa
@@ -1,4 +1,5 @@
1
1
  from . import ( # noqa
2
+ asyncs,
2
3
  logging,
3
4
  managermarks,
4
5
  pydevd,
@@ -0,0 +1,136 @@
1
+ """
2
+ TODO:
3
+ - auto drain_asyncio
4
+
5
+ ==
6
+
7
+ _pytest:
8
+ https://github.com/pytest-dev/pytest/blob/ef9b8f9d748b6f50eab5d43e32d93008f7880899/src/_pytest/python.py#L155
9
+ "async def function and no async plugin installed (see warnings)"
10
+
11
+ pytest_trio:
12
+ https://github.com/python-trio/pytest-trio/blob/f03160aa1dd355a12d39fa21f4aee4e1239efea3/pytest_trio/plugin.py
13
+ - pytest_addoption
14
+ - pytest_configure
15
+ - pytest_collection_modifyitems
16
+ - pytest_runtest_call @(hookwrapper=True)
17
+ - pytest_fixture_setup
18
+
19
+ pytest_asyncio:
20
+ https://github.com/pytest-dev/pytest-asyncio/blob/f45aa18cf3eeeb94075de16a1d797858facab863/pytest_asyncio/plugin.py
21
+ - pytest_addoption
22
+ - pytest_configure
23
+ - pytest_pycollect_makeitem_preprocess_async_fixtures @(specname="pytest_pycollect_makeitem", tryfirst=True)
24
+ - pytest_pycollect_makeitem_convert_async_functions_to_subclass @(specname="pytest_pycollect_makeitem", hookwrapper=True)
25
+ - pytest_generate_tests @(tryfirst=True)
26
+ - pytest_runtest_setup(item: pytest.Item) -> None:
27
+ - pytest_pyfunc_call @(tryfirst=True, hookwrapper=True)
28
+ - pytest_collectstart @()
29
+ - pytest_report_header @(tryfirst=True)
30
+ - pytest_fixture_setup @(hookwrapper=True)
31
+
32
+ anyio.pytest_plugin:
33
+ https://github.com/agronholm/anyio/blob/8907964926a24461840eee0925d3f355e729f15d/src/anyio/pytest_plugin.py
34
+ - pytest_configure
35
+ - pytest_pycollect_makeitem @(tryfirst=True)
36
+ - pytest_pyfunc_call @(tryfirst=True)
37
+ - pytest_fixture_setup
38
+
39
+ """ # noqa
40
+ import functools
41
+ import inspect
42
+ import typing as ta
43
+
44
+ import pytest
45
+
46
+ from .... import lang
47
+ from ....asyncs import trio_asyncio as trai
48
+ from ....diag import pydevd as pdu
49
+ from ._registry import register
50
+
51
+
52
+ if ta.TYPE_CHECKING:
53
+ import trio_asyncio
54
+ else:
55
+ trio_asyncio = lang.proxy_import('trio_asyncio')
56
+
57
+
58
+ ASYNCS_MARK = 'asyncs'
59
+
60
+ KNOWN_BACKENDS = (
61
+ 'asyncio',
62
+ 'trio',
63
+ 'trio_asyncio',
64
+ )
65
+
66
+ PARAM_NAME = '__async_backend'
67
+
68
+
69
+ def iscoroutinefunction(func: ta.Any) -> bool:
70
+ return inspect.iscoroutinefunction(func) or getattr(func, '_is_coroutine', False)
71
+
72
+
73
+ def is_async_function(func: ta.Any) -> bool:
74
+ return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
75
+
76
+
77
+ @register
78
+ class AsyncsPlugin:
79
+ ASYNC_BACKENDS: ta.ClassVar[ta.Sequence[str]] = [
80
+ *[s for s in KNOWN_BACKENDS if lang.can_import(s)],
81
+ ]
82
+
83
+ def pytest_configure(self, config):
84
+ config.addinivalue_line('markers', f'{ASYNCS_MARK}: marks for all async backends')
85
+ config.addinivalue_line('markers', 'trio_asyncio: marks for trio_asyncio backend')
86
+
87
+ def pytest_generate_tests(self, metafunc):
88
+ if (m := metafunc.definition.get_closest_marker(ASYNCS_MARK)) is not None:
89
+ if m.args:
90
+ bes = m.args
91
+ else:
92
+ bes = self.ASYNC_BACKENDS
93
+ elif metafunc.definition.get_closest_marker('trio_asyncio') is not None:
94
+ bes = ['trio_asyncio']
95
+ else:
96
+ return
97
+
98
+ if pdu.is_present():
99
+ pdu.patch_for_trio_asyncio()
100
+
101
+ metafunc.fixturenames.append(PARAM_NAME)
102
+ metafunc.parametrize(PARAM_NAME, bes)
103
+
104
+ for c in metafunc._calls: # noqa
105
+ be = c.params[PARAM_NAME]
106
+ if be == 'trio_asyncio':
107
+ c.marks.extend([
108
+ pytest.mark.trio.mark,
109
+ pytest.mark.trio_asyncio.mark,
110
+ ])
111
+
112
+ else:
113
+ c.marks.append(getattr(pytest.mark, be).mark)
114
+
115
+ if pdu.is_present():
116
+ c.marks.append(pytest.mark.drain_asyncio.mark)
117
+
118
+ @pytest.hookimpl(hookwrapper=True)
119
+ def pytest_runtest_call(self, item):
120
+ bes = [be for be in self.ASYNC_BACKENDS if item.get_closest_marker(be) is not None]
121
+ if len(bes) > 1 and set(bes) != {'trio', 'trio_asyncio'}:
122
+ raise Exception(f'{item.nodeid}: multiple async backends specified: {bes}')
123
+ elif is_async_function(item.obj) and not bes:
124
+ raise Exception(f'{item.nodeid}: async def function and no async plugin specified')
125
+
126
+ if 'trio_asyncio' in bes:
127
+ obj = item.obj
128
+
129
+ @functools.wraps(obj)
130
+ @trai.with_trio_asyncio_loop(wait=True)
131
+ async def run(*args, **kwargs):
132
+ await trio_asyncio.aio_as_trio(obj)(*args, **kwargs)
133
+
134
+ item.obj = run
135
+
136
+ yield
@@ -1,4 +1,4 @@
1
- from ... import pydevd as opd
1
+ from ....diag import pydevd as opd
2
2
  from ._registry import register
3
3
 
4
4