omlish 0.0.0.dev212__py3-none-any.whl → 0.0.0.dev214__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.
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"