omlish 0.0.0.dev213__py3-none-any.whl → 0.0.0.dev215__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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"