omlish 0.0.0.dev212__py3-none-any.whl → 0.0.0.dev214__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. omlish/.manifests.json +4 -4
  2. omlish/__about__.py +3 -5
  3. omlish/antlr/__init__.py +3 -0
  4. omlish/antlr/parsing.py +34 -2
  5. omlish/asyncs/asyncio/asyncio.py +34 -0
  6. omlish/asyncs/ioproxy/__init__.py +1 -0
  7. omlish/asyncs/ioproxy/all.py +32 -0
  8. omlish/asyncs/ioproxy/io.py +242 -0
  9. omlish/asyncs/ioproxy/proxier.py +154 -0
  10. omlish/asyncs/ioproxy/proxy.py +141 -0
  11. omlish/asyncs/ioproxy/typing.py +108 -0
  12. omlish/check.py +1 -0
  13. omlish/configs/processing/matching.py +9 -1
  14. omlish/formats/json/stream/lex.py +1 -0
  15. omlish/formats/json5/Json5.g4 +172 -0
  16. omlish/formats/json5/__init__.py +8 -0
  17. omlish/formats/json5/_antlr/Json5Lexer.py +353 -0
  18. omlish/formats/json5/_antlr/Json5Listener.py +78 -0
  19. omlish/formats/json5/_antlr/Json5Parser.py +616 -0
  20. omlish/formats/json5/_antlr/Json5Visitor.py +51 -0
  21. omlish/formats/json5/_antlr/__init__.py +0 -0
  22. omlish/formats/{json5.py → json5/codec.py} +6 -11
  23. omlish/formats/json5/errors.py +2 -0
  24. omlish/formats/json5/literals.py +130 -0
  25. omlish/formats/json5/parsing.py +79 -0
  26. omlish/io/abc.py +1 -0
  27. omlish/lang/__init__.py +2 -0
  28. omlish/lang/imports.py +4 -0
  29. omlish/lang/strings.py +33 -1
  30. omlish/lite/check.py +23 -0
  31. omlish/lite/contextmanagers.py +39 -0
  32. omlish/os/files.py +17 -30
  33. omlish/os/temp.py +50 -0
  34. {omlish-0.0.0.dev212.dist-info → omlish-0.0.0.dev214.dist-info}/METADATA +5 -7
  35. {omlish-0.0.0.dev212.dist-info → omlish-0.0.0.dev214.dist-info}/RECORD +39 -22
  36. {omlish-0.0.0.dev212.dist-info → omlish-0.0.0.dev214.dist-info}/LICENSE +0 -0
  37. {omlish-0.0.0.dev212.dist-info → omlish-0.0.0.dev214.dist-info}/WHEEL +0 -0
  38. {omlish-0.0.0.dev212.dist-info → omlish-0.0.0.dev214.dist-info}/entry_points.txt +0 -0
  39. {omlish-0.0.0.dev212.dist-info → omlish-0.0.0.dev214.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,130 @@
1
+ """
2
+ https://spec.json5.org/
3
+ """
4
+ import io
5
+ import json
6
+ import typing as ta
7
+
8
+ from ... import lang
9
+ from .errors import Json5Error
10
+
11
+
12
+ ##
13
+
14
+
15
+ LITERAL_VALUES: ta.Mapping[str, ta.Any] = {
16
+ 'true': True,
17
+ 'false': False,
18
+ 'null': None,
19
+ }
20
+
21
+
22
+ ##
23
+
24
+
25
+ STRING_LITERAL_ESCAPES: ta.Mapping[str, str] = {
26
+ 'b': '\\u0008',
27
+ 'f': '\\u000C',
28
+ 'n': '\\u000A',
29
+ 'r': '\\u000D',
30
+ 't': '\\u0009',
31
+ 'v': '\\u000B',
32
+ '0': '\\u0000',
33
+ 'u': '\\u',
34
+ '"': '\\"',
35
+ "'": "'",
36
+ '\\': '\\\\',
37
+ }
38
+
39
+
40
+ def _check_state(b: bool, fmt: str = 'Json5 error', *args: ta.Any) -> None:
41
+ if not b:
42
+ raise Json5Error(fmt % args)
43
+
44
+
45
+ def translate_string_literal(s: str) -> str:
46
+ _check_state(len(s) > 1)
47
+ q = s[0]
48
+ _check_state(q in '\'"')
49
+ _check_state(s[-1] == q)
50
+
51
+ c = 1
52
+ e = len(s) - 1
53
+
54
+ b = io.StringIO()
55
+ b.write('"')
56
+
57
+ ds = '\\\'"'
58
+ while True:
59
+ n = lang.find_any(s, ds, c, e)
60
+ if n < 0:
61
+ b.write(s[c:e])
62
+ break
63
+
64
+ _check_state(n < e)
65
+ b.write(s[c:n])
66
+
67
+ x = s[n]
68
+ if x == '\\':
69
+ _check_state(n < (e - 1))
70
+
71
+ y = s[n + 1]
72
+ if y in '\n\u2028\u2029':
73
+ c = n + 2
74
+
75
+ elif y == '\r':
76
+ c = n + 2
77
+ if c < e and s[c] == '\n':
78
+ c += 1
79
+
80
+ elif y in 'x':
81
+ _check_state(n < (e - 3))
82
+ u = int(s[n + 2:n + 4], 16)
83
+ b.write(f'\\u00{u:02x}')
84
+ c = n + 4
85
+
86
+ elif (g := STRING_LITERAL_ESCAPES.get(y)) is not None:
87
+ b.write(g)
88
+ c = n + 2
89
+
90
+ elif not ('0' <= y <= '9'):
91
+ b.write(y)
92
+ c = n + 2
93
+
94
+ else:
95
+ raise Json5Error(f'Invalid string literal escape: {x}{y}')
96
+
97
+ elif x in '\\\'"':
98
+ _check_state(x != q)
99
+ if x == '"':
100
+ b.write('\\"')
101
+ else:
102
+ b.write(x)
103
+ c = n + 1
104
+
105
+ else:
106
+ raise RuntimeError
107
+
108
+ b.write('"')
109
+ return b.getvalue()
110
+
111
+
112
+ def parse_string_literal(s: str) -> str:
113
+ j = translate_string_literal(s)
114
+
115
+ try:
116
+ return json.loads(j)
117
+ except json.JSONDecodeError as e:
118
+ raise Json5Error from e
119
+
120
+
121
+ ##
122
+
123
+
124
+ def parse_number_literal(s: str) -> int | float:
125
+ s = s.lower()
126
+
127
+ if 'x' in s:
128
+ return int(s, 16)
129
+ else:
130
+ return float(s)
@@ -0,0 +1,79 @@
1
+ # ruff: noqa: N802
2
+ import typing as ta
3
+
4
+ from omlish import antlr
5
+
6
+ from ._antlr.Json5Lexer import Json5Lexer # type: ignore
7
+ from ._antlr.Json5Parser import Json5Parser # type: ignore
8
+ from ._antlr.Json5Visitor import Json5Visitor # type: ignore
9
+ from .errors import Json5Error
10
+ from .literals import LITERAL_VALUES
11
+ from .literals import parse_number_literal
12
+ from .literals import parse_string_literal
13
+
14
+
15
+ class Json5ParseVisitor(antlr.parsing.StandardParseTreeVisitor, Json5Visitor):
16
+ def visitArr(self, ctx: Json5Parser.ArrContext):
17
+ return [self.visit(e) for e in ctx.value()]
18
+
19
+ def visitKey(self, ctx: Json5Parser.KeyContext):
20
+ if (s := ctx.STRING()) is not None:
21
+ return parse_string_literal(s.getText())
22
+
23
+ elif (i := ctx.IDENTIFIER()) is not None:
24
+ return parse_string_literal(''.join(['"', i.getText(), '"']))
25
+
26
+ elif (l := ctx.LITERAL()) is not None:
27
+ return LITERAL_VALUES[l.getText()]
28
+
29
+ elif (n := ctx.NUMERIC_LITERAL()) is not None:
30
+ return n.getText()
31
+
32
+ else:
33
+ raise RuntimeError(ctx)
34
+
35
+ def visitNumber(self, ctx: Json5Parser.NumberContext):
36
+ return parse_number_literal(ctx.getText())
37
+
38
+ def visitObj(self, ctx: Json5Parser.ObjContext):
39
+ dct: dict[ta.Any, ta.Any] = {}
40
+ for pair in ctx.pair():
41
+ key, value = self.visit(pair)
42
+ dct[key] = value
43
+ return dct
44
+
45
+ def visitPair(self, ctx: Json5Parser.PairContext):
46
+ key = self.visit(ctx.key())
47
+ value = self.visit(ctx.value())
48
+ return (key, value)
49
+
50
+ def visitValue(self, ctx: Json5Parser.ValueContext):
51
+ if (s := ctx.STRING()) is not None:
52
+ return parse_string_literal(s.getText())
53
+
54
+ elif (n := ctx.LITERAL()) is not None:
55
+ return LITERAL_VALUES[n.getText()]
56
+
57
+ else:
58
+ return super().visitChildren(ctx)
59
+
60
+
61
+ def parse(buf: str) -> ta.Any:
62
+ try:
63
+ parser = antlr.parsing.make_parser(
64
+ buf,
65
+ Json5Lexer,
66
+ Json5Parser,
67
+ silent_errors=True,
68
+ )
69
+
70
+ root = parser.json5()
71
+
72
+ except antlr.errors.ParseError as e:
73
+ raise Json5Error from e
74
+
75
+ if antlr.parsing.is_eof_context(root):
76
+ raise Json5Error('Empty input')
77
+
78
+ visitor = Json5ParseVisitor()
79
+ return visitor.visit(root)
omlish/io/abc.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # ruff: noqa: ANN204
2
2
 
3
+
3
4
  class IOBase:
4
5
  def seek(self, pos, whence=0): ...
5
6
 
omlish/lang/__init__.py CHANGED
@@ -200,6 +200,7 @@ from .strings import ( # noqa
200
200
  BOOL_TRUE_STRINGS,
201
201
  STRING_BOOL_VALUES,
202
202
  camel_case,
203
+ find_any,
203
204
  indent_lines,
204
205
  is_dunder,
205
206
  is_ident,
@@ -209,6 +210,7 @@ from .strings import ( # noqa
209
210
  prefix_delimited,
210
211
  prefix_lines,
211
212
  replace_many,
213
+ rfind_any,
212
214
  snake_case,
213
215
  strip_prefix,
214
216
  strip_suffix,
omlish/lang/imports.py CHANGED
@@ -1,3 +1,7 @@
1
+ """
2
+ TODO:
3
+ - proxy_init 'as' alias support - attrs of (src, dst)
4
+ """
1
5
  import contextlib
2
6
  import importlib.util
3
7
  import sys
omlish/lang/strings.py CHANGED
@@ -42,7 +42,8 @@ def strip_suffix(s: StrOrBytesT, sfx: StrOrBytesT) -> StrOrBytesT:
42
42
  def replace_many(
43
43
  s: StrOrBytesT,
44
44
  old: ta.Iterable[StrOrBytesT],
45
- new: StrOrBytesT, count_each: int = -1,
45
+ new: StrOrBytesT,
46
+ count_each: int = -1,
46
47
  ) -> StrOrBytesT:
47
48
  for o in old:
48
49
  s = s.replace(o, new, count_each) # type: ignore
@@ -52,6 +53,37 @@ def replace_many(
52
53
  ##
53
54
 
54
55
 
56
+ def find_any(
57
+ string: StrOrBytesT,
58
+ subs: ta.Iterable[StrOrBytesT],
59
+ start: int | None = None,
60
+ end: int | None = None,
61
+ ) -> int:
62
+ r = -1
63
+ for sub in subs:
64
+ if (p := string.find(sub, start, end)) >= 0: # type: ignore
65
+ if r < 0 or p < r:
66
+ r = p
67
+ return r
68
+
69
+
70
+ def rfind_any(
71
+ string: StrOrBytesT,
72
+ subs: ta.Iterable[StrOrBytesT],
73
+ start: int | None = None,
74
+ end: int | None = None,
75
+ ) -> int:
76
+ r = -1
77
+ for sub in subs:
78
+ if (p := string.rfind(sub, start, end)) >= 0: # type: ignore
79
+ if r < 0 or p > r:
80
+ r = p
81
+ return r
82
+
83
+
84
+ ##
85
+
86
+
55
87
  def camel_case(name: str, *, lower: bool = False) -> str:
56
88
  if not name:
57
89
  return ''
omlish/lite/check.py CHANGED
@@ -49,6 +49,17 @@ class Checks:
49
49
 
50
50
  #
51
51
 
52
+ def register_on_raise_breakpoint_if_env_var_set(self, key: str) -> None:
53
+ import os
54
+
55
+ def on_raise(exc: Exception) -> None: # noqa
56
+ if key in os.environ:
57
+ breakpoint() # noqa
58
+
59
+ self.register_on_raise(on_raise)
60
+
61
+ #
62
+
52
63
  def set_exception_factory(self, factory: CheckExceptionFactory) -> None:
53
64
  self._exception_factory = factory
54
65
 
@@ -364,6 +375,18 @@ class Checks:
364
375
 
365
376
  return v
366
377
 
378
+ def not_equal(self, v: T, o: ta.Any, msg: CheckMessage = None) -> T:
379
+ if o == v:
380
+ self._raise(
381
+ ValueError,
382
+ 'Must not be equal',
383
+ msg,
384
+ Checks._ArgsKwargs(v, o),
385
+ render_fmt='%s == %s',
386
+ )
387
+
388
+ return v
389
+
367
390
  def is_(self, v: T, o: ta.Any, msg: CheckMessage = None) -> T:
368
391
  if o is not v:
369
392
  self._raise(
@@ -7,6 +7,7 @@ from .check import check
7
7
 
8
8
  T = ta.TypeVar('T')
9
9
  ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
10
+ AsyncExitStackedT = ta.TypeVar('AsyncExitStackedT', bound='AsyncExitStacked')
10
11
 
11
12
 
12
13
  ##
@@ -35,6 +36,33 @@ class ExitStacked:
35
36
  return es.enter_context(cm)
36
37
 
37
38
 
39
+ class AsyncExitStacked:
40
+ _exit_stack: ta.Optional[contextlib.AsyncExitStack] = None
41
+
42
+ async def __aenter__(self: AsyncExitStackedT) -> AsyncExitStackedT:
43
+ check.state(self._exit_stack is None)
44
+ es = self._exit_stack = contextlib.AsyncExitStack()
45
+ await es.__aenter__()
46
+ return self
47
+
48
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
49
+ if (es := self._exit_stack) is None:
50
+ return None
51
+ await self._async_exit_contexts()
52
+ return await es.__aexit__(exc_type, exc_val, exc_tb)
53
+
54
+ async def _async_exit_contexts(self) -> None:
55
+ pass
56
+
57
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
58
+ es = check.not_none(self._exit_stack)
59
+ return es.enter_context(cm)
60
+
61
+ async def _enter_async_context(self, cm: ta.AsyncContextManager[T]) -> T:
62
+ es = check.not_none(self._exit_stack)
63
+ return await es.enter_async_context(cm)
64
+
65
+
38
66
  ##
39
67
 
40
68
 
@@ -46,6 +74,17 @@ def defer(fn: ta.Callable) -> ta.Generator[ta.Callable, None, None]:
46
74
  fn()
47
75
 
48
76
 
77
+ @contextlib.asynccontextmanager
78
+ async def adefer(fn: ta.Callable) -> ta.AsyncGenerator[ta.Callable, None]:
79
+ try:
80
+ yield fn
81
+ finally:
82
+ await fn()
83
+
84
+
85
+ ##
86
+
87
+
49
88
  @contextlib.contextmanager
50
89
  def attr_setting(obj, attr, val, *, default=None): # noqa
51
90
  not_set = object()
omlish/os/files.py CHANGED
@@ -1,38 +1,10 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
1
3
  import contextlib
2
4
  import os
3
- import shutil
4
- import tempfile
5
5
  import typing as ta
6
6
 
7
7
 
8
- @contextlib.contextmanager
9
- def tmp_dir(
10
- root_dir: str | None = None,
11
- cleanup: bool = True,
12
- **kwargs: ta.Any,
13
- ) -> ta.Iterator[str]:
14
- path = tempfile.mkdtemp(dir=root_dir, **kwargs)
15
- try:
16
- yield path
17
- finally:
18
- if cleanup:
19
- shutil.rmtree(path, ignore_errors=True)
20
-
21
-
22
- @contextlib.contextmanager
23
- def tmp_file(
24
- root_dir: str | None = None,
25
- cleanup: bool = True,
26
- **kwargs: ta.Any,
27
- ) -> ta.Iterator[tempfile._TemporaryFileWrapper]: # noqa
28
- with tempfile.NamedTemporaryFile(dir=root_dir, delete=False, **kwargs) as f:
29
- try:
30
- yield f
31
- finally:
32
- if cleanup:
33
- shutil.rmtree(f.name, ignore_errors=True)
34
-
35
-
36
8
  def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
37
9
  if exist_ok:
38
10
  # First try to bump modification time
@@ -48,3 +20,18 @@ def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
48
20
  flags |= os.O_EXCL
49
21
  fd = os.open(self, flags, mode)
50
22
  os.close(fd)
23
+
24
+
25
+ def unlink_if_exists(path: str) -> None:
26
+ try:
27
+ os.unlink(path)
28
+ except FileNotFoundError:
29
+ pass
30
+
31
+
32
+ @contextlib.contextmanager
33
+ def unlinking_if_exists(path: str) -> ta.Iterator[None]:
34
+ try:
35
+ yield
36
+ finally:
37
+ unlink_if_exists(path)
omlish/os/temp.py ADDED
@@ -0,0 +1,50 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import contextlib
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ import typing as ta
8
+
9
+ from .files import unlink_if_exists
10
+
11
+
12
+ def make_temp_file(**kwargs: ta.Any) -> str:
13
+ file_fd, file = tempfile.mkstemp(**kwargs)
14
+ os.close(file_fd)
15
+ return file
16
+
17
+
18
+ @contextlib.contextmanager
19
+ def temp_file_context(**kwargs: ta.Any) -> ta.Iterator[str]:
20
+ path = make_temp_file(**kwargs)
21
+ try:
22
+ yield path
23
+ finally:
24
+ unlink_if_exists(path)
25
+
26
+
27
+ @contextlib.contextmanager
28
+ def temp_dir_context(
29
+ root_dir: ta.Optional[str] = None,
30
+ **kwargs: ta.Any,
31
+ ) -> ta.Iterator[str]:
32
+ path = tempfile.mkdtemp(dir=root_dir, **kwargs)
33
+ try:
34
+ yield path
35
+ finally:
36
+ shutil.rmtree(path, ignore_errors=True)
37
+
38
+
39
+ @contextlib.contextmanager
40
+ def temp_named_file_context(
41
+ root_dir: ta.Optional[str] = None,
42
+ cleanup: bool = True,
43
+ **kwargs: ta.Any,
44
+ ) -> ta.Iterator[tempfile._TemporaryFileWrapper]: # noqa
45
+ with tempfile.NamedTemporaryFile(dir=root_dir, delete=False, **kwargs) as f:
46
+ try:
47
+ yield f
48
+ finally:
49
+ if cleanup:
50
+ shutil.rmtree(f.name, ignore_errors=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: omlish
3
- Version: 0.0.0.dev212
3
+ Version: 0.0.0.dev214
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -23,11 +23,10 @@ Requires-Dist: python-snappy~=0.7; extra == "all"
23
23
  Requires-Dist: zstandard~=0.23; extra == "all"
24
24
  Requires-Dist: brotli~=1.1; extra == "all"
25
25
  Requires-Dist: asttokens~=3.0; extra == "all"
26
- Requires-Dist: executing~=2.1; extra == "all"
26
+ Requires-Dist: executing~=2.2; extra == "all"
27
27
  Requires-Dist: psutil~=6.0; extra == "all"
28
28
  Requires-Dist: orjson~=3.10; extra == "all"
29
29
  Requires-Dist: ujson~=5.10; extra == "all"
30
- Requires-Dist: json5~=0.9; extra == "all"
31
30
  Requires-Dist: pyyaml~=6.0; extra == "all"
32
31
  Requires-Dist: cbor2~=5.6; extra == "all"
33
32
  Requires-Dist: cloudpickle~=3.1; extra == "all"
@@ -47,7 +46,7 @@ Requires-Dist: pytest~=8.0; extra == "all"
47
46
  Requires-Dist: anyio~=4.8; extra == "all"
48
47
  Requires-Dist: sniffio~=1.3; extra == "all"
49
48
  Requires-Dist: asttokens~=3.0; extra == "all"
50
- Requires-Dist: executing~=2.1; extra == "all"
49
+ Requires-Dist: executing~=2.2; extra == "all"
51
50
  Requires-Dist: orjson~=3.10; extra == "all"
52
51
  Requires-Dist: pyyaml~=6.0; extra == "all"
53
52
  Requires-Dist: wrapt~=1.17; extra == "all"
@@ -64,12 +63,11 @@ Requires-Dist: zstandard~=0.23; extra == "compress"
64
63
  Requires-Dist: brotli~=1.1; extra == "compress"
65
64
  Provides-Extra: diag
66
65
  Requires-Dist: asttokens~=3.0; extra == "diag"
67
- Requires-Dist: executing~=2.1; extra == "diag"
66
+ Requires-Dist: executing~=2.2; extra == "diag"
68
67
  Requires-Dist: psutil~=6.0; extra == "diag"
69
68
  Provides-Extra: formats
70
69
  Requires-Dist: orjson~=3.10; extra == "formats"
71
70
  Requires-Dist: ujson~=5.10; extra == "formats"
72
- Requires-Dist: json5~=0.9; extra == "formats"
73
71
  Requires-Dist: pyyaml~=6.0; extra == "formats"
74
72
  Requires-Dist: cbor2~=5.6; extra == "formats"
75
73
  Requires-Dist: cloudpickle~=3.1; extra == "formats"
@@ -96,7 +94,7 @@ Provides-Extra: plus
96
94
  Requires-Dist: anyio~=4.8; extra == "plus"
97
95
  Requires-Dist: sniffio~=1.3; extra == "plus"
98
96
  Requires-Dist: asttokens~=3.0; extra == "plus"
99
- Requires-Dist: executing~=2.1; extra == "plus"
97
+ Requires-Dist: executing~=2.2; extra == "plus"
100
98
  Requires-Dist: orjson~=3.10; extra == "plus"
101
99
  Requires-Dist: pyyaml~=6.0; extra == "plus"
102
100
  Requires-Dist: wrapt~=1.17; extra == "plus"