rapydscript-ns 0.9.0 → 0.9.1

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.
package/src/lib/io.pyj ADDED
@@ -0,0 +1,500 @@
1
+ ###########################################################
2
+ # RapydScript Standard Library
3
+ # Author: RapydScript-NS Contributors
4
+ # Copyright 2024 RapydScript-NS Contributors
5
+ # License: Apache License 2.0
6
+ ###########################################################
7
+
8
+ # Python-compatible io module.
9
+ #
10
+ # Provides StringIO and BytesIO — in-memory file-like objects that implement
11
+ # the same read/write/seek interface as Python's io.StringIO and io.BytesIO.
12
+ #
13
+ # Usage:
14
+ # from io import StringIO, BytesIO, UnsupportedOperation
15
+ #
16
+ # sio = StringIO('hello')
17
+ # sio.seek(0)
18
+ # print(sio.read()) # 'hello'
19
+ #
20
+ # bio = BytesIO(bytes([72, 105]))
21
+ # bio.seek(0)
22
+ # print(bio.read()) # bytes([72, 105])
23
+ #
24
+ # Implementation notes:
25
+ # - StringIO is backed by a plain JS string with an integer position cursor.
26
+ # - BytesIO is backed by a plain JS array of integers (0-255), with read()
27
+ # returning a bytes() object.
28
+ # - write() at a position in the middle of the buffer overwrites bytes in
29
+ # place (matching Python semantics); write() past the end zero-pads.
30
+ # - The `closed` attribute is a plain instance variable (not a property),
31
+ # since RapydScript does not support @property descriptors.
32
+ # - `newline` parameter to StringIO is accepted for API compatibility but
33
+ # not processed (universal newline translation is not performed).
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Internal JS helper
38
+ # ---------------------------------------------------------------------------
39
+
40
+ v"""
41
+ // Convert a bytes-like value to a plain JS array of integers.
42
+ // Handles: ρσ_bytes / ρσ_bytearray (._data), Uint8Array, Int8Array,
43
+ // plain JS Array, and strings (latin-1 codepoints).
44
+ function _io_bytes_to_array(b) {
45
+ if (b === null || b === undefined) return [];
46
+ // RapydScript bytes / bytearray carry their data in ._data
47
+ if (b._data !== undefined && b._data !== null) {
48
+ return b._data.slice();
49
+ }
50
+ if (b instanceof Uint8Array || b instanceof Int8Array) {
51
+ var r = [];
52
+ for (var i = 0; i < b.length; i++) r.push(b[i] & 0xFF);
53
+ return r;
54
+ }
55
+ if (Array.isArray(b)) {
56
+ return b.slice();
57
+ }
58
+ if (typeof b === 'string') {
59
+ var r2 = [];
60
+ for (var j = 0; j < b.length; j++) r2.push(b.charCodeAt(j) & 0xFF);
61
+ return r2;
62
+ }
63
+ // fallback: try length-indexed access
64
+ var r3 = [];
65
+ for (var k = 0; k < (b.length || 0); k++) r3.push((b[k] | 0) & 0xFF);
66
+ return r3;
67
+ }
68
+ """
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Exceptions
73
+ # ---------------------------------------------------------------------------
74
+
75
+ class UnsupportedOperation(Exception):
76
+ """Raised when an unsupported IO operation is attempted."""
77
+ pass
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Base class
82
+ # ---------------------------------------------------------------------------
83
+
84
+ class IOBase:
85
+ """Minimal base class shared by StringIO and BytesIO."""
86
+
87
+ def __init__(self):
88
+ self.closed = False
89
+
90
+ def _check_closed(self):
91
+ if self.closed:
92
+ raise ValueError('I/O operation on closed file.')
93
+
94
+ def close(self):
95
+ """Mark the stream as closed."""
96
+ self.closed = True
97
+
98
+ def readable(self):
99
+ """Return True — StringIO/BytesIO are always readable."""
100
+ return True
101
+
102
+ def writable(self):
103
+ """Return True — StringIO/BytesIO are always writable."""
104
+ return True
105
+
106
+ def seekable(self):
107
+ """Return True — StringIO/BytesIO are always seekable."""
108
+ return True
109
+
110
+ def __enter__(self):
111
+ self._check_closed()
112
+ return self
113
+
114
+ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
115
+ self.close()
116
+ return False
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # StringIO
121
+ # ---------------------------------------------------------------------------
122
+
123
+ class StringIO(IOBase):
124
+ """
125
+ In-memory text stream backed by a string.
126
+
127
+ Mirrors the interface of Python's io.StringIO:
128
+ read([size]) — read up to *size* characters (-1 or None = all)
129
+ readline([size]) — read one line (up to *size* chars)
130
+ readlines([hint]) — read all lines; stop early when total ≥ hint
131
+ write(s) — write string *s* at the current position
132
+ writelines(lines) — write each item in *lines*
133
+ getvalue() — return the full buffer contents
134
+ seek(pos[, whence]) — reposition (0=absolute, 1=relative, 2=from end)
135
+ tell() — return the current position
136
+ truncate([pos]) — truncate buffer at *pos* (default: current pos)
137
+ close() — mark the stream as closed
138
+ closed — True after close()
139
+
140
+ Example::
141
+
142
+ sio = StringIO()
143
+ sio.write('hello ')
144
+ sio.write('world')
145
+ sio.seek(0)
146
+ print(sio.read()) # 'hello world'
147
+
148
+ The stream also works as a context manager::
149
+
150
+ with StringIO('data') as f:
151
+ text = f.read()
152
+ """
153
+
154
+ def __init__(self, initial_value='', newline='\n'):
155
+ IOBase.__init__(self)
156
+ self._buf = str(initial_value) if initial_value is not None else ''
157
+ self._pos = 0
158
+
159
+ def getvalue(self):
160
+ """Return the entire contents of the buffer as a string."""
161
+ self._check_closed()
162
+ return self._buf
163
+
164
+ def read(self, size=-1):
165
+ """
166
+ Read up to *size* characters from the current position.
167
+
168
+ If *size* is -1 or None, read until end of stream.
169
+ Returns the data as a string.
170
+ """
171
+ self._check_closed()
172
+ buflen = len(self._buf)
173
+ if self._pos >= buflen:
174
+ return ''
175
+ if size is None or size < 0:
176
+ result = self._buf[self._pos:]
177
+ self._pos = buflen
178
+ else:
179
+ end = min(self._pos + size, buflen)
180
+ result = self._buf[self._pos:end]
181
+ self._pos = end
182
+ return result
183
+
184
+ def readline(self, size=-1):
185
+ """
186
+ Read until a newline or end of stream.
187
+
188
+ If *size* is given and non-negative, read at most *size* characters.
189
+ The trailing newline is included in the returned string.
190
+ """
191
+ self._check_closed()
192
+ buflen = len(self._buf)
193
+ if self._pos >= buflen:
194
+ return ''
195
+ nl = self._buf.indexOf('\n', self._pos)
196
+ if nl == -1:
197
+ end = buflen
198
+ else:
199
+ end = nl + 1
200
+ if size is not None and size >= 0:
201
+ limit = self._pos + size
202
+ if limit < end:
203
+ end = limit
204
+ result = self._buf[self._pos:end]
205
+ self._pos = end
206
+ return result
207
+
208
+ def readlines(self, hint=-1):
209
+ """
210
+ Read and return a list of lines from the stream.
211
+
212
+ *hint* is the approximate number of bytes/characters to read;
213
+ reading stops once the accumulated size reaches or exceeds *hint*.
214
+ """
215
+ self._check_closed()
216
+ lines = []
217
+ total = 0
218
+ while self._pos < len(self._buf):
219
+ line = self.readline()
220
+ lines.append(line)
221
+ total += len(line)
222
+ if hint is not None and hint >= 0 and total >= hint:
223
+ break
224
+ return lines
225
+
226
+ def write(self, s):
227
+ """
228
+ Write the string *s* to the stream at the current position.
229
+
230
+ The position is advanced by the number of characters written.
231
+ Returns the number of characters written.
232
+ """
233
+ self._check_closed()
234
+ s = str(s)
235
+ n = len(s)
236
+ if n == 0:
237
+ return 0
238
+ pos = self._pos
239
+ buflen = len(self._buf)
240
+ if pos >= buflen:
241
+ # Append (may need zero-padding for gaps)
242
+ if pos > buflen:
243
+ # Gap: fill with null characters like Python does
244
+ self._buf += v'"\x00".repeat(pos - buflen)'
245
+ self._buf += s
246
+ else:
247
+ # Overwrite in place (do not extend by skipping)
248
+ self._buf = self._buf[:pos] + s + self._buf[pos + n:]
249
+ self._pos = pos + n
250
+ return n
251
+
252
+ def writelines(self, lines):
253
+ """Write each item from *lines* to the stream. No newlines are added."""
254
+ self._check_closed()
255
+ for line in lines:
256
+ self.write(line)
257
+
258
+ def seek(self, pos, whence=0):
259
+ """
260
+ Change the stream position.
261
+
262
+ *whence* controls the reference point:
263
+ 0 — absolute position (default)
264
+ 1 — relative to the current position
265
+ 2 — relative to the end of the stream
266
+ Returns the new absolute position.
267
+ """
268
+ self._check_closed()
269
+ if whence == 0:
270
+ new_pos = pos
271
+ elif whence == 1:
272
+ new_pos = self._pos + pos
273
+ elif whence == 2:
274
+ new_pos = len(self._buf) + pos
275
+ else:
276
+ raise ValueError('Invalid whence: ' + str(whence))
277
+ self._pos = max(0, new_pos)
278
+ return self._pos
279
+
280
+ def tell(self):
281
+ """Return the current stream position."""
282
+ self._check_closed()
283
+ return self._pos
284
+
285
+ def truncate(self, pos=None):
286
+ """
287
+ Truncate the file to at most *pos* characters.
288
+
289
+ If *pos* is omitted or None, truncate at the current position.
290
+ Returns the new size. The stream position is not changed.
291
+ """
292
+ self._check_closed()
293
+ if pos is None:
294
+ pos = self._pos
295
+ if pos < 0:
296
+ raise ValueError('truncate size must be non-negative')
297
+ self._buf = self._buf[:pos]
298
+ return pos
299
+
300
+ def __iter__(self):
301
+ """Iterate over lines (starting from the current position)."""
302
+ self._check_closed()
303
+ return iter(self.readlines())
304
+
305
+ def __repr__(self):
306
+ return '<_io.StringIO object>'
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # BytesIO
311
+ # ---------------------------------------------------------------------------
312
+
313
+ class BytesIO(IOBase):
314
+ """
315
+ In-memory binary stream backed by a bytes buffer.
316
+
317
+ Mirrors the interface of Python's io.BytesIO:
318
+ read([size]) — return up to *size* bytes (-1 or None = all)
319
+ readline([size]) — read one line of bytes
320
+ readlines([hint]) — read all lines as a list of bytes objects
321
+ write(b) — write bytes-like object *b* at current position
322
+ writelines(lines) — write each item in *lines*
323
+ getvalue() — return the full buffer as a bytes object
324
+ seek(pos[, whence]) — reposition (0=absolute, 1=relative, 2=from end)
325
+ tell() — return the current position
326
+ truncate([pos]) — truncate buffer at *pos* (default: current pos)
327
+ close() — mark the stream as closed
328
+ closed — True after close()
329
+
330
+ The *initial_bytes* argument may be a ``bytes``, ``bytearray``,
331
+ ``Uint8Array``, or a plain list of integers.
332
+
333
+ Example::
334
+
335
+ bio = BytesIO()
336
+ bio.write(bytes([1, 2, 3]))
337
+ bio.seek(0)
338
+ data = bio.read() # bytes([1, 2, 3])
339
+
340
+ The stream also works as a context manager::
341
+
342
+ with BytesIO(bytes([0x48, 0x69])) as f:
343
+ f.seek(0)
344
+ print(f.read()) # bytes([72, 105])
345
+ """
346
+
347
+ def __init__(self, initial_bytes=None):
348
+ IOBase.__init__(self)
349
+ self._pos = 0
350
+ if initial_bytes is None:
351
+ self._data = []
352
+ else:
353
+ self._data = v'_io_bytes_to_array(initial_bytes)'
354
+
355
+ def getvalue(self):
356
+ """Return the entire contents of the buffer as a bytes object."""
357
+ self._check_closed()
358
+ return bytes(self._data)
359
+
360
+ def read(self, size=-1):
361
+ """
362
+ Read up to *size* bytes from the current position.
363
+
364
+ If *size* is -1 or None, read until end of stream.
365
+ Returns a bytes object.
366
+ """
367
+ self._check_closed()
368
+ datalen = self._data.length
369
+ if self._pos >= datalen:
370
+ return bytes()
371
+ if size is None or size < 0:
372
+ result = self._data.slice(self._pos)
373
+ self._pos = datalen
374
+ else:
375
+ end = min(self._pos + size, datalen)
376
+ result = self._data.slice(self._pos, end)
377
+ self._pos = end
378
+ return bytes(result)
379
+
380
+ def readline(self, size=-1):
381
+ """
382
+ Read bytes until a b'\\n' byte or end of stream.
383
+
384
+ If *size* is given and non-negative, read at most *size* bytes.
385
+ The trailing newline byte (0x0a) is included in the result.
386
+ Returns a bytes object.
387
+ """
388
+ self._check_closed()
389
+ datalen = self._data.length
390
+ if self._pos >= datalen:
391
+ return bytes()
392
+ # search for newline (0x0a)
393
+ end = datalen
394
+ for v'var i = this._pos; i < datalen; i++':
395
+ if self._data[i] == 10:
396
+ end = i + 1
397
+ break
398
+ if size is not None and size >= 0:
399
+ limit = self._pos + size
400
+ if limit < end:
401
+ end = limit
402
+ result = self._data.slice(self._pos, end)
403
+ self._pos = end
404
+ return bytes(result)
405
+
406
+ def readlines(self, hint=-1):
407
+ """
408
+ Read and return a list of bytes objects, one per line.
409
+
410
+ Reading stops once the accumulated byte count reaches or exceeds *hint*
411
+ (if *hint* is positive).
412
+ """
413
+ self._check_closed()
414
+ lines = []
415
+ total = 0
416
+ while self._pos < self._data.length:
417
+ line = self.readline()
418
+ lines.append(line)
419
+ total += len(line)
420
+ if hint is not None and hint >= 0 and total >= hint:
421
+ break
422
+ return lines
423
+
424
+ def write(self, b):
425
+ """
426
+ Write bytes-like object *b* to the stream at the current position.
427
+
428
+ If the write goes past the end of the current buffer, the buffer is
429
+ zero-extended. Returns the number of bytes written.
430
+ """
431
+ self._check_closed()
432
+ arr = v'_io_bytes_to_array(b)'
433
+ n = arr.length
434
+ if n == 0:
435
+ return 0
436
+ pos = self._pos
437
+ end_pos = pos + n
438
+ # Zero-extend if needed
439
+ while self._data.length < end_pos:
440
+ self._data.push(0)
441
+ for v'var i = 0; i < n; i++':
442
+ self._data[pos + i] = arr[i]
443
+ self._pos = end_pos
444
+ return n
445
+
446
+ def writelines(self, lines):
447
+ """Write each bytes-like item from *lines* to the stream."""
448
+ self._check_closed()
449
+ for line in lines:
450
+ self.write(line)
451
+
452
+ def seek(self, pos, whence=0):
453
+ """
454
+ Change the stream position.
455
+
456
+ *whence* controls the reference point:
457
+ 0 — absolute position (default)
458
+ 1 — relative to the current position
459
+ 2 — relative to the end of the stream
460
+ Returns the new absolute position.
461
+ """
462
+ self._check_closed()
463
+ if whence == 0:
464
+ new_pos = pos
465
+ elif whence == 1:
466
+ new_pos = self._pos + pos
467
+ elif whence == 2:
468
+ new_pos = self._data.length + pos
469
+ else:
470
+ raise ValueError('Invalid whence: ' + str(whence))
471
+ self._pos = max(0, new_pos)
472
+ return self._pos
473
+
474
+ def tell(self):
475
+ """Return the current stream position."""
476
+ self._check_closed()
477
+ return self._pos
478
+
479
+ def truncate(self, pos=None):
480
+ """
481
+ Truncate the stream to at most *pos* bytes.
482
+
483
+ If *pos* is omitted or None, truncate at the current position.
484
+ Returns the new size. The stream position is not changed.
485
+ """
486
+ self._check_closed()
487
+ if pos is None:
488
+ pos = self._pos
489
+ if pos < 0:
490
+ raise ValueError('truncate size must be non-negative')
491
+ self._data = self._data.slice(0, pos)
492
+ return pos
493
+
494
+ def __iter__(self):
495
+ """Iterate over lines (starting from the current position)."""
496
+ self._check_closed()
497
+ return iter(self.readlines())
498
+
499
+ def __repr__(self):
500
+ return '<_io.BytesIO object>'