omlish 0.0.0.dev136__py3-none-any.whl → 0.0.0.dev138__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omlish/__about__.py +2 -2
- omlish/cached.py +2 -2
- omlish/collections/mappings.py +1 -1
- omlish/configs/flattening.py +1 -1
- omlish/diag/_pycharm/runhack.py +3 -0
- omlish/formats/json/stream/errors.py +2 -0
- omlish/formats/json/stream/lex.py +11 -5
- omlish/formats/json/stream/parse.py +37 -21
- omlish/funcs/genmachine.py +5 -4
- omlish/io/compress/__init__.py +0 -0
- omlish/io/compress/abc.py +104 -0
- omlish/io/compress/adapters.py +147 -0
- omlish/io/compress/bz2.py +42 -0
- omlish/io/compress/gzip.py +306 -0
- omlish/io/compress/lzma.py +32 -0
- omlish/io/compress/types.py +29 -0
- omlish/io/generators/__init__.py +0 -0
- omlish/io/generators/readers.py +183 -0
- omlish/io/generators/stepped.py +104 -0
- omlish/lang/__init__.py +11 -1
- omlish/lang/functions.py +0 -2
- omlish/lang/generators.py +243 -0
- omlish/lang/iterables.py +28 -51
- omlish/lang/maybes.py +4 -4
- {omlish-0.0.0.dev136.dist-info → omlish-0.0.0.dev138.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev136.dist-info → omlish-0.0.0.dev138.dist-info}/RECORD +34 -22
- /omlish/collections/{_abc.py → abc.py} +0 -0
- /omlish/io/{_abc.py → abc.py} +0 -0
- /omlish/logs/{_abc.py → abc.py} +0 -0
- /omlish/sql/{_abc.py → abc.py} +0 -0
- {omlish-0.0.0.dev136.dist-info → omlish-0.0.0.dev138.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev136.dist-info → omlish-0.0.0.dev138.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev136.dist-info → omlish-0.0.0.dev138.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev136.dist-info → omlish-0.0.0.dev138.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,306 @@
|
|
1
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
2
|
+
# --------------------------------------------
|
3
|
+
#
|
4
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
5
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
6
|
+
# documentation.
|
7
|
+
#
|
8
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
9
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
10
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
11
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
12
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
|
13
|
+
# alone or in any derivative version prepared by Licensee.
|
14
|
+
#
|
15
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
16
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
17
|
+
# any such work a brief summary of the changes made to Python.
|
18
|
+
#
|
19
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
20
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
21
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
22
|
+
# RIGHTS.
|
23
|
+
#
|
24
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
25
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
26
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
27
|
+
#
|
28
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
29
|
+
#
|
30
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
31
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
32
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
33
|
+
#
|
34
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
35
|
+
# License Agreement.
|
36
|
+
import functools
|
37
|
+
import os.path
|
38
|
+
import struct
|
39
|
+
import time
|
40
|
+
import typing as ta
|
41
|
+
|
42
|
+
from ... import cached
|
43
|
+
from ... import check
|
44
|
+
from ... import lang
|
45
|
+
from ..generators.readers import PrependableBytesGeneratorReader
|
46
|
+
from .types import IncrementalCompressor
|
47
|
+
from .types import IncrementalDecompressor
|
48
|
+
|
49
|
+
|
50
|
+
if ta.TYPE_CHECKING:
|
51
|
+
import gzip
|
52
|
+
import zlib
|
53
|
+
else:
|
54
|
+
gzip = lang.proxy_import('gzip')
|
55
|
+
zlib = lang.proxy_import('zlib')
|
56
|
+
|
57
|
+
|
58
|
+
##
|
59
|
+
|
60
|
+
|
61
|
+
COMPRESS_LEVEL_FAST = 1
|
62
|
+
COMPRESS_LEVEL_TRADEOFF = 6
|
63
|
+
COMPRESS_LEVEL_BEST = 9
|
64
|
+
|
65
|
+
|
66
|
+
@cached.function
|
67
|
+
def _zero_crc() -> int:
|
68
|
+
return zlib.crc32(b'')
|
69
|
+
|
70
|
+
|
71
|
+
##
|
72
|
+
|
73
|
+
|
74
|
+
class IncrementalGzipCompressor:
|
75
|
+
def __init__(
|
76
|
+
self,
|
77
|
+
*,
|
78
|
+
compresslevel: int = COMPRESS_LEVEL_BEST,
|
79
|
+
name: str | bytes | None = None,
|
80
|
+
mtime: float | None = None,
|
81
|
+
) -> None:
|
82
|
+
super().__init__()
|
83
|
+
|
84
|
+
self._name = name or ''
|
85
|
+
self._compresslevel = compresslevel
|
86
|
+
self._mtime = mtime
|
87
|
+
|
88
|
+
def _write_gzip_header(self) -> ta.Generator[bytes, None, None]:
|
89
|
+
check.none((yield b'\037\213')) # magic header
|
90
|
+
check.none((yield b'\010')) # compression method
|
91
|
+
|
92
|
+
try:
|
93
|
+
# RFC 1952 requires the FNAME field to be Latin-1. Do not include filenames that cannot be represented that
|
94
|
+
# way.
|
95
|
+
fname = os.path.basename(self._name)
|
96
|
+
if not isinstance(fname, bytes):
|
97
|
+
fname = fname.encode('latin-1')
|
98
|
+
if fname.endswith(b'.gz'):
|
99
|
+
fname = fname[:-3]
|
100
|
+
except UnicodeEncodeError:
|
101
|
+
fname = b''
|
102
|
+
|
103
|
+
flags = 0
|
104
|
+
if fname:
|
105
|
+
flags = gzip.FNAME
|
106
|
+
check.none((yield chr(flags).encode('latin-1')))
|
107
|
+
|
108
|
+
mtime = self._mtime
|
109
|
+
if mtime is None:
|
110
|
+
mtime = time.time()
|
111
|
+
check.none((yield struct.pack('<L', int(mtime))))
|
112
|
+
|
113
|
+
if self._compresslevel == COMPRESS_LEVEL_BEST:
|
114
|
+
xfl = b'\002'
|
115
|
+
elif self._compresslevel == COMPRESS_LEVEL_FAST:
|
116
|
+
xfl = b'\004'
|
117
|
+
else:
|
118
|
+
xfl = b'\000'
|
119
|
+
check.none((yield xfl))
|
120
|
+
|
121
|
+
check.none((yield b'\377'))
|
122
|
+
|
123
|
+
if fname:
|
124
|
+
check.none((yield fname + b'\000'))
|
125
|
+
|
126
|
+
@lang.autostart
|
127
|
+
def __call__(self) -> IncrementalCompressor:
|
128
|
+
crc = _zero_crc()
|
129
|
+
size = 0
|
130
|
+
offset = 0 # Current file offset for seek(), tell(), etc
|
131
|
+
wrote_header = False
|
132
|
+
|
133
|
+
compress = zlib.compressobj(
|
134
|
+
self._compresslevel,
|
135
|
+
zlib.DEFLATED,
|
136
|
+
-zlib.MAX_WBITS,
|
137
|
+
zlib.DEF_MEM_LEVEL,
|
138
|
+
0,
|
139
|
+
)
|
140
|
+
|
141
|
+
while True:
|
142
|
+
data: ta.Any = check.isinstance((yield None), bytes)
|
143
|
+
|
144
|
+
if not wrote_header:
|
145
|
+
yield from self._write_gzip_header()
|
146
|
+
wrote_header = True
|
147
|
+
|
148
|
+
if not data:
|
149
|
+
break
|
150
|
+
|
151
|
+
# Called by our self._buffer underlying BufferedWriterDelegate.
|
152
|
+
if isinstance(data, (bytes, bytearray)):
|
153
|
+
length = len(data)
|
154
|
+
else:
|
155
|
+
# accept any data that supports the buffer protocol
|
156
|
+
data = memoryview(data)
|
157
|
+
length = data.nbytes
|
158
|
+
|
159
|
+
if length > 0:
|
160
|
+
if (fl := compress.compress(data)):
|
161
|
+
check.none((yield fl))
|
162
|
+
size += length
|
163
|
+
crc = zlib.crc32(data, crc)
|
164
|
+
offset += length
|
165
|
+
|
166
|
+
if (fl := compress.flush()):
|
167
|
+
check.none((yield fl))
|
168
|
+
|
169
|
+
yield struct.pack('<L', crc)
|
170
|
+
# size may exceed 2 GiB, or even 4 GiB
|
171
|
+
yield struct.pack('<L', size & 0xffffffff)
|
172
|
+
|
173
|
+
yield b''
|
174
|
+
|
175
|
+
|
176
|
+
##
|
177
|
+
|
178
|
+
|
179
|
+
class IncrementalGzipDecompressor:
|
180
|
+
def __init__(self) -> None:
|
181
|
+
super().__init__()
|
182
|
+
|
183
|
+
self._factory = functools.partial(
|
184
|
+
zlib.decompressobj,
|
185
|
+
wbits=-zlib.MAX_WBITS,
|
186
|
+
)
|
187
|
+
|
188
|
+
def _read_gzip_header(
|
189
|
+
self,
|
190
|
+
rdr: PrependableBytesGeneratorReader,
|
191
|
+
) -> ta.Generator[int | None, bytes, int | None]:
|
192
|
+
magic = yield from rdr.read(2)
|
193
|
+
if magic == b'':
|
194
|
+
return None
|
195
|
+
|
196
|
+
if magic != b'\037\213':
|
197
|
+
raise gzip.BadGzipFile(f'Not a gzipped file ({magic!r})')
|
198
|
+
|
199
|
+
buf = yield from rdr.read(8)
|
200
|
+
method, flag, last_mtime = struct.unpack('<BBIxx', buf)
|
201
|
+
if method != 8:
|
202
|
+
raise gzip.BadGzipFile('Unknown compression method')
|
203
|
+
|
204
|
+
if flag & gzip.FEXTRA:
|
205
|
+
# Read & discard the extra field, if present
|
206
|
+
buf = yield from rdr.read(2)
|
207
|
+
extra_len, = struct.unpack('<H', buf)
|
208
|
+
if extra_len:
|
209
|
+
yield from rdr.read(extra_len)
|
210
|
+
|
211
|
+
if flag & gzip.FNAME:
|
212
|
+
# Read and discard a null-terminated string containing the filename
|
213
|
+
while True:
|
214
|
+
s = yield from rdr.read(1)
|
215
|
+
if not s or s == b'\000':
|
216
|
+
break
|
217
|
+
|
218
|
+
if flag & gzip.FCOMMENT:
|
219
|
+
# Read and discard a null-terminated string containing a comment
|
220
|
+
while True:
|
221
|
+
s = yield from rdr.read(1)
|
222
|
+
if not s or s == b'\000':
|
223
|
+
break
|
224
|
+
|
225
|
+
if flag & gzip.FHCRC:
|
226
|
+
yield from rdr.read(2) # Read & discard the 16-bit header CRC
|
227
|
+
|
228
|
+
return last_mtime
|
229
|
+
|
230
|
+
def _read_eof(
|
231
|
+
self,
|
232
|
+
rdr: PrependableBytesGeneratorReader,
|
233
|
+
crc: int,
|
234
|
+
stream_size: int,
|
235
|
+
) -> ta.Generator[int | None, bytes, None]:
|
236
|
+
# We've read to the end of the file.
|
237
|
+
# We check that the computed CRC and size of the uncompressed data matches the stored values. Note that the size
|
238
|
+
# stored is the true file size mod 2**32.
|
239
|
+
buf = yield from rdr.read(8)
|
240
|
+
crc32, isize = struct.unpack('<II', buf)
|
241
|
+
if crc32 != crc:
|
242
|
+
raise gzip.BadGzipFile(f'CRC check failed {hex(crc32)} != {hex(crc)}')
|
243
|
+
elif isize != (stream_size & 0xffffffff):
|
244
|
+
raise gzip.BadGzipFile('Incorrect length of data produced')
|
245
|
+
|
246
|
+
# Gzip files can be padded with zeroes and still have archives. Consume all zero bytes and set the file position
|
247
|
+
# to the first non-zero byte. See http://www.gzip.org/#faq8
|
248
|
+
c = b'\0'
|
249
|
+
while c == b'\0':
|
250
|
+
c = yield from rdr.read(1)
|
251
|
+
if c:
|
252
|
+
rdr.prepend(c)
|
253
|
+
|
254
|
+
def __call__(self) -> IncrementalDecompressor:
|
255
|
+
rdr = PrependableBytesGeneratorReader()
|
256
|
+
|
257
|
+
pos = 0 # Current offset in decompressed stream
|
258
|
+
|
259
|
+
crc = _zero_crc()
|
260
|
+
stream_size = 0 # Decompressed size of unconcatenated stream
|
261
|
+
new_member = True
|
262
|
+
|
263
|
+
decompressor = self._factory()
|
264
|
+
|
265
|
+
while True:
|
266
|
+
# For certain input data, a single call to decompress() may not return any data. In this case, retry until
|
267
|
+
# we get some data or reach EOF.
|
268
|
+
while True:
|
269
|
+
if decompressor.eof:
|
270
|
+
# Ending case: we've come to the end of a member in the file, so finish up this member, and read a
|
271
|
+
# new gzip header. Check the CRC and file size, and set the flag so we read a new member
|
272
|
+
yield from self._read_eof(rdr, crc, stream_size)
|
273
|
+
new_member = True
|
274
|
+
decompressor = self._factory()
|
275
|
+
|
276
|
+
if new_member:
|
277
|
+
# If the _new_member flag is set, we have to jump to the next member, if there is one.
|
278
|
+
crc = _zero_crc()
|
279
|
+
stream_size = 0 # Decompressed size of unconcatenated stream
|
280
|
+
last_mtime = yield from self._read_gzip_header(rdr)
|
281
|
+
if not last_mtime:
|
282
|
+
check.none((yield b''))
|
283
|
+
return
|
284
|
+
new_member = False
|
285
|
+
|
286
|
+
# Read a chunk of data from the file
|
287
|
+
if not decompressor.unconsumed_tail:
|
288
|
+
buf = yield from rdr.read(None)
|
289
|
+
uncompress = decompressor.decompress(buf)
|
290
|
+
else:
|
291
|
+
uncompress = decompressor.decompress(b'')
|
292
|
+
|
293
|
+
if decompressor.unused_data != b'':
|
294
|
+
# Prepend the already read bytes to the fileobj so they can be seen by _read_eof() and
|
295
|
+
# _read_gzip_header()
|
296
|
+
rdr.prepend(decompressor.unused_data)
|
297
|
+
|
298
|
+
if uncompress != b'':
|
299
|
+
break
|
300
|
+
if buf == b'': # noqa
|
301
|
+
raise EOFError('Compressed file ended before the end-of-stream marker was reached')
|
302
|
+
|
303
|
+
crc = zlib.crc32(uncompress, crc)
|
304
|
+
stream_size += len(uncompress)
|
305
|
+
pos += len(uncompress)
|
306
|
+
check.none((yield uncompress))
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
from ... import lang
|
4
|
+
from .adapters import CompressorIncrementalAdapter
|
5
|
+
from .adapters import DecompressorIncrementalAdapter
|
6
|
+
from .types import IncrementalCompressor
|
7
|
+
from .types import IncrementalDecompressor
|
8
|
+
|
9
|
+
|
10
|
+
if ta.TYPE_CHECKING:
|
11
|
+
import lzma
|
12
|
+
else:
|
13
|
+
lzma = lang.proxy_import('lzma')
|
14
|
+
|
15
|
+
|
16
|
+
class IncrementalLzmaCompressor:
|
17
|
+
def __init__(self) -> None:
|
18
|
+
super().__init__()
|
19
|
+
|
20
|
+
@lang.autostart
|
21
|
+
def __call__(self) -> IncrementalCompressor:
|
22
|
+
return CompressorIncrementalAdapter(
|
23
|
+
lzma.LZMACompressor, # type: ignore
|
24
|
+
)()
|
25
|
+
|
26
|
+
|
27
|
+
class IncrementalLzmaDecompressor:
|
28
|
+
def __call__(self) -> IncrementalDecompressor:
|
29
|
+
return DecompressorIncrementalAdapter(
|
30
|
+
lzma.LZMADecompressor, # type: ignore
|
31
|
+
trailing_error=lzma.LZMAError,
|
32
|
+
)()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# ruff: noqa: UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
|
5
|
+
IncrementalCompressor: ta.TypeAlias = ta.Generator[
|
6
|
+
ta.Union[
|
7
|
+
bytes, # Compressed output
|
8
|
+
None, # Need input
|
9
|
+
],
|
10
|
+
ta.Union[
|
11
|
+
bytes, # Input bytes
|
12
|
+
None, # Need output
|
13
|
+
],
|
14
|
+
None,
|
15
|
+
]
|
16
|
+
|
17
|
+
|
18
|
+
IncrementalDecompressor: ta.TypeAlias = ta.Generator[
|
19
|
+
ta.Union[
|
20
|
+
bytes, # Uncompressed output
|
21
|
+
int, # Need exactly n bytes
|
22
|
+
None, # Need any amount of bytes
|
23
|
+
],
|
24
|
+
ta.Union[
|
25
|
+
bytes, # Input bytes
|
26
|
+
None, # Need output
|
27
|
+
],
|
28
|
+
None,
|
29
|
+
]
|
File without changes
|
@@ -0,0 +1,183 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- BufferedBytesGeneratorReader
|
4
|
+
- docstrings
|
5
|
+
- memoryviews
|
6
|
+
"""
|
7
|
+
import abc
|
8
|
+
import typing as ta
|
9
|
+
|
10
|
+
from ... import check
|
11
|
+
|
12
|
+
|
13
|
+
T = ta.TypeVar('T')
|
14
|
+
I = ta.TypeVar('I')
|
15
|
+
R = ta.TypeVar('R')
|
16
|
+
AnyT = ta.TypeVar('AnyT', bound=ta.Any)
|
17
|
+
|
18
|
+
|
19
|
+
ReaderGenerator: ta.TypeAlias = ta.Generator[int | None, I, R]
|
20
|
+
ExactReaderGenerator: ta.TypeAlias = ta.Generator[int, I, R]
|
21
|
+
|
22
|
+
BytesReaderGenerator: ta.TypeAlias = ReaderGenerator[bytes, R]
|
23
|
+
BytesExactReaderGenerator: ta.TypeAlias = ExactReaderGenerator[bytes, R]
|
24
|
+
|
25
|
+
StrReaderGenerator: ta.TypeAlias = ReaderGenerator[str, R]
|
26
|
+
StrExactReaderGenerator: ta.TypeAlias = ExactReaderGenerator[str, R]
|
27
|
+
|
28
|
+
|
29
|
+
##
|
30
|
+
|
31
|
+
|
32
|
+
class _BytesJoiner:
|
33
|
+
def _join(self, lst: list[bytes]) -> bytes:
|
34
|
+
return b''.join(lst)
|
35
|
+
|
36
|
+
|
37
|
+
class _StrJoiner:
|
38
|
+
def _join(self, lst: list[str]) -> str:
|
39
|
+
return ''.join(lst)
|
40
|
+
|
41
|
+
|
42
|
+
##
|
43
|
+
|
44
|
+
|
45
|
+
class GeneratorReader(abc.ABC, ta.Generic[T]):
|
46
|
+
@abc.abstractmethod
|
47
|
+
def read(self, sz: int | None) -> ta.Generator[int | None, T, T]:
|
48
|
+
raise NotImplementedError
|
49
|
+
|
50
|
+
def read_exact(self, sz: int) -> ta.Generator[int | None, T, T]:
|
51
|
+
d: ta.Any = yield from self.read(sz)
|
52
|
+
if len(d) != sz:
|
53
|
+
raise EOFError(f'GeneratorReader got {len(d)}, expected {sz}')
|
54
|
+
return d
|
55
|
+
|
56
|
+
|
57
|
+
##
|
58
|
+
|
59
|
+
|
60
|
+
class PrependableGeneratorReader(GeneratorReader[AnyT]):
|
61
|
+
def __init__(self) -> None:
|
62
|
+
super().__init__()
|
63
|
+
|
64
|
+
self._queue: list[tuple[AnyT, int]] = []
|
65
|
+
|
66
|
+
@abc.abstractmethod
|
67
|
+
def _join(self, lst: list[AnyT]) -> AnyT:
|
68
|
+
raise NotImplementedError
|
69
|
+
|
70
|
+
def read(self, sz: int | None) -> ta.Generator[int | None, AnyT, AnyT]:
|
71
|
+
if not self._queue:
|
72
|
+
d: AnyT = check.not_none((yield sz))
|
73
|
+
return d
|
74
|
+
|
75
|
+
if sz is None:
|
76
|
+
return self._queue.pop(0)[0]
|
77
|
+
|
78
|
+
lst: list[AnyT] = []
|
79
|
+
rem = sz
|
80
|
+
while rem > 0 and self._queue:
|
81
|
+
c, p = self._queue[0]
|
82
|
+
|
83
|
+
if len(c) - p > rem:
|
84
|
+
lst.append(c[p:p + rem])
|
85
|
+
self._queue[0] = (c, p + rem)
|
86
|
+
return self._join(lst)
|
87
|
+
|
88
|
+
lst.append(c[p:])
|
89
|
+
rem -= len(c) - p
|
90
|
+
self._queue.pop(0)
|
91
|
+
|
92
|
+
if rem:
|
93
|
+
d = check.not_none((yield rem))
|
94
|
+
if d:
|
95
|
+
lst.append(d) # type: ignore[unreachable]
|
96
|
+
|
97
|
+
if len(lst) == 1:
|
98
|
+
return lst[0]
|
99
|
+
else:
|
100
|
+
return self._join(lst)
|
101
|
+
|
102
|
+
def prepend(self, d: AnyT, p: int | None = None) -> None:
|
103
|
+
if d:
|
104
|
+
self._queue.insert(0, (d, p or 0))
|
105
|
+
|
106
|
+
|
107
|
+
class PrependableBytesGeneratorReader(
|
108
|
+
_BytesJoiner,
|
109
|
+
PrependableGeneratorReader[bytes],
|
110
|
+
):
|
111
|
+
pass
|
112
|
+
|
113
|
+
|
114
|
+
class PrependableStrGeneratorReader(
|
115
|
+
_StrJoiner,
|
116
|
+
PrependableGeneratorReader[str],
|
117
|
+
):
|
118
|
+
pass
|
119
|
+
|
120
|
+
|
121
|
+
prependable_bytes_generator_reader = PrependableBytesGeneratorReader
|
122
|
+
prependable_str_generator_reader = PrependableStrGeneratorReader
|
123
|
+
|
124
|
+
|
125
|
+
##
|
126
|
+
|
127
|
+
|
128
|
+
class BufferedGeneratorReader(PrependableGeneratorReader[AnyT], abc.ABC):
|
129
|
+
DEFAULT_BUFFER_SIZE = 4 * 0x1000
|
130
|
+
|
131
|
+
def __init__(
|
132
|
+
self,
|
133
|
+
buffer_size: int = DEFAULT_BUFFER_SIZE,
|
134
|
+
) -> None:
|
135
|
+
check.arg(buffer_size > 0)
|
136
|
+
|
137
|
+
super().__init__()
|
138
|
+
|
139
|
+
self._buffer_size = buffer_size
|
140
|
+
|
141
|
+
def read(self, sz: int | None) -> ta.Generator[int | None, AnyT, AnyT]:
|
142
|
+
g = super().read(sz)
|
143
|
+
i: ta.Any = None
|
144
|
+
while True:
|
145
|
+
try:
|
146
|
+
q = g.send(i)
|
147
|
+
except StopIteration as e:
|
148
|
+
return e.value
|
149
|
+
|
150
|
+
check.state(not self._queue)
|
151
|
+
|
152
|
+
if q is None:
|
153
|
+
i = check.not_none((yield None))
|
154
|
+
continue
|
155
|
+
|
156
|
+
r = max(q, self._buffer_size)
|
157
|
+
d: AnyT = check.not_none((yield r))
|
158
|
+
if len(d) < q:
|
159
|
+
i = d
|
160
|
+
continue
|
161
|
+
|
162
|
+
i = d[:q]
|
163
|
+
self.prepend(d, q)
|
164
|
+
|
165
|
+
|
166
|
+
class BufferedBytesGeneratorReader(
|
167
|
+
_BytesJoiner,
|
168
|
+
BufferedGeneratorReader[bytes],
|
169
|
+
PrependableGeneratorReader[bytes],
|
170
|
+
):
|
171
|
+
pass
|
172
|
+
|
173
|
+
|
174
|
+
class BufferedStrGeneratorReader(
|
175
|
+
_StrJoiner,
|
176
|
+
BufferedGeneratorReader[str],
|
177
|
+
PrependableGeneratorReader[str],
|
178
|
+
):
|
179
|
+
pass
|
180
|
+
|
181
|
+
|
182
|
+
buffered_bytes_generator_reader = BufferedBytesGeneratorReader
|
183
|
+
buffered_str_generator_reader = BufferedStrGeneratorReader
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
from ... import lang
|
4
|
+
|
5
|
+
|
6
|
+
T = ta.TypeVar('T')
|
7
|
+
I = ta.TypeVar('I')
|
8
|
+
O = ta.TypeVar('O')
|
9
|
+
OF = ta.TypeVar('OF')
|
10
|
+
OT = ta.TypeVar('OT')
|
11
|
+
R = ta.TypeVar('R')
|
12
|
+
|
13
|
+
|
14
|
+
SteppedGenerator: ta.TypeAlias = ta.Generator[O | None, I | None, R]
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
|
19
|
+
|
20
|
+
@lang.autostart
|
21
|
+
def flatmap_stepped_generator(
|
22
|
+
fn: ta.Callable[[list[OF]], OT],
|
23
|
+
g: SteppedGenerator[OF, I, R],
|
24
|
+
*,
|
25
|
+
terminate: ta.Callable[[OF], bool] | None = None,
|
26
|
+
) -> ta.Generator[OT, I, lang.Maybe[R]]:
|
27
|
+
"""
|
28
|
+
Given a 'stepped generator' - a generator which accepts input items and yields zero or more non-None values in
|
29
|
+
response until it signals it's ready for the next input by yielding None - and a function taking a list, returns a
|
30
|
+
1:1 generator which accepts input, builds a list of yielded generator output, calls the given function with that
|
31
|
+
list, and yields the result.
|
32
|
+
|
33
|
+
An optional terminate function may be provided which will cause this function to return early if it returns true for
|
34
|
+
an encountered yielded value. The encountered value causing termination will be included in the list sent to the
|
35
|
+
given fn.
|
36
|
+
|
37
|
+
Returns a Maybe of either the given generator's return value or empty if the terminator was encountered.
|
38
|
+
"""
|
39
|
+
|
40
|
+
l: list[OF]
|
41
|
+
i: I | None = yield # type: ignore
|
42
|
+
while True:
|
43
|
+
l = []
|
44
|
+
|
45
|
+
while True:
|
46
|
+
try:
|
47
|
+
o = g.send(i)
|
48
|
+
except StopIteration as e:
|
49
|
+
if l:
|
50
|
+
yield fn(l)
|
51
|
+
return lang.just(e.value)
|
52
|
+
|
53
|
+
i = None
|
54
|
+
|
55
|
+
if o is None:
|
56
|
+
break
|
57
|
+
|
58
|
+
l.append(o)
|
59
|
+
|
60
|
+
if terminate is not None and terminate(o):
|
61
|
+
yield fn(l)
|
62
|
+
return lang.empty()
|
63
|
+
|
64
|
+
i = yield fn(l)
|
65
|
+
|
66
|
+
|
67
|
+
##
|
68
|
+
|
69
|
+
|
70
|
+
def _join_bytes(l: ta.Sequence[bytes]) -> bytes:
|
71
|
+
if not l:
|
72
|
+
return b''
|
73
|
+
elif len(l) == 1:
|
74
|
+
return l[0]
|
75
|
+
else:
|
76
|
+
return b''.join(l)
|
77
|
+
|
78
|
+
|
79
|
+
def _join_str(l: ta.Sequence[str]) -> str:
|
80
|
+
if not l:
|
81
|
+
return ''
|
82
|
+
elif len(l) == 1:
|
83
|
+
return l[0]
|
84
|
+
else:
|
85
|
+
return ''.join(l)
|
86
|
+
|
87
|
+
|
88
|
+
def _is_empty(o: T) -> bool:
|
89
|
+
return len(o) < 1 # type: ignore
|
90
|
+
|
91
|
+
|
92
|
+
##
|
93
|
+
|
94
|
+
|
95
|
+
def joined_bytes_stepped_generator(
|
96
|
+
g: ta.Generator[bytes | None, bytes | None, R],
|
97
|
+
) -> ta.Generator[bytes, bytes, R]:
|
98
|
+
return flatmap_stepped_generator(_join_bytes, g, terminate=_is_empty)
|
99
|
+
|
100
|
+
|
101
|
+
def joined_str_stepped_generator(
|
102
|
+
g: ta.Generator[str | None, str | None, R],
|
103
|
+
) -> ta.Generator[str, str, R]:
|
104
|
+
return flatmap_stepped_generator(_join_str, g, terminate=_is_empty)
|
omlish/lang/__init__.py
CHANGED
@@ -120,6 +120,17 @@ from .functions import ( # noqa
|
|
120
120
|
void,
|
121
121
|
)
|
122
122
|
|
123
|
+
from .generators import ( # noqa
|
124
|
+
CoroutineGenerator,
|
125
|
+
Generator,
|
126
|
+
GeneratorLike,
|
127
|
+
GeneratorMappedIterator,
|
128
|
+
autostart,
|
129
|
+
corogen,
|
130
|
+
genmap,
|
131
|
+
nextgen,
|
132
|
+
)
|
133
|
+
|
123
134
|
from .imports import ( # noqa
|
124
135
|
can_import,
|
125
136
|
import_all,
|
@@ -136,7 +147,6 @@ from .imports import ( # noqa
|
|
136
147
|
|
137
148
|
from .iterables import ( # noqa
|
138
149
|
BUILTIN_SCALAR_ITERABLE_TYPES,
|
139
|
-
Generator,
|
140
150
|
asrange,
|
141
151
|
exhaust,
|
142
152
|
flatmap,
|
omlish/lang/functions.py
CHANGED
@@ -82,7 +82,6 @@ def identity(obj: T) -> T:
|
|
82
82
|
|
83
83
|
|
84
84
|
class constant(ta.Generic[T]): # noqa
|
85
|
-
|
86
85
|
def __init__(self, obj: T) -> None:
|
87
86
|
super().__init__()
|
88
87
|
|
@@ -116,7 +115,6 @@ class VoidError(Exception):
|
|
116
115
|
|
117
116
|
|
118
117
|
class Void:
|
119
|
-
|
120
118
|
def __new__(cls, *args: ta.Any, **kwargs: ta.Any) -> None: # type: ignore # noqa
|
121
119
|
raise VoidError
|
122
120
|
|