arrayfile 0.0.1__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.0.1 → arrayfile-0.1.1}/PKG-INFO +23 -8
- {arrayfile-0.0.1 → arrayfile-0.1.1}/README.md +22 -6
- {arrayfile-0.0.1 → arrayfile-0.1.1}/pyproject.toml +2 -3
- {arrayfile-0.0.1 → arrayfile-0.1.1}/src/arrayfile/array.py +180 -51
- {arrayfile-0.0.1 → arrayfile-0.1.1}/LICENSE.md +0 -0
- {arrayfile-0.0.1 → 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.
|
|
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
|
|
@@ -27,14 +26,14 @@ Requires-Dist: ruff ; extra == "dev"
|
|
|
27
26
|
Requires-Dist: pydoc-markdown ; extra == "dev"
|
|
28
27
|
Provides-Extra: dev
|
|
29
28
|
|
|
30
|
-
# arrayfile
|
|
29
|
+
# 📃 arrayfile
|
|
31
30
|
|
|
32
31
|
A file-backed numeric array using struct.pack. Does not support inserts or
|
|
33
32
|
slicing.
|
|
34
33
|
|
|
35
34
|
Smaller than relying on numpy though.
|
|
36
35
|
|
|
37
|
-
## Installation
|
|
36
|
+
## ▶️ Installation
|
|
38
37
|
|
|
39
38
|
```bash
|
|
40
39
|
pip install arrayfile
|
|
@@ -42,9 +41,10 @@ pip install arrayfile
|
|
|
42
41
|
|
|
43
42
|
## Usage
|
|
44
43
|
|
|
45
|
-
### Temporary Array
|
|
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
|
|
@@ -60,7 +60,7 @@ print(f"Values: {[arr[i] for i in range(len(arr))]}")
|
|
|
60
60
|
arr.close() # Clean up resources
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### Persistent Array
|
|
63
|
+
### 💽 Persistent Array
|
|
64
64
|
|
|
65
65
|
You can use the same file, if you want to persist your data across sessions:
|
|
66
66
|
|
|
@@ -84,7 +84,7 @@ arr.append(2000)
|
|
|
84
84
|
arr.close()
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
## Context manager
|
|
87
|
+
## 🔧 Context manager
|
|
88
88
|
|
|
89
89
|
It has a finalizer in case you forget to call `close()`, but if you like to keep
|
|
90
90
|
your code tidy, you can use a context manager, like so:
|
|
@@ -101,4 +101,19 @@ with Array('d', 'measurements.array', 'w+b') as arr:
|
|
|
101
101
|
print(f" {i}: {value:.15f}")
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
## ⚖️ License
|
|
105
|
+
|
|
106
|
+
WTFPL with one extra clause:
|
|
107
|
+
|
|
108
|
+
1. Don't blame me.
|
|
109
|
+
|
|
110
|
+
Do what you like, but you're on your own.
|
|
111
|
+
|
|
112
|
+
## 🔗 Links
|
|
113
|
+
|
|
114
|
+
* [🏠 home](https://bitplane.net/dev/python/arrayfile)
|
|
115
|
+
* [📖 pydoc](https://bitplane.net/dev/python/arrayfile/pydoc)
|
|
116
|
+
* [🐍 pypi](https://pypi.org/project/arrayfile)
|
|
117
|
+
* [🐱 github](https://github.com/bitplane/arrayfile)
|
|
118
|
+
|
|
104
119
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# arrayfile
|
|
1
|
+
# 📃 arrayfile
|
|
2
2
|
|
|
3
3
|
A file-backed numeric array using struct.pack. Does not support inserts or
|
|
4
4
|
slicing.
|
|
5
5
|
|
|
6
6
|
Smaller than relying on numpy though.
|
|
7
7
|
|
|
8
|
-
## Installation
|
|
8
|
+
## ▶️ Installation
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
pip install arrayfile
|
|
@@ -13,9 +13,10 @@ pip install arrayfile
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
### Temporary Array
|
|
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
|
|
@@ -31,7 +32,7 @@ print(f"Values: {[arr[i] for i in range(len(arr))]}")
|
|
|
31
32
|
arr.close() # Clean up resources
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
### Persistent Array
|
|
35
|
+
### 💽 Persistent Array
|
|
35
36
|
|
|
36
37
|
You can use the same file, if you want to persist your data across sessions:
|
|
37
38
|
|
|
@@ -55,7 +56,7 @@ arr.append(2000)
|
|
|
55
56
|
arr.close()
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
## Context manager
|
|
59
|
+
## 🔧 Context manager
|
|
59
60
|
|
|
60
61
|
It has a finalizer in case you forget to call `close()`, but if you like to keep
|
|
61
62
|
your code tidy, you can use a context manager, like so:
|
|
@@ -72,3 +73,18 @@ with Array('d', 'measurements.array', 'w+b') as arr:
|
|
|
72
73
|
print(f" {i}: {value:.15f}")
|
|
73
74
|
```
|
|
74
75
|
|
|
76
|
+
## ⚖️ License
|
|
77
|
+
|
|
78
|
+
WTFPL with one extra clause:
|
|
79
|
+
|
|
80
|
+
1. Don't blame me.
|
|
81
|
+
|
|
82
|
+
Do what you like, but you're on your own.
|
|
83
|
+
|
|
84
|
+
## 🔗 Links
|
|
85
|
+
|
|
86
|
+
* [🏠 home](https://bitplane.net/dev/python/arrayfile)
|
|
87
|
+
* [📖 pydoc](https://bitplane.net/dev/python/arrayfile/pydoc)
|
|
88
|
+
* [🐍 pypi](https://pypi.org/project/arrayfile)
|
|
89
|
+
* [🐱 github](https://github.com/bitplane/arrayfile)
|
|
90
|
+
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "arrayfile"
|
|
3
3
|
description = "Arrays backed by disk"
|
|
4
|
-
version = "0.
|
|
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,5 +1,7 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import mmap
|
|
2
3
|
import os
|
|
4
|
+
import re
|
|
3
5
|
import struct
|
|
4
6
|
import tempfile
|
|
5
7
|
import threading
|
|
@@ -8,6 +10,7 @@ import weakref
|
|
|
8
10
|
|
|
9
11
|
class Array:
|
|
10
12
|
CHUNK_SIZE_BYTES = 4096
|
|
13
|
+
MAX_DTYPE_LENGTH = 8
|
|
11
14
|
|
|
12
15
|
# Header constants
|
|
13
16
|
MAGIC = b"ARYF"
|
|
@@ -19,8 +22,36 @@ class Array:
|
|
|
19
22
|
|
|
20
23
|
def __init__(self, dtype, filename=None, mode="r+b", initial_elements=0):
|
|
21
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")
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
try:
|
|
33
|
+
dtype_bytes = dtype.encode("ascii")
|
|
34
|
+
except (AttributeError, UnicodeEncodeError) as error:
|
|
35
|
+
raise ValueError("dtype must be an ASCII struct format string") from error
|
|
36
|
+
if len(dtype_bytes) > self.MAX_DTYPE_LENGTH:
|
|
37
|
+
raise ValueError(f"dtype must be at most {self.MAX_DTYPE_LENGTH} ASCII bytes")
|
|
38
|
+
|
|
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:
|
|
24
55
|
fd, filename = tempfile.mkstemp()
|
|
25
56
|
os.close(fd)
|
|
26
57
|
mode = "w+b" # Always create new temp files
|
|
@@ -28,7 +59,7 @@ class Array:
|
|
|
28
59
|
self._filename = filename
|
|
29
60
|
self._dtype = dtype
|
|
30
61
|
self._dtype_format = dtype
|
|
31
|
-
self._element_size =
|
|
62
|
+
self._element_size = element_size
|
|
32
63
|
self._file = None
|
|
33
64
|
self._mmap = None
|
|
34
65
|
self._len = 0
|
|
@@ -36,31 +67,63 @@ class Array:
|
|
|
36
67
|
self._capacity_bytes = 0 # Initialize _capacity_bytes here
|
|
37
68
|
self._data_offset = self.HEADER_SIZE # All data starts after header
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
115
|
|
|
62
116
|
# Set up finalizer to ensure cleanup even if close() isn't called
|
|
63
|
-
self.
|
|
117
|
+
self._finalizer_state = {
|
|
118
|
+
"dtype": self._dtype,
|
|
119
|
+
"element_size": self._element_size,
|
|
120
|
+
"file": self._file,
|
|
121
|
+
"length": self._len,
|
|
122
|
+
"mmap": self._mmap,
|
|
123
|
+
"writable": self._writable,
|
|
124
|
+
"temporary_path": filename if temporary else None,
|
|
125
|
+
}
|
|
126
|
+
self._finalizer = weakref.finalize(self, self._finalize_resources, self._finalizer_state)
|
|
64
127
|
|
|
65
128
|
def __len__(self):
|
|
66
129
|
return self._len
|
|
@@ -91,10 +154,52 @@ class Array:
|
|
|
91
154
|
except struct.error as e:
|
|
92
155
|
raise TypeError(f"Value {value} cannot be packed as {self._dtype_format}: {e}")
|
|
93
156
|
|
|
157
|
+
def _ensure_writable(self):
|
|
158
|
+
if not self._writable:
|
|
159
|
+
raise io.UnsupportedOperation("Array is not writable")
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def _finalize_resources(cls, state):
|
|
163
|
+
mapped_file = state["mmap"]
|
|
164
|
+
file = state["file"]
|
|
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
|
|
199
|
+
|
|
94
200
|
def _write_header(self):
|
|
95
201
|
"""Write header to the beginning of the file."""
|
|
96
|
-
dtype_bytes = self._dtype.encode("ascii")
|
|
97
|
-
dtype_bytes = dtype_bytes.ljust(8, b"\x00") # Pad with nulls
|
|
202
|
+
dtype_bytes = self._dtype.encode("ascii").ljust(self.MAX_DTYPE_LENGTH, b"\x00")
|
|
98
203
|
|
|
99
204
|
header = struct.pack(
|
|
100
205
|
self.HEADER_FORMAT,
|
|
@@ -126,6 +231,9 @@ class Array:
|
|
|
126
231
|
if version != self.HEADER_VERSION:
|
|
127
232
|
raise ValueError(f"Unsupported header version: {version}")
|
|
128
233
|
|
|
234
|
+
if dtype_len > self.MAX_DTYPE_LENGTH:
|
|
235
|
+
raise ValueError(f"File dtype length {dtype_len} exceeds header capacity")
|
|
236
|
+
|
|
129
237
|
# Extract dtype string
|
|
130
238
|
dtype = dtype_bytes[:dtype_len].decode("ascii")
|
|
131
239
|
|
|
@@ -153,6 +261,7 @@ class Array:
|
|
|
153
261
|
index = self._validate_index(index)
|
|
154
262
|
|
|
155
263
|
with self._lock:
|
|
264
|
+
self._ensure_writable()
|
|
156
265
|
if not self._mmap:
|
|
157
266
|
raise RuntimeError("Array is not memory-mapped. This should not happen if len > 0.")
|
|
158
267
|
|
|
@@ -162,6 +271,7 @@ class Array:
|
|
|
162
271
|
|
|
163
272
|
def append(self, value):
|
|
164
273
|
with self._lock:
|
|
274
|
+
self._ensure_writable()
|
|
165
275
|
if self._len == self._capacity:
|
|
166
276
|
self._resize(self._len + 1)
|
|
167
277
|
|
|
@@ -170,22 +280,41 @@ class Array:
|
|
|
170
280
|
|
|
171
281
|
self._mmap[offset : offset + self._element_size] = packed_value
|
|
172
282
|
self._len += 1
|
|
283
|
+
self._finalizer_state["length"] = self._len
|
|
173
284
|
|
|
174
285
|
def _allocate_capacity(self, min_elements):
|
|
175
286
|
"""Allocate capacity for at least min_elements, rounded up to chunk boundary."""
|
|
176
287
|
bytes_needed = min_elements * self._element_size + self.HEADER_SIZE
|
|
177
288
|
chunks_needed = (bytes_needed + self.CHUNK_SIZE_BYTES - 1) // self.CHUNK_SIZE_BYTES
|
|
178
289
|
total_file_size = chunks_needed * self.CHUNK_SIZE_BYTES
|
|
290
|
+
self._file.truncate(total_file_size)
|
|
179
291
|
self._capacity_bytes = total_file_size - self.HEADER_SIZE
|
|
180
292
|
self._capacity = self._capacity_bytes // self._element_size
|
|
181
|
-
self._file.truncate(total_file_size)
|
|
182
293
|
|
|
183
294
|
def _resize(self, min_new_len):
|
|
184
|
-
|
|
295
|
+
old_capacity = self._capacity
|
|
296
|
+
old_capacity_bytes = self._capacity_bytes
|
|
297
|
+
if self._mmap is not None:
|
|
185
298
|
self._mmap.close()
|
|
299
|
+
self._mmap = None
|
|
300
|
+
self._finalizer_state["mmap"] = None
|
|
186
301
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
189
318
|
|
|
190
319
|
def extend(self, iterable):
|
|
191
320
|
values = list(iterable)
|
|
@@ -195,6 +324,7 @@ class Array:
|
|
|
195
324
|
return
|
|
196
325
|
|
|
197
326
|
with self._lock:
|
|
327
|
+
self._ensure_writable()
|
|
198
328
|
new_len = self._len + num_new_elements
|
|
199
329
|
if new_len > self._capacity:
|
|
200
330
|
self._resize(new_len)
|
|
@@ -207,6 +337,7 @@ class Array:
|
|
|
207
337
|
offset += self._element_size
|
|
208
338
|
|
|
209
339
|
self._len = new_len
|
|
340
|
+
self._finalizer_state["length"] = self._len
|
|
210
341
|
|
|
211
342
|
def __contains__(self, value):
|
|
212
343
|
for i in range(self._len):
|
|
@@ -225,17 +356,22 @@ class Array:
|
|
|
225
356
|
return NotImplemented
|
|
226
357
|
|
|
227
358
|
with self._lock:
|
|
359
|
+
self._ensure_writable()
|
|
228
360
|
if value == 0:
|
|
229
361
|
self._len = 0
|
|
230
362
|
if self._mmap:
|
|
231
363
|
self._mmap.close()
|
|
232
364
|
self._mmap = None
|
|
365
|
+
self._finalizer_state["mmap"] = None
|
|
233
366
|
if self._file:
|
|
234
367
|
self._file.truncate(0)
|
|
235
368
|
self._capacity = 0
|
|
236
369
|
self._capacity_bytes = 0
|
|
370
|
+
self._finalizer_state["length"] = self._len
|
|
237
371
|
elif value > 1:
|
|
238
372
|
original_len = self._len
|
|
373
|
+
if original_len == 0:
|
|
374
|
+
return self
|
|
239
375
|
new_total_len = original_len * value
|
|
240
376
|
|
|
241
377
|
# Resize if needed
|
|
@@ -253,31 +389,24 @@ class Array:
|
|
|
253
389
|
dst_offset += chunk_size
|
|
254
390
|
|
|
255
391
|
self._len = new_total_len
|
|
392
|
+
self._finalizer_state["length"] = self._len
|
|
256
393
|
return self
|
|
257
394
|
|
|
258
395
|
def flush(self):
|
|
259
|
-
|
|
260
|
-
self._mmap
|
|
396
|
+
with self._lock:
|
|
397
|
+
if self._mmap:
|
|
398
|
+
self._mmap.flush()
|
|
399
|
+
if self._file and self._writable:
|
|
400
|
+
self._write_header()
|
|
261
401
|
|
|
262
402
|
def close(self):
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# Update header with final length
|
|
271
|
-
self._write_header()
|
|
272
|
-
|
|
273
|
-
# Only truncate if the file was opened in a writable mode
|
|
274
|
-
# and if the current size is greater than the actual data length
|
|
275
|
-
current_file_size = os.fstat(self._file.fileno()).st_size
|
|
276
|
-
actual_total_size = self.HEADER_SIZE + self._len * self._element_size
|
|
277
|
-
if current_file_size > actual_total_size:
|
|
278
|
-
self._file.truncate(actual_total_size)
|
|
279
|
-
self._file.close()
|
|
280
|
-
self._file = None
|
|
403
|
+
with self._lock:
|
|
404
|
+
try:
|
|
405
|
+
if self._finalizer.alive:
|
|
406
|
+
self._finalizer()
|
|
407
|
+
finally:
|
|
408
|
+
self._mmap = None
|
|
409
|
+
self._file = None
|
|
281
410
|
|
|
282
411
|
def __enter__(self):
|
|
283
412
|
return self
|
|
File without changes
|
|
File without changes
|