omlish 0.0.0.dev80__py3-none-any.whl → 0.0.0.dev82__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 (35) hide show
  1. omlish/__about__.py +4 -4
  2. omlish/dataclasses/impl/__init__.py +8 -0
  3. omlish/dataclasses/impl/params.py +3 -0
  4. omlish/dataclasses/impl/slots.py +61 -7
  5. omlish/formats/json/__init__.py +8 -1
  6. omlish/formats/json/backends/__init__.py +7 -0
  7. omlish/formats/json/backends/base.py +38 -0
  8. omlish/formats/json/backends/default.py +10 -0
  9. omlish/formats/json/backends/jiter.py +25 -0
  10. omlish/formats/json/backends/orjson.py +46 -2
  11. omlish/formats/json/backends/std.py +39 -0
  12. omlish/formats/json/backends/ujson.py +49 -0
  13. omlish/formats/json/cli.py +125 -31
  14. omlish/formats/json/consts.py +22 -0
  15. omlish/formats/json/encoding.py +17 -0
  16. omlish/formats/json/json.py +9 -39
  17. omlish/formats/json/render.py +49 -24
  18. omlish/formats/json/stream/__init__.py +0 -0
  19. omlish/formats/json/stream/build.py +113 -0
  20. omlish/formats/json/stream/lex.py +285 -0
  21. omlish/formats/json/stream/parse.py +244 -0
  22. omlish/formats/json/stream/render.py +119 -0
  23. omlish/genmachine.py +56 -10
  24. omlish/lang/resources.py +6 -1
  25. omlish/marshal/base.py +2 -0
  26. omlish/marshal/newtypes.py +24 -0
  27. omlish/marshal/standard.py +4 -0
  28. omlish/reflect/__init__.py +1 -0
  29. omlish/reflect/types.py +6 -1
  30. {omlish-0.0.0.dev80.dist-info → omlish-0.0.0.dev82.dist-info}/METADATA +5 -5
  31. {omlish-0.0.0.dev80.dist-info → omlish-0.0.0.dev82.dist-info}/RECORD +35 -24
  32. {omlish-0.0.0.dev80.dist-info → omlish-0.0.0.dev82.dist-info}/LICENSE +0 -0
  33. {omlish-0.0.0.dev80.dist-info → omlish-0.0.0.dev82.dist-info}/WHEEL +0 -0
  34. {omlish-0.0.0.dev80.dist-info → omlish-0.0.0.dev82.dist-info}/entry_points.txt +0 -0
  35. {omlish-0.0.0.dev80.dist-info → omlish-0.0.0.dev82.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev80'
2
- __revision__ = '7640968fc4c06ed0d54ea53c36daae3574c9071f'
1
+ __version__ = '0.0.0.dev82'
2
+ __revision__ = '025f44593a2c0c59d06340e1d7dbcd15b1ab7684'
3
3
 
4
4
 
5
5
  #
@@ -45,7 +45,7 @@ class Project(ProjectBase):
45
45
  'lz4 ~= 4.3',
46
46
  # 'lz4 @ git+https://github.com/wrmsr/python-lz4@wrmsr_20240830_GIL_NOT_USED'
47
47
 
48
- 'python-snappy ~= 0.7; python_version < "3.13"',
48
+ 'python-snappy ~= 0.7',
49
49
 
50
50
  'zstd ~= 1.5',
51
51
  ],
@@ -97,7 +97,7 @@ class Project(ProjectBase):
97
97
 
98
98
  'aiomysql ~= 0.2',
99
99
  'aiosqlite ~= 0.20',
100
- 'asyncpg ~= 0.30; python_version < "3.13"',
100
+ 'asyncpg ~= 0.30',
101
101
 
102
102
  'apsw ~= 3.46',
103
103
 
@@ -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
@@ -1,15 +1,28 @@
1
+ """
2
+ TODO:
3
+ - xml - [{"att", {"el", {"cdata", ...
4
+ - csv - dict if headers, array if not
5
+ """
1
6
  import argparse
7
+ import codecs
2
8
  import contextlib
3
9
  import dataclasses as dc
4
10
  import enum
11
+ import io
5
12
  import json
13
+ import os
6
14
  import subprocess
7
15
  import sys
8
16
  import typing as ta
9
17
 
18
+ from ... import check
10
19
  from ... import lang
11
20
  from ... import term
12
21
  from .render import JsonRenderer
22
+ from .stream.build import JsonObjectBuilder
23
+ from .stream.lex import JsonStreamLexer
24
+ from .stream.parse import JsonStreamParser
25
+ from .stream.render import StreamJsonRenderer
13
26
 
14
27
 
15
28
  if ta.TYPE_CHECKING:
@@ -67,15 +80,26 @@ def _main() -> None:
67
80
  parser = argparse.ArgumentParser()
68
81
 
69
82
  parser.add_argument('file', nargs='?')
83
+
84
+ parser.add_argument('--stream', action='store_true')
85
+ parser.add_argument('--stream-build', action='store_true')
86
+ parser.add_argument('--stream-buffer-size', type=int, default=0x4000)
87
+
70
88
  parser.add_argument('-f', '--format')
89
+
71
90
  parser.add_argument('-z', '--compact', action='store_true')
72
91
  parser.add_argument('-p', '--pretty', action='store_true')
73
92
  parser.add_argument('-i', '--indent')
74
93
  parser.add_argument('-s', '--sort-keys', action='store_true')
94
+
75
95
  parser.add_argument('-c', '--color', action='store_true')
96
+
76
97
  parser.add_argument('-l', '--less', action='store_true')
98
+
77
99
  args = parser.parse_args()
78
100
 
101
+ #
102
+
79
103
  separators = None
80
104
  if args.compact:
81
105
  separators = (',', ':')
@@ -89,6 +113,28 @@ def _main() -> None:
89
113
  except ValueError:
90
114
  indent = args.indent
91
115
 
116
+ kw: dict[str, ta.Any] = dict(
117
+ indent=indent,
118
+ separators=separators,
119
+ sort_keys=args.sort_keys,
120
+ )
121
+
122
+ def render_one(v: ta.Any) -> str:
123
+ if args.color:
124
+ return JsonRenderer.render_str(
125
+ v,
126
+ **kw,
127
+ style=term_color,
128
+ )
129
+
130
+ else:
131
+ return json.dumps(
132
+ v,
133
+ **kw,
134
+ )
135
+
136
+ #
137
+
92
138
  fmt_name = args.format
93
139
  if fmt_name is None:
94
140
  if args.file is not None:
@@ -99,45 +145,93 @@ def _main() -> None:
99
145
  fmt_name = 'json'
100
146
  fmt = FORMATS_BY_NAME[fmt_name]
101
147
 
148
+ if args.stream:
149
+ check.arg(fmt is Formats.JSON.value)
150
+
151
+ #
152
+
102
153
  with contextlib.ExitStack() as es:
103
154
  if args.file is None:
104
- in_file = sys.stdin
155
+ in_file = sys.stdin.buffer
156
+
105
157
  else:
106
- in_file = es.enter_context(open(args.file))
158
+ in_file = es.enter_context(open(args.file, 'rb'))
107
159
 
108
- data = fmt.load(in_file)
160
+ #
109
161
 
110
- kw: dict[str, ta.Any] = dict(
111
- indent=indent,
112
- separators=separators,
113
- sort_keys=args.sort_keys,
114
- )
162
+ if args.less:
163
+ less = subprocess.Popen(
164
+ [
165
+ 'less',
166
+ *(['-R'] if args.color else []),
167
+ ],
168
+ stdin=subprocess.PIPE,
169
+ encoding='utf-8',
170
+ )
171
+ out = check.not_none(less.stdin)
115
172
 
116
- if args.color:
117
- out = JsonRenderer.render_str(
118
- data,
119
- **kw,
120
- style=term_color,
121
- )
173
+ def close_less():
174
+ out.close()
175
+ less.wait()
122
176
 
123
- else:
124
- out = json.dumps(
125
- data,
126
- **kw,
127
- )
128
-
129
- if args.less:
130
- subprocess.run(
131
- [
132
- 'less',
133
- *(['-R'] if args.color else []),
134
- ],
135
- input=out.encode(),
136
- check=True,
137
- )
177
+ es.enter_context(lang.defer(close_less)) # noqa
138
178
 
139
- else:
140
- print(out)
179
+ else:
180
+ out = sys.stdout
181
+
182
+ #
183
+
184
+ if args.stream:
185
+ fd = in_file.fileno()
186
+ decoder = codecs.getincrementaldecoder('utf-8')()
187
+
188
+ with contextlib.ExitStack() as es2:
189
+ lex = es2.enter_context(JsonStreamLexer())
190
+ parse = es2.enter_context(JsonStreamParser())
191
+
192
+ if args.stream_build:
193
+ build = es2.enter_context(JsonObjectBuilder())
194
+ renderer = None
195
+
196
+ else:
197
+ renderer = StreamJsonRenderer(
198
+ out,
199
+ StreamJsonRenderer.Options(
200
+ **kw,
201
+ style=term_color if args.color else None,
202
+ ),
203
+ )
204
+ build = None
205
+
206
+ while True:
207
+ buf = os.read(fd, args.stream_buffer_size)
208
+
209
+ for s in decoder.decode(buf, not buf):
210
+ n = 0
211
+ for c in s:
212
+ for t in lex(c):
213
+ for e in parse(t):
214
+ if renderer is not None:
215
+ renderer.render((e,))
216
+
217
+ if build is not None:
218
+ for v in build(e):
219
+ print(render_one(v), file=out)
220
+
221
+ n += 1
222
+
223
+ if n:
224
+ out.flush()
225
+
226
+ if not buf:
227
+ break
228
+
229
+ out.write('\n')
230
+
231
+ else:
232
+ with io.TextIOWrapper(in_file) as tw:
233
+ v = fmt.load(tw)
234
+ print(render_one(v), file=out)
141
235
 
142
236
 
143
237
  if __name__ == '__main__':
@@ -0,0 +1,22 @@
1
+ import typing as ta
2
+
3
+
4
+ ##
5
+
6
+
7
+ PRETTY_INDENT = 2
8
+
9
+ PRETTY_KWARGS: ta.Mapping[str, ta.Any] = dict(
10
+ indent=PRETTY_INDENT,
11
+ )
12
+
13
+
14
+ ##
15
+
16
+
17
+ COMPACT_SEPARATORS = (',', ':')
18
+
19
+ COMPACT_KWARGS: ta.Mapping[str, ta.Any] = dict(
20
+ indent=None,
21
+ separators=COMPACT_SEPARATORS,
22
+ )