omlish 0.0.0.dev81__py3-none-any.whl → 0.0.0.dev83__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 +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
+ }