omlish 0.0.0.dev21__py3-none-any.whl → 0.0.0.dev23__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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev21'
2
- __revision__ = '4cb4af9e253f312d5f301449f08dfa7168234e07'
1
+ __version__ = '0.0.0.dev23'
2
+ __revision__ = '1d019b6879a9552ea31622e40f50d260d6b08824'
3
3
 
4
4
 
5
5
  #
@@ -33,10 +33,10 @@ class Project(ProjectBase):
33
33
  'anyio ~= 4.4',
34
34
  'sniffio ~= 1.3',
35
35
 
36
- 'greenlet ~= 3.0; python_version < "3.13"',
36
+ 'greenlet ~= 3.1',
37
37
 
38
38
  'trio ~= 0.26',
39
- 'trio-asyncio ~= 0.15; python_version < "3.13"',
39
+ 'trio-asyncio ~= 0.15',
40
40
  ],
41
41
 
42
42
  'compression': [
@@ -46,12 +46,15 @@ class Project(ProjectBase):
46
46
  ],
47
47
 
48
48
  'diag': [
49
+ 'asttokens ~= 2.4',
50
+ 'executing ~= 2.1',
51
+
49
52
  'psutil ~= 6.0',
50
53
  ],
51
54
 
52
55
  'formats': [
53
56
  'orjson ~= 3.10',
54
- # 'ujson ~= 5.10',
57
+ 'ujson ~= 5.10',
55
58
 
56
59
  'json5 ~= 0.9',
57
60
 
@@ -74,11 +77,14 @@ class Project(ProjectBase):
74
77
  ],
75
78
 
76
79
  'sql': [
77
- 'sqlalchemy ~= 2.0; python_version ~= "3.13"',
78
- 'sqlalchemy[asyncio] ~= 2.0; python_version < "3.13"',
80
+ 'sqlalchemy[asyncio] ~= 2.0',
79
81
 
80
82
  'pg8000 ~= 1.31',
83
+ # 'psycopg2 ~= 2.9',
84
+
81
85
  'pymysql ~= 1.1',
86
+ # 'mysql-connector-python ~= 9.0',
87
+ # 'mysqlclient ~= 2.2',
82
88
 
83
89
  'aiomysql ~= 0.2',
84
90
  'aiosqlite ~= 0.20',
@@ -108,7 +114,10 @@ class SetuptoolsBase:
108
114
  include_package_data = False
109
115
 
110
116
  find_packages = {
111
- 'exclude': ['*.tests', '*.tests.*'],
117
+ 'exclude': [
118
+ '*.tests',
119
+ '*.tests.*',
120
+ ],
112
121
  }
113
122
 
114
123
 
omlish/asyncs/bridge.py CHANGED
@@ -8,6 +8,9 @@ KEEP THE SPACE SHUTTLE FLYING.
8
8
 
9
9
  TODO:
10
10
  - reuse greenlet if nested somehow?
11
+
12
+ See:
13
+ - https://greenback.readthedocs.io/en/latest/
11
14
  """
12
15
  import itertools
13
16
  import sys
@@ -15,22 +18,21 @@ import types
15
18
  import typing as ta
16
19
  import weakref
17
20
 
21
+ from .. import check
18
22
  from .. import lang
23
+ from .. import sync
24
+ from ..concurrent import threadlets
19
25
  from .asyncs import sync_await
20
26
 
21
27
 
22
28
  if ta.TYPE_CHECKING:
23
29
  import asyncio
24
30
 
25
- import greenlet
26
-
27
31
  from . import anyio as aiu
28
32
 
29
33
  else:
30
34
  asyncio = lang.proxy_import('asyncio')
31
35
 
32
- greenlet = lang.proxy_import('greenlet')
33
-
34
36
  aiu = lang.proxy_import('.anyio', __package__)
35
37
 
36
38
 
@@ -56,6 +58,20 @@ def trivial_a_to_s(fn):
56
58
  # https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef
57
59
 
58
60
 
61
+ _THREADLETS_IMPL = threadlets.GreenletThreadlets
62
+ # from ..concurrent.tests.real import RealThreadlets
63
+ # _THREADLETS_IMPL = RealThreadlets
64
+
65
+ _THREADLETS = sync.LazyFn(lambda: _THREADLETS_IMPL())
66
+
67
+
68
+ def _threadlets() -> threadlets.Threadlets:
69
+ return _THREADLETS.get()
70
+
71
+
72
+ #
73
+
74
+
59
75
  class BridgeAwaitRequiredError(Exception):
60
76
  pass
61
77
 
@@ -101,7 +117,7 @@ def _make_transition(seq: int, a_to_s: bool, obj: ta.Any) -> _BridgeTransition:
101
117
 
102
118
  _BRIDGED_TASKS: ta.MutableMapping[ta.Any, list[_BridgeTransition]] = weakref.WeakKeyDictionary()
103
119
 
104
- _BRIDGE_GREENLET_ATTR = f'__{__package__.replace(".", "__")}__bridge_greenlet__'
120
+ _BRIDGE_THREADLET_ATTR = f'__{__package__.replace(".", "__")}__bridge_threadlet__'
105
121
 
106
122
 
107
123
  def _push_transition(a_to_s: bool, l: list[_BridgeTransition], t: _BridgeTransition) -> _BridgeTransition:
@@ -129,9 +145,9 @@ def _get_transitions() -> list[_BridgeTransition]:
129
145
  else:
130
146
  l.extend(tl)
131
147
 
132
- g = greenlet.getcurrent()
148
+ g = _threadlets().get_current()
133
149
  try:
134
- gl = getattr(g, _BRIDGE_GREENLET_ATTR)
150
+ gl = getattr(g.underlying, _BRIDGE_THREADLET_ATTR)
135
151
  except AttributeError:
136
152
  pass
137
153
  else:
@@ -158,9 +174,9 @@ def is_in_bridge() -> bool:
158
174
  else:
159
175
  last_t = None
160
176
 
161
- g = greenlet.getcurrent()
177
+ g = _threadlets().get_current()
162
178
  try:
163
- gl = getattr(g, _BRIDGE_GREENLET_ATTR)
179
+ gl = getattr(g.underlying, _BRIDGE_THREADLET_ATTR)
164
180
  except AttributeError:
165
181
  last_g = None
166
182
  else:
@@ -194,13 +210,13 @@ def _safe_cancel_awaitable(awaitable: ta.Awaitable[ta.Any]) -> None:
194
210
 
195
211
 
196
212
  def s_to_a_await(awaitable: ta.Awaitable[T]) -> T:
197
- g = greenlet.getcurrent()
213
+ g = _threadlets().get_current()
198
214
 
199
- if not getattr(g, _BRIDGE_GREENLET_ATTR, False):
215
+ if not getattr(g.underlying, _BRIDGE_THREADLET_ATTR, False):
200
216
  _safe_cancel_awaitable(awaitable)
201
217
  raise MissingBridgeGreenletError
202
218
 
203
- return g.parent.switch(awaitable)
219
+ return check.not_none(g.parent).switch(awaitable)
204
220
 
205
221
 
206
222
  def s_to_a(fn, *, require_await=False):
@@ -210,7 +226,7 @@ def s_to_a(fn, *, require_await=False):
210
226
  try:
211
227
  return fn(*args, **kwargs)
212
228
  finally:
213
- if (gl2 := getattr(g, _BRIDGE_GREENLET_ATTR)) is not gl: # noqa
229
+ if (gl2 := getattr(g.underlying, _BRIDGE_THREADLET_ATTR)) is not gl: # noqa
214
230
  raise UnexpectedBridgeNestingError
215
231
  if (cur_g := _pop_transition(False, gl)) is not added_g: # noqa
216
232
  raise UnexpectedBridgeNestingError
@@ -219,8 +235,8 @@ def s_to_a(fn, *, require_await=False):
219
235
 
220
236
  seq = next(_BRIDGE_TRANSITIONS_SEQ)
221
237
 
222
- g = greenlet.greenlet(inner)
223
- setattr(g, _BRIDGE_GREENLET_ATTR, gl := []) # type: ignore
238
+ g = _threadlets().spawn(inner)
239
+ setattr(g.underlying, _BRIDGE_THREADLET_ATTR, gl := []) # type: ignore
224
240
  added_g = _push_transition(False, gl, _make_transition(seq, False, g))
225
241
 
226
242
  if (t := aiu.get_current_backend_task()) is not None:
@@ -270,11 +286,11 @@ def a_to_s(fn):
270
286
  else:
271
287
  added_t = None
272
288
 
273
- g = greenlet.getcurrent()
289
+ g = _threadlets().get_current()
274
290
  try:
275
- gl = getattr(g, _BRIDGE_GREENLET_ATTR)
291
+ gl = getattr(g.underlying, _BRIDGE_THREADLET_ATTR)
276
292
  except AttributeError:
277
- setattr(g, _BRIDGE_GREENLET_ATTR, gl := [])
293
+ setattr(g.underlying, _BRIDGE_THREADLET_ATTR, gl := [])
278
294
  added_g = _push_transition(True, gl, _make_transition(seq, True, g))
279
295
 
280
296
  try:
@@ -306,7 +322,7 @@ def a_to_s(fn):
306
322
  if (cur_t := _pop_transition(True, tl)) is not added_t: # noqa
307
323
  raise UnexpectedBridgeNestingError
308
324
 
309
- if (gl2 := getattr(g, _BRIDGE_GREENLET_ATTR)) is not gl: # noqa
325
+ if (gl2 := getattr(g.underlying, _BRIDGE_THREADLET_ATTR)) is not gl: # noqa
310
326
  raise UnexpectedBridgeNestingError
311
327
  if (cur_g := _pop_transition(True, gl)) is not added_g: # noqa
312
328
  raise UnexpectedBridgeNestingError
@@ -1,3 +1,42 @@
1
+ from .base import ( # noqa
2
+ Bootstrap,
3
+ ContextBootstrap,
4
+ SimpleBootstrap,
5
+ )
6
+
7
+ from .diag import ( # noqa
8
+ CheckBootstrap,
9
+ CprofileBootstrap,
10
+ PycharmBootstrap,
11
+ ThreadDumpBootstrap,
12
+ TimebombBootstrap,
13
+ )
14
+
1
15
  from .harness import ( # noqa
2
16
  bootstrap,
3
17
  )
18
+
19
+ from .sys import ( # noqa
20
+ CwdBootstrap,
21
+ EnvBootstrap,
22
+ FaulthandlerBootstrap,
23
+ FdsBootstrap,
24
+ GcBootstrap,
25
+ GcDebugFlag,
26
+ ImportBootstrap,
27
+ LogBootstrap,
28
+ NiceBootstrap,
29
+ PidfileBootstrap,
30
+ PrctlBootstrap,
31
+ PrintPidBootstrap,
32
+ RlimitBootstrap,
33
+ SetuidBootstrap,
34
+ )
35
+
36
+
37
+ ##
38
+
39
+
40
+ from ..lang.imports import _register_conditional_import # noqa
41
+
42
+ _register_conditional_import('..marshal', '.marshal', __package__)
omlish/bootstrap/base.py CHANGED
@@ -2,6 +2,8 @@ import abc
2
2
  import dataclasses as dc
3
3
  import typing as ta
4
4
 
5
+ from omlish import lang
6
+
5
7
 
6
8
  BootstrapConfigT = ta.TypeVar('BootstrapConfigT', bound='Bootstrap.Config')
7
9
 
@@ -9,7 +11,7 @@ BootstrapConfigT = ta.TypeVar('BootstrapConfigT', bound='Bootstrap.Config')
9
11
  ##
10
12
 
11
13
 
12
- class Bootstrap(abc.ABC, ta.Generic[BootstrapConfigT]):
14
+ class Bootstrap(abc.ABC, lang.PackageSealed, ta.Generic[BootstrapConfigT]):
13
15
  @dc.dataclass(frozen=True)
14
16
  class Config(abc.ABC): # noqa
15
17
  pass
omlish/bootstrap/diag.py CHANGED
@@ -5,28 +5,56 @@ import signal
5
5
  import sys
6
6
  import typing as ta
7
7
 
8
+ from .. import check
8
9
  from .. import lang
9
10
  from .base import Bootstrap
10
11
  from .base import ContextBootstrap
12
+ from .base import SimpleBootstrap
11
13
 
12
14
 
13
15
  if ta.TYPE_CHECKING:
14
16
  import cProfile # noqa
15
17
  import pstats
16
18
 
19
+ from ..diag import pycharm as diagpc
17
20
  from ..diag import threads as diagt
18
21
 
19
22
  else:
20
23
  cProfile = lang.proxy_import('cProfile') # noqa
21
24
  pstats = lang.proxy_import('pstats')
22
25
 
26
+ diagpc = lang.proxy_import('..diag.pycharm', __package__)
23
27
  diagt = lang.proxy_import('..diag.threads', __package__)
24
28
 
25
29
 
26
30
  ##
27
31
 
28
32
 
29
- class ProfilingBootstrap(ContextBootstrap['ProfilingBootstrap.Config']):
33
+ class CheckBootstrap(ContextBootstrap['CheckBootstrap.Config']):
34
+ @dc.dataclass(frozen=True)
35
+ class Config(Bootstrap.Config):
36
+ breakpoint: bool = False
37
+
38
+ @staticmethod
39
+ def _breakpoint(exc: Exception) -> None: # noqa
40
+ breakpoint() # noqa
41
+
42
+ @contextlib.contextmanager
43
+ def enter(self) -> ta.Iterator[None]:
44
+ if not self._config.breakpoint:
45
+ return
46
+
47
+ check.register_on_raise(CheckBootstrap._breakpoint)
48
+ try:
49
+ yield
50
+ finally:
51
+ check.unregister_on_raise(CheckBootstrap._breakpoint)
52
+
53
+
54
+ ##
55
+
56
+
57
+ class CprofileBootstrap(ContextBootstrap['CprofileBootstrap.Config']):
30
58
  @dc.dataclass(frozen=True)
31
59
  class Config(Bootstrap.Config):
32
60
  enable: bool = False
@@ -70,6 +98,7 @@ class ThreadDumpBootstrap(ContextBootstrap['ThreadDumpBootstrap.Config']):
70
98
  @dc.dataclass(frozen=True)
71
99
  class Config(Bootstrap.Config):
72
100
  interval_s: ta.Optional[float] = None
101
+ nodaemon: bool = False
73
102
 
74
103
  on_sigquit: bool = False
75
104
 
@@ -79,6 +108,7 @@ class ThreadDumpBootstrap(ContextBootstrap['ThreadDumpBootstrap.Config']):
79
108
  tdt = diagt.create_thread_dump_thread(
80
109
  interval_s=self._config.interval_s,
81
110
  start=True,
111
+ nodaemon=self._config.nodaemon,
82
112
  )
83
113
  else:
84
114
  tdt = None
@@ -97,7 +127,7 @@ class ThreadDumpBootstrap(ContextBootstrap['ThreadDumpBootstrap.Config']):
97
127
  yield
98
128
 
99
129
  finally:
100
- if tdt is not None:
130
+ if tdt is not None and not self._config.nodaemon:
101
131
  tdt.stop_nowait()
102
132
 
103
133
  if prev_sq.present:
@@ -126,3 +156,22 @@ class TimebombBootstrap(ContextBootstrap['TimebombBootstrap.Config']):
126
156
  yield
127
157
  finally:
128
158
  tbt.stop_nowait()
159
+
160
+
161
+ ##
162
+
163
+
164
+ class PycharmBootstrap(SimpleBootstrap['PycharmBootstrap.Config']):
165
+ @dc.dataclass(frozen=True)
166
+ class Config(Bootstrap.Config):
167
+ debug_host: ta.Optional[str] = None
168
+ debug_port: ta.Optional[int] = None
169
+ debug_version: ta.Optional[str] = None
170
+
171
+ def run(self) -> None:
172
+ if self._config.debug_port is not None:
173
+ diagpc.pycharm_remote_debugger_attach(
174
+ self._config.debug_host,
175
+ self._config.debug_port,
176
+ version=self._config.debug_version,
177
+ )
omlish/bootstrap/main.py CHANGED
@@ -156,8 +156,9 @@ def _main() -> int:
156
156
  with bootstrap(*cfgs):
157
157
  tgt = args.target
158
158
 
159
+ sys.argv = [tgt, *(args.args or ())]
160
+
159
161
  if args.module:
160
- sys.argv = [tgt, *(args.args or ())]
161
162
  runpy._run_module_as_main(tgt) # type: ignore # noqa
162
163
 
163
164
  else:
@@ -0,0 +1,18 @@
1
+ from .. import lang
2
+ from .. import marshal as msh
3
+ from .base import Bootstrap
4
+ from .harness import BOOTSTRAP_TYPES_BY_NAME
5
+
6
+
7
+ @lang.cached_function
8
+ def _install_standard_marshalling() -> None:
9
+ cfgs_poly = msh.Polymorphism(
10
+ Bootstrap.Config,
11
+ [msh.Impl(b.Config, n) for n, b in BOOTSTRAP_TYPES_BY_NAME.items()],
12
+ )
13
+
14
+ msh.STANDARD_MARSHALER_FACTORIES[0:0] = [msh.PolymorphismMarshalerFactory(cfgs_poly)]
15
+ msh.STANDARD_UNMARSHALER_FACTORIES[0:0] = [msh.PolymorphismUnmarshalerFactory(cfgs_poly)]
16
+
17
+
18
+ _install_standard_marshalling()
omlish/check.py CHANGED
@@ -3,6 +3,7 @@ TODO:
3
3
  - def maybe(v: lang.Maybe[T])
4
4
  """
5
5
  import collections
6
+ import threading
6
7
  import typing as ta
7
8
 
8
9
 
@@ -21,6 +22,62 @@ _callable = callable
21
22
  ##
22
23
 
23
24
 
25
+ _CONFIG_LOCK = threading.RLock()
26
+
27
+
28
+ OnRaiseFn: ta.TypeAlias = ta.Callable[[Exception], None]
29
+ _ON_RAISE: ta.Sequence[OnRaiseFn] = []
30
+
31
+
32
+ def register_on_raise(fn: OnRaiseFn) -> None:
33
+ global _ON_RAISE
34
+ with _CONFIG_LOCK:
35
+ _ON_RAISE = [*_ON_RAISE, fn]
36
+
37
+
38
+ def unregister_on_raise(fn: OnRaiseFn) -> None:
39
+ global _ON_RAISE
40
+ with _CONFIG_LOCK:
41
+ _ON_RAISE = [e for e in _ON_RAISE if e != fn]
42
+
43
+
44
+ #
45
+
46
+
47
+ _render_args: ta.Callable[..., str | None] | None = None
48
+
49
+
50
+ def enable_args_rendering() -> bool:
51
+ global _render_args
52
+ if _render_args is not None:
53
+ return True
54
+
55
+ with _CONFIG_LOCK:
56
+ if _render_args is not None:
57
+ return True # type: ignore
58
+
59
+ try:
60
+ from .diag.asts import ArgsRenderer
61
+
62
+ ArgsRenderer.smoketest()
63
+
64
+ except Exception: # noqa
65
+ return False
66
+
67
+ def _real_render_args(fmt: str, *args: ta.Any) -> str | None:
68
+ ra = ArgsRenderer(back=3).render_args(*args)
69
+ if ra is None:
70
+ return None
71
+
72
+ return fmt % tuple(str(a) for a in ra)
73
+
74
+ _render_args = _real_render_args
75
+ return True
76
+
77
+
78
+ #
79
+
80
+
24
81
  def _default_exception_factory(exc_cls: type[Exception], *args, **kwargs) -> Exception:
25
82
  return exc_cls(*args, **kwargs) # noqa
26
83
 
@@ -28,20 +85,45 @@ def _default_exception_factory(exc_cls: type[Exception], *args, **kwargs) -> Exc
28
85
  _EXCEPTION_FACTORY = _default_exception_factory
29
86
 
30
87
 
88
+ class _Args:
89
+ def __init__(self, *args, **kwargs):
90
+ self.args = args
91
+ self.kwargs = kwargs
92
+
93
+
31
94
  def _raise(
32
95
  exception_type: type[Exception],
33
96
  default_message: str,
34
97
  message: Message,
35
- *args: ta.Any,
36
- **kwargs: ta.Any,
98
+ ak: _Args = _Args(),
99
+ *,
100
+ render_fmt: str | None = None,
37
101
  ) -> ta.NoReturn:
102
+ exc_args = ()
38
103
  if _callable(message):
39
- message = ta.cast(ta.Callable, message)(*args, **kwargs)
104
+ message = ta.cast(ta.Callable, message)(*ak.args, **ak.kwargs)
40
105
  if _isinstance(message, tuple):
41
- message, *args = message # type: ignore
106
+ message, *exc_args = message # type: ignore
107
+
42
108
  if message is None:
43
109
  message = default_message
44
- exc = _EXCEPTION_FACTORY(exception_type, message, *args, **kwargs)
110
+
111
+ if render_fmt is not None and _render_args is not None:
112
+ rendered_args = _render_args(render_fmt, *ak.args)
113
+ if rendered_args is not None:
114
+ message = f'{message} : {rendered_args}'
115
+
116
+ exc = _EXCEPTION_FACTORY(
117
+ exception_type,
118
+ message,
119
+ *exc_args,
120
+ *ak.args,
121
+ **ak.kwargs,
122
+ )
123
+
124
+ for fn in _ON_RAISE:
125
+ fn(exc)
126
+
45
127
  raise exc
46
128
 
47
129
 
@@ -60,7 +142,7 @@ def _unpack_isinstance_spec(spec: ta.Any) -> tuple:
60
142
 
61
143
  def isinstance(v: ta.Any, spec: type[T] | tuple, msg: Message = None) -> T: # noqa
62
144
  if not _isinstance(v, _unpack_isinstance_spec(spec)):
63
- _raise(TypeError, 'Must be instance', msg, v, spec)
145
+ _raise(TypeError, 'Must be instance', msg, _Args(v, spec))
64
146
  return v
65
147
 
66
148
 
@@ -73,7 +155,7 @@ def of_isinstance(spec: type[T] | tuple, msg: Message = None) -> ta.Callable[[ta
73
155
 
74
156
  def cast(v: ta.Any, cls: type[T], msg: Message = None) -> T: # noqa
75
157
  if not _isinstance(v, cls):
76
- _raise(TypeError, 'Must be instance', msg, v, cls)
158
+ _raise(TypeError, 'Must be instance', msg, _Args(v, cls))
77
159
  return v
78
160
 
79
161
 
@@ -86,7 +168,7 @@ def of_cast(cls: type[T], msg: Message = None) -> ta.Callable[[T], T]:
86
168
 
87
169
  def not_isinstance(v: T, spec: ta.Any, msg: Message = None) -> T: # noqa
88
170
  if _isinstance(v, _unpack_isinstance_spec(spec)):
89
- _raise(TypeError, 'Must not be instance', msg, v, spec)
171
+ _raise(TypeError, 'Must not be instance', msg, _Args(v, spec))
90
172
  return v
91
173
 
92
174
 
@@ -102,13 +184,13 @@ def of_not_isinstance(spec: ta.Any, msg: Message = None) -> ta.Callable[[T], T]:
102
184
 
103
185
  def issubclass(v: type[T], spec: ta.Any, msg: Message = None) -> type[T]: # noqa
104
186
  if not _issubclass(v, spec):
105
- _raise(TypeError, 'Must be subclass', msg, v, spec)
187
+ _raise(TypeError, 'Must be subclass', msg, _Args(v, spec))
106
188
  return v
107
189
 
108
190
 
109
191
  def not_issubclass(v: type[T], spec: ta.Any, msg: Message = None) -> type[T]: # noqa
110
192
  if _issubclass(v, spec):
111
- _raise(TypeError, 'Must not be subclass', msg, v, spec)
193
+ _raise(TypeError, 'Must not be subclass', msg, _Args(v, spec))
112
194
  return v
113
195
 
114
196
 
@@ -117,32 +199,43 @@ def not_issubclass(v: type[T], spec: ta.Any, msg: Message = None) -> type[T]: #
117
199
 
118
200
  def in_(v: T, c: ta.Container[T], msg: Message = None) -> T:
119
201
  if v not in c:
120
- _raise(ValueError, 'Must be in', msg, v, c)
202
+ _raise(ValueError, 'Must be in', msg, _Args(v, c))
121
203
  return v
122
204
 
123
205
 
124
206
  def not_in(v: T, c: ta.Container[T], msg: Message = None) -> T:
125
207
  if v in c:
126
- _raise(ValueError, 'Must not be in', msg, v, c)
208
+ _raise(ValueError, 'Must not be in', msg, _Args(v, c))
127
209
  return v
128
210
 
129
211
 
130
212
  def empty(v: SizedT, msg: Message = None) -> SizedT:
131
213
  if len(v) != 0:
132
- _raise(ValueError, 'Must be empty', msg, v)
214
+ _raise(ValueError, 'Must be empty', msg, _Args(v))
215
+ return v
216
+
217
+
218
+ def iterempty(v: ta.Iterable[T], msg: Message = None) -> ta.Iterable[T]:
219
+ it = iter(v)
220
+ try:
221
+ next(it)
222
+ except StopIteration:
223
+ pass
224
+ else:
225
+ _raise(ValueError, 'Must be empty', msg, _Args(v))
133
226
  return v
134
227
 
135
228
 
136
229
  def not_empty(v: SizedT, msg: Message = None) -> SizedT:
137
230
  if len(v) == 0:
138
- _raise(ValueError, 'Must not be empty', msg, v)
231
+ _raise(ValueError, 'Must not be empty', msg, _Args(v))
139
232
  return v
140
233
 
141
234
 
142
235
  def unique(it: ta.Iterable[T], msg: Message = None) -> ta.Iterable[T]:
143
236
  dupes = [e for e, c in collections.Counter(it).items() if c > 1]
144
237
  if dupes:
145
- _raise(ValueError, 'Must be unique', msg, it, dupes)
238
+ _raise(ValueError, 'Must be unique', msg, _Args(it, dupes))
146
239
  return it
147
240
 
148
241
 
@@ -150,7 +243,7 @@ def single(obj: ta.Iterable[T], message: Message = None) -> T:
150
243
  try:
151
244
  [value] = obj
152
245
  except ValueError:
153
- _raise(ValueError, 'Must be single', message, obj)
246
+ _raise(ValueError, 'Must be single', message, _Args(obj))
154
247
  else:
155
248
  return value
156
249
 
@@ -165,7 +258,7 @@ def optional_single(obj: ta.Iterable[T], message: Message = None) -> T | None:
165
258
  next(it)
166
259
  except StopIteration:
167
260
  return value # noqa
168
- _raise(ValueError, 'Must be empty or single', message, obj)
261
+ _raise(ValueError, 'Must be empty or single', message, _Args(obj))
169
262
 
170
263
 
171
264
  ##
@@ -173,48 +266,45 @@ def optional_single(obj: ta.Iterable[T], message: Message = None) -> T | None:
173
266
 
174
267
  def none(v: ta.Any, msg: Message = None) -> None:
175
268
  if v is not None:
176
- _raise(ValueError, 'Must be None', msg, v)
269
+ _raise(ValueError, 'Must be None', msg, _Args(v))
177
270
 
178
271
 
179
272
  def not_none(v: T | None, msg: Message = None) -> T:
180
273
  if v is None:
181
- _raise(ValueError, 'Must not be None', msg, v)
274
+ _raise(ValueError, 'Must not be None', msg, _Args(v))
182
275
  return v
183
276
 
184
277
 
185
278
  ##
186
279
 
187
280
 
188
- def equal(v: T, *os: ta.Any, msg: Message = None) -> T:
189
- for o in os:
190
- if o != v:
191
- _raise(ValueError, 'Must be equal', msg, v, os)
281
+ def equal(v: T, o: ta.Any, msg: Message = None) -> T:
282
+ if o != v:
283
+ _raise(ValueError, 'Must be equal', msg, _Args(v, o), render_fmt='%s != %s')
192
284
  return v
193
285
 
194
286
 
195
- def is_(v: T, *os: ta.Any, msg: Message = None) -> T:
196
- for o in os:
197
- if o is not v:
198
- _raise(ValueError, 'Must be the same', msg, v, os)
287
+ def is_(v: T, o: ta.Any, msg: Message = None) -> T:
288
+ if o is not v:
289
+ _raise(ValueError, 'Must be the same', msg, _Args(v, o), render_fmt='%s is not %s')
199
290
  return v
200
291
 
201
292
 
202
- def is_not(v: T, *os: ta.Any, msg: Message = None) -> T:
203
- for o in os:
204
- if o is v:
205
- _raise(ValueError, 'Must not be the same', msg, v, os)
293
+ def is_not(v: T, o: ta.Any, msg: Message = None) -> T:
294
+ if o is v:
295
+ _raise(ValueError, 'Must not be the same', msg, _Args(v, o), render_fmt='%s is %s')
206
296
  return v
207
297
 
208
298
 
209
299
  def callable(v: T, msg: Message = None) -> T: # noqa
210
300
  if not _callable(v):
211
- _raise(TypeError, 'Must be callable', msg, v)
301
+ _raise(TypeError, 'Must be callable', msg, _Args(v))
212
302
  return v # type: ignore
213
303
 
214
304
 
215
305
  def non_empty_str(v: str | None, msg: Message = None) -> str:
216
306
  if not _isinstance(v, str) or not v:
217
- _raise(ValueError, 'Must be non-empty str', msg, v)
307
+ _raise(ValueError, 'Must be non-empty str', msg, _Args(v))
218
308
  return v
219
309
 
220
310