omlish 0.0.0.dev213__py3-none-any.whl → 0.0.0.dev215__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 (33) 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/all.py +32 -0
  7. omlish/asyncs/ioproxy/io.py +13 -13
  8. omlish/asyncs/ioproxy/proxier.py +30 -30
  9. omlish/asyncs/ioproxy/typing.py +3 -3
  10. omlish/formats/json/stream/lex.py +1 -0
  11. omlish/formats/json5/Json5.g4 +172 -0
  12. omlish/formats/json5/__init__.py +8 -0
  13. omlish/formats/json5/_antlr/Json5Lexer.py +353 -0
  14. omlish/formats/json5/_antlr/Json5Listener.py +78 -0
  15. omlish/formats/json5/_antlr/Json5Parser.py +616 -0
  16. omlish/formats/json5/_antlr/Json5Visitor.py +51 -0
  17. omlish/formats/json5/_antlr/__init__.py +0 -0
  18. omlish/formats/{json5.py → json5/codec.py} +6 -11
  19. omlish/formats/json5/errors.py +2 -0
  20. omlish/formats/json5/literals.py +130 -0
  21. omlish/formats/json5/parsing.py +79 -0
  22. omlish/lang/__init__.py +2 -0
  23. omlish/lang/imports.py +4 -0
  24. omlish/lang/strings.py +33 -1
  25. omlish/lite/marshal.py +79 -12
  26. omlish/os/files.py +17 -30
  27. omlish/os/temp.py +50 -0
  28. {omlish-0.0.0.dev213.dist-info → omlish-0.0.0.dev215.dist-info}/METADATA +5 -7
  29. {omlish-0.0.0.dev213.dist-info → omlish-0.0.0.dev215.dist-info}/RECORD +33 -21
  30. {omlish-0.0.0.dev213.dist-info → omlish-0.0.0.dev215.dist-info}/LICENSE +0 -0
  31. {omlish-0.0.0.dev213.dist-info → omlish-0.0.0.dev215.dist-info}/WHEEL +0 -0
  32. {omlish-0.0.0.dev213.dist-info → omlish-0.0.0.dev215.dist-info}/entry_points.txt +0 -0
  33. {omlish-0.0.0.dev213.dist-info → omlish-0.0.0.dev215.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/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/marshal.py CHANGED
@@ -2,6 +2,7 @@
2
2
  TODO:
3
3
  - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
4
4
  - literals
5
+ - Options.sequence_cls = list, mapping_cls = dict, ... - def with_mutable_containers() -> Options
5
6
  """
6
7
  # ruff: noqa: UP006 UP007
7
8
  import abc
@@ -183,21 +184,55 @@ class IterableObjMarshaler(ObjMarshaler):
183
184
  @dc.dataclass(frozen=True)
184
185
  class FieldsObjMarshaler(ObjMarshaler):
185
186
  ty: type
186
- fs: ta.Mapping[str, ObjMarshaler]
187
+
188
+ @dc.dataclass(frozen=True)
189
+ class Field:
190
+ att: str
191
+ key: str
192
+ m: ObjMarshaler
193
+
194
+ omit_if_none: bool = False
195
+
196
+ fs: ta.Sequence[Field]
197
+
187
198
  non_strict: bool = False
188
199
 
200
+ #
201
+
202
+ _fs_by_att: ta.ClassVar[ta.Mapping[str, Field]]
203
+ _fs_by_key: ta.ClassVar[ta.Mapping[str, Field]]
204
+
205
+ def __post_init__(self) -> None:
206
+ fs_by_att: dict = {}
207
+ fs_by_key: dict = {}
208
+ for f in self.fs:
209
+ check.not_in(check.non_empty_str(f.att), fs_by_att)
210
+ check.not_in(check.non_empty_str(f.key), fs_by_key)
211
+ fs_by_att[f.att] = f
212
+ fs_by_key[f.key] = f
213
+ self.__dict__['_fs_by_att'] = fs_by_att
214
+ self.__dict__['_fs_by_key'] = fs_by_key
215
+
216
+ #
217
+
189
218
  def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
190
- return {
191
- k: m.marshal(getattr(o, k), ctx)
192
- for k, m in self.fs.items()
193
- }
219
+ d = {}
220
+ for f in self.fs:
221
+ mv = f.m.marshal(getattr(o, f.att), ctx)
222
+ if mv is None and f.omit_if_none:
223
+ continue
224
+ d[f.key] = mv
225
+ return d
194
226
 
195
227
  def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
196
- return self.ty(**{
197
- k: self.fs[k].unmarshal(v, ctx)
198
- for k, v in o.items()
199
- if not (self.non_strict or ctx.options.non_strict_fields) or k in self.fs
200
- })
228
+ kw = {}
229
+ for k, v in o.items():
230
+ if (f := self._fs_by_key.get(k)) is None:
231
+ if not (self.non_strict or ctx.options.non_strict_fields):
232
+ raise KeyError(k)
233
+ continue
234
+ kw[f.att] = f.m.unmarshal(v, ctx)
235
+ return self.ty(**kw)
201
236
 
202
237
 
203
238
  @dc.dataclass(frozen=True)
@@ -332,6 +367,22 @@ def register_single_field_type_obj_marshaler(fld, ty=None):
332
367
  ##
333
368
 
334
369
 
370
+ class ObjMarshalerFieldMetadata:
371
+ def __new__(cls, *args, **kwargs): # noqa
372
+ raise TypeError
373
+
374
+
375
+ class OBJ_MARSHALER_FIELD_KEY(ObjMarshalerFieldMetadata): # noqa
376
+ pass
377
+
378
+
379
+ class OBJ_MARSHALER_OMIT_IF_NONE(ObjMarshalerFieldMetadata): # noqa
380
+ pass
381
+
382
+
383
+ ##
384
+
385
+
335
386
  class ObjMarshalerManager:
336
387
  def __init__(
337
388
  self,
@@ -391,14 +442,30 @@ class ObjMarshalerManager:
391
442
  if dc.is_dataclass(ty):
392
443
  return FieldsObjMarshaler(
393
444
  ty,
394
- {f.name: rec(f.type) for f in dc.fields(ty)},
445
+ [
446
+ FieldsObjMarshaler.Field(
447
+ att=f.name,
448
+ key=check.non_empty_str(fk),
449
+ m=rec(f.type),
450
+ omit_if_none=check.isinstance(f.metadata.get(OBJ_MARSHALER_OMIT_IF_NONE, False), bool),
451
+ )
452
+ for f in dc.fields(ty)
453
+ if (fk := f.metadata.get(OBJ_MARSHALER_FIELD_KEY, f.name)) is not None
454
+ ],
395
455
  non_strict=non_strict_fields,
396
456
  )
397
457
 
398
458
  if issubclass(ty, tuple) and hasattr(ty, '_fields'):
399
459
  return FieldsObjMarshaler(
400
460
  ty,
401
- {p.name: rec(p.annotation) for p in inspect.signature(ty).parameters.values()},
461
+ [
462
+ FieldsObjMarshaler.Field(
463
+ att=p.name,
464
+ key=p.name,
465
+ m=rec(p.annotation),
466
+ )
467
+ for p in inspect.signature(ty).parameters.values()
468
+ ],
402
469
  non_strict=non_strict_fields,
403
470
  )
404
471
 
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.dev213
3
+ Version: 0.0.0.dev215
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"