omlish 0.0.0.dev81__py3-none-any.whl → 0.0.0.dev83__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 +3 -3
  2. omlish/__about__.py +2 -2
  3. omlish/dataclasses/impl/__init__.py +8 -0
  4. omlish/dataclasses/impl/params.py +3 -0
  5. omlish/dataclasses/impl/slots.py +61 -7
  6. omlish/formats/json/__init__.py +8 -1
  7. omlish/formats/json/backends/__init__.py +7 -0
  8. omlish/formats/json/backends/base.py +38 -0
  9. omlish/formats/json/backends/default.py +10 -0
  10. omlish/formats/json/backends/jiter.py +25 -0
  11. omlish/formats/json/backends/orjson.py +46 -2
  12. omlish/formats/json/backends/std.py +39 -0
  13. omlish/formats/json/backends/ujson.py +49 -0
  14. omlish/formats/json/cli/__init__.py +0 -0
  15. omlish/formats/json/{cli.py → cli/cli.py} +50 -48
  16. omlish/formats/json/cli/formats.py +64 -0
  17. omlish/formats/json/consts.py +22 -0
  18. omlish/formats/json/encoding.py +17 -0
  19. omlish/formats/json/json.py +9 -39
  20. omlish/formats/json/render.py +49 -28
  21. omlish/formats/json/stream/__init__.py +0 -0
  22. omlish/formats/json/stream/build.py +113 -0
  23. omlish/formats/json/{stream.py → stream/lex.py} +68 -172
  24. omlish/formats/json/stream/parse.py +244 -0
  25. omlish/formats/json/stream/render.py +119 -0
  26. omlish/formats/xml.py +63 -0
  27. omlish/genmachine.py +14 -2
  28. omlish/marshal/base.py +2 -0
  29. omlish/marshal/newtypes.py +24 -0
  30. omlish/marshal/standard.py +4 -0
  31. omlish/reflect/__init__.py +1 -0
  32. omlish/reflect/types.py +6 -1
  33. {omlish-0.0.0.dev81.dist-info → omlish-0.0.0.dev83.dist-info}/METADATA +1 -1
  34. {omlish-0.0.0.dev81.dist-info → omlish-0.0.0.dev83.dist-info}/RECORD +39 -26
  35. /omlish/formats/json/{__main__.py → cli/__main__.py} +0 -0
  36. {omlish-0.0.0.dev81.dist-info → omlish-0.0.0.dev83.dist-info}/LICENSE +0 -0
  37. {omlish-0.0.0.dev81.dist-info → omlish-0.0.0.dev83.dist-info}/WHEEL +0 -0
  38. {omlish-0.0.0.dev81.dist-info → omlish-0.0.0.dev83.dist-info}/entry_points.txt +0 -0
  39. {omlish-0.0.0.dev81.dist-info → omlish-0.0.0.dev83.dist-info}/top_level.txt +0 -0
omlish/.manifests.json CHANGED
@@ -36,14 +36,14 @@
36
36
  }
37
37
  },
38
38
  {
39
- "module": ".formats.json.__main__",
39
+ "module": ".formats.json.cli.__main__",
40
40
  "attr": "_CLI_MODULE",
41
- "file": "omlish/formats/json/__main__.py",
41
+ "file": "omlish/formats/json/cli/__main__.py",
42
42
  "line": 1,
43
43
  "value": {
44
44
  "$omdev.cli.types.CliModule": {
45
45
  "cmd_name": "json",
46
- "mod_name": "omlish.formats.json.__main__"
46
+ "mod_name": "omlish.formats.json.cli.__main__"
47
47
  }
48
48
  }
49
49
  },
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev81'
2
- __revision__ = 'a6b71055e7603077b28ba1999d389905e5663aac'
1
+ __version__ = '0.0.0.dev83'
2
+ __revision__ = '25a87321f2e19f536595fa049cd2f53fb5497448'
3
3
 
4
4
 
5
5
  #
@@ -22,4 +22,12 @@ TODO:
22
22
  - enums
23
23
  - nodal
24
24
  - embedding? forward kwargs in general? or only for replace?
25
+
26
+ TODO refs:
27
+ - batch up exec calls
28
+ - https://github.com/python/cpython/commit/8945b7ff55b87d11c747af2dad0e3e4d631e62d6
29
+ - add doc parameter to dataclasses.field
30
+ - https://github.com/python/cpython/commit/9c7657f09914254724683d91177aed7947637be5
31
+ - add decorator argument to make_dataclass
32
+ - https://github.com/python/cpython/commit/3e3a4d231518f91ff2f3c5a085b3849e32f1d548
25
33
  """
@@ -12,6 +12,9 @@ class Field_:
12
12
  metadata: Metadata | None = None
13
13
  kw_only: bool | MISSING = MISSING
14
14
 
15
+ if sys.version_info >= (3, 13):
16
+ doc: str | None = None
17
+
15
18
  _field_type: Any = None
16
19
 
17
20
 
@@ -1,5 +1,7 @@
1
1
  import dataclasses as dc
2
+ import inspect
2
3
  import itertools
4
+ import types
3
5
 
4
6
 
5
7
  MISSING = dc.MISSING
@@ -25,7 +27,7 @@ def _get_slots(cls):
25
27
  slots = []
26
28
  if getattr(cls, '__weakrefoffset__', -1) != 0:
27
29
  slots.append('__weakref__')
28
- if getattr(cls, '__dictrefoffset__', -1) != 0:
30
+ if getattr(cls, '__dictoffset__', -1) != 0:
29
31
  slots.append('__dict__')
30
32
  yield from slots
31
33
  case str(slot):
@@ -37,44 +39,96 @@ def _get_slots(cls):
37
39
  raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")
38
40
 
39
41
 
42
+ def _update_func_cell_for__class__(f, oldcls, newcls):
43
+ # Returns True if we update a cell, else False.
44
+ if f is None:
45
+ # f will be None in the case of a property where not all of fget, fset, and fdel are used. Nothing to do in
46
+ # that case.
47
+ return False
48
+ try:
49
+ idx = f.__code__.co_freevars.index('__class__')
50
+ except ValueError:
51
+ # This function doesn't reference __class__, so nothing to do.
52
+ return False
53
+ # Fix the cell to point to the new class, if it's already pointing at the old class. I'm not convinced that the "is
54
+ # oldcls" test is needed, but other than performance can't hurt.
55
+ closure = f.__closure__[idx]
56
+ if closure.cell_contents is oldcls:
57
+ closure.cell_contents = newcls
58
+ return True
59
+ return False
60
+
61
+
40
62
  def add_slots(
41
63
  cls: type,
42
64
  is_frozen: bool,
43
65
  weakref_slot: bool,
44
66
  ) -> type:
67
+ # Need to create a new class, since we can't set __slots__ after a class has been created, and the @dataclass
68
+ # decorator is called after the class is created.
69
+
70
+ # Make sure __slots__ isn't already set.
45
71
  if '__slots__' in cls.__dict__:
46
72
  raise TypeError(f'{cls.__name__} already specifies __slots__')
47
73
 
74
+ # Create a new dict for our new class.
48
75
  cls_dict = dict(cls.__dict__)
49
76
  field_names = tuple(f.name for f in dc.fields(cls)) # noqa
50
77
 
78
+ # Make sure slots don't overlap with those in base classes.
51
79
  inherited_slots = set(itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1])))
52
80
 
81
+ # The slots for our class. Remove slots from our base classes. Add '__weakref__' if weakref_slot was given, unless
82
+ # it is already present.
53
83
  cls_dict['__slots__'] = tuple(
54
84
  itertools.filterfalse(
55
85
  inherited_slots.__contains__,
56
86
  itertools.chain(
57
87
  field_names,
88
+ # gh-93521: '__weakref__' also needs to be filtered out if already present in inherited_slots
58
89
  ('__weakref__',) if weakref_slot else (),
59
90
  ),
60
91
  ),
61
92
  )
62
93
 
63
94
  for field_name in field_names:
95
+ # Remove our attributes, if present. They'll still be available in _MARKER.
64
96
  cls_dict.pop(field_name, None)
65
97
 
98
+ # Remove __dict__ itself.
66
99
  cls_dict.pop('__dict__', None)
100
+
101
+ # Clear existing `__weakref__` descriptor, it belongs to a previous type:
67
102
  cls_dict.pop('__weakref__', None)
68
103
 
69
104
  qualname = getattr(cls, '__qualname__', None)
70
- cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
105
+ newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
71
106
  if qualname is not None:
72
- cls.__qualname__ = qualname
107
+ newcls.__qualname__ = qualname
73
108
 
74
109
  if is_frozen:
110
+ # Need this for pickling frozen classes with slots.
75
111
  if '__getstate__' not in cls_dict:
76
- cls.__getstate__ = _dataclass_getstate # type: ignore
112
+ newcls.__getstate__ = _dataclass_getstate # type: ignore
77
113
  if '__setstate__' not in cls_dict:
78
- cls.__setstate__ = _dataclass_setstate # type: ignore
79
-
80
- return cls
114
+ newcls.__setstate__ = _dataclass_setstate # type: ignore
115
+
116
+ # Fix up any closures which reference __class__. This is used to fix zero argument super so that it points to the
117
+ # correct class (the newly created one, which we're returning) and not the original class. We can break out of this
118
+ # loop as soon as we make an update, since all closures for a class will share a given cell.
119
+ for member in newcls.__dict__.values():
120
+ # If this is a wrapped function, unwrap it.
121
+ member = inspect.unwrap(member)
122
+ if isinstance(member, types.FunctionType):
123
+ if _update_func_cell_for__class__(member, cls, newcls):
124
+ break
125
+
126
+ elif isinstance(member, property):
127
+ if (
128
+ _update_func_cell_for__class__(member.fget, cls, newcls) or
129
+ _update_func_cell_for__class__(member.fset, cls, newcls) or
130
+ _update_func_cell_for__class__(member.fdel, cls, newcls)
131
+ ):
132
+ break
133
+
134
+ return newcls
@@ -1,9 +1,16 @@
1
- from .json import ( # noqa
1
+ from .consts import ( # noqa
2
2
  COMPACT_KWARGS,
3
3
  COMPACT_SEPARATORS,
4
4
  PRETTY_INDENT,
5
5
  PRETTY_KWARGS,
6
+ )
7
+
8
+ from .encoding import ( # noqa
9
+ decodes,
6
10
  detect_encoding,
11
+ )
12
+
13
+ from .json import ( # noqa
7
14
  dump,
8
15
  dump_compact,
9
16
  dump_pretty,
@@ -0,0 +1,7 @@
1
+ from .base import ( # noqa
2
+ Backend,
3
+ )
4
+
5
+ from .default import ( # noqa
6
+ DEFAULT_BACKED,
7
+ )
@@ -0,0 +1,38 @@
1
+ import abc
2
+ import typing as ta
3
+
4
+ from .... import lang
5
+
6
+
7
+ class Backend(lang.Abstract):
8
+ @abc.abstractmethod
9
+ def dump(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
10
+ raise NotImplementedError
11
+
12
+ @abc.abstractmethod
13
+ def dumps(self, obj: ta.Any, **kwargs: ta.Any) -> str:
14
+ raise NotImplementedError
15
+
16
+ @abc.abstractmethod
17
+ def load(self, fp: ta.Any, **kwargs: ta.Any) -> ta.Any:
18
+ raise NotImplementedError
19
+
20
+ @abc.abstractmethod
21
+ def loads(self, s: str | bytes | bytearray, **kwargs: ta.Any) -> ta.Any:
22
+ raise NotImplementedError
23
+
24
+ @abc.abstractmethod
25
+ def dump_pretty(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
26
+ raise NotImplementedError
27
+
28
+ @abc.abstractmethod
29
+ def dumps_pretty(self, obj: ta.Any, **kwargs: ta.Any) -> str:
30
+ raise NotImplementedError
31
+
32
+ @abc.abstractmethod
33
+ def dump_compact(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
34
+ raise NotImplementedError
35
+
36
+ @abc.abstractmethod
37
+ def dumps_compact(self, obj: ta.Any, **kwargs: ta.Any) -> str:
38
+ raise NotImplementedError
@@ -0,0 +1,10 @@
1
+ from .base import Backend
2
+ from .std import STD_BACKEND
3
+ from .ujson import UJSON_BACKEND
4
+
5
+
6
+ DEFAULT_BACKED: Backend
7
+ if UJSON_BACKEND is not None:
8
+ DEFAULT_BACKED = UJSON_BACKEND
9
+ else:
10
+ DEFAULT_BACKED = STD_BACKEND
@@ -0,0 +1,25 @@
1
+ """
2
+ from_json(
3
+ json_data: bytes,
4
+ /,
5
+ *,
6
+ allow_inf_nan: bool = True,
7
+ cache_mode: Literal[True, False, "all", "keys", "none"] = "all",
8
+ partial_mode: Literal[True, False, "off", "on", "trailing-strings"] = False,
9
+ catch_duplicate_keys: bool = False,
10
+ lossless_floats: bool = False,
11
+ ) -> Any:
12
+ json_data: The JSON data to parse
13
+ allow_inf_nan: Whether to allow infinity (`Infinity` an `-Infinity`) and `NaN` values to float fields.
14
+ Defaults to True.
15
+ cache_mode: cache Python strings to improve performance at the cost of some memory usage
16
+ - True / 'all' - cache all strings
17
+ - 'keys' - cache only object keys
18
+ - False / 'none' - cache nothing
19
+ partial_mode: How to handle incomplete strings:
20
+ - False / 'off' - raise an exception if the input is incomplete
21
+ - True / 'on' - allow incomplete JSON but discard the last string if it is incomplete
22
+ - 'trailing-strings' - allow incomplete JSON, and include the last incomplete string in the output
23
+ catch_duplicate_keys: if True, raise an exception if objects contain the same key multiple times
24
+ lossless_floats: if True, preserve full detail on floats using `LosslessFloat`
25
+ """
@@ -1,11 +1,16 @@
1
1
  """
2
- def loads(obj: str | bytes | bytearray | memoryview) -> ta.Any | oj.JSONDEcodeError
3
- def dumps(obj: ta.Any, **DumpOpts) -> bytes
2
+ loads(obj: str | bytes | bytearray | memoryview) -> ta.Any | oj.JSONDEcodeError
3
+ dumps(
4
+ obj: ta.Any,
5
+ default: ta.Callable[[ta.Any], Ata.ny] | None = ...,
6
+ option: int | None = ...,
7
+ ) -> bytes
4
8
  """
5
9
  import dataclasses as dc
6
10
  import typing as ta
7
11
 
8
12
  from .... import lang
13
+ from .base import Backend
9
14
 
10
15
 
11
16
  if ta.TYPE_CHECKING:
@@ -14,6 +19,9 @@ else:
14
19
  oj = lang.proxy_import('orjson')
15
20
 
16
21
 
22
+ ##
23
+
24
+
17
25
  @dc.dataclass(frozen=True, kw_only=True)
18
26
  class Options:
19
27
  append_newline: bool = False # append \n to the output
@@ -70,3 +78,39 @@ class Options:
70
78
  class DumpOpts:
71
79
  default: ta.Callable[[ta.Any], ta.Any] | None = None
72
80
  option: Options = Options()
81
+
82
+
83
+ ##
84
+
85
+
86
+ class OrjsonBackend(Backend):
87
+ def dump(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
88
+ fp.write(self.dumps(obj, **kwargs))
89
+
90
+ def dumps(self, obj: ta.Any, **kwargs: ta.Any) -> str:
91
+ return oj.dumps(obj, **kwargs).decode('utf-8')
92
+
93
+ def load(self, fp: ta.Any, **kwargs: ta.Any) -> ta.Any:
94
+ return oj.loads(fp.read(), **kwargs)
95
+
96
+ def loads(self, s: str | bytes | bytearray, **kwargs: ta.Any) -> ta.Any:
97
+ return oj.loads(s, **kwargs)
98
+
99
+ def dump_pretty(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
100
+ fp.write(self.dumps_pretty(obj, **kwargs))
101
+
102
+ def dumps_pretty(self, obj: ta.Any, **kwargs: ta.Any) -> str:
103
+ return self.dumps(obj, option=kwargs.pop('option', 0) | oj.OPT_INDENT_2, **kwargs)
104
+
105
+ def dump_compact(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
106
+ return self.dump(obj, fp, **kwargs)
107
+
108
+ def dumps_compact(self, obj: ta.Any, **kwargs: ta.Any) -> str:
109
+ return self.dumps(obj, **kwargs)
110
+
111
+
112
+ ORJSON_BACKEND: OrjsonBackend | None
113
+ if lang.can_import('orjson'):
114
+ ORJSON_BACKEND = OrjsonBackend()
115
+ else:
116
+ ORJSON_BACKEND = None
@@ -8,6 +8,13 @@ import dataclasses as dc
8
8
  import json
9
9
  import typing as ta
10
10
 
11
+ from ..consts import COMPACT_KWARGS
12
+ from ..consts import PRETTY_KWARGS
13
+ from .base import Backend
14
+
15
+
16
+ ##
17
+
11
18
 
12
19
  @dc.dataclass(frozen=True, kw_only=True)
13
20
  class DumpOpts:
@@ -35,3 +42,35 @@ class LoadOpts:
35
42
 
36
43
  # called with the result of any object literal decoded with an ordered list of pairs, by default dict # noqa
37
44
  object_pairs_hook: ta.Callable[[list[tuple[str, ta.Any]]], ta.Any] | None = None
45
+
46
+
47
+ ##
48
+
49
+
50
+ class StdBackend(Backend):
51
+ def dump(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
52
+ json.dump(obj, fp, **kwargs)
53
+
54
+ def dumps(self, obj: ta.Any, **kwargs: ta.Any) -> str:
55
+ return json.dumps(obj, **kwargs)
56
+
57
+ def load(self, fp: ta.Any, **kwargs: ta.Any) -> ta.Any:
58
+ return json.load(fp, **kwargs)
59
+
60
+ def loads(self, s: str | bytes | bytearray, **kwargs: ta.Any) -> ta.Any:
61
+ return json.loads(s, **kwargs)
62
+
63
+ def dump_pretty(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
64
+ json.dump(obj, fp, **PRETTY_KWARGS, **kwargs)
65
+
66
+ def dumps_pretty(self, obj: ta.Any, **kwargs: ta.Any) -> str:
67
+ return json.dumps(obj, **PRETTY_KWARGS, **kwargs)
68
+
69
+ def dump_compact(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
70
+ json.dump(obj, fp, **COMPACT_KWARGS, **kwargs)
71
+
72
+ def dumps_compact(self, obj: ta.Any, **kwargs: ta.Any) -> str:
73
+ return json.dumps(obj, **COMPACT_KWARGS, **kwargs)
74
+
75
+
76
+ STD_BACKEND = StdBackend()
@@ -7,6 +7,19 @@ dumps(obj: ta.Any, **DumpOpts) -> None
7
7
  import dataclasses as dc
8
8
  import typing as ta
9
9
 
10
+ from .... import lang
11
+ from ..consts import PRETTY_INDENT
12
+ from .base import Backend
13
+
14
+
15
+ if ta.TYPE_CHECKING:
16
+ import ujson as uj
17
+ else:
18
+ uj = lang.proxy_import('ujson')
19
+
20
+
21
+ ##
22
+
10
23
 
11
24
  @dc.dataclass(frozen=True, kw_only=True)
12
25
  class DumpOpts:
@@ -23,3 +36,39 @@ class DumpOpts:
23
36
  reject_bytes: bool = True
24
37
  default: ta.Callable[[ta.Any], ta.Any] | None = None # should return a serializable version of obj or raise TypeError # noqa
25
38
  separators: tuple[str, str] | None = None
39
+
40
+
41
+ ##
42
+
43
+
44
+ class UjsonBackend(Backend):
45
+ def dump(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
46
+ uj.dump(obj, fp, **kwargs)
47
+
48
+ def dumps(self, obj: ta.Any, **kwargs: ta.Any) -> str:
49
+ return uj.dumps(obj, **kwargs)
50
+
51
+ def load(self, fp: ta.Any, **kwargs: ta.Any) -> ta.Any:
52
+ return uj.load(fp, **kwargs)
53
+
54
+ def loads(self, s: str | bytes | bytearray, **kwargs: ta.Any) -> ta.Any:
55
+ return uj.loads(s, **kwargs)
56
+
57
+ def dump_pretty(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
58
+ uj.dump(obj, fp, indent=PRETTY_INDENT, **kwargs)
59
+
60
+ def dumps_pretty(self, obj: ta.Any, **kwargs: ta.Any) -> str:
61
+ return uj.dumps(obj, indent=PRETTY_INDENT, **kwargs)
62
+
63
+ def dump_compact(self, obj: ta.Any, fp: ta.Any, **kwargs: ta.Any) -> None:
64
+ uj.dump(obj, fp, **kwargs)
65
+
66
+ def dumps_compact(self, obj: ta.Any, **kwargs: ta.Any) -> str:
67
+ return uj.dumps(obj, **kwargs)
68
+
69
+
70
+ UJSON_BACKEND: UjsonBackend | None
71
+ if lang.can_import('ujson'):
72
+ UJSON_BACKEND = UjsonBackend()
73
+ else:
74
+ UJSON_BACKEND = None
File without changes
@@ -1,8 +1,6 @@
1
1
  import argparse
2
2
  import codecs
3
3
  import contextlib
4
- import dataclasses as dc
5
- import enum
6
4
  import io
7
5
  import json
8
6
  import os
@@ -10,31 +8,25 @@ import subprocess
10
8
  import sys
11
9
  import typing as ta
12
10
 
13
- from ... import check
14
- from ... import lang
15
- from ... import term
16
- from .render import JsonRenderer
17
- from .stream import JsonStreamLexer
18
- from .stream import JsonStreamValueBuilder
11
+ from .... import check
12
+ from .... import lang
13
+ from .... import term
14
+ from ..render import JsonRenderer
15
+ from ..stream.build import JsonObjectBuilder
16
+ from ..stream.lex import JsonStreamLexer
17
+ from ..stream.parse import JsonStreamParser
18
+ from ..stream.render import StreamJsonRenderer
19
+ from .formats import FORMATS_BY_NAME
20
+ from .formats import Formats
19
21
 
20
22
 
21
23
  if ta.TYPE_CHECKING:
22
- import ast
23
- import tomllib
24
-
25
- import yaml
26
-
27
- from .. import dotenv
28
- from .. import props
29
-
24
+ from ....specs import jmespath
30
25
  else:
31
- ast = lang.proxy_import('ast')
32
- tomllib = lang.proxy_import('tomllib')
26
+ jmespath = lang.proxy_import('....specs.jmespath', __package__)
33
27
 
34
- yaml = lang.proxy_import('yaml')
35
28
 
36
- dotenv = lang.proxy_import('..dotenv', __package__)
37
- props = lang.proxy_import('..props', __package__)
29
+ ##
38
30
 
39
31
 
40
32
  def term_color(o: ta.Any, state: JsonRenderer.State) -> tuple[str, str]:
@@ -46,39 +38,19 @@ def term_color(o: ta.Any, state: JsonRenderer.State) -> tuple[str, str]:
46
38
  return '', ''
47
39
 
48
40
 
49
- @dc.dataclass(frozen=True)
50
- class Format:
51
- names: ta.Sequence[str]
52
- load: ta.Callable[[ta.TextIO], ta.Any]
53
-
54
-
55
- class Formats(enum.Enum):
56
- JSON = Format(['json'], json.load)
57
- YAML = Format(['yaml', 'yml'], lambda f: yaml.safe_load(f))
58
- TOML = Format(['toml'], lambda f: tomllib.loads(f.read()))
59
- ENV = Format(['env', 'dotenv'], lambda f: dotenv.dotenv_values(stream=f))
60
- PROPS = Format(['properties', 'props'], lambda f: dict(props.Properties().load(f.read())))
61
- PY = Format(['py', 'python', 'repr'], lambda f: ast.literal_eval(f.read()))
62
-
63
-
64
- FORMATS_BY_NAME: ta.Mapping[str, Format] = {
65
- n: f
66
- for e in Formats
67
- for f in [e.value]
68
- for n in f.names
69
- }
70
-
71
-
72
41
  def _main() -> None:
73
42
  parser = argparse.ArgumentParser()
74
43
 
75
44
  parser.add_argument('file', nargs='?')
76
45
 
77
46
  parser.add_argument('--stream', action='store_true')
78
- parser.add_argument('--stream-buffer-size', type=int, default=0x1000)
47
+ parser.add_argument('--stream-build', action='store_true')
48
+ parser.add_argument('--stream-buffer-size', type=int, default=0x4000)
79
49
 
80
50
  parser.add_argument('-f', '--format')
81
51
 
52
+ parser.add_argument('-x', '--jmespath-expr')
53
+
82
54
  parser.add_argument('-z', '--compact', action='store_true')
83
55
  parser.add_argument('-p', '--pretty', action='store_true')
84
56
  parser.add_argument('-i', '--indent')
@@ -111,7 +83,15 @@ def _main() -> None:
111
83
  sort_keys=args.sort_keys,
112
84
  )
113
85
 
86
+ if args.jmespath_expr is not None:
87
+ jp_expr = jmespath.compile(args.jmespath_expr)
88
+ else:
89
+ jp_expr = None
90
+
114
91
  def render_one(v: ta.Any) -> str:
92
+ if jp_expr is not None:
93
+ v = jp_expr.search(v)
94
+
115
95
  if args.color:
116
96
  return JsonRenderer.render_str(
117
97
  v,
@@ -179,7 +159,21 @@ def _main() -> None:
179
159
 
180
160
  with contextlib.ExitStack() as es2:
181
161
  lex = es2.enter_context(JsonStreamLexer())
182
- vb = es2.enter_context(JsonStreamValueBuilder())
162
+ parse = es2.enter_context(JsonStreamParser())
163
+
164
+ if args.stream_build:
165
+ build = es2.enter_context(JsonObjectBuilder())
166
+ renderer = None
167
+
168
+ else:
169
+ renderer = StreamJsonRenderer(
170
+ out,
171
+ StreamJsonRenderer.Options(
172
+ **kw,
173
+ style=term_color if args.color else None,
174
+ ),
175
+ )
176
+ build = None
183
177
 
184
178
  while True:
185
179
  buf = os.read(fd, args.stream_buffer_size)
@@ -188,8 +182,14 @@ def _main() -> None:
188
182
  n = 0
189
183
  for c in s:
190
184
  for t in lex(c):
191
- for v in vb(t):
192
- print(render_one(v), file=out)
185
+ for e in parse(t):
186
+ if renderer is not None:
187
+ renderer.render((e,))
188
+
189
+ if build is not None:
190
+ for v in build(e):
191
+ print(render_one(v), file=out)
192
+
193
193
  n += 1
194
194
 
195
195
  if n:
@@ -198,6 +198,8 @@ def _main() -> None:
198
198
  if not buf:
199
199
  break
200
200
 
201
+ out.write('\n')
202
+
201
203
  else:
202
204
  with io.TextIOWrapper(in_file) as tw:
203
205
  v = fmt.load(tw)
@@ -0,0 +1,64 @@
1
+ """
2
+ TODO:
3
+ - options lol - csv header, newline, etc
4
+ """
5
+ import dataclasses as dc
6
+ import enum
7
+ import json
8
+ import typing as ta
9
+
10
+ from .... import lang
11
+
12
+
13
+ if ta.TYPE_CHECKING:
14
+ import ast
15
+ import csv
16
+ import tomllib
17
+
18
+ import yaml
19
+
20
+ from ... import dotenv
21
+ from ... import props
22
+ from ... import xml
23
+
24
+ else:
25
+ ast = lang.proxy_import('ast')
26
+ csv = lang.proxy_import('csv')
27
+ tomllib = lang.proxy_import('tomllib')
28
+
29
+ yaml = lang.proxy_import('yaml')
30
+
31
+ dotenv = lang.proxy_import('...dotenv', __package__)
32
+ props = lang.proxy_import('...props', __package__)
33
+ xml = lang.proxy_import('...xml', __package__)
34
+
35
+
36
+ ##
37
+
38
+
39
+ @dc.dataclass(frozen=True)
40
+ class Format:
41
+ names: ta.Sequence[str]
42
+ load: ta.Callable[[ta.TextIO], ta.Any]
43
+
44
+
45
+ class Formats(enum.Enum):
46
+ JSON = Format(['json'], json.load)
47
+ YAML = Format(['yaml', 'yml'], lambda f: yaml.safe_load(f))
48
+ TOML = Format(['toml'], lambda f: tomllib.loads(f.read()))
49
+ ENV = Format(['env', 'dotenv'], lambda f: dotenv.dotenv_values(stream=f))
50
+ PROPS = Format(['properties', 'props'], lambda f: dict(props.Properties().load(f.read())))
51
+ PY = Format(['py', 'python', 'repr'], lambda f: ast.literal_eval(f.read()))
52
+ XML = Format(['xml'], lambda f: xml.build_simple_element(xml.parse_tree(f.read()).getroot()).as_dict())
53
+ CSV = Format(['csv'], lambda f: list(csv.DictReader(f)))
54
+ TSV = Format(['tsv'], lambda f: list(csv.DictReader(f, delimiter='\t')))
55
+ FLAT_CSV = Format(['fcsv'], lambda f: list(csv.reader(f)))
56
+ FLAT_TSV = Format(['ftsv'], lambda f: list(csv.reader(f, delimiter='\t')))
57
+
58
+
59
+ FORMATS_BY_NAME: ta.Mapping[str, Format] = {
60
+ n: f
61
+ for e in Formats
62
+ for f in [e.value]
63
+ for n in f.names
64
+ }