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.
- spatialscape/__init__.py +13 -0
- spatialscape/_app/assets/array-utils-flat-BL8Cekj1.js +13 -0
- spatialscape/_app/assets/blosc-DEOJs6D5.js +18 -0
- spatialscape/_app/assets/blosc-Df7TknHH.js +18 -0
- spatialscape/_app/assets/chunk-INHXZS53-DAOLzMPX.js +2 -0
- spatialscape/_app/assets/chunk-INHXZS53-DfBSTIxr.js +2 -0
- spatialscape/_app/assets/deflate-B2AbjNo_.js +2 -0
- spatialscape/_app/assets/expression-E359vKZW.js +368 -0
- spatialscape/_app/assets/get-attribute-from-layouts-Dd2gixpo.js +2 -0
- spatialscape/_app/assets/index-BKPnXJSt.css +1 -0
- spatialscape/_app/assets/index-Gxpj1e1n.js +8454 -0
- spatialscape/_app/assets/jpeg-b0MwBR_9.js +2 -0
- spatialscape/_app/assets/lerc-ujbbHc0C.js +2 -0
- spatialscape/_app/assets/lz4-BFJkNc3o.js +16 -0
- spatialscape/_app/assets/lz4-DNE3IovK.js +16 -0
- spatialscape/_app/assets/lzw-DfRkoFB0.js +2 -0
- spatialscape/_app/assets/packbits-BDCGuk_Z.js +2 -0
- spatialscape/_app/assets/pako.esm-Ch1S5qKo.js +2 -0
- spatialscape/_app/assets/raw-vNZ-QFXZ.js +2 -0
- spatialscape/_app/assets/webgl-D2Fkccwd.js +345 -0
- spatialscape/_app/assets/webimage-DDU0IC05.js +2 -0
- spatialscape/_app/assets/worker-CUy87eWB.js +8 -0
- spatialscape/_app/assets/zstd-CGD_7wnU.js +16 -0
- spatialscape/_app/assets/zstd-qXhZ_1DN.js +16 -0
- spatialscape/_app/favicon.svg +161 -0
- spatialscape/_app/index.html +23 -0
- spatialscape/build.py +630 -0
- spatialscape/cli.py +399 -0
- spatialscape/config.py +570 -0
- spatialscape/convert.py +321 -0
- spatialscape/discover.py +98 -0
- spatialscape/expression.py +300 -0
- spatialscape/fingerprint.py +152 -0
- spatialscape/genes.py +251 -0
- spatialscape/geometry.py +132 -0
- spatialscape/images.py +240 -0
- spatialscape/init.py +578 -0
- spatialscape/manifest.py +162 -0
- spatialscape/obs.py +48 -0
- spatialscape/outlines.py +145 -0
- spatialscape/polygons.py +85 -0
- spatialscape/rds.py +609 -0
- spatialscape/rdsread.py +556 -0
- spatialscape/readers.py +195 -0
- spatialscape/registry.py +83 -0
- spatialscape/serve.py +262 -0
- spatialscape/session.py +474 -0
- spatialscape/site.py +274 -0
- spatialscape/synthetic.py +142 -0
- spatialscape/thumbnail.py +95 -0
- spatialscape/tracks.py +551 -0
- spatialscape/validate.py +193 -0
- spatialscape/vocab.py +188 -0
- spatialscape-0.3.0.dist-info/METADATA +68 -0
- spatialscape-0.3.0.dist-info/RECORD +58 -0
- spatialscape-0.3.0.dist-info/WHEEL +4 -0
- spatialscape-0.3.0.dist-info/entry_points.txt +3 -0
- spatialscape-0.3.0.dist-info/licenses/LICENSE +21 -0
spatialscape/rdsread.py
ADDED
|
@@ -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
|