spatialscape 0.3.0__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 (58) hide show
  1. spatialscape/__init__.py +13 -0
  2. spatialscape/_app/assets/array-utils-flat-BL8Cekj1.js +13 -0
  3. spatialscape/_app/assets/blosc-DEOJs6D5.js +18 -0
  4. spatialscape/_app/assets/blosc-Df7TknHH.js +18 -0
  5. spatialscape/_app/assets/chunk-INHXZS53-DAOLzMPX.js +2 -0
  6. spatialscape/_app/assets/chunk-INHXZS53-DfBSTIxr.js +2 -0
  7. spatialscape/_app/assets/deflate-B2AbjNo_.js +2 -0
  8. spatialscape/_app/assets/expression-E359vKZW.js +368 -0
  9. spatialscape/_app/assets/get-attribute-from-layouts-Dd2gixpo.js +2 -0
  10. spatialscape/_app/assets/index-BKPnXJSt.css +1 -0
  11. spatialscape/_app/assets/index-Gxpj1e1n.js +8454 -0
  12. spatialscape/_app/assets/jpeg-b0MwBR_9.js +2 -0
  13. spatialscape/_app/assets/lerc-ujbbHc0C.js +2 -0
  14. spatialscape/_app/assets/lz4-BFJkNc3o.js +16 -0
  15. spatialscape/_app/assets/lz4-DNE3IovK.js +16 -0
  16. spatialscape/_app/assets/lzw-DfRkoFB0.js +2 -0
  17. spatialscape/_app/assets/packbits-BDCGuk_Z.js +2 -0
  18. spatialscape/_app/assets/pako.esm-Ch1S5qKo.js +2 -0
  19. spatialscape/_app/assets/raw-vNZ-QFXZ.js +2 -0
  20. spatialscape/_app/assets/webgl-D2Fkccwd.js +345 -0
  21. spatialscape/_app/assets/webimage-DDU0IC05.js +2 -0
  22. spatialscape/_app/assets/worker-CUy87eWB.js +8 -0
  23. spatialscape/_app/assets/zstd-CGD_7wnU.js +16 -0
  24. spatialscape/_app/assets/zstd-qXhZ_1DN.js +16 -0
  25. spatialscape/_app/favicon.svg +161 -0
  26. spatialscape/_app/index.html +23 -0
  27. spatialscape/build.py +630 -0
  28. spatialscape/cli.py +399 -0
  29. spatialscape/config.py +570 -0
  30. spatialscape/convert.py +321 -0
  31. spatialscape/discover.py +98 -0
  32. spatialscape/expression.py +300 -0
  33. spatialscape/fingerprint.py +152 -0
  34. spatialscape/genes.py +251 -0
  35. spatialscape/geometry.py +132 -0
  36. spatialscape/images.py +240 -0
  37. spatialscape/init.py +578 -0
  38. spatialscape/manifest.py +162 -0
  39. spatialscape/obs.py +48 -0
  40. spatialscape/outlines.py +145 -0
  41. spatialscape/polygons.py +85 -0
  42. spatialscape/rds.py +609 -0
  43. spatialscape/rdsread.py +556 -0
  44. spatialscape/readers.py +195 -0
  45. spatialscape/registry.py +83 -0
  46. spatialscape/serve.py +262 -0
  47. spatialscape/session.py +474 -0
  48. spatialscape/site.py +274 -0
  49. spatialscape/synthetic.py +142 -0
  50. spatialscape/thumbnail.py +95 -0
  51. spatialscape/tracks.py +551 -0
  52. spatialscape/validate.py +193 -0
  53. spatialscape/vocab.py +188 -0
  54. spatialscape-0.3.0.dist-info/METADATA +68 -0
  55. spatialscape-0.3.0.dist-info/RECORD +58 -0
  56. spatialscape-0.3.0.dist-info/WHEEL +4 -0
  57. spatialscape-0.3.0.dist-info/entry_points.txt +3 -0
  58. spatialscape-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,556 @@
1
+ """Read R's serialization format (.rds, and .rda / .RData) without R.
2
+
3
+ Why our own reader: the C++ one (rds2py) aborts on any closure inside an object, and Seurat records the functions it
4
+ was called with; the pure-Python one (rdata) builds one object per string, so an H&E raster of a few million colour
5
+ strings takes minutes and tens of GB. This reader streams the file once (gzip, bzip2, xz or plain; XDR format,
6
+ versions 2 and 3), keeps vectors as numpy arrays, decodes uniform string vectors in bulk, and walks past closures,
7
+ environments, promises, language objects and bytecode without building them. Slots named in `skip` are walked
8
+ past too, which is how a Seurat object's scaled data and graphs stay out of memory.
9
+
10
+ Every value comes back as a dict the readers in rds.py work on:
11
+ {"type": "integer" | "double" | "boolean" | "string" | "complex" | "raw" | "vector" | "S4" | "null" |
12
+ "symbol" | "unsupported" | "skipped",
13
+ "class_name": the S4 class, or "<type>_vector" / "vector",
14
+ "data": numpy array (numbers; strings as a `U` array when uniform) or a list (lists; strings),
15
+ "attributes": {name: value}}
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import bz2
20
+ import gzip
21
+ import io
22
+ import lzma
23
+ import struct
24
+ from collections.abc import Iterable
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ import numpy as np
29
+
30
+ R_NA_INT = -2147483648
31
+ UTF8_MASK = 1 << 3
32
+ LATIN1_MASK = 1 << 2
33
+
34
+ # SEXP types and the serialization-only pseudo types (R's serialize.c)
35
+ NILSXP, SYMSXP, LISTSXP, CLOSXP, ENVSXP, PROMSXP, LANGSXP, SPECIALSXP, BUILTINSXP, CHARSXP = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
36
+ LGLSXP, INTSXP, REALSXP, CPLXSXP, STRSXP, DOTSXP, VECSXP, EXPRSXP, BCODESXP = 10, 13, 14, 15, 16, 17, 19, 20, 21
37
+ EXTPTRSXP, WEAKREFSXP, RAWSXP, S4SXP = 22, 23, 24, 25
38
+ REFSXP, NILVALUE_SXP, GLOBALENV_SXP, UNBOUNDVALUE_SXP, MISSINGARG_SXP = 255, 254, 253, 252, 251
39
+ BASENAMESPACE_SXP, NAMESPACESXP, PACKAGESXP, PERSISTSXP = 250, 249, 248, 247
40
+ CLASSREFSXP, GENERICREFSXP, BCREPDEF, BCREPREF, EMPTYENV_SXP, BASEENV_SXP = 246, 245, 244, 243, 242, 241
41
+ ATTRLANGSXP, ATTRLISTSXP, ALTREP_SXP = 240, 239, 238
42
+
43
+ PAIRLIKE = (LISTSXP, LANGSXP, CLOSXP, PROMSXP, DOTSXP)
44
+ SKIP_PLACEHOLDER = {"type": "skipped"}
45
+ NULL = {"type": "null"}
46
+
47
+
48
+ class RdsError(ValueError):
49
+ pass
50
+
51
+
52
+ def _open(path: Path) -> io.BufferedReader:
53
+ with open(path, "rb") as fh:
54
+ magic = fh.read(6)
55
+ if magic[:2] == b"\x1f\x8b":
56
+ return io.BufferedReader(gzip.open(path, "rb"), buffer_size=1 << 23) # type: ignore[arg-type]
57
+ if magic[:3] == b"BZh":
58
+ return io.BufferedReader(bz2.open(path, "rb"), buffer_size=1 << 23) # type: ignore[arg-type]
59
+ if magic[:6] == b"\xfd7zXZ\x00":
60
+ return io.BufferedReader(lzma.open(path, "rb"), buffer_size=1 << 23) # type: ignore[arg-type]
61
+ return open(path, "rb", buffering=1 << 23)
62
+
63
+
64
+ class Reader:
65
+ def __init__(self, fh: io.BufferedReader, skip: Iterable[str] = ()):
66
+ self.fh = fh
67
+ self.skip = frozenset(skip)
68
+ self.refs: list[Any] = [] # symbols, environments, namespaces, ... in order of first appearance
69
+ self.discard = 0 # > 0 while walking something nobody needs
70
+ self.pending = b"" # bytes read ahead of their use (a string vector guessed uniform that was not)
71
+ self.path: list[str] = [] # slot / attribute names being read, for error messages
72
+
73
+ # ------------------------------------------------------------ raw reads
74
+ def read(self, n: int) -> bytes:
75
+ if self.pending:
76
+ head, self.pending = self.pending[:n], self.pending[n:]
77
+ if len(head) == n:
78
+ return head
79
+ rest = self.fh.read(n - len(head))
80
+ if len(rest) != n - len(head):
81
+ raise RdsError(f"unexpected end of file ({len(head) + len(rest)} of {n} bytes)")
82
+ return head + rest
83
+ b = self.fh.read(n)
84
+ if len(b) != n:
85
+ raise RdsError(f"unexpected end of file ({len(b)} of {n} bytes)")
86
+ return b
87
+
88
+ def read_upto(self, n: int) -> bytes:
89
+ """Up to n bytes (fewer at the end of the file)."""
90
+ head, self.pending = self.pending[:n], self.pending[n:]
91
+ return head if len(head) == n else head + self.fh.read(n - len(head))
92
+
93
+ def i32(self) -> int:
94
+ return struct.unpack(">i", self.read(4))[0]
95
+
96
+ def u32(self) -> int:
97
+ return struct.unpack(">I", self.read(4))[0]
98
+
99
+ def length(self) -> int:
100
+ n = self.i32()
101
+ if n == -1: # a long vector: two more ints
102
+ return (self.u32() << 32) + self.u32()
103
+ return n
104
+
105
+ # ------------------------------------------------------------ entry
106
+ def header(self) -> tuple[int, bool]:
107
+ """Reads the stream header; (format version, rda) where rda means the stream holds named objects."""
108
+ magic = self.read(2)
109
+ rda = False
110
+ if magic in (b"RD", b"RB", b"RA"):
111
+ rest = self.read(3) # "X2\n" / "X3\n"
112
+ if rest[:1] != b"X":
113
+ raise RdsError("only XDR-format .rda files are supported (saved with ascii = FALSE)")
114
+ rda = True
115
+ magic = self.read(2)
116
+ if magic != b"X\n":
117
+ raise RdsError("not an XDR-serialized R object (ascii or native-binary serialization is not supported)")
118
+ version = self.u32()
119
+ self.u32() # R version that wrote it
120
+ self.u32() # minimal R version to read it
121
+ if version == 3:
122
+ n = self.u32()
123
+ self.read(n) # native encoding name
124
+ elif version != 2:
125
+ raise RdsError(f"unsupported serialization version {version}")
126
+ return version, rda
127
+
128
+ # ------------------------------------------------------------ items
129
+ def item(self) -> Any:
130
+ return self.item_flags(self.u32())
131
+
132
+ def item_flags(self, flags: int) -> Any:
133
+ """An item whose flags word has already been read."""
134
+ t = flags & 0xFF
135
+ levels = flags >> 12
136
+ has_attr = bool(flags & 0x200)
137
+ has_tag = bool(flags & 0x400)
138
+
139
+ if t in (NILVALUE_SXP, NILSXP):
140
+ return NULL
141
+ if t == REFSXP:
142
+ idx = flags >> 8
143
+ if idx == 0:
144
+ idx = self.u32()
145
+ return self.refs[idx - 1]
146
+ if t in (EMPTYENV_SXP, BASEENV_SXP, GLOBALENV_SXP, UNBOUNDVALUE_SXP, MISSINGARG_SXP, BASENAMESPACE_SXP):
147
+ return {"type": "unsupported", "class_name": "environment"}
148
+ if t == PERSISTSXP:
149
+ names = self.string_vec_plain()
150
+ obj = {"type": "unsupported", "class_name": "persistent", "data": names}
151
+ self.refs.append(obj)
152
+ return obj
153
+ if t in (NAMESPACESXP, PACKAGESXP):
154
+ names = self.string_vec_plain()
155
+ obj = {"type": "unsupported", "class_name": "namespace" if t == NAMESPACESXP else "package", "data": names}
156
+ self.refs.append(obj)
157
+ return obj
158
+ if t == SYMSXP:
159
+ name = self.charsxp()
160
+ obj = NULL if name == "\x01NULL\x01" else {"type": "symbol", "data": [name]} # an S4 slot holding NULL
161
+ self.refs.append(obj)
162
+ return obj
163
+ if t == ENVSXP:
164
+ obj = {"type": "unsupported", "class_name": "environment"}
165
+ self.refs.append(obj) # before its contents, which may refer back to it
166
+ self.u32() # locked
167
+ self.discard += 1
168
+ try:
169
+ self.item() # enclos
170
+ self.item() # frame
171
+ self.item() # hashtab
172
+ self.item() # attrib
173
+ finally:
174
+ self.discard -= 1
175
+ return obj
176
+ if t in PAIRLIKE:
177
+ return self.pairlist(t, has_attr, has_tag)
178
+ if t == ATTRLISTSXP:
179
+ return self.pairlist(LISTSXP, True, has_tag)
180
+ if t == ATTRLANGSXP:
181
+ return self.pairlist(LANGSXP, True, has_tag)
182
+ if t == EXTPTRSXP:
183
+ obj = {"type": "unsupported", "class_name": "externalptr"}
184
+ self.refs.append(obj)
185
+ self.discard += 1
186
+ try:
187
+ self.item() # prot
188
+ self.item() # tag
189
+ finally:
190
+ self.discard -= 1
191
+ if has_attr:
192
+ self.item()
193
+ return obj
194
+ if t == WEAKREFSXP:
195
+ obj = {"type": "unsupported", "class_name": "weakref"}
196
+ self.refs.append(obj)
197
+ if has_attr:
198
+ self.item()
199
+ return obj
200
+ if t in (SPECIALSXP, BUILTINSXP):
201
+ n = self.i32()
202
+ name = self.read(n).decode("latin-1")
203
+ return {"type": "unsupported", "class_name": "builtin", "data": [name]}
204
+ if t == CHARSXP:
205
+ return {"type": "string", "class_name": "string_vector", "data": [self.charsxp_body(levels)], "attributes": {}}
206
+ if t == BCODESXP:
207
+ self.bytecode()
208
+ obj: dict = {"type": "unsupported", "class_name": "bytecode"}
209
+ if has_attr:
210
+ self.item()
211
+ return obj
212
+ if t == ALTREP_SXP:
213
+ return self.altrep()
214
+ if t == S4SXP:
215
+ attrs = self.attributes() if has_attr else {}
216
+ cls = attrs.get("class", {}).get("data")
217
+ cls_attrs = attrs.get("class", {}).get("attributes", {})
218
+ pkg = cls_attrs.get("package", {}).get("data")
219
+ return {
220
+ "type": "S4", "class_name": str(cls[0]) if cls is not None and len(cls) else None,
221
+ "package_name": str(pkg[0]) if pkg is not None and len(pkg) else None, "attributes": attrs,
222
+ }
223
+
224
+ # vectors
225
+ if t == STRSXP:
226
+ n = self.length()
227
+ data = self.strings(n)
228
+ obj = {"type": "string", "class_name": "string_vector", "data": data}
229
+ elif t in (VECSXP, EXPRSXP):
230
+ n = self.length()
231
+ items = []
232
+ for i in range(n):
233
+ self.path.append(f"[{i}]")
234
+ try:
235
+ items.append(self.item())
236
+ finally:
237
+ self.path.pop()
238
+ obj = {"type": "vector", "class_name": "vector", "data": items}
239
+ elif t == LGLSXP:
240
+ obj = {"type": "boolean", "class_name": "boolean_vector", "data": self.logicals(self.length())}
241
+ elif t == INTSXP:
242
+ obj = {"type": "integer", "class_name": "integer_vector", "data": self.numbers(self.length(), ">i4", np.int32)}
243
+ elif t == REALSXP:
244
+ obj = {"type": "double", "class_name": "double_vector", "data": self.numbers(self.length(), ">f8", np.float64)}
245
+ elif t == CPLXSXP:
246
+ obj = {"type": "complex", "class_name": "complex_vector", "data": self.numbers(self.length(), ">c16", np.complex128)}
247
+ elif t == RAWSXP:
248
+ n = self.length()
249
+ obj = {"type": "raw", "class_name": "raw_vector", "data": self.read(n) if not self.discard else self.skip_bytes(n)}
250
+ else:
251
+ raise RdsError(f"unsupported SEXP type {t} at {'/'.join(self.path) or 'top'}")
252
+ obj["attributes"] = self.attributes() if has_attr else {}
253
+ if self.discard:
254
+ return SKIP_PLACEHOLDER
255
+ return obj
256
+
257
+ # ------------------------------------------------------------ pieces
258
+ def pairlist(self, t: int, has_attr: bool, has_tag: bool) -> Any:
259
+ """A dotted pair chain. As attributes it is returned as a dict elsewhere; as data, a named list. For
260
+ closures, promises and calls the pieces are walked and dropped."""
261
+ if t != LISTSXP:
262
+ self.discard += 1
263
+ try:
264
+ if has_attr:
265
+ self.item()
266
+ if has_tag:
267
+ self.item()
268
+ self.item() # car (formals / value / function)
269
+ self.item() # cdr (body / expression / arguments)
270
+ finally:
271
+ self.discard -= 1
272
+ return {"type": "unsupported", "class_name": {CLOSXP: "closure", PROMSXP: "promise", LANGSXP: "language", DOTSXP: "dots"}[t]}
273
+ names: list[str | None] = []
274
+ values: list[Any] = []
275
+ attrs: dict = {}
276
+ first = True
277
+ while True:
278
+ if first:
279
+ a, g = has_attr, has_tag
280
+ first = False
281
+ else:
282
+ flags = self.u32()
283
+ tt = flags & 0xFF
284
+ if tt in (NILVALUE_SXP, NILSXP):
285
+ break
286
+ if tt == REFSXP: # a shared tail, unusual
287
+ idx = flags >> 8
288
+ if idx == 0:
289
+ idx = self.u32()
290
+ tail = self.refs[idx - 1]
291
+ if isinstance(tail, dict) and tail.get("type") == "vector":
292
+ values.extend(tail["data"])
293
+ names.extend(tail.get("attributes", {}).get("names", {}).get("data", [None] * len(tail["data"])))
294
+ break
295
+ if tt not in (LISTSXP, ATTRLISTSXP): # a dotted pair: the tail is a plain value (an ALTREP state, for one)
296
+ values.append(self.item_flags(flags))
297
+ names.append(None)
298
+ break
299
+ a, g = bool(flags & 0x200) or tt == ATTRLISTSXP, bool(flags & 0x400)
300
+ node_attrs = self.attributes() if a else {}
301
+ if not attrs:
302
+ attrs = node_attrs
303
+ tag = None
304
+ if g:
305
+ sym = self.item()
306
+ tag = sym["data"][0] if isinstance(sym, dict) and sym.get("type") == "symbol" else None
307
+ self.path.append(tag if tag is not None else f"({len(values)})")
308
+ try:
309
+ if tag is not None and tag in self.skip and not self.discard:
310
+ self.discard += 1
311
+ try:
312
+ self.item()
313
+ finally:
314
+ self.discard -= 1
315
+ values.append(SKIP_PLACEHOLDER)
316
+ else:
317
+ values.append(self.item())
318
+ finally:
319
+ self.path.pop()
320
+ names.append(tag)
321
+ if self.discard:
322
+ return SKIP_PLACEHOLDER
323
+ out = {"type": "vector", "class_name": "vector", "data": values, "attributes": dict(attrs)}
324
+ if any(n is not None for n in names):
325
+ out["attributes"]["names"] = {"type": "string", "class_name": "string_vector", "data": [n or "" for n in names], "attributes": {}}
326
+ return out
327
+
328
+ def attributes(self) -> dict:
329
+ """The attribute pairlist that follows an object -> {name: value}."""
330
+ node = self.item()
331
+ if node is NULL or node is SKIP_PLACEHOLDER or not isinstance(node, dict):
332
+ return {}
333
+ if node.get("type") != "vector":
334
+ return {}
335
+ names = node.get("attributes", {}).get("names", {}).get("data", [])
336
+ return {str(n): v for n, v in zip(names, node["data"], strict=False) if n}
337
+
338
+ def charsxp(self) -> str | None:
339
+ flags = self.u32()
340
+ if (flags & 0xFF) != CHARSXP:
341
+ raise RdsError("expected a CHARSXP")
342
+ return self.charsxp_body(flags >> 12)
343
+
344
+ def charsxp_body(self, levels: int) -> str | None:
345
+ n = self.i32()
346
+ if n == -1:
347
+ return None
348
+ b = self.read(n)
349
+ return b.decode("latin-1") if (levels & LATIN1_MASK and not levels & UTF8_MASK) else b.decode("utf-8", "replace")
350
+
351
+ def string_vec_plain(self) -> list[str | None]:
352
+ """InStringVec: used for namespace / package / persistent names."""
353
+ if self.u32() != 0:
354
+ raise RdsError("malformed string vector")
355
+ n = self.i32()
356
+ return [self.charsxp() for _ in range(n)]
357
+
358
+ def strings(self, n: int) -> Any:
359
+ """A STRSXP body. Vectors whose strings all have the same length and encoding (hex colours of a raster,
360
+ barcodes) are read in one go and come back as a numpy `U` array; anything else as a list with None for NA."""
361
+ if n == 0:
362
+ return []
363
+ if self.discard:
364
+ for _ in range(n):
365
+ flags = self.u32()
366
+ m = self.i32()
367
+ if m > 0:
368
+ self.skip_bytes(m)
369
+ return []
370
+ first_flags = self.u32()
371
+ m = self.i32()
372
+ if m < 0 or m > 64 or n < 64:
373
+ first = None if m < 0 else self._decode(self.read(m), first_flags >> 12)
374
+ out = [first]
375
+ out.extend(self.charsxp() for _ in range(n - 1))
376
+ return out
377
+ first_bytes = self.read(m)
378
+ stride = 8 + m
379
+ buf = self.read_upto(stride * (n - 1))
380
+ if len(buf) == stride * (n - 1):
381
+ arr = np.frombuffer(buf, dtype=np.dtype([("flags", ">u4"), ("len", ">i4"), ("s", f"S{m}")]))
382
+ if np.all(arr["flags"] == first_flags) and np.all(arr["len"] == m):
383
+ s = np.concatenate([np.array([first_bytes], dtype=f"S{m}"), arr["s"]])
384
+ try:
385
+ return s.astype(f"U{m}") if not (first_flags >> 12) & LATIN1_MASK else np.array([x.decode("latin-1") for x in s])
386
+ except UnicodeDecodeError:
387
+ return [x.decode("utf-8", "replace") for x in s]
388
+ # not uniform after all: parse the buffered bytes item by item, reading on from the stream when an item runs
389
+ # past them, and hand back whatever is left over (it belongs to the next object)
390
+ pos = 0
391
+ total = len(buf)
392
+
393
+ def take(k: int) -> bytes:
394
+ nonlocal pos
395
+ if pos + k <= total:
396
+ piece = buf[pos : pos + k]
397
+ pos += k
398
+ return piece
399
+ piece = buf[pos:]
400
+ pos = total
401
+ return piece + self.read(k - len(piece))
402
+
403
+ out: list[str | None] = [self._decode(first_bytes, first_flags >> 12)]
404
+ for _ in range(n - 1):
405
+ flags, m2 = struct.unpack(">Ii", take(8))
406
+ out.append(None if m2 < 0 else self._decode(take(m2), flags >> 12))
407
+ if pos < total:
408
+ self.pending = buf[pos:] + self.pending
409
+ return out
410
+
411
+ @staticmethod
412
+ def _decode(b: bytes, levels: int) -> str:
413
+ return b.decode("latin-1") if (levels & LATIN1_MASK and not levels & UTF8_MASK) else b.decode("utf-8", "replace")
414
+
415
+ def numbers(self, n: int, dtype: str, native: Any) -> np.ndarray:
416
+ size = np.dtype(dtype).itemsize
417
+ if self.discard:
418
+ self.skip_bytes(n * size)
419
+ return np.zeros(0, dtype=native)
420
+ return np.frombuffer(self.read(n * size), dtype=dtype).astype(native)
421
+
422
+ def logicals(self, n: int) -> np.ndarray:
423
+ raw = self.numbers(n, ">i4", np.int32)
424
+ if self.discard:
425
+ return raw
426
+ na = raw == R_NA_INT
427
+ if na.any(): # NA logicals as NaN among 0/1 (what the readers expect)
428
+ out = raw.astype(np.float64)
429
+ out[na] = np.nan
430
+ return out
431
+ return raw.astype(bool)
432
+
433
+ def skip_bytes(self, n: int) -> bytes:
434
+ if self.pending:
435
+ head, self.pending = self.pending[:n], self.pending[n:]
436
+ n -= len(head)
437
+ while n > 0:
438
+ chunk = self.fh.read(min(n, 1 << 24))
439
+ if not chunk:
440
+ raise RdsError("unexpected end of file")
441
+ n -= len(chunk)
442
+ return b""
443
+
444
+ # ------------------------------------------------------------ ALTREP and bytecode
445
+ def altrep(self) -> Any:
446
+ info = self.item()
447
+ state = self.item()
448
+ attr = self.item()
449
+ attrs: dict = {}
450
+ if isinstance(attr, dict) and attr.get("type") == "vector":
451
+ names = attr.get("attributes", {}).get("names", {}).get("data", [])
452
+ attrs = {str(k): v for k, v in zip(names, attr["data"], strict=False) if k}
453
+ cls = ""
454
+ if isinstance(info, dict) and info.get("type") == "vector" and info["data"]:
455
+ head = info["data"][0]
456
+ cls = head["data"][0] if isinstance(head, dict) and head.get("type") == "symbol" else ""
457
+ if self.discard:
458
+ return SKIP_PLACEHOLDER
459
+ if cls in ("compact_intseq", "compact_realseq"):
460
+ n, start, step = (float(v) for v in state["data"][:3])
461
+ arr = start + step * np.arange(int(n))
462
+ if cls == "compact_intseq":
463
+ return {"type": "integer", "class_name": "integer_vector", "data": arr.astype(np.int32), "attributes": attrs}
464
+ return {"type": "double", "class_name": "double_vector", "data": arr.astype(np.float64), "attributes": attrs}
465
+ if cls == "deferred_string":
466
+ arg = state["data"][0] if isinstance(state, dict) and state.get("type") == "vector" else state
467
+ vals = arg.get("data", []) if isinstance(arg, dict) else []
468
+ strs = [None if (isinstance(v, (int, np.integer)) and int(v) == R_NA_INT) or (isinstance(v, float) and np.isnan(v)) else (str(int(v)) if float(v).is_integer() else repr(float(v))) for v in vals]
469
+ return {"type": "string", "class_name": "string_vector", "data": strs, "attributes": attrs}
470
+ if cls.startswith("wrap_"):
471
+ inner = state["data"][0] if isinstance(state, dict) and state.get("type") == "vector" else state
472
+ if isinstance(inner, dict):
473
+ inner = dict(inner)
474
+ inner["attributes"] = {**inner.get("attributes", {}), **attrs}
475
+ return inner
476
+ raise RdsError(f"unsupported ALTREP class {cls!r}")
477
+
478
+ def bytecode(self) -> None:
479
+ reps = self.i32()
480
+ self._bc_reps: list[Any] = [None] * max(reps, 0)
481
+ self.discard += 1
482
+ try:
483
+ self.bytecode1()
484
+ finally:
485
+ self.discard -= 1
486
+
487
+ def bytecode1(self) -> None:
488
+ self.item() # the code (an integer vector)
489
+ n = self.i32()
490
+ for _ in range(n):
491
+ t = self.i32()
492
+ if t == BCODESXP:
493
+ self.bytecode1()
494
+ elif t in (LANGSXP, LISTSXP, BCREPDEF, BCREPREF, ATTRLANGSXP, ATTRLISTSXP):
495
+ self.bc_lang(t)
496
+ else:
497
+ self.item()
498
+
499
+ def bc_lang(self, t: int) -> None:
500
+ if t == BCREPREF:
501
+ self.i32()
502
+ return
503
+ if t in (BCREPDEF, LANGSXP, LISTSXP, ATTRLANGSXP, ATTRLISTSXP):
504
+ if t == BCREPDEF:
505
+ self.i32() # position
506
+ t = self.i32()
507
+ if t in (ATTRLANGSXP, ATTRLISTSXP):
508
+ self.item() # attributes
509
+ self.item() # tag
510
+ self.bc_lang(self.i32()) # car
511
+ self.bc_lang(self.i32()) # cdr
512
+ return
513
+ self.item()
514
+
515
+
516
+ def read_rds(path: Path | str, *, skip: Iterable[str] = ()) -> Any:
517
+ """One object from an .rds file."""
518
+ with _open(Path(path)) as fh:
519
+ r = Reader(fh, skip)
520
+ _, rda = r.header()
521
+ if rda:
522
+ raise RdsError(f"{path}: an .rda file; use read_rda")
523
+ try:
524
+ return r.item()
525
+ except (RdsError, struct.error) as e:
526
+ raise RdsError(f"{path}: {e} (while reading {'/'.join(r.path) or 'the top-level object'})") from None
527
+
528
+
529
+ def read_rda(path: Path | str, *, skip: Iterable[str] = (), only: str | None = None) -> dict[str, Any]:
530
+ """The named objects of an .rda / .RData file -> {name: object}. With `only`, the others are walked past."""
531
+ with _open(Path(path)) as fh:
532
+ r = Reader(fh, skip)
533
+ _, rda = r.header()
534
+ if not rda:
535
+ raise RdsError(f"{path}: an .rds file; use read_rds")
536
+ out: dict[str, Any] = {}
537
+ node_flags = r.u32()
538
+ while (node_flags & 0xFF) in (LISTSXP, ATTRLISTSXP):
539
+ has_attr = bool(node_flags & 0x200) or (node_flags & 0xFF) == ATTRLISTSXP
540
+ has_tag = bool(node_flags & 0x400)
541
+ if has_attr:
542
+ r.attributes()
543
+ name = None
544
+ if has_tag:
545
+ sym = r.item()
546
+ name = sym["data"][0] if isinstance(sym, dict) and sym.get("type") == "symbol" else None
547
+ if only is not None and name != only:
548
+ r.discard += 1
549
+ try:
550
+ r.item()
551
+ finally:
552
+ r.discard -= 1
553
+ else:
554
+ out[str(name)] = r.item()
555
+ node_flags = r.u32()
556
+ return out