arrayfile 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {arrayfile-0.1.0 → arrayfile-0.1.1}/PKG-INFO +3 -3
- {arrayfile-0.1.0 → arrayfile-0.1.1}/README.md +2 -1
- {arrayfile-0.1.0 → arrayfile-0.1.1}/pyproject.toml +2 -3
- {arrayfile-0.1.0 → arrayfile-0.1.1}/src/arrayfile/array.py +135 -66
- {arrayfile-0.1.0 → arrayfile-0.1.1}/LICENSE.md +0 -0
- {arrayfile-0.1.0 → arrayfile-0.1.1}/src/arrayfile/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arrayfile
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Arrays backed by disk
|
|
5
5
|
Keywords: logging,terminal,scrollback,indexing
|
|
6
6
|
Author-email: Gareth Davidson <gaz@bitplane.net>
|
|
@@ -8,7 +8,6 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Classifier: Development Status :: 3 - Alpha
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
11
|
Classifier: Programming Language :: Python :: 3
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -44,7 +43,8 @@ pip install arrayfile
|
|
|
44
43
|
|
|
45
44
|
### 🗑 Temporary Array
|
|
46
45
|
|
|
47
|
-
This creates an array in your temp dir
|
|
46
|
+
This creates an array in your temp dir. Its file is deleted when the array is
|
|
47
|
+
closed or garbage-collected. Arrays created with an explicit filename are kept:
|
|
48
48
|
|
|
49
49
|
```python
|
|
50
50
|
from arrayfile import Array
|
|
@@ -15,7 +15,8 @@ pip install arrayfile
|
|
|
15
15
|
|
|
16
16
|
### 🗑 Temporary Array
|
|
17
17
|
|
|
18
|
-
This creates an array in your temp dir
|
|
18
|
+
This creates an array in your temp dir. Its file is deleted when the array is
|
|
19
|
+
closed or garbage-collected. Arrays created with an explicit filename are kept:
|
|
19
20
|
|
|
20
21
|
```python
|
|
21
22
|
from arrayfile import Array
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "arrayfile"
|
|
3
3
|
description = "Arrays backed by disk"
|
|
4
|
-
version = "0.1.
|
|
4
|
+
version = "0.1.1"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Gareth Davidson", email = "gaz@bitplane.net" }
|
|
7
7
|
]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
|
-
license = {
|
|
10
|
+
license = {file = "LICENSE.md"}
|
|
11
11
|
keywords = ["logging", "terminal", "scrollback", "indexing"]
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Development Status :: 3 - Alpha",
|
|
14
14
|
"Intended Audience :: Developers",
|
|
15
|
-
"License :: OSI Approved :: MIT License",
|
|
16
15
|
"Programming Language :: Python :: 3",
|
|
17
16
|
"Programming Language :: Python :: 3.10",
|
|
18
17
|
"Programming Language :: Python :: 3.11",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import mmap
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import struct
|
|
5
6
|
import tempfile
|
|
6
7
|
import threading
|
|
@@ -21,6 +22,12 @@ class Array:
|
|
|
21
22
|
|
|
22
23
|
def __init__(self, dtype, filename=None, mode="r+b", initial_elements=0):
|
|
23
24
|
self._lock = threading.Lock()
|
|
25
|
+
if isinstance(mode, str) and "a" in mode:
|
|
26
|
+
raise ValueError("Append mode is not supported; use r+b and append()")
|
|
27
|
+
if mode not in ("rb", "r+b", "rb+", "wb", "w+b", "wb+", "xb", "x+b", "xb+"):
|
|
28
|
+
raise ValueError("mode must be a binary read, write, or exclusive-create mode")
|
|
29
|
+
if filename is None and mode == "rb":
|
|
30
|
+
raise ValueError("Read-only mode requires a filename")
|
|
24
31
|
|
|
25
32
|
try:
|
|
26
33
|
dtype_bytes = dtype.encode("ascii")
|
|
@@ -29,7 +36,22 @@ class Array:
|
|
|
29
36
|
if len(dtype_bytes) > self.MAX_DTYPE_LENGTH:
|
|
30
37
|
raise ValueError(f"dtype must be at most {self.MAX_DTYPE_LENGTH} ASCII bytes")
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
try:
|
|
40
|
+
element_size = struct.calcsize(dtype)
|
|
41
|
+
except struct.error as error:
|
|
42
|
+
raise ValueError("dtype must be a valid struct format string") from error
|
|
43
|
+
value_count = 0
|
|
44
|
+
for count, code in re.findall(r"(\d*)([A-Za-z?])", dtype):
|
|
45
|
+
repeat = int(count) if count else 1
|
|
46
|
+
if code == "p" and repeat == 0:
|
|
47
|
+
raise ValueError("dtype cannot contain a zero-length Pascal string")
|
|
48
|
+
if code != "x":
|
|
49
|
+
value_count += 1 if code in "sp" else repeat
|
|
50
|
+
if element_size == 0 or value_count != 1:
|
|
51
|
+
raise ValueError("dtype must have positive size and contain exactly one value")
|
|
52
|
+
|
|
53
|
+
temporary = filename is None
|
|
54
|
+
if temporary:
|
|
33
55
|
fd, filename = tempfile.mkstemp()
|
|
34
56
|
os.close(fd)
|
|
35
57
|
mode = "w+b" # Always create new temp files
|
|
@@ -37,7 +59,7 @@ class Array:
|
|
|
37
59
|
self._filename = filename
|
|
38
60
|
self._dtype = dtype
|
|
39
61
|
self._dtype_format = dtype
|
|
40
|
-
self._element_size =
|
|
62
|
+
self._element_size = element_size
|
|
41
63
|
self._file = None
|
|
42
64
|
self._mmap = None
|
|
43
65
|
self._len = 0
|
|
@@ -45,37 +67,51 @@ class Array:
|
|
|
45
67
|
self._capacity_bytes = 0 # Initialize _capacity_bytes here
|
|
46
68
|
self._data_offset = self.HEADER_SIZE # All data starts after header
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
try:
|
|
71
|
+
if "w" in mode or "x" in mode:
|
|
72
|
+
# Mapping and header updates require random-access reads and writes.
|
|
73
|
+
mode = "x+b" if "x" in mode else "w+b"
|
|
74
|
+
self._file = open(filename, mode) # noqa: SIM115 - kept open for the Array lifetime
|
|
75
|
+
self._len = 0
|
|
76
|
+
self._allocate_capacity(initial_elements)
|
|
77
|
+
self._write_header()
|
|
78
|
+
else:
|
|
79
|
+
# Open existing file
|
|
80
|
+
self._file = open(filename, mode) # noqa: SIM115 - kept open for the Array lifetime
|
|
81
|
+
if not self._read_header():
|
|
82
|
+
raise ValueError("File does not have a valid array header")
|
|
83
|
+
|
|
84
|
+
current_file_size = os.fstat(self._file.fileno()).st_size
|
|
85
|
+
data_size = current_file_size - self.HEADER_SIZE
|
|
86
|
+
required_data_size = self._len * self._element_size
|
|
87
|
+
if required_data_size > data_size:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Array header declares {self._len} elements, but file only contains "
|
|
90
|
+
f"{data_size // self._element_size}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self._capacity_bytes = data_size
|
|
94
|
+
self._capacity = data_size // self._element_size
|
|
95
|
+
|
|
96
|
+
self._writable = any(flag in mode for flag in "+wa")
|
|
97
|
+
|
|
98
|
+
# Only mmap if the file has a non-zero size
|
|
99
|
+
if self._capacity_bytes > 0:
|
|
100
|
+
access = mmap.ACCESS_WRITE if self._writable else mmap.ACCESS_READ
|
|
101
|
+
self._mmap = mmap.mmap(self._file.fileno(), 0, access=access)
|
|
102
|
+
|
|
103
|
+
except BaseException:
|
|
104
|
+
try:
|
|
105
|
+
if self._mmap is not None:
|
|
106
|
+
self._mmap.close()
|
|
107
|
+
finally:
|
|
108
|
+
try:
|
|
109
|
+
if self._file is not None:
|
|
110
|
+
self._file.close()
|
|
111
|
+
finally:
|
|
112
|
+
if temporary:
|
|
113
|
+
os.unlink(filename)
|
|
114
|
+
raise
|
|
79
115
|
|
|
80
116
|
# Set up finalizer to ensure cleanup even if close() isn't called
|
|
81
117
|
self._finalizer_state = {
|
|
@@ -85,6 +121,7 @@ class Array:
|
|
|
85
121
|
"length": self._len,
|
|
86
122
|
"mmap": self._mmap,
|
|
87
123
|
"writable": self._writable,
|
|
124
|
+
"temporary_path": filename if temporary else None,
|
|
88
125
|
}
|
|
89
126
|
self._finalizer = weakref.finalize(self, self._finalize_resources, self._finalizer_state)
|
|
90
127
|
|
|
@@ -124,31 +161,41 @@ class Array:
|
|
|
124
161
|
@classmethod
|
|
125
162
|
def _finalize_resources(cls, state):
|
|
126
163
|
mapped_file = state["mmap"]
|
|
127
|
-
if mapped_file:
|
|
128
|
-
mapped_file.flush()
|
|
129
|
-
mapped_file.close()
|
|
130
|
-
|
|
131
164
|
file = state["file"]
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
dtype_bytes,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
try:
|
|
166
|
+
if mapped_file is not None:
|
|
167
|
+
try:
|
|
168
|
+
mapped_file.flush()
|
|
169
|
+
finally:
|
|
170
|
+
mapped_file.close()
|
|
171
|
+
if file is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if state["writable"]:
|
|
175
|
+
dtype_bytes = state["dtype"].encode("ascii").ljust(cls.MAX_DTYPE_LENGTH, b"\x00")
|
|
176
|
+
header = struct.pack(
|
|
177
|
+
cls.HEADER_FORMAT,
|
|
178
|
+
cls.MAGIC,
|
|
179
|
+
cls.HEADER_VERSION,
|
|
180
|
+
len(state["dtype"]),
|
|
181
|
+
dtype_bytes,
|
|
182
|
+
state["element_size"],
|
|
183
|
+
state["length"],
|
|
184
|
+
)
|
|
185
|
+
file.seek(0)
|
|
186
|
+
file.write(header)
|
|
187
|
+
file.truncate(cls.HEADER_SIZE + state["length"] * state["element_size"])
|
|
188
|
+
file.flush()
|
|
189
|
+
finally:
|
|
190
|
+
try:
|
|
191
|
+
if file is not None:
|
|
192
|
+
file.close()
|
|
193
|
+
finally:
|
|
194
|
+
if state["temporary_path"] is not None:
|
|
195
|
+
try:
|
|
196
|
+
os.unlink(state["temporary_path"])
|
|
197
|
+
except FileNotFoundError:
|
|
198
|
+
pass
|
|
152
199
|
|
|
153
200
|
def _write_header(self):
|
|
154
201
|
"""Write header to the beginning of the file."""
|
|
@@ -240,17 +287,34 @@ class Array:
|
|
|
240
287
|
bytes_needed = min_elements * self._element_size + self.HEADER_SIZE
|
|
241
288
|
chunks_needed = (bytes_needed + self.CHUNK_SIZE_BYTES - 1) // self.CHUNK_SIZE_BYTES
|
|
242
289
|
total_file_size = chunks_needed * self.CHUNK_SIZE_BYTES
|
|
290
|
+
self._file.truncate(total_file_size)
|
|
243
291
|
self._capacity_bytes = total_file_size - self.HEADER_SIZE
|
|
244
292
|
self._capacity = self._capacity_bytes // self._element_size
|
|
245
|
-
self._file.truncate(total_file_size)
|
|
246
293
|
|
|
247
294
|
def _resize(self, min_new_len):
|
|
248
|
-
|
|
295
|
+
old_capacity = self._capacity
|
|
296
|
+
old_capacity_bytes = self._capacity_bytes
|
|
297
|
+
if self._mmap is not None:
|
|
249
298
|
self._mmap.close()
|
|
299
|
+
self._mmap = None
|
|
300
|
+
self._finalizer_state["mmap"] = None
|
|
250
301
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
302
|
+
try:
|
|
303
|
+
self._allocate_capacity(min_new_len)
|
|
304
|
+
self._mmap = mmap.mmap(self._file.fileno(), 0)
|
|
305
|
+
except Exception:
|
|
306
|
+
self._capacity = old_capacity
|
|
307
|
+
self._capacity_bytes = old_capacity_bytes
|
|
308
|
+
# Restore access to existing values if possible. Cleanup must remain
|
|
309
|
+
# usable even when the operating system cannot create any mapping.
|
|
310
|
+
if old_capacity_bytes:
|
|
311
|
+
try:
|
|
312
|
+
self._mmap = mmap.mmap(self._file.fileno(), 0)
|
|
313
|
+
except OSError:
|
|
314
|
+
pass
|
|
315
|
+
raise
|
|
316
|
+
finally:
|
|
317
|
+
self._finalizer_state["mmap"] = self._mmap
|
|
254
318
|
|
|
255
319
|
def extend(self, iterable):
|
|
256
320
|
values = list(iterable)
|
|
@@ -306,6 +370,8 @@ class Array:
|
|
|
306
370
|
self._finalizer_state["length"] = self._len
|
|
307
371
|
elif value > 1:
|
|
308
372
|
original_len = self._len
|
|
373
|
+
if original_len == 0:
|
|
374
|
+
return self
|
|
309
375
|
new_total_len = original_len * value
|
|
310
376
|
|
|
311
377
|
# Resize if needed
|
|
@@ -334,10 +400,13 @@ class Array:
|
|
|
334
400
|
self._write_header()
|
|
335
401
|
|
|
336
402
|
def close(self):
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
403
|
+
with self._lock:
|
|
404
|
+
try:
|
|
405
|
+
if self._finalizer.alive:
|
|
406
|
+
self._finalizer()
|
|
407
|
+
finally:
|
|
408
|
+
self._mmap = None
|
|
409
|
+
self._file = None
|
|
341
410
|
|
|
342
411
|
def __enter__(self):
|
|
343
412
|
return self
|
|
File without changes
|
|
File without changes
|