python-filewrap 0.1.3__py3-none-any.whl → 0.2__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.
filewrap/__init__.py CHANGED
@@ -2,10 +2,10 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
- __version__ = (0, 1, 3)
5
+ __version__ = (0, 2)
6
6
  __all__ = [
7
- "Buffer", "SupportsRead", "SupportsReadinto",
8
- "SupportsWrite", "SupportsSeek",
7
+ "Buffer", "SupportsRead", "SupportsReadinto", "SupportsWrite", "SupportsSeek",
8
+ "AsyncBufferedReader", "AsyncTextIOWrapper",
9
9
  "bio_chunk_iter", "bio_chunk_async_iter",
10
10
  "bio_skip_iter", "bio_skip_async_iter",
11
11
  "bytes_iter", "bytes_async_iter",
@@ -16,58 +16,52 @@ __all__ = [
16
16
  "progress_bytes_iter", "progress_bytes_async_iter",
17
17
  ]
18
18
 
19
- from asyncio import to_thread, Lock as AsyncLock
19
+ from asyncio import run as run_async, to_thread, Lock as AsyncLock
20
20
  from collections.abc import Awaitable, AsyncIterable, AsyncIterator, Callable, Iterable, Iterator
21
21
  from functools import update_wrapper
22
+ from io import BufferedIOBase, BufferedReader, BytesIO, RawIOBase, TextIOWrapper
22
23
  from inspect import isawaitable, iscoroutinefunction, isasyncgen, isgenerator
23
24
  from itertools import chain
25
+ from os import linesep
26
+ from re import compile as re_compile
24
27
  from shutil import COPY_BUFSIZE # type: ignore
25
28
  from threading import Lock
26
- from typing import cast, runtime_checkable, Any, ParamSpec, Protocol, TypeVar
29
+ from typing import cast, runtime_checkable, Any, BinaryIO, ParamSpec, Protocol, Self, TypeVar
27
30
 
28
31
  try:
29
32
  from collections.abc import Buffer # type: ignore
30
33
  except ImportError:
31
- from abc import ABC, abstractmethod
34
+ from _ctypes import _SimpleCData
32
35
  from array import array
33
36
 
34
- def _check_methods(C, *methods):
35
- mro = C.__mro__
36
- for method in methods:
37
- for B in mro:
38
- if method in B.__dict__:
39
- if B.__dict__[method] is None:
40
- return NotImplemented
41
- break
42
- else:
43
- return NotImplemented
44
- return True
45
-
46
- class Buffer(ABC): # type: ignore
47
- __slots__ = ()
48
-
49
- @abstractmethod
37
+ @runtime_checkable
38
+ class Buffer(Protocol): # type: ignore
50
39
  def __buffer__(self, flags: int, /) -> memoryview:
51
- raise NotImplementedError
52
-
53
- @classmethod
54
- def __subclasshook__(cls, C):
55
- if cls is Buffer:
56
- return _check_methods(C, "__buffer__")
57
- return NotImplemented
40
+ pass
58
41
 
59
42
  Buffer.register(bytes)
60
43
  Buffer.register(bytearray)
61
44
  Buffer.register(memoryview)
45
+ Buffer.register(_SimpleCData)
62
46
  Buffer.register(array)
63
47
 
64
- from asynctools import async_chain, ensure_async, ensure_aiter
48
+ from asynctools import async_chain, ensure_async, ensure_aiter, ensure_coroutine
49
+ from property import staticproperty
65
50
 
66
51
 
67
52
  Args = ParamSpec("Args")
68
53
  _T_co = TypeVar("_T_co", covariant=True)
69
54
  _T_contra = TypeVar("_T_contra", contravariant=True)
70
55
 
56
+ @BufferedIOBase.register
57
+ class VirtualBufferedReader:
58
+ def __new__(cls, /, *a, **k):
59
+ if cls is __class__: # type: ignore
60
+ raise TypeError("not allowed to create instances")
61
+ return super().__new__(cls, *a, **k)
62
+
63
+ CRE_NOT_UNIX_NEWLINES_sub = re_compile("\r\n|\r").sub
64
+
71
65
 
72
66
  @runtime_checkable
73
67
  class SupportsRead(Protocol[_T_co]):
@@ -89,8 +83,834 @@ class SupportsSeek(Protocol):
89
83
  def seek(self, /, __offset: int, __whence: int = 0) -> int: ...
90
84
 
91
85
 
86
+ class AsyncBufferedReader(BufferedReader):
87
+
88
+ def __init__(
89
+ self,
90
+ /,
91
+ raw: RawIOBase,
92
+ buffer_size: int = 8192,
93
+ ):
94
+ super().__init__(raw, min(buffer_size, 1))
95
+ self._buf = bytearray(buffer_size)
96
+ self._buf_view = memoryview(self._buf)
97
+ self._buf_pos = 0
98
+ self._buf_stop = 0
99
+ self._pos = raw.tell()
100
+
101
+ def __del__(self, /):
102
+ try:
103
+ self.close()
104
+ except:
105
+ pass
106
+
107
+ async def __aenter__(self, /) -> Self:
108
+ return self
109
+
110
+ async def __aexit__(self, /, *exc_info):
111
+ await self.aclose()
112
+
113
+ def __aiter__(self, /):
114
+ return self
115
+
116
+ async def __anext__(self, /):
117
+ if line := await self.readline():
118
+ return line
119
+ else:
120
+ raise StopAsyncIteration
121
+
122
+ def __getattr__(self, attr, /):
123
+ return getattr(self.raw, attr)
124
+
125
+ def __len__(self, /) -> int:
126
+ return self.length
127
+
128
+ @property
129
+ def length(self, /) -> int:
130
+ return getattr(self.raw, "length")
131
+
132
+ def calibrate(self, /, target: int = -1) -> bool:
133
+ pos = self._pos
134
+ if target < 0:
135
+ target = self.raw.tell()
136
+ if pos == target:
137
+ return True
138
+ buf_pos = self._buf_pos
139
+ buf_stop = self._buf_stop
140
+ self._pos = target
141
+ move_left = pos - target
142
+ reusable = 0 <= move_left <= buf_pos
143
+ if reusable:
144
+ width = buf_pos - move_left
145
+ self._buf_view[:width] = self._buf_view[move_left:buf_pos]
146
+ self._buf_pos = self._buf_stop = width
147
+ else:
148
+ self._buf_pos = self._buf_stop = 0
149
+ return reusable
150
+
151
+ async def aclose(self, /):
152
+ raw = self.raw
153
+ try:
154
+ ret = getattr(raw, "aclose")()
155
+ except (AttributeError, TypeError):
156
+ ret = getattr(raw, "close")()
157
+ if isawaitable(ret):
158
+ await ret
159
+
160
+ def close(self, /):
161
+ raw = self.raw
162
+ try:
163
+ ret = getattr(raw, "aclose")()
164
+ except (AttributeError, TypeError):
165
+ ret = getattr(raw, "close")()
166
+ if isawaitable(ret):
167
+ run_async(ensure_coroutine(ret))
168
+
169
+ async def flush(self, /):
170
+ return await ensure_async(self.raw.flush, threaded=True)()
171
+
172
+ def peek(self, size: int = 0, /) -> bytes:
173
+ start, stop = self._buf_pos, self._buf_stop
174
+ if size > 0:
175
+ stop = min(start + size, stop)
176
+ return self._buf_view[start:stop].tobytes()
177
+
178
+ def review(self, size: int = 0, /) -> bytes:
179
+ start, stop = 0, self._buf_pos
180
+ if size > 0:
181
+ start = max(0, stop - size)
182
+ return self._buf_view[start:stop].tobytes()
183
+
184
+ def context(self, /) -> tuple[bytes, int]:
185
+ start, stop = self._buf_pos, self._buf_stop
186
+ return self._buf_view[0:stop].tobytes(), start
187
+
188
+ async def read(self, size: None | int = -1, /) -> bytes: # type: ignore
189
+ if self.closed:
190
+ raise ValueError("I/O operation on closed file.")
191
+ if size == 0:
192
+ return b""
193
+ if size is None:
194
+ size = -1
195
+ buf_view = self._buf_view
196
+ buf_pos = self._buf_pos
197
+ buf_stop = self._buf_stop
198
+ buf_size = buf_stop - buf_pos
199
+ if size > 0:
200
+ if buf_size >= size:
201
+ buf_pos_stop = self._buf_pos = buf_pos + size
202
+ self._pos += size
203
+ return buf_view[buf_pos:buf_pos_stop].tobytes()
204
+ buffer_view = memoryview(bytearray(size))
205
+ buffer_view[:buf_size] = buf_view[buf_pos:buf_stop]
206
+ self._buf_pos = buf_stop
207
+ self._pos += buf_size
208
+ buf_size += await self.readinto(buffer_view[buf_size:])
209
+ return buffer_view[:buf_size].tobytes()
210
+ BUFSIZE = len(buf_view)
211
+ read = ensure_async(self.raw.read, threaded=True)
212
+ buffer = bytearray(buf_view[buf_pos:buf_stop])
213
+ try:
214
+ while data := await read(BUFSIZE):
215
+ buffer += data
216
+ length = len(data)
217
+ self._pos += length
218
+ if BUFSIZE == length:
219
+ buf_view[:] = data
220
+ if buf_pos != BUFSIZE:
221
+ buf_pos = self._buf_pos = self._buf_stop = BUFSIZE
222
+ else:
223
+ buf_pos_stop = buf_stop + length
224
+ if buf_pos_stop <= BUFSIZE:
225
+ buf_view[buf_stop:buf_pos_stop] = data
226
+ self._buf_pos = self._buf_stop = buf_pos_stop
227
+ else:
228
+ index = BUFSIZE - length
229
+ buf_view[:index] = buf_view[-index:]
230
+ buf_view[-length:] = data
231
+ self._buf_pos = self._buf_stop = BUFSIZE
232
+ break
233
+ return bytes(buffer)
234
+ except:
235
+ self.calibrate()
236
+ raise
237
+
238
+ async def read1(self, size: None | int = -1, /) -> bytes: # type: ignore
239
+ if self.closed:
240
+ raise ValueError("I/O operation on closed file.")
241
+ if size == 0:
242
+ return b""
243
+ if size is None:
244
+ size = -1
245
+ buf_view = self._buf_view
246
+ buf_pos = self._buf_pos
247
+ buf_stop = self._buf_stop
248
+ buf_size = buf_stop - buf_pos
249
+ if size > 0:
250
+ if buf_size >= size:
251
+ buf_pos_stop = self._buf_pos = buf_pos + size
252
+ self._pos += size
253
+ return buf_view[buf_pos:buf_pos_stop].tobytes()
254
+ size -= buf_size
255
+ try:
256
+ data = await ensure_async(self.raw.read, threaded=True)(size)
257
+ except:
258
+ self.calibrate()
259
+ raise
260
+ prev_data = buf_view[buf_pos:buf_stop].tobytes()
261
+ if data:
262
+ BUFSIZE = len(buf_view)
263
+ length = len(data)
264
+ self._pos += len(prev_data) + length
265
+ if BUFSIZE <= length:
266
+ buf_view[:] = memoryview(data)[-BUFSIZE:]
267
+ self._buf_pos = self._buf_stop = BUFSIZE
268
+ else:
269
+ buf_pos_stop = buf_stop + length
270
+ if buf_pos_stop <= BUFSIZE:
271
+ buf_view[buf_stop:buf_pos_stop] = data
272
+ self._buf_pos = self._buf_stop = buf_pos_stop
273
+ else:
274
+ index = BUFSIZE - length
275
+ buf_view[:index] = buf_view[-index:]
276
+ buf_view[-length:] = data
277
+ self._buf_pos = self._buf_stop = BUFSIZE
278
+ return prev_data + data
279
+ else:
280
+ return prev_data
281
+
282
+ async def readinto(self, buffer, /) -> int: # type: ignore
283
+ if self.closed:
284
+ raise ValueError("I/O operation on closed file.")
285
+ size = len(buffer)
286
+ if size == 0:
287
+ return 0
288
+ buf_view = self._buf_view
289
+ buf_pos = self._buf_pos
290
+ buf_stop = self._buf_stop
291
+ buf_size = buf_stop - buf_pos
292
+ if buf_size >= size:
293
+ buf_pos_stop = buf_pos + size
294
+ buffer[:] = buf_view[buf_pos:buf_pos_stop]
295
+ self._buf_pos = buf_pos_stop
296
+ self._pos += size
297
+ return size
298
+ try:
299
+ readinto = ensure_async(self.raw.readinto, threaded=True)
300
+ except AttributeError:
301
+ read = ensure_async(self.raw.read, threaded=True)
302
+ async def readinto(buffer, /) -> int:
303
+ data = await read(len(buffer))
304
+ if data:
305
+ size = len(data)
306
+ buffer[:size] = data
307
+ return size
308
+ else:
309
+ return 0
310
+ BUFSIZE = len(buf_view)
311
+ buffer_view = memoryview(buffer)
312
+ buffer_view[:buf_size] = buf_view[buf_pos:buf_stop]
313
+ buf_pos = self._buf_pos = buf_stop
314
+ self._pos += buf_size
315
+ buffer_pos = buf_size
316
+ size -= buf_size
317
+ try:
318
+ running = size > 0
319
+ while running:
320
+ if buf_stop < BUFSIZE:
321
+ length = await readinto(buf_view[buf_stop:])
322
+ if not length:
323
+ break
324
+ buf_stop = self._buf_stop = buf_stop + length
325
+ if buf_stop < BUFSIZE:
326
+ running = False
327
+ else:
328
+ length = await readinto(buf_view)
329
+ if not length:
330
+ break
331
+ if length < BUFSIZE:
332
+ part1, part2 = buf_view[length:].tobytes(), buf_view[:length].tobytes()
333
+ buf_view[:length] = part1
334
+ buf_view[length:] = part2
335
+ running = False
336
+ buf_pos = self._buf_pos = BUFSIZE - length
337
+ else:
338
+ buf_pos = self._buf_pos = 0
339
+ move = min(length, size)
340
+ buffer_view[buffer_pos:buffer_pos+move] = buf_view[buf_pos:buf_pos+move]
341
+ self._buf_pos += move
342
+ self._pos += move
343
+ buffer_pos += move
344
+ if move == size:
345
+ running = False
346
+ else:
347
+ size -= length
348
+ return buffer_pos
349
+ except:
350
+ self.calibrate()
351
+ raise
352
+
353
+ async def readinto1(self, buffer, /) -> int: # type: ignore
354
+ if self.closed:
355
+ raise ValueError("I/O operation on closed file.")
356
+ size = len(buffer)
357
+ if size == 0:
358
+ return 0
359
+ buf_view = self._buf_view
360
+ buf_pos = self._buf_pos
361
+ buf_stop = self._buf_stop
362
+ buf_size = buf_stop - buf_pos
363
+ if buf_size >= size:
364
+ buf_pos_stop = buf_pos + size
365
+ buffer[:] = buf_view[buf_pos:buf_pos_stop]
366
+ self._buf_pos = buf_pos_stop
367
+ self._pos += size
368
+ return size
369
+ try:
370
+ readinto = ensure_async(self.raw.readinto, threaded=True)
371
+ except AttributeError:
372
+ read = ensure_async(self.raw.read, threaded=True)
373
+ async def readinto(buffer, /) -> int:
374
+ data = await read(len(buffer))
375
+ if data:
376
+ size = len(data)
377
+ buffer[:size] = data
378
+ return size
379
+ else:
380
+ return 0
381
+ BUFSIZE = len(buf_view)
382
+ buffer_view = memoryview(buffer)
383
+ buffer_view[:buf_size] = buf_view[buf_pos:buf_stop]
384
+ buf_pos = self._buf_pos = buf_stop
385
+ self._pos += buf_size
386
+ buffer_pos = buf_size
387
+ size -= buf_size
388
+ try:
389
+ length = await readinto(buffer_view[buf_size:])
390
+ except:
391
+ self.calibrate()
392
+ raise
393
+ if length:
394
+ BUFSIZE = len(buf_view)
395
+ buffer_pos += length
396
+ self._pos += length
397
+ if BUFSIZE <= buffer_pos:
398
+ buf_view[:] = buffer_view[-BUFSIZE:]
399
+ self._buf_pos = self._buf_stop = BUFSIZE
400
+ else:
401
+ buf_pos_stop = buf_stop + length
402
+ if buf_pos_stop <= BUFSIZE:
403
+ buf_view[buf_stop:buf_pos_stop] = buffer_view[-length:]
404
+ self._buf_pos = self._buf_stop = buf_pos_stop
405
+ else:
406
+ index = BUFSIZE - length
407
+ buf_view[:index] = buf_view[-index:]
408
+ buf_view[-length:] = buffer_view[-length:]
409
+ self._buf_pos = self._buf_stop = BUFSIZE
410
+ return buffer_pos
411
+
412
+ async def readline(self, size: int | None = -1, /) -> bytes: # type: ignore
413
+ if self.closed:
414
+ raise ValueError("I/O operation on closed file.")
415
+ if size == 0:
416
+ return b""
417
+ if size is None:
418
+ size = -1
419
+ buf = self._buf
420
+ buf_pos = self._buf_pos
421
+ buf_stop = self._buf_stop
422
+ buf_size = buf_stop - buf_pos
423
+ if size > 0 and size <= buf_size:
424
+ stop = buf_pos + size
425
+ index = buf.find(b"\n", buf_pos, stop)
426
+ if index > 0:
427
+ buf_pos_stop = self._buf_pos = index + 1
428
+ self._pos += buf_pos_stop - buf_pos
429
+ else:
430
+ buf_pos_stop = self._buf_pos = stop
431
+ self._pos += size
432
+ return self._buf_view[buf_pos:buf_pos_stop].tobytes()
433
+ index = buf.find(b"\n", buf_pos, buf_stop)
434
+ if index > 0:
435
+ buf_pos_stop = self._buf_pos = index + 1
436
+ self._pos += buf_pos_stop - buf_pos
437
+ return self._buf_view[buf_pos:buf_pos_stop].tobytes()
438
+ try:
439
+ readline = ensure_async(self.raw.readline, threaded=True)
440
+ except AttributeError:
441
+ async def readline(size: None | int = -1, /) -> bytes:
442
+ if size == 0:
443
+ return b""
444
+ if size is None:
445
+ size = -1
446
+ read = ensure_async(self.raw.read, threaded=True)
447
+ cache = bytearray()
448
+ if size > 0:
449
+ while size and (c := await read(1)):
450
+ cache += c
451
+ if c == b"\n":
452
+ break
453
+ size -= 1
454
+ else:
455
+ while c := await read(1):
456
+ cache += c
457
+ if c == b"\n":
458
+ break
459
+ return bytes(cache)
460
+ if size > 0:
461
+ size -= buf_size
462
+ try:
463
+ data = await readline(size)
464
+ except:
465
+ self.calibrate()
466
+ raise
467
+ buf_view = self._buf_view
468
+ BUFSIZE = len(buf_view)
469
+ length = len(data)
470
+ prev_data = buf_view[buf_pos:buf_stop].tobytes()
471
+ self._pos += len(prev_data) + length
472
+ if BUFSIZE <= length:
473
+ buf_view[:] = memoryview(data)[-BUFSIZE:]
474
+ self._buf_pos = self._buf_stop = BUFSIZE
475
+ else:
476
+ buf_pos_stop = buf_stop + length
477
+ if buf_pos_stop <= BUFSIZE:
478
+ buf_view[buf_stop:buf_pos_stop] = data
479
+ self._buf_pos = self._buf_stop = buf_pos_stop
480
+ else:
481
+ index = BUFSIZE - length
482
+ buf_view[:index] = buf_view[-index:]
483
+ buf_view[-length:] = data
484
+ self._buf_pos = self._buf_stop = BUFSIZE
485
+ return prev_data + data
486
+
487
+ async def readlines(self, hint: int = -1, /) -> list[bytes]: # type: ignore
488
+ if self.closed:
489
+ raise ValueError("I/O operation on closed file.")
490
+ readline = self.readline
491
+ lines: list[bytes] = []
492
+ append = lines.append
493
+ if hint <= 0:
494
+ while line := await readline():
495
+ append(line)
496
+ else:
497
+ while hint > 0 and (line := await readline()):
498
+ append(line)
499
+ hint -= len(line)
500
+ return lines
501
+
502
+ async def seek(self, target: int, whence: int = 0, /) -> int: # type: ignore
503
+ pos = self._pos
504
+ if whence == 1:
505
+ target += pos
506
+ elif whence == 2:
507
+ if target > 0:
508
+ raise ValueError("target out of range: overflow")
509
+ target = self._pos = await ensure_async(self.raw.seek, threaded=True)(target, 2)
510
+ if target != pos:
511
+ self.calibrate(target)
512
+ return target
513
+ if target < 0:
514
+ raise ValueError("target out of range: underflow")
515
+ if target != pos:
516
+ buf_pos = target - pos + self._buf_pos
517
+ if 0 <= buf_pos <= self._buf_stop:
518
+ self._buf_pos = buf_pos
519
+ pos = self._pos = target
520
+ else:
521
+ pos = self._pos = await ensure_async(self.raw.seek, threaded=True)(target, 0)
522
+ self._buf_pos = self._buf_stop = 0
523
+ return pos
524
+
525
+ def tell(self, /) -> int:
526
+ return self._pos
527
+
528
+
529
+ class AsyncTextIOWrapper(TextIOWrapper):
530
+
531
+ def __init__(
532
+ self,
533
+ /,
534
+ buffer: BinaryIO,
535
+ encoding: None | str = None,
536
+ errors: None | str = None,
537
+ newline: None | str = None,
538
+ line_buffering: bool = False,
539
+ write_through: bool = False,
540
+ ):
541
+ super().__init__(
542
+ buffer,
543
+ encoding=encoding,
544
+ errors=errors,
545
+ newline=newline,
546
+ line_buffering=line_buffering,
547
+ write_through=write_through,
548
+ )
549
+ self.newline = newline
550
+
551
+ def __del__(self, /):
552
+ try:
553
+ self.close()
554
+ except:
555
+ pass
556
+
557
+ async def __aenter__(self, /) -> Self:
558
+ return self
559
+
560
+ async def __aexit__(self, /, *exc_info):
561
+ await self.aclose()
562
+
563
+ def __aiter__(self, /):
564
+ return self
565
+
566
+ async def __anext__(self, /):
567
+ if line := await self.readline():
568
+ return line
569
+ else:
570
+ raise StopAsyncIteration
571
+
572
+ def __getattr__(self, attr, /):
573
+ return getattr(self.buffer, attr)
574
+
575
+ async def aclose(self, /):
576
+ buffer = self.buffer
577
+ try:
578
+ ret = getattr(buffer, "aclose")()
579
+ except (AttributeError, TypeError):
580
+ ret = getattr(buffer, "close")()
581
+ if isawaitable(ret):
582
+ await ret
583
+
584
+ def close(self, /):
585
+ buffer = self.buffer
586
+ try:
587
+ ret = getattr(buffer, "aclose")()
588
+ except (AttributeError, TypeError):
589
+ ret = getattr(buffer, "close")()
590
+ if isawaitable(ret):
591
+ run_async(ensure_coroutine(ret))
592
+
593
+ async def flush(self, /):
594
+ return await ensure_async(self.buffer.flush, threaded=True)()
595
+
596
+ async def read(self, size: None | int = -1, /) -> str: # type: ignore
597
+ if self.closed:
598
+ raise ValueError("I/O operation on closed file.")
599
+ if size == 0:
600
+ return ""
601
+ if size is None:
602
+ size = -1
603
+ read = ensure_async(self.buffer.read, threaded=True)
604
+ encoding = self.encoding
605
+ errors = self.errors or "strict"
606
+ newline = self.newline
607
+ if size < 0:
608
+ data = await read(-1)
609
+ else:
610
+ data = await read(size)
611
+ if size < 0 or len(data) < size:
612
+ text = str(data, encoding, errors)
613
+ if newline is None:
614
+ text = CRE_NOT_UNIX_NEWLINES_sub("\n", text)
615
+ return text
616
+
617
+ def process_part(data, errors="strict", /) -> int:
618
+ text = str(data, encoding, errors)
619
+ if newline is None:
620
+ text = CRE_NOT_UNIX_NEWLINES_sub("\n", text)
621
+ add_part(text)
622
+ return len(text)
623
+
624
+ ls_parts: list[str] = []
625
+ add_part = ls_parts.append
626
+ cache = b""
627
+ while data := await read(size):
628
+ cache += data
629
+ while cache:
630
+ try:
631
+ size -= process_part(cache)
632
+ cache = b""
633
+ except UnicodeDecodeError as e:
634
+ start, stop = e.start, e.end
635
+ if start:
636
+ size -= process_part(cache[:start])
637
+ if e.reason == "unexpected end of data" and stop == len(cache):
638
+ cache = cache[start:]
639
+ break
640
+ if errors == "strict":
641
+ raise
642
+ size -= process_part(cache[start:stop], errors)
643
+ cache = cache[stop:]
644
+ if len(data) < size:
645
+ break
646
+ if cache:
647
+ process_part(cache, errors)
648
+ return "".join(ls_parts)
649
+
650
+ async def readline(self, size=-1, /) -> str: # type: ignore
651
+ if self.closed:
652
+ raise ValueError("I/O operation on closed file.")
653
+ if size == 0:
654
+ return ""
655
+ if size is None:
656
+ size = -1
657
+ read = ensure_async(self.buffer.read, threaded=True)
658
+ seek = self.seek
659
+ encoding = self.encoding
660
+ errors = self.errors or "strict"
661
+ newline = self.newline
662
+ peek = getattr(self.buffer, "peek", None)
663
+ if not callable(peek):
664
+ peek = None
665
+ if newline:
666
+ sepb = bytes(newline, encoding)
667
+ else:
668
+ crb = bytes("\r", encoding)
669
+ lfb = bytes("\n", encoding)
670
+ lfb_len = len(lfb)
671
+ buf = bytearray()
672
+ text = ""
673
+ reach_end = False
674
+ if size < 0:
675
+ while True:
676
+ if peek is None:
677
+ while c := await read(1):
678
+ buf += c
679
+ if newline:
680
+ if buf.endswith(sepb):
681
+ break
682
+ elif buf.endswith(lfb):
683
+ break
684
+ elif buf.endswith(crb):
685
+ peek_maybe_lfb = await read(lfb_len)
686
+ if peek_maybe_lfb == lfb:
687
+ buf += lfb
688
+ elif peek_maybe_lfb:
689
+ # TODO: 这是一个提前量,未必需要立即往回 seek,因为转换为 str 后可能尾部不是 \r(因为可以和前面的符号结合),所以这个可能可以被复用,如果需要优化,可以在程序结束时的 finally 部分最终执行 seek(可能最终字符被消耗所以不需要 seek)
690
+ await seek(-len(peek_maybe_lfb), 1)
691
+ if len(peek_maybe_lfb) < lfb_len:
692
+ reach_end = True
693
+ break
694
+ else:
695
+ reach_end = True
696
+ else:
697
+ while True:
698
+ buf_stop = len(buf)
699
+ peek_b = peek()
700
+ if peek_b:
701
+ buf += peek_b
702
+ if newline:
703
+ if (idx := buf.find(sepb)) > -1:
704
+ idx += 1
705
+ await read(idx - buf_stop)
706
+ del buf[idx:]
707
+ break
708
+ elif (idx := buf.find(lfb)) > -1:
709
+ idx += 1
710
+ await read(idx - buf_stop)
711
+ del buf[idx:]
712
+ break
713
+ elif (idx := buf.find(crb)) > -1:
714
+ idx += 1
715
+ await read(idx - buf_stop)
716
+ if buf.startswith(lfb, idx):
717
+ await read(lfb_len)
718
+ del buf[idx+lfb_len:]
719
+ else:
720
+ del buf[idx:]
721
+ break
722
+ c = await read(1)
723
+ if not c:
724
+ reach_end = True
725
+ break
726
+ buf += c
727
+ while buf:
728
+ try:
729
+ text += str(buf, encoding)
730
+ buf.clear()
731
+ except UnicodeEncodeError as e:
732
+ start, stop = e.start, e.end
733
+ if start:
734
+ text += str(buf[:start], encoding)
735
+ if e.reason == "unexpected end of data" and stop == len(buf):
736
+ buf = buf[start:]
737
+ break
738
+ if errors == "strict":
739
+ raise
740
+ text += str(buf[start:stop], encoding, errors)
741
+ buf = buf[stop:]
742
+ else:
743
+ if newline:
744
+ if text.endswith(newline):
745
+ return text[:-len(newline)] + "\n"
746
+ elif newline is None:
747
+ if text.endswith("\r\n"):
748
+ return text[:-2] + "\n"
749
+ elif text.endswith("\r"):
750
+ return text[:-1] + "\n"
751
+ elif text.endswith("\n"):
752
+ return text
753
+ elif text.endswith(("\r\n", "\r", "\n")):
754
+ return text
755
+ if reach_end:
756
+ return text
757
+ else:
758
+ while True:
759
+ rem = size - len(text)
760
+ if peek is None:
761
+ while rem and (c := await read(1)):
762
+ buf += c
763
+ rem -= 1
764
+ if newline:
765
+ if buf.endswith(sepb):
766
+ break
767
+ elif buf.endswith(lfb):
768
+ break
769
+ elif buf.endswith(crb):
770
+ peek_maybe_lfb = await read(lfb_len)
771
+ if peek_maybe_lfb == lfb:
772
+ buf += lfb
773
+ elif peek_maybe_lfb:
774
+ await seek(-len(peek_maybe_lfb), 1)
775
+ if len(peek_maybe_lfb) < lfb_len:
776
+ reach_end = True
777
+ break
778
+ else:
779
+ reach_end = True
780
+ else:
781
+ while rem:
782
+ buf_stop = len(buf)
783
+ peek_b = peek()
784
+ if peek_b:
785
+ if len(peek_b) >= rem:
786
+ buf += peek_b[:rem]
787
+ rem = 0
788
+ else:
789
+ buf += peek_b
790
+ rem -= len(peek_b)
791
+ if newline:
792
+ if (idx := buf.find(sepb)) > -1:
793
+ idx += 1
794
+ await read(idx - buf_stop)
795
+ del buf[idx:]
796
+ break
797
+ elif (idx := buf.find(lfb)) > -1:
798
+ idx += 1
799
+ await read(idx - buf_stop)
800
+ del buf[idx:]
801
+ break
802
+ elif (idx := buf.find(crb)) > -1:
803
+ idx += 1
804
+ await read(idx - buf_stop)
805
+ if buf.startswith(lfb, idx):
806
+ await read(lfb_len)
807
+ del buf[idx+lfb_len:]
808
+ else:
809
+ del buf[idx:]
810
+ break
811
+ if rem:
812
+ c = await read(1)
813
+ if not c:
814
+ reach_end = True
815
+ break
816
+ rem -= 1
817
+ buf += c
818
+ while buf:
819
+ try:
820
+ text += str(buf, encoding)
821
+ buf.clear()
822
+ except UnicodeEncodeError as e:
823
+ start, stop = e.start, e.end
824
+ if start:
825
+ text += str(buf[:start], encoding)
826
+ if e.reason == "unexpected end of data" and stop == len(buf):
827
+ buf = buf[start:]
828
+ break
829
+ if errors == "strict":
830
+ raise
831
+ text += str(buf[start:stop], encoding, errors)
832
+ buf = buf[stop:]
833
+ else:
834
+ if newline:
835
+ if text.endswith(newline):
836
+ return text[:-len(newline)] + "\n"
837
+ elif newline is None:
838
+ if text.endswith("\r\n"):
839
+ return text[:-2] + "\n"
840
+ elif text.endswith("\r"):
841
+ return text[:-1] + "\n"
842
+ elif text.endswith("\n"):
843
+ return text
844
+ elif text.endswith(("\r\n", "\r", "\n")):
845
+ return text
846
+ if reach_end or len(text) == size:
847
+ return text
848
+
849
+ async def readlines(self, hint=-1, /) -> list[str]: # type: ignore
850
+ if self.closed:
851
+ raise ValueError("I/O operation on closed file.")
852
+ readline = self.readline
853
+ lines: list[str] = []
854
+ append = lines.append
855
+ if hint <= 0:
856
+ while line := await readline():
857
+ append(line)
858
+ else:
859
+ while hint > 0 and (line := await readline()):
860
+ append(line)
861
+ hint -= len(line)
862
+ return lines
863
+
864
+ def reconfigure(
865
+ self,
866
+ /,
867
+ encoding: None | str = None,
868
+ errors: None | str = None,
869
+ newline: None | str = None,
870
+ line_buffering: None | bool = None,
871
+ write_through: None | bool = None,
872
+ ):
873
+ super().reconfigure(
874
+ encoding=encoding,
875
+ errors=errors,
876
+ newline=newline,
877
+ line_buffering=line_buffering,
878
+ write_through=write_through,
879
+ )
880
+ self.newline = newline
881
+
882
+ async def seek(self, target: int, whence: int = 0, /) -> int: # type: ignore
883
+ return await ensure_async(self.buffer.seek, threaded=True)(target, whence)
884
+
885
+ def tell(self, /) -> int:
886
+ return self.buffer.tell()
887
+
888
+ async def truncate(self, pos: None | int = None, /) -> int: # type: ignore
889
+ return await ensure_async(self.buffer.truncate, threaded=True)(pos)
890
+
891
+ async def write(self, text: str, /) -> int: # type: ignore
892
+ match self.newline:
893
+ case "" | "\n":
894
+ pass
895
+ case None:
896
+ if linesep != "\n":
897
+ text = text.replace("\n", linesep)
898
+ case _:
899
+ text = text.replace("\n", linesep)
900
+ data = bytes(text, self.encoding, self.errors or "strict")
901
+ await ensure_async(self.buffer.write, threaded=True)(data)
902
+ if self.write_through or self.line_buffering and ("\n" in text or "\r" in text):
903
+ await self.flush()
904
+ return len(text)
905
+
906
+ async def writelines(self, lines: Iterable[str], /): # type: ignore
907
+ write = self.write
908
+ for line in lines:
909
+ await write(line)
910
+
911
+
92
912
  def bio_chunk_iter(
93
- bio: SupportsRead[Buffer] | Callable[[int], Buffer],
913
+ bio: SupportsRead[Buffer] | SupportsReadinto | Callable[[int], Buffer],
94
914
  /,
95
915
  size: int = -1,
96
916
  chunksize: int = COPY_BUFSIZE,
@@ -100,11 +920,19 @@ def bio_chunk_iter(
100
920
  use_readinto = False
101
921
  if callable(bio):
102
922
  read = bio
103
- elif can_buffer and hasattr(bio, "readinto"):
923
+ elif can_buffer and isinstance(bio, SupportsReadinto):
104
924
  readinto = bio.readinto
105
925
  use_readinto = True
106
- else:
926
+ elif isinstance(bio, SupportsRead):
107
927
  read = bio.read
928
+ else:
929
+ readinto = bio.readinto
930
+ def read(_):
931
+ buf = bytearray(chunksize)
932
+ length = readinto(buf)
933
+ if length == chunksize:
934
+ return buf
935
+ return buf[:length]
108
936
  if not callable(callback):
109
937
  callback = None
110
938
  if use_readinto:
@@ -151,7 +979,7 @@ def bio_chunk_iter(
151
979
 
152
980
 
153
981
  async def bio_chunk_async_iter(
154
- bio: SupportsRead[Buffer] | Callable[[int], Buffer | Awaitable[Buffer]],
982
+ bio: SupportsRead[Buffer] | SupportsReadinto | Callable[[int], Buffer | Awaitable[Buffer]],
155
983
  /,
156
984
  size: int = -1,
157
985
  chunksize: int = COPY_BUFSIZE,
@@ -160,12 +988,20 @@ async def bio_chunk_async_iter(
160
988
  ) -> AsyncIterator[Buffer]:
161
989
  use_readinto = False
162
990
  if callable(bio):
163
- read = ensure_async(bio)
164
- elif can_buffer and hasattr(bio, "readinto"):
165
- readinto = ensure_async(bio.readinto)
991
+ read = ensure_async(bio, threaded=True)
992
+ elif can_buffer and isinstance(bio, SupportsReadinto):
993
+ readinto = ensure_async(bio.readinto, threaded=True)
166
994
  use_readinto = True
995
+ elif isinstance(bio, SupportsRead):
996
+ read = ensure_async(bio.read, threaded=True)
167
997
  else:
168
- read = ensure_async(bio.read)
998
+ readinto = ensure_async(bio.readinto, threaded=True)
999
+ async def read(_):
1000
+ buf = bytearray(chunksize)
1001
+ length = await readinto(buf)
1002
+ if length == chunksize:
1003
+ return buf
1004
+ return buf[:length]
169
1005
  callback = ensure_async(callback) if callable(callback) else None
170
1006
  if use_readinto:
171
1007
  buf = bytearray(chunksize)
@@ -211,7 +1047,7 @@ async def bio_chunk_async_iter(
211
1047
 
212
1048
 
213
1049
  def bio_skip_iter(
214
- bio: SupportsRead[Buffer] | Callable[[int], Buffer],
1050
+ bio: SupportsRead[Buffer] | SupportsReadinto | Callable[[int], Buffer],
215
1051
  /,
216
1052
  size: int = -1,
217
1053
  chunksize: int = COPY_BUFSIZE,
@@ -282,7 +1118,7 @@ def bio_skip_iter(
282
1118
 
283
1119
 
284
1120
  async def bio_skip_async_iter(
285
- bio: SupportsRead[Buffer] | Callable[[int], Buffer | Awaitable[Buffer]],
1121
+ bio: SupportsRead[Buffer] | SupportsReadinto | Callable[[int], Buffer | Awaitable[Buffer]],
286
1122
  /,
287
1123
  size: int = -1,
288
1124
  chunksize: int = COPY_BUFSIZE,
@@ -293,7 +1129,7 @@ async def bio_skip_async_iter(
293
1129
  callback = ensure_async(callback) if callable(callback) else None
294
1130
  length: int
295
1131
  try:
296
- seek = ensure_async(getattr(bio, "seek"))
1132
+ seek = ensure_async(getattr(bio, "seek"), threaded=True)
297
1133
  curpos = await seek(0, 1)
298
1134
  if size > 0:
299
1135
  length = (await seek(size, 1)) - curpos
@@ -303,9 +1139,9 @@ async def bio_skip_async_iter(
303
1139
  if chunksize <= 0:
304
1140
  chunksize = COPY_BUFSIZE
305
1141
  if callable(bio):
306
- read = ensure_async(bio)
1142
+ read = ensure_async(bio, threaded=True)
307
1143
  elif hasattr(bio, "readinto"):
308
- readinto = ensure_async(bio.readinto)
1144
+ readinto = ensure_async(bio.readinto, threaded=True)
309
1145
  buf = bytearray(chunksize)
310
1146
  if size > 0:
311
1147
  while size >= chunksize:
@@ -329,7 +1165,7 @@ async def bio_skip_async_iter(
329
1165
  await callback(length)
330
1166
  yield length
331
1167
  else:
332
- read = ensure_async(bio.read)
1168
+ read = ensure_async(bio.read, threaded=True)
333
1169
  if size > 0:
334
1170
  while size:
335
1171
  readsize = min(chunksize, size)
@@ -469,35 +1305,56 @@ def bytes_iter_to_reader(
469
1305
  /,
470
1306
  ) -> SupportsRead[bytearray]:
471
1307
  getnext = iter(it).__next__
1308
+ pos = 0
472
1309
  at_end = False
473
1310
  unconsumed: bytearray = bytearray()
474
1311
  lock = Lock()
475
- def read(n=-1, /) -> bytearray:
476
- nonlocal at_end, unconsumed
1312
+ def __del__():
1313
+ try:
1314
+ close()
1315
+ except:
1316
+ pass
1317
+ def close():
1318
+ nonlocal at_end
1319
+ getattr(it, "close")()
1320
+ at_end = True
1321
+ def peek(n: int = 0, /) -> bytearray:
1322
+ if n <= 0:
1323
+ return unconsumed[:]
1324
+ return unconsumed[:n]
1325
+ def read(n: None | int = -1, /) -> bytearray:
1326
+ nonlocal pos, at_end, unconsumed
477
1327
  if at_end or n == 0:
478
1328
  return bytearray()
1329
+ if n is None:
1330
+ n = -1
479
1331
  with lock:
480
1332
  try:
481
- if n is None or n < 0:
1333
+ if n < 0:
482
1334
  while True:
483
1335
  unconsumed += getnext()
484
1336
  else:
485
1337
  while n > len(unconsumed):
486
1338
  unconsumed += getnext()
487
1339
  b, unconsumed = unconsumed[:n], unconsumed[n:]
1340
+ pos += len(b)
488
1341
  return b
489
1342
  except StopIteration:
490
1343
  at_end = True
491
- return unconsumed
1344
+ b = unconsumed[:]
1345
+ del unconsumed[:]
1346
+ pos += len(b)
1347
+ return b
492
1348
  def readinto(buf, /) -> int:
493
- nonlocal at_end, unconsumed
1349
+ nonlocal pos, at_end, unconsumed
494
1350
  if at_end or not (bufsize := len(buf)):
495
1351
  return 0
496
1352
  with lock:
497
- if bufsize <= len(unconsumed):
1353
+ n = len(unconsumed)
1354
+ if bufsize <= n:
498
1355
  buf[:], unconsumed = unconsumed[:bufsize], unconsumed[bufsize:]
1356
+ pos += bufsize
499
1357
  return bufsize
500
- n = len(unconsumed)
501
1358
  buf[:n] = unconsumed
502
1359
  del unconsumed[:]
503
1360
  try:
@@ -508,47 +1365,95 @@ def bytes_iter_to_reader(
508
1365
  m = n + len(b)
509
1366
  if m >= bufsize:
510
1367
  buf[n:] = b[:bufsize-n]
511
- unconsumed += b[m-bufsize:]
1368
+ unconsumed += b[bufsize-n:]
1369
+ pos += bufsize
512
1370
  return bufsize
513
1371
  else:
514
1372
  buf[n:m] = b
1373
+ pos += len(b)
515
1374
  n = m
516
1375
  except StopIteration:
517
1376
  at_end = True
518
1377
  return n
519
- def __next__() -> bytearray:
520
- nonlocal unconsumed, at_end
521
- if at_end:
522
- raise StopIteration
523
- if unconsumed:
524
- # search for b"\n"
525
- if (idx := unconsumed.find(49)) > -1:
526
- idx += 1
527
- b, unconsumed = unconsumed[:idx], unconsumed[idx:]
528
- return b
529
- try:
530
- while True:
531
- r = getnext()
532
- if not r:
533
- continue
534
- if (idx := r.find(49)) > -1:
1378
+ def readline(n: None | int = -1, /) -> bytearray:
1379
+ nonlocal pos, unconsumed, at_end
1380
+ if at_end or n == 0:
1381
+ return bytearray()
1382
+ if n is None:
1383
+ n = -1
1384
+ with lock:
1385
+ if unconsumed:
1386
+ # search for b"\n"
1387
+ if (idx := unconsumed.find(49)) > -1:
535
1388
  idx += 1
536
- unconsumed += r[:idx]
537
- b, unconsumed = unconsumed, bytearray(r[idx:])
1389
+ if n < 0 or idx <= n:
1390
+ b, unconsumed = unconsumed[:idx], unconsumed[idx:]
1391
+ pos += idx
1392
+ return b
1393
+ if n > 0 and len(unconsumed) >= n:
1394
+ b, unconsumed = unconsumed[:n], unconsumed[n:]
1395
+ pos += n
538
1396
  return b
539
- unconsumed += r
540
- except StopIteration:
541
- at_end = True
542
- if unconsumed:
543
- return unconsumed
544
- raise
1397
+ try:
1398
+ start = len(unconsumed)
1399
+ while True:
1400
+ r = getnext()
1401
+ if not r:
1402
+ continue
1403
+ unconsumed += r
1404
+ if (idx := unconsumed.find(49, start)) > -1:
1405
+ idx += 1
1406
+ if n < 0 or idx <= n:
1407
+ b, unconsumed = unconsumed[:idx], unconsumed[idx:]
1408
+ pos += idx
1409
+ return b
1410
+ start = len(unconsumed)
1411
+ if n > 0 and start >= n:
1412
+ b, unconsumed = unconsumed[:n], unconsumed[n:]
1413
+ pos += n
1414
+ return b
1415
+ except StopIteration:
1416
+ at_end = True
1417
+ if unconsumed:
1418
+ b = unconsumed[:]
1419
+ del unconsumed[:]
1420
+ pos += len(b)
1421
+ return b
1422
+ raise
1423
+ def readlines(hint: int = -1, /) -> list[bytearray]:
1424
+ if at_end:
1425
+ return []
1426
+ lines: list[bytearray] = []
1427
+ append = lines.append
1428
+ if hint <= 0:
1429
+ while line := readline():
1430
+ append(line)
1431
+ else:
1432
+ while hint > 0 and (line := readline()):
1433
+ append(line)
1434
+ hint -= len(line)
1435
+ return lines
1436
+ def __next__() -> bytearray:
1437
+ if at_end or not (b := readline()):
1438
+ raise StopIteration
1439
+ return b
545
1440
  reprs = f"<reader for {it!r}>"
546
- return type("reader", (), {
547
- "read": staticmethod(read),
548
- "readinto": staticmethod(readinto),
549
- "__iter__": lambda self, /: self,
1441
+ return type("reader", (VirtualBufferedReader,), {
1442
+ "__del__": staticmethod(__del__),
1443
+ "__getattr__": staticmethod(lambda attr, /: getattr(it, attr)),
1444
+ "__iter__": lambda self: self,
550
1445
  "__next__": staticmethod(__next__),
551
1446
  "__repr__": staticmethod(lambda: reprs),
1447
+ "close": staticmethod(close),
1448
+ "closed": staticproperty(lambda: at_end),
1449
+ "peek": staticmethod(peek),
1450
+ "read": staticmethod(read),
1451
+ "readinto": staticmethod(readinto),
1452
+ "readline": staticmethod(readline),
1453
+ "readlines": staticmethod(readlines),
1454
+ "readable": staticmethod(lambda: True),
1455
+ "seekable": staticmethod(lambda: False),
1456
+ "tell": staticmethod(lambda: pos),
552
1457
  })()
553
1458
 
554
1459
 
@@ -561,35 +1466,68 @@ def bytes_iter_to_async_reader(
561
1466
  getnext = aiter(it).__anext__
562
1467
  else:
563
1468
  getnext = ensure_async(iter(it).__next__, threaded=threaded)
1469
+ pos = 0
564
1470
  at_end = False
565
1471
  unconsumed: bytearray = bytearray()
566
1472
  lock = AsyncLock()
567
- async def read(n=-1, /) -> bytearray:
568
- nonlocal at_end, unconsumed
1473
+ def __del__():
1474
+ try:
1475
+ close()
1476
+ except:
1477
+ pass
1478
+ def close():
1479
+ try:
1480
+ method = getattr(it, "aclose")
1481
+ except AttributeError:
1482
+ method = getattr(it, "close")
1483
+ ret = method()
1484
+ if isawaitable(ret):
1485
+ run_async(ensure_coroutine(ret))
1486
+ async def aclose():
1487
+ try:
1488
+ method = getattr(it, "aclose")
1489
+ except AttributeError:
1490
+ method = getattr(it, "close")
1491
+ ret = method()
1492
+ if isawaitable(ret):
1493
+ await ret
1494
+ def peek(n: int = 0, /) -> bytearray:
1495
+ if n <= 0:
1496
+ return unconsumed[:]
1497
+ return unconsumed[:n]
1498
+ async def read(n: None | int = -1, /) -> bytearray:
1499
+ nonlocal pos, at_end, unconsumed
569
1500
  if at_end or n == 0:
570
1501
  return bytearray()
1502
+ if n is None:
1503
+ n = -1
571
1504
  async with lock:
572
1505
  try:
573
- if n is None or n < 0:
1506
+ if n < 0:
574
1507
  while True:
575
1508
  unconsumed += await getnext()
576
1509
  else:
577
1510
  while n > len(unconsumed):
578
1511
  unconsumed += await getnext()
579
1512
  b, unconsumed = unconsumed[:n], unconsumed[n:]
1513
+ pos += len(b)
580
1514
  return b
581
- except StopAsyncIteration:
1515
+ except (StopIteration, StopAsyncIteration):
582
1516
  at_end = True
583
- return unconsumed
1517
+ b = unconsumed[:]
1518
+ del unconsumed[:]
1519
+ pos += len(b)
1520
+ return b
584
1521
  async def readinto(buf, /) -> int:
585
- nonlocal at_end, unconsumed
1522
+ nonlocal pos, at_end, unconsumed
586
1523
  if at_end or not (bufsize := len(buf)):
587
1524
  return 0
588
1525
  async with lock:
589
- if bufsize <= len(unconsumed):
1526
+ n = len(unconsumed)
1527
+ if bufsize <= n:
590
1528
  buf[:], unconsumed = unconsumed[:bufsize], unconsumed[bufsize:]
1529
+ pos += bufsize
591
1530
  return bufsize
592
- n = len(unconsumed)
593
1531
  buf[:n] = unconsumed
594
1532
  del unconsumed[:]
595
1533
  try:
@@ -600,47 +1538,99 @@ def bytes_iter_to_async_reader(
600
1538
  m = n + len(b)
601
1539
  if m >= bufsize:
602
1540
  buf[n:] = b[:bufsize-n]
603
- unconsumed += b[m-bufsize:]
1541
+ unconsumed += b[bufsize-n:]
1542
+ pos += bufsize
604
1543
  return bufsize
605
1544
  else:
606
1545
  buf[n:m] = b
1546
+ pos += len(b)
607
1547
  n = m
608
- except StopAsyncIteration:
1548
+ except (StopIteration, StopAsyncIteration):
609
1549
  at_end = True
610
1550
  return n
611
- async def __next__() -> bytearray:
612
- nonlocal unconsumed, at_end
613
- if at_end:
614
- raise StopIteration
615
- if unconsumed:
616
- # search for b"\n"
617
- if (idx := unconsumed.find(49)) > -1:
618
- idx += 1
619
- b, unconsumed = unconsumed[:idx], unconsumed[idx:]
620
- return b
621
- try:
622
- while True:
623
- r = await getnext()
624
- if not r:
625
- continue
626
- if (idx := r.find(49)) > -1:
1551
+ async def readline(n: None | int = -1, /) -> bytearray:
1552
+ nonlocal pos, unconsumed, at_end
1553
+ if at_end or n == 0:
1554
+ return bytearray()
1555
+ if n is None:
1556
+ n = -1
1557
+ async with lock:
1558
+ if unconsumed:
1559
+ # search for b"\n"
1560
+ if (idx := unconsumed.find(49)) > -1:
627
1561
  idx += 1
628
- unconsumed += r[:idx]
629
- b, unconsumed = unconsumed, bytearray(r[idx:])
1562
+ if n < 0 or idx <= n:
1563
+ b, unconsumed = unconsumed[:idx], unconsumed[idx:]
1564
+ pos += idx
1565
+ return b
1566
+ if n > 0 and len(unconsumed) >= n:
1567
+ b, unconsumed = unconsumed[:n], unconsumed[n:]
1568
+ pos += n
630
1569
  return b
631
- unconsumed += r
632
- except StopIteration:
633
- at_end = True
634
- if unconsumed:
635
- return unconsumed
636
- raise
637
- reprs = f"<reader for {it!r}>"
638
- return type("reader", (), {
1570
+ try:
1571
+ start = len(unconsumed)
1572
+ while True:
1573
+ r = await getnext()
1574
+ if not r:
1575
+ continue
1576
+ unconsumed += r
1577
+ if (idx := unconsumed.find(49, start)) > -1:
1578
+ idx += 1
1579
+ if n < 0 or idx <= n:
1580
+ b, unconsumed = unconsumed[:idx], unconsumed[idx:]
1581
+ pos += idx
1582
+ return b
1583
+ start = len(unconsumed)
1584
+ if n > 0 and start >= n:
1585
+ b, unconsumed = unconsumed[:n], unconsumed[n:]
1586
+ pos += n
1587
+ return b
1588
+ except (StopIteration, StopAsyncIteration):
1589
+ at_end = True
1590
+ if unconsumed:
1591
+ b = unconsumed[:]
1592
+ del unconsumed[:]
1593
+ pos += len(b)
1594
+ return b
1595
+ raise
1596
+ async def readlines(hint: int = -1, /) -> list[bytearray]:
1597
+ if at_end:
1598
+ return []
1599
+ if hint is None:
1600
+ hint = -1
1601
+ lines: list[bytearray] = []
1602
+ append = lines.append
1603
+ async with lock:
1604
+ if hint <= 0:
1605
+ while line := await readline():
1606
+ append(line)
1607
+ else:
1608
+ while hint > 0 and (line := await readline()):
1609
+ append(line)
1610
+ hint -= len(line)
1611
+ return lines
1612
+ async def __anext__() -> bytearray:
1613
+ if at_end or not (b := await readline()):
1614
+ raise StopAsyncIteration
1615
+ return b
1616
+ reprs = f"<async_reader for {it!r}>"
1617
+ return type("async_reader", (VirtualBufferedReader,), {
1618
+ "__del__": staticmethod(__del__),
1619
+ "__getattr__": staticmethod(lambda attr, /: getattr(it, attr)),
1620
+ "__aiter__": lambda self: self,
1621
+ "__anext__": staticmethod(__anext__),
1622
+ "__repr__": staticmethod(lambda: reprs),
1623
+ "close": staticmethod(close),
1624
+ "aclose": staticmethod(aclose),
1625
+ "closed": staticproperty(lambda: at_end),
1626
+ "peek": staticmethod(peek),
639
1627
  "read": staticmethod(read),
640
1628
  "readinto": staticmethod(readinto),
641
- "__iter__": lambda self, /: self,
642
- "__next__": staticmethod(__next__),
643
- "__repr__": staticmethod(lambda: reprs),
1629
+ "readline": staticmethod(readline),
1630
+ "readlines": staticmethod(readlines),
1631
+ "readable": staticmethod(lambda: True),
1632
+ "seekable": staticmethod(lambda: False),
1633
+ "tell": staticmethod(lambda: pos),
644
1634
  })()
645
1635
 
646
1636