PersistentObjects 0.1.6__tar.gz → 0.2.0__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.
- persistentobjects-0.2.0/PKG-INFO +143 -0
- persistentobjects-0.2.0/PersistentObjects/core.py +383 -0
- persistentobjects-0.2.0/PersistentObjects.egg-info/PKG-INFO +143 -0
- persistentobjects-0.2.0/README.md +130 -0
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/pyproject.toml +1 -1
- persistentobjects-0.1.6/PKG-INFO +0 -41
- persistentobjects-0.1.6/PersistentObjects/core.py +0 -135
- persistentobjects-0.1.6/PersistentObjects.egg-info/PKG-INFO +0 -41
- persistentobjects-0.1.6/README.md +0 -28
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/LICENSE +0 -0
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/PersistentObjects/__init__.py +0 -0
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/PersistentObjects.egg-info/SOURCES.txt +0 -0
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/PersistentObjects.egg-info/dependency_links.txt +0 -0
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/PersistentObjects.egg-info/top_level.txt +0 -0
- {persistentobjects-0.1.6 → persistentobjects-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PersistentObjects
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: JSON-backed attribute-persistent object with namespace support
|
|
5
|
+
Author: Tornado300
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# PersistentObjects
|
|
15
|
+
|
|
16
|
+
A simple Python library that provides the `PersistentObject` class, which automatically saves all its attributes to a JSON file. Attributes are saved as soon as they are set.
|
|
17
|
+
|
|
18
|
+
> [!IMPORTANT]
|
|
19
|
+
> In-place mutations like `.append()`, `.insert()`, `.extend()`, etc. will **not** automatically save!
|
|
20
|
+
> Create a temp variable, mutate it, then reassign it back:
|
|
21
|
+
> ```python
|
|
22
|
+
> temp = pobject.my_list
|
|
23
|
+
> temp.append("new item")
|
|
24
|
+
> pobject.my_list = temp # triggers save
|
|
25
|
+
> ```
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install PersistentObjects
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from PersistentObjects import PersistentObject
|
|
37
|
+
|
|
38
|
+
pobject = PersistentObject("save.json")
|
|
39
|
+
|
|
40
|
+
# attributes are saved to the json file immediately
|
|
41
|
+
pobject.first_value = 10
|
|
42
|
+
pobject.second_value = "foo"
|
|
43
|
+
|
|
44
|
+
# namespaces create sub-dicts in the json file
|
|
45
|
+
pobject.namespace("test_namespace")
|
|
46
|
+
pobject.test_namespace.third_value = [10, 5]
|
|
47
|
+
|
|
48
|
+
# or save the namespace to a variable
|
|
49
|
+
namespace = pobject.namespace("other_namespace")
|
|
50
|
+
namespace.value = 42
|
|
51
|
+
|
|
52
|
+
# suffix with '_' to make an attribute non-persistent (normal python attribute)
|
|
53
|
+
pobject.temp_ = True
|
|
54
|
+
|
|
55
|
+
# delete a persistent attribute
|
|
56
|
+
del pobject.first_value
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Namespaces
|
|
60
|
+
|
|
61
|
+
Use `namespace()` to create a named section within the JSON file. After calling `namespace()`, you can access it via dot notation. Namespaces can be nested arbitrarily deep.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# create and access via dot notation
|
|
65
|
+
pobject.namespace("ui")
|
|
66
|
+
pobject.ui.theme = "dark"
|
|
67
|
+
pobject.ui.font_size = 14
|
|
68
|
+
|
|
69
|
+
# nested namespaces
|
|
70
|
+
pobject.namespace("ui").namespace("colors")
|
|
71
|
+
pobject.ui.colors.primary = "#ff0000"
|
|
72
|
+
pobject.ui.colors.accent = "#0000ff"
|
|
73
|
+
|
|
74
|
+
# or chain it
|
|
75
|
+
colors = pobject.namespace("ui").namespace("colors")
|
|
76
|
+
colors.primary = "#ff0000"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> You must call `.namespace()` at least once before using dot-access. This is by design — without it, accessing a nonexistent attribute like `pobject.typo` would silently create an empty namespace instead of raising an `AttributeError`.
|
|
81
|
+
|
|
82
|
+
## Supported types
|
|
83
|
+
|
|
84
|
+
Beyond the standard JSON types (`str`, `int`, `float`, `bool`, `list`, `dict`, `None`), the following Python types are automatically encoded and decoded:
|
|
85
|
+
|
|
86
|
+
| Type | JSON representation |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `tuple` | `{"__type__": "tuple", "__value__": [...]}` |
|
|
89
|
+
| `set` | `{"__type__": "set", "__value__": [...]}` |
|
|
90
|
+
| `frozenset` | `{"__type__": "frozenset", "__value__": [...]}` |
|
|
91
|
+
| `bytes` | `{"__type__": "bytes", "__value__": "<base64>"}` |
|
|
92
|
+
| `datetime` | `{"__type__": "datetime", "__value__": "<isoformat>"}` |
|
|
93
|
+
| `date` | `{"__type__": "date", "__value__": "<isoformat>"}` |
|
|
94
|
+
| `time` | `{"__type__": "time", "__value__": "<isoformat>"}` |
|
|
95
|
+
|
|
96
|
+
Nested structures work too (e.g. a list of tuples, a set of tuples).
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from datetime import datetime
|
|
100
|
+
|
|
101
|
+
pobject.my_set = {1, 2, 3}
|
|
102
|
+
pobject.my_tuple = ("a", "b", "c")
|
|
103
|
+
pobject.my_bytes = b"hello"
|
|
104
|
+
pobject.my_datetime = datetime(2025, 6, 15, 12, 30)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Type enforcement
|
|
108
|
+
|
|
109
|
+
Use `settype()` to register a type constraint on an attribute. Assigning a value of the wrong type raises a `TypeError`.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from typing import Any
|
|
113
|
+
|
|
114
|
+
pobject.settype("count", int)
|
|
115
|
+
pobject.count = 5 # ok
|
|
116
|
+
pobject.count = "five" # TypeError
|
|
117
|
+
|
|
118
|
+
# union types (Python 3.10+)
|
|
119
|
+
pobject.settype("value", int | None)
|
|
120
|
+
pobject.value = None # ok
|
|
121
|
+
|
|
122
|
+
# tuple of types
|
|
123
|
+
pobject.settype("multi", (int, str))
|
|
124
|
+
|
|
125
|
+
# Any removes the constraint
|
|
126
|
+
pobject.settype("count", Any)
|
|
127
|
+
|
|
128
|
+
# also works on namespaces
|
|
129
|
+
namespace.settype("volume", int | float)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Defaults
|
|
133
|
+
|
|
134
|
+
Use `setdefault()` to set a value only if the attribute doesn't already exist. It also registers the value for `reset()`. An optional `type` parameter combines setting a default with type enforcement.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
pobject.setdefault("name", "default_name")
|
|
138
|
+
pobject.setdefault("count", 0, type=int) # sets default and enforces type
|
|
139
|
+
|
|
140
|
+
pobject.count = 99
|
|
141
|
+
pobject.reset("count") # back to 0
|
|
142
|
+
pobject.reset() # resets all attributes that have a registered default
|
|
143
|
+
```
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import base64, json, os, threading, types
|
|
2
|
+
from datetime import datetime, date, time as time_
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
_TYPE_TAG = "__type__"
|
|
6
|
+
_VALUE_TAG = "__value__"
|
|
7
|
+
_TAGGED_TYPES = {"tuple", "set", "frozenset", "bytes", "datetime", "date", "time"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _encode(value):
|
|
11
|
+
"""Recursively convert non-JSON-serializable types into tagged dicts."""
|
|
12
|
+
if isinstance(value, dict):
|
|
13
|
+
return {k: _encode(v) for k, v in value.items()}
|
|
14
|
+
if isinstance(value, list):
|
|
15
|
+
return [_encode(x) for x in value]
|
|
16
|
+
if isinstance(value, tuple):
|
|
17
|
+
return {_TYPE_TAG: "tuple", _VALUE_TAG: [_encode(x) for x in value]}
|
|
18
|
+
if isinstance(value, set):
|
|
19
|
+
return {_TYPE_TAG: "set", _VALUE_TAG: [_encode(x) for x in value]}
|
|
20
|
+
if isinstance(value, frozenset):
|
|
21
|
+
return {_TYPE_TAG: "frozenset", _VALUE_TAG: [_encode(x) for x in value]}
|
|
22
|
+
if isinstance(value, bytes):
|
|
23
|
+
return {_TYPE_TAG: "bytes", _VALUE_TAG: base64.b64encode(value).decode("ascii")}
|
|
24
|
+
if isinstance(value, datetime): # must be before date (datetime is subclass of date)
|
|
25
|
+
return {_TYPE_TAG: "datetime", _VALUE_TAG: value.isoformat()}
|
|
26
|
+
if isinstance(value, date):
|
|
27
|
+
return {_TYPE_TAG: "date", _VALUE_TAG: value.isoformat()}
|
|
28
|
+
if isinstance(value, time_):
|
|
29
|
+
return {_TYPE_TAG: "time", _VALUE_TAG: value.isoformat()}
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _decode_hook(obj):
|
|
34
|
+
"""json.load object_hook: convert tagged dicts back to Python types."""
|
|
35
|
+
if len(obj) == 2 and obj.get(_TYPE_TAG) in _TAGGED_TYPES and _VALUE_TAG in obj:
|
|
36
|
+
t, v = obj[_TYPE_TAG], obj[_VALUE_TAG]
|
|
37
|
+
if t == "tuple":
|
|
38
|
+
return tuple(v)
|
|
39
|
+
if t == "set":
|
|
40
|
+
return set(v)
|
|
41
|
+
if t == "frozenset":
|
|
42
|
+
return frozenset(v)
|
|
43
|
+
if t == "bytes":
|
|
44
|
+
return base64.b64decode(v)
|
|
45
|
+
if t == "datetime":
|
|
46
|
+
return datetime.fromisoformat(v)
|
|
47
|
+
if t == "date":
|
|
48
|
+
return date.fromisoformat(v)
|
|
49
|
+
if t == "time":
|
|
50
|
+
return time_.fromisoformat(v)
|
|
51
|
+
return obj
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _PersistentState:
|
|
55
|
+
_instances = {}
|
|
56
|
+
|
|
57
|
+
def __new__(cls, path):
|
|
58
|
+
path = os.path.abspath(path)
|
|
59
|
+
|
|
60
|
+
if path in cls._instances:
|
|
61
|
+
return cls._instances[path]
|
|
62
|
+
|
|
63
|
+
instance = super().__new__(cls)
|
|
64
|
+
cls._instances[path] = instance
|
|
65
|
+
return instance
|
|
66
|
+
|
|
67
|
+
def __init__(self, path: str) -> None:
|
|
68
|
+
# Prevent re-initialization when __new__ returns an existing instance
|
|
69
|
+
if hasattr(self, "_initialized"):
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
self._initialized = True
|
|
73
|
+
self._path = os.path.abspath(path)
|
|
74
|
+
self._lock = threading.Lock()
|
|
75
|
+
self._data = {}
|
|
76
|
+
self._defaults = {}
|
|
77
|
+
self._types = {}
|
|
78
|
+
self._load()
|
|
79
|
+
|
|
80
|
+
def _load(self) -> None:
|
|
81
|
+
if os.path.exists(self._path):
|
|
82
|
+
try:
|
|
83
|
+
with open(self._path, "r", encoding="utf-8") as f:
|
|
84
|
+
self._data = json.load(f, object_hook=_decode_hook)
|
|
85
|
+
except Exception:
|
|
86
|
+
self._data = {}
|
|
87
|
+
|
|
88
|
+
def _save(self) -> None:
|
|
89
|
+
tmp = self._path + ".tmp"
|
|
90
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
91
|
+
json.dump(_encode(self._data), f, indent=2)
|
|
92
|
+
os.replace(tmp, self._path)
|
|
93
|
+
|
|
94
|
+
def __getattr__(self, name) -> "Any | _Namespace":
|
|
95
|
+
if name.startswith("_"):
|
|
96
|
+
raise AttributeError(f"Cannot access private attribute '{name}'")
|
|
97
|
+
if name.endswith("_"):
|
|
98
|
+
try:
|
|
99
|
+
return super().__getattribute__(name[:-1])
|
|
100
|
+
except AttributeError:
|
|
101
|
+
raise AttributeError(
|
|
102
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'.\n Notice: Attribute '{name}' is not persistent!"
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
value = self._data[name]
|
|
106
|
+
# If it's a dict, return it as a namespace
|
|
107
|
+
if isinstance(value, dict):
|
|
108
|
+
return _Namespace(self, name)
|
|
109
|
+
return value
|
|
110
|
+
except KeyError:
|
|
111
|
+
raise AttributeError(
|
|
112
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _check_type(name: str, value: Any, expected: Any) -> None:
|
|
117
|
+
if expected is None:
|
|
118
|
+
expected = type(None)
|
|
119
|
+
elif isinstance(expected, tuple):
|
|
120
|
+
expected = tuple(type(None) if t is None else t for t in expected)
|
|
121
|
+
if not isinstance(value, expected):
|
|
122
|
+
if isinstance(expected, types.UnionType):
|
|
123
|
+
type_name = str(expected)
|
|
124
|
+
elif isinstance(expected, tuple):
|
|
125
|
+
type_name = " | ".join(t.__name__ for t in expected)
|
|
126
|
+
else:
|
|
127
|
+
type_name = expected.__name__
|
|
128
|
+
raise TypeError(
|
|
129
|
+
f"Attribute '{name}' expected type '{type_name}', got '{type(value).__name__}'"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
133
|
+
if name.startswith("_"):
|
|
134
|
+
super().__setattr__(name, value)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if name.endswith("_"):
|
|
138
|
+
super().__setattr__(name[:-1], value)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if name in self._types:
|
|
142
|
+
self._check_type(name, value, self._types[name])
|
|
143
|
+
|
|
144
|
+
with self._lock:
|
|
145
|
+
self._data[name] = value
|
|
146
|
+
self._save()
|
|
147
|
+
|
|
148
|
+
def __delattr__(self, name: str) -> None:
|
|
149
|
+
if name.startswith("_"):
|
|
150
|
+
super().__delattr__(name)
|
|
151
|
+
return
|
|
152
|
+
if name.endswith("_"):
|
|
153
|
+
super().__delattr__(name[:-1])
|
|
154
|
+
return
|
|
155
|
+
with self._lock:
|
|
156
|
+
if name not in self._data:
|
|
157
|
+
raise AttributeError(
|
|
158
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
159
|
+
)
|
|
160
|
+
del self._data[name]
|
|
161
|
+
self._types.pop(name, None)
|
|
162
|
+
self._defaults.pop(name, None)
|
|
163
|
+
self._save()
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return f"{type(self).__name__}({self._path!r}, {self._data})"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class _Namespace:
|
|
170
|
+
_instances: dict = {}
|
|
171
|
+
|
|
172
|
+
def __new__(cls, root: _PersistentState, *path: str) -> "_Namespace":
|
|
173
|
+
instance_key = (id(root), path)
|
|
174
|
+
|
|
175
|
+
if instance_key in cls._instances:
|
|
176
|
+
return cls._instances[instance_key]
|
|
177
|
+
|
|
178
|
+
instance = super().__new__(cls)
|
|
179
|
+
cls._instances[instance_key] = instance
|
|
180
|
+
return instance
|
|
181
|
+
|
|
182
|
+
def __init__(self, root: _PersistentState, *path: str) -> None:
|
|
183
|
+
if hasattr(self, "_initialized"):
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
self._initialized = True
|
|
187
|
+
self._root = root
|
|
188
|
+
self._path = path
|
|
189
|
+
self._defaults: dict = {}
|
|
190
|
+
self._types: dict = {}
|
|
191
|
+
|
|
192
|
+
def _get_bucket(self, create: bool = False) -> dict:
|
|
193
|
+
"""Navigate the path into root._data, returning the nested dict."""
|
|
194
|
+
data = self._root._data
|
|
195
|
+
for key in self._path:
|
|
196
|
+
if create:
|
|
197
|
+
data = data.setdefault(key, {})
|
|
198
|
+
else:
|
|
199
|
+
data = data[key]
|
|
200
|
+
return data
|
|
201
|
+
|
|
202
|
+
def __getattr__(self, name: str) -> "Any | _Namespace":
|
|
203
|
+
if name.startswith("_"):
|
|
204
|
+
raise AttributeError(name)
|
|
205
|
+
if name.endswith("_"):
|
|
206
|
+
try:
|
|
207
|
+
return super().__getattribute__(name[:-1])
|
|
208
|
+
except AttributeError:
|
|
209
|
+
raise AttributeError(
|
|
210
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'.\n Notice: Attribute '{name}' is not persistent!"
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
value = self._get_bucket()[name]
|
|
214
|
+
if isinstance(value, dict):
|
|
215
|
+
return _Namespace(self._root, *self._path, name)
|
|
216
|
+
return value
|
|
217
|
+
except KeyError:
|
|
218
|
+
raise AttributeError(name)
|
|
219
|
+
|
|
220
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
221
|
+
if name.startswith("_"):
|
|
222
|
+
super().__setattr__(name, value)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if name.endswith("_"):
|
|
226
|
+
super().__setattr__(name[:-1], value)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
if name in self._types:
|
|
230
|
+
_PersistentState._check_type(name, value, self._types[name])
|
|
231
|
+
|
|
232
|
+
with self._root._lock:
|
|
233
|
+
bucket = self._get_bucket(create=True)
|
|
234
|
+
bucket[name] = value
|
|
235
|
+
self._root._save()
|
|
236
|
+
|
|
237
|
+
def __delattr__(self, name: str) -> None:
|
|
238
|
+
if name.startswith("_"):
|
|
239
|
+
super().__delattr__(name)
|
|
240
|
+
return
|
|
241
|
+
if name.endswith("_"):
|
|
242
|
+
super().__delattr__(name[:-1])
|
|
243
|
+
return
|
|
244
|
+
with self._root._lock:
|
|
245
|
+
try:
|
|
246
|
+
bucket = self._get_bucket()
|
|
247
|
+
except KeyError:
|
|
248
|
+
raise AttributeError(
|
|
249
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
250
|
+
)
|
|
251
|
+
if name not in bucket:
|
|
252
|
+
raise AttributeError(
|
|
253
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
254
|
+
)
|
|
255
|
+
del bucket[name]
|
|
256
|
+
self._types.pop(name, None)
|
|
257
|
+
self._defaults.pop(name, None)
|
|
258
|
+
self._root._save()
|
|
259
|
+
|
|
260
|
+
def __repr__(self) -> str:
|
|
261
|
+
try:
|
|
262
|
+
data = self._get_bucket()
|
|
263
|
+
except KeyError:
|
|
264
|
+
data = {}
|
|
265
|
+
path = ".".join(self._path)
|
|
266
|
+
return f"Namespace({path!r}, {data})"
|
|
267
|
+
|
|
268
|
+
def namespace(self, name: str) -> "_Namespace":
|
|
269
|
+
"""Return a nested sub-namespace."""
|
|
270
|
+
with self._root._lock:
|
|
271
|
+
bucket = self._get_bucket(create=True)
|
|
272
|
+
if name not in bucket:
|
|
273
|
+
bucket[name] = {}
|
|
274
|
+
self._root._save()
|
|
275
|
+
return _Namespace(self._root, *self._path, name)
|
|
276
|
+
|
|
277
|
+
def settype(self, name: str, type_hint: Any) -> None:
|
|
278
|
+
"""Register a type constraint for an attribute.
|
|
279
|
+
|
|
280
|
+
Accepted: a type, tuple of types, union type (int | str), None (means NoneType), or Any (removes constraint).
|
|
281
|
+
If the attribute already has a value, it is validated immediately.
|
|
282
|
+
"""
|
|
283
|
+
if type_hint is Any:
|
|
284
|
+
self._types.pop(name, None)
|
|
285
|
+
return
|
|
286
|
+
self._types[name] = type_hint
|
|
287
|
+
try:
|
|
288
|
+
bucket = self._get_bucket()
|
|
289
|
+
if name in bucket:
|
|
290
|
+
_PersistentState._check_type(name, bucket[name], type_hint)
|
|
291
|
+
except KeyError:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def setdefault(self, name: str, value: Any, type: Any = None) -> Any:
|
|
295
|
+
"""Set a value only if the key doesn't already exist. Registers the value as the default for reset().
|
|
296
|
+
|
|
297
|
+
If type is provided, also registers a type constraint via settype().
|
|
298
|
+
"""
|
|
299
|
+
if type is not None:
|
|
300
|
+
self.settype(name, type)
|
|
301
|
+
_PersistentState._check_type(name, value, type if type is not Any else value.__class__)
|
|
302
|
+
self._defaults[name] = value
|
|
303
|
+
with self._root._lock:
|
|
304
|
+
bucket = self._get_bucket(create=True)
|
|
305
|
+
if name not in bucket:
|
|
306
|
+
bucket[name] = value
|
|
307
|
+
self._root._save()
|
|
308
|
+
return self._get_bucket()[name]
|
|
309
|
+
|
|
310
|
+
def reset(self, name: str | None = None) -> None:
|
|
311
|
+
"""Reset a key to its default value, or all keys if no name is given.
|
|
312
|
+
|
|
313
|
+
Raises KeyError if the key has no registered default.
|
|
314
|
+
"""
|
|
315
|
+
with self._root._lock:
|
|
316
|
+
bucket = self._get_bucket(create=True)
|
|
317
|
+
if name is not None:
|
|
318
|
+
if name not in self._defaults:
|
|
319
|
+
raise KeyError(
|
|
320
|
+
f"No default registered for '{name}'. Use setdefault() first."
|
|
321
|
+
)
|
|
322
|
+
bucket[name] = self._defaults[name]
|
|
323
|
+
else:
|
|
324
|
+
for key, value in self._defaults.items():
|
|
325
|
+
bucket[key] = value
|
|
326
|
+
self._root._save()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class PersistentObject(_PersistentState):
|
|
330
|
+
def namespace(self, name: str) -> _Namespace:
|
|
331
|
+
with self._lock:
|
|
332
|
+
if name not in self._data:
|
|
333
|
+
self._data[name] = {}
|
|
334
|
+
self._save()
|
|
335
|
+
return _Namespace(self, name)
|
|
336
|
+
|
|
337
|
+
def settype(self, name: str, type_hint: Any) -> None:
|
|
338
|
+
"""Register a type constraint for an attribute.
|
|
339
|
+
|
|
340
|
+
Accepted: a type, tuple of types, union type (int | str), None (means NoneType), or Any (removes constraint).
|
|
341
|
+
If the attribute already has a value, it is validated immediately.
|
|
342
|
+
"""
|
|
343
|
+
if type_hint is Any:
|
|
344
|
+
self._types.pop(name, None)
|
|
345
|
+
return
|
|
346
|
+
self._types[name] = type_hint
|
|
347
|
+
if name in self._data:
|
|
348
|
+
self._check_type(name, self._data[name], type_hint)
|
|
349
|
+
|
|
350
|
+
def setdefault(self, name: str, value: Any, type: Any = None) -> Any:
|
|
351
|
+
"""Set a value only if the key doesn't already exist. Registers the value as the default for reset().
|
|
352
|
+
|
|
353
|
+
If type is provided, also registers a type constraint via settype().
|
|
354
|
+
"""
|
|
355
|
+
if type is not None:
|
|
356
|
+
self.settype(name, type)
|
|
357
|
+
self._check_type(name, value, type if type is not Any else value.__class__)
|
|
358
|
+
self._defaults[name] = value
|
|
359
|
+
with self._lock:
|
|
360
|
+
if name not in self._data:
|
|
361
|
+
self._data[name] = value
|
|
362
|
+
self._save()
|
|
363
|
+
return self._data[name]
|
|
364
|
+
|
|
365
|
+
def reset(self, name: str | None = None) -> None:
|
|
366
|
+
"""Reset a key to its default value, or all keys if no name is given.
|
|
367
|
+
|
|
368
|
+
Raises KeyError if the key has no registered default.
|
|
369
|
+
"""
|
|
370
|
+
with self._lock:
|
|
371
|
+
if name is not None:
|
|
372
|
+
if name not in self._defaults:
|
|
373
|
+
raise KeyError(
|
|
374
|
+
f"No default registered for '{name}'. Use setdefault() first."
|
|
375
|
+
)
|
|
376
|
+
self._data[name] = self._defaults[name]
|
|
377
|
+
else:
|
|
378
|
+
for key, value in self._defaults.items():
|
|
379
|
+
self._data[key] = value
|
|
380
|
+
self._save()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
__all__ = ["PersistentObject"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PersistentObjects
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: JSON-backed attribute-persistent object with namespace support
|
|
5
|
+
Author: Tornado300
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# PersistentObjects
|
|
15
|
+
|
|
16
|
+
A simple Python library that provides the `PersistentObject` class, which automatically saves all its attributes to a JSON file. Attributes are saved as soon as they are set.
|
|
17
|
+
|
|
18
|
+
> [!IMPORTANT]
|
|
19
|
+
> In-place mutations like `.append()`, `.insert()`, `.extend()`, etc. will **not** automatically save!
|
|
20
|
+
> Create a temp variable, mutate it, then reassign it back:
|
|
21
|
+
> ```python
|
|
22
|
+
> temp = pobject.my_list
|
|
23
|
+
> temp.append("new item")
|
|
24
|
+
> pobject.my_list = temp # triggers save
|
|
25
|
+
> ```
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install PersistentObjects
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from PersistentObjects import PersistentObject
|
|
37
|
+
|
|
38
|
+
pobject = PersistentObject("save.json")
|
|
39
|
+
|
|
40
|
+
# attributes are saved to the json file immediately
|
|
41
|
+
pobject.first_value = 10
|
|
42
|
+
pobject.second_value = "foo"
|
|
43
|
+
|
|
44
|
+
# namespaces create sub-dicts in the json file
|
|
45
|
+
pobject.namespace("test_namespace")
|
|
46
|
+
pobject.test_namespace.third_value = [10, 5]
|
|
47
|
+
|
|
48
|
+
# or save the namespace to a variable
|
|
49
|
+
namespace = pobject.namespace("other_namespace")
|
|
50
|
+
namespace.value = 42
|
|
51
|
+
|
|
52
|
+
# suffix with '_' to make an attribute non-persistent (normal python attribute)
|
|
53
|
+
pobject.temp_ = True
|
|
54
|
+
|
|
55
|
+
# delete a persistent attribute
|
|
56
|
+
del pobject.first_value
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Namespaces
|
|
60
|
+
|
|
61
|
+
Use `namespace()` to create a named section within the JSON file. After calling `namespace()`, you can access it via dot notation. Namespaces can be nested arbitrarily deep.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# create and access via dot notation
|
|
65
|
+
pobject.namespace("ui")
|
|
66
|
+
pobject.ui.theme = "dark"
|
|
67
|
+
pobject.ui.font_size = 14
|
|
68
|
+
|
|
69
|
+
# nested namespaces
|
|
70
|
+
pobject.namespace("ui").namespace("colors")
|
|
71
|
+
pobject.ui.colors.primary = "#ff0000"
|
|
72
|
+
pobject.ui.colors.accent = "#0000ff"
|
|
73
|
+
|
|
74
|
+
# or chain it
|
|
75
|
+
colors = pobject.namespace("ui").namespace("colors")
|
|
76
|
+
colors.primary = "#ff0000"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> You must call `.namespace()` at least once before using dot-access. This is by design — without it, accessing a nonexistent attribute like `pobject.typo` would silently create an empty namespace instead of raising an `AttributeError`.
|
|
81
|
+
|
|
82
|
+
## Supported types
|
|
83
|
+
|
|
84
|
+
Beyond the standard JSON types (`str`, `int`, `float`, `bool`, `list`, `dict`, `None`), the following Python types are automatically encoded and decoded:
|
|
85
|
+
|
|
86
|
+
| Type | JSON representation |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `tuple` | `{"__type__": "tuple", "__value__": [...]}` |
|
|
89
|
+
| `set` | `{"__type__": "set", "__value__": [...]}` |
|
|
90
|
+
| `frozenset` | `{"__type__": "frozenset", "__value__": [...]}` |
|
|
91
|
+
| `bytes` | `{"__type__": "bytes", "__value__": "<base64>"}` |
|
|
92
|
+
| `datetime` | `{"__type__": "datetime", "__value__": "<isoformat>"}` |
|
|
93
|
+
| `date` | `{"__type__": "date", "__value__": "<isoformat>"}` |
|
|
94
|
+
| `time` | `{"__type__": "time", "__value__": "<isoformat>"}` |
|
|
95
|
+
|
|
96
|
+
Nested structures work too (e.g. a list of tuples, a set of tuples).
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from datetime import datetime
|
|
100
|
+
|
|
101
|
+
pobject.my_set = {1, 2, 3}
|
|
102
|
+
pobject.my_tuple = ("a", "b", "c")
|
|
103
|
+
pobject.my_bytes = b"hello"
|
|
104
|
+
pobject.my_datetime = datetime(2025, 6, 15, 12, 30)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Type enforcement
|
|
108
|
+
|
|
109
|
+
Use `settype()` to register a type constraint on an attribute. Assigning a value of the wrong type raises a `TypeError`.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from typing import Any
|
|
113
|
+
|
|
114
|
+
pobject.settype("count", int)
|
|
115
|
+
pobject.count = 5 # ok
|
|
116
|
+
pobject.count = "five" # TypeError
|
|
117
|
+
|
|
118
|
+
# union types (Python 3.10+)
|
|
119
|
+
pobject.settype("value", int | None)
|
|
120
|
+
pobject.value = None # ok
|
|
121
|
+
|
|
122
|
+
# tuple of types
|
|
123
|
+
pobject.settype("multi", (int, str))
|
|
124
|
+
|
|
125
|
+
# Any removes the constraint
|
|
126
|
+
pobject.settype("count", Any)
|
|
127
|
+
|
|
128
|
+
# also works on namespaces
|
|
129
|
+
namespace.settype("volume", int | float)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Defaults
|
|
133
|
+
|
|
134
|
+
Use `setdefault()` to set a value only if the attribute doesn't already exist. It also registers the value for `reset()`. An optional `type` parameter combines setting a default with type enforcement.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
pobject.setdefault("name", "default_name")
|
|
138
|
+
pobject.setdefault("count", 0, type=int) # sets default and enforces type
|
|
139
|
+
|
|
140
|
+
pobject.count = 99
|
|
141
|
+
pobject.reset("count") # back to 0
|
|
142
|
+
pobject.reset() # resets all attributes that have a registered default
|
|
143
|
+
```
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# PersistentObjects
|
|
2
|
+
|
|
3
|
+
A simple Python library that provides the `PersistentObject` class, which automatically saves all its attributes to a JSON file. Attributes are saved as soon as they are set.
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> In-place mutations like `.append()`, `.insert()`, `.extend()`, etc. will **not** automatically save!
|
|
7
|
+
> Create a temp variable, mutate it, then reassign it back:
|
|
8
|
+
> ```python
|
|
9
|
+
> temp = pobject.my_list
|
|
10
|
+
> temp.append("new item")
|
|
11
|
+
> pobject.my_list = temp # triggers save
|
|
12
|
+
> ```
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install PersistentObjects
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from PersistentObjects import PersistentObject
|
|
24
|
+
|
|
25
|
+
pobject = PersistentObject("save.json")
|
|
26
|
+
|
|
27
|
+
# attributes are saved to the json file immediately
|
|
28
|
+
pobject.first_value = 10
|
|
29
|
+
pobject.second_value = "foo"
|
|
30
|
+
|
|
31
|
+
# namespaces create sub-dicts in the json file
|
|
32
|
+
pobject.namespace("test_namespace")
|
|
33
|
+
pobject.test_namespace.third_value = [10, 5]
|
|
34
|
+
|
|
35
|
+
# or save the namespace to a variable
|
|
36
|
+
namespace = pobject.namespace("other_namespace")
|
|
37
|
+
namespace.value = 42
|
|
38
|
+
|
|
39
|
+
# suffix with '_' to make an attribute non-persistent (normal python attribute)
|
|
40
|
+
pobject.temp_ = True
|
|
41
|
+
|
|
42
|
+
# delete a persistent attribute
|
|
43
|
+
del pobject.first_value
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Namespaces
|
|
47
|
+
|
|
48
|
+
Use `namespace()` to create a named section within the JSON file. After calling `namespace()`, you can access it via dot notation. Namespaces can be nested arbitrarily deep.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# create and access via dot notation
|
|
52
|
+
pobject.namespace("ui")
|
|
53
|
+
pobject.ui.theme = "dark"
|
|
54
|
+
pobject.ui.font_size = 14
|
|
55
|
+
|
|
56
|
+
# nested namespaces
|
|
57
|
+
pobject.namespace("ui").namespace("colors")
|
|
58
|
+
pobject.ui.colors.primary = "#ff0000"
|
|
59
|
+
pobject.ui.colors.accent = "#0000ff"
|
|
60
|
+
|
|
61
|
+
# or chain it
|
|
62
|
+
colors = pobject.namespace("ui").namespace("colors")
|
|
63
|
+
colors.primary = "#ff0000"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> [!NOTE]
|
|
67
|
+
> You must call `.namespace()` at least once before using dot-access. This is by design — without it, accessing a nonexistent attribute like `pobject.typo` would silently create an empty namespace instead of raising an `AttributeError`.
|
|
68
|
+
|
|
69
|
+
## Supported types
|
|
70
|
+
|
|
71
|
+
Beyond the standard JSON types (`str`, `int`, `float`, `bool`, `list`, `dict`, `None`), the following Python types are automatically encoded and decoded:
|
|
72
|
+
|
|
73
|
+
| Type | JSON representation |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `tuple` | `{"__type__": "tuple", "__value__": [...]}` |
|
|
76
|
+
| `set` | `{"__type__": "set", "__value__": [...]}` |
|
|
77
|
+
| `frozenset` | `{"__type__": "frozenset", "__value__": [...]}` |
|
|
78
|
+
| `bytes` | `{"__type__": "bytes", "__value__": "<base64>"}` |
|
|
79
|
+
| `datetime` | `{"__type__": "datetime", "__value__": "<isoformat>"}` |
|
|
80
|
+
| `date` | `{"__type__": "date", "__value__": "<isoformat>"}` |
|
|
81
|
+
| `time` | `{"__type__": "time", "__value__": "<isoformat>"}` |
|
|
82
|
+
|
|
83
|
+
Nested structures work too (e.g. a list of tuples, a set of tuples).
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from datetime import datetime
|
|
87
|
+
|
|
88
|
+
pobject.my_set = {1, 2, 3}
|
|
89
|
+
pobject.my_tuple = ("a", "b", "c")
|
|
90
|
+
pobject.my_bytes = b"hello"
|
|
91
|
+
pobject.my_datetime = datetime(2025, 6, 15, 12, 30)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Type enforcement
|
|
95
|
+
|
|
96
|
+
Use `settype()` to register a type constraint on an attribute. Assigning a value of the wrong type raises a `TypeError`.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from typing import Any
|
|
100
|
+
|
|
101
|
+
pobject.settype("count", int)
|
|
102
|
+
pobject.count = 5 # ok
|
|
103
|
+
pobject.count = "five" # TypeError
|
|
104
|
+
|
|
105
|
+
# union types (Python 3.10+)
|
|
106
|
+
pobject.settype("value", int | None)
|
|
107
|
+
pobject.value = None # ok
|
|
108
|
+
|
|
109
|
+
# tuple of types
|
|
110
|
+
pobject.settype("multi", (int, str))
|
|
111
|
+
|
|
112
|
+
# Any removes the constraint
|
|
113
|
+
pobject.settype("count", Any)
|
|
114
|
+
|
|
115
|
+
# also works on namespaces
|
|
116
|
+
namespace.settype("volume", int | float)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Defaults
|
|
120
|
+
|
|
121
|
+
Use `setdefault()` to set a value only if the attribute doesn't already exist. It also registers the value for `reset()`. An optional `type` parameter combines setting a default with type enforcement.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
pobject.setdefault("name", "default_name")
|
|
125
|
+
pobject.setdefault("count", 0, type=int) # sets default and enforces type
|
|
126
|
+
|
|
127
|
+
pobject.count = 99
|
|
128
|
+
pobject.reset("count") # back to 0
|
|
129
|
+
pobject.reset() # resets all attributes that have a registered default
|
|
130
|
+
```
|
persistentobjects-0.1.6/PKG-INFO
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: PersistentObjects
|
|
3
|
-
Version: 0.1.6
|
|
4
|
-
Summary: JSON-backed attribute-persistent object with namespace support
|
|
5
|
-
Author: Tornado300
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Dynamic: license-file
|
|
13
|
-
|
|
14
|
-
This is a very simple lib that provides the PersistantObject class, which saves all its attributes to the given json file. Exceptions are attributes suffixed with '_'.
|
|
15
|
-
Attributes are saved as soon as they are set.
|
|
16
|
-
|
|
17
|
-
[!IMPORTANT]
|
|
18
|
-
> morphing actions like appending, inserting, extending, etc. will not automaticly save!
|
|
19
|
-
> so its recomended to create a temp var, then append to it and at the end set the PersistantObject attr to the temp var.
|
|
20
|
-
|
|
21
|
-
with namespace(NAME) seperate sub namespaces can be created inside the object.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## Usage example:
|
|
25
|
-
```python
|
|
26
|
-
|
|
27
|
-
from PersistantObject import PersistantObject
|
|
28
|
-
|
|
29
|
-
pobject = PersistantObject("save.json")
|
|
30
|
-
|
|
31
|
-
pobject.first_value = 10
|
|
32
|
-
pobject.second_value = "foo"
|
|
33
|
-
|
|
34
|
-
namespace = pobject.namespace("test_namespace") # creates a new namespace (new subdict in json)
|
|
35
|
-
namespace.third_value = [10, 5]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
print(pobject.test_namespace.third_value) # alternate way to access namespaces
|
|
39
|
-
|
|
40
|
-
pobject.temp_ = True # suffixed with '_' so it will not be saved and behaves like a normal attribute.
|
|
41
|
-
```
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import json, os, threading
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class _PersistentState:
|
|
5
|
-
_instances = {}
|
|
6
|
-
|
|
7
|
-
def __new__(cls, path):
|
|
8
|
-
path = os.path.abspath(path)
|
|
9
|
-
|
|
10
|
-
if path in cls._instances:
|
|
11
|
-
return cls._instances[path]
|
|
12
|
-
|
|
13
|
-
instance = super().__new__(cls)
|
|
14
|
-
cls._instances[path] = instance
|
|
15
|
-
return instance
|
|
16
|
-
|
|
17
|
-
def __init__(self, path):
|
|
18
|
-
# Prevent re-initialization when __new__ returns an existing instance
|
|
19
|
-
if hasattr(self, "_initialized"):
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
self._initialized = True
|
|
23
|
-
self._path = os.path.abspath(path)
|
|
24
|
-
self._lock = threading.Lock()
|
|
25
|
-
self._data = {}
|
|
26
|
-
self._load()
|
|
27
|
-
|
|
28
|
-
def _load(self):
|
|
29
|
-
if os.path.exists(self._path):
|
|
30
|
-
try:
|
|
31
|
-
with open(self._path, "r", encoding="utf-8") as f:
|
|
32
|
-
self._data = json.load(f)
|
|
33
|
-
except Exception:
|
|
34
|
-
self._data = {}
|
|
35
|
-
|
|
36
|
-
def _save(self):
|
|
37
|
-
tmp = self._path + ".tmp"
|
|
38
|
-
with open(tmp, "w", encoding="utf-8") as f:
|
|
39
|
-
json.dump(self._data, f, indent=2)
|
|
40
|
-
os.replace(tmp, self._path)
|
|
41
|
-
|
|
42
|
-
def __getattr__(self, name):
|
|
43
|
-
if name.startswith("_"):
|
|
44
|
-
raise AttributeError(f"Cannot access private attribute '{name}'")
|
|
45
|
-
if name.endswith("_"):
|
|
46
|
-
try:
|
|
47
|
-
return super().__getattribute__(name[:-1])
|
|
48
|
-
except AttributeError:
|
|
49
|
-
raise AttributeError(
|
|
50
|
-
f"'{self.__class__.__name__}' object has no attribute '{name}'.\n Notice: Attribute '{name}' is not persistent!"
|
|
51
|
-
)
|
|
52
|
-
try:
|
|
53
|
-
value = self._data[name]
|
|
54
|
-
# If it's a dict, return it as a namespace
|
|
55
|
-
if isinstance(value, dict):
|
|
56
|
-
return _Namespace(self, name)
|
|
57
|
-
return value
|
|
58
|
-
except KeyError:
|
|
59
|
-
raise AttributeError(
|
|
60
|
-
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
def __setattr__(self, name, value):
|
|
64
|
-
if name.startswith("_"):
|
|
65
|
-
super().__setattr__(name, value)
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
if name.endswith("_"):
|
|
69
|
-
super().__setattr__(name[:-1], value)
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
with self._lock:
|
|
73
|
-
self._data[name] = value
|
|
74
|
-
self._save()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class _Namespace:
|
|
78
|
-
_instances = {}
|
|
79
|
-
|
|
80
|
-
def __new__(cls, root, key):
|
|
81
|
-
# Create unique identifier for this namespace
|
|
82
|
-
instance_key = (id(root), key)
|
|
83
|
-
|
|
84
|
-
if instance_key in cls._instances:
|
|
85
|
-
return cls._instances[instance_key]
|
|
86
|
-
|
|
87
|
-
instance = super().__new__(cls)
|
|
88
|
-
cls._instances[instance_key] = instance
|
|
89
|
-
return instance
|
|
90
|
-
|
|
91
|
-
def __init__(self, root, key):
|
|
92
|
-
# Prevent re-initialization
|
|
93
|
-
if hasattr(self, "_initialized"):
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
self._initialized = True
|
|
97
|
-
self._root = root
|
|
98
|
-
self._key = key
|
|
99
|
-
|
|
100
|
-
def __getattr__(self, name):
|
|
101
|
-
if name.startswith("_"):
|
|
102
|
-
raise AttributeError(name)
|
|
103
|
-
if name.endswith("_"):
|
|
104
|
-
try:
|
|
105
|
-
return super().__getattribute__(name[:-1])
|
|
106
|
-
except AttributeError:
|
|
107
|
-
raise AttributeError(
|
|
108
|
-
f"'{self.__class__.__name__}' object has no attribute '{name}'.\n Notice: Attribute '{name}' is not persistent!"
|
|
109
|
-
)
|
|
110
|
-
try:
|
|
111
|
-
return self._root._data[self._key][name]
|
|
112
|
-
except KeyError:
|
|
113
|
-
raise AttributeError(name)
|
|
114
|
-
|
|
115
|
-
def __setattr__(self, name, value):
|
|
116
|
-
if name.startswith("_"):
|
|
117
|
-
super().__setattr__(name, value)
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
if name.endswith("_"):
|
|
121
|
-
super().__setattr__(name[:-1], value)
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
with self._root._lock:
|
|
125
|
-
bucket = self._root._data.setdefault(self._key, {})
|
|
126
|
-
bucket[name] = value
|
|
127
|
-
self._root._save()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class PersistentObject(_PersistentState):
|
|
131
|
-
def namespace(self, name) -> _Namespace:
|
|
132
|
-
return _Namespace(self, name)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
__all__ = ["PersistentObject"]
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: PersistentObjects
|
|
3
|
-
Version: 0.1.6
|
|
4
|
-
Summary: JSON-backed attribute-persistent object with namespace support
|
|
5
|
-
Author: Tornado300
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Dynamic: license-file
|
|
13
|
-
|
|
14
|
-
This is a very simple lib that provides the PersistantObject class, which saves all its attributes to the given json file. Exceptions are attributes suffixed with '_'.
|
|
15
|
-
Attributes are saved as soon as they are set.
|
|
16
|
-
|
|
17
|
-
[!IMPORTANT]
|
|
18
|
-
> morphing actions like appending, inserting, extending, etc. will not automaticly save!
|
|
19
|
-
> so its recomended to create a temp var, then append to it and at the end set the PersistantObject attr to the temp var.
|
|
20
|
-
|
|
21
|
-
with namespace(NAME) seperate sub namespaces can be created inside the object.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## Usage example:
|
|
25
|
-
```python
|
|
26
|
-
|
|
27
|
-
from PersistantObject import PersistantObject
|
|
28
|
-
|
|
29
|
-
pobject = PersistantObject("save.json")
|
|
30
|
-
|
|
31
|
-
pobject.first_value = 10
|
|
32
|
-
pobject.second_value = "foo"
|
|
33
|
-
|
|
34
|
-
namespace = pobject.namespace("test_namespace") # creates a new namespace (new subdict in json)
|
|
35
|
-
namespace.third_value = [10, 5]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
print(pobject.test_namespace.third_value) # alternate way to access namespaces
|
|
39
|
-
|
|
40
|
-
pobject.temp_ = True # suffixed with '_' so it will not be saved and behaves like a normal attribute.
|
|
41
|
-
```
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
This is a very simple lib that provides the PersistantObject class, which saves all its attributes to the given json file. Exceptions are attributes suffixed with '_'.
|
|
2
|
-
Attributes are saved as soon as they are set.
|
|
3
|
-
|
|
4
|
-
[!IMPORTANT]
|
|
5
|
-
> morphing actions like appending, inserting, extending, etc. will not automaticly save!
|
|
6
|
-
> so its recomended to create a temp var, then append to it and at the end set the PersistantObject attr to the temp var.
|
|
7
|
-
|
|
8
|
-
with namespace(NAME) seperate sub namespaces can be created inside the object.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Usage example:
|
|
12
|
-
```python
|
|
13
|
-
|
|
14
|
-
from PersistantObject import PersistantObject
|
|
15
|
-
|
|
16
|
-
pobject = PersistantObject("save.json")
|
|
17
|
-
|
|
18
|
-
pobject.first_value = 10
|
|
19
|
-
pobject.second_value = "foo"
|
|
20
|
-
|
|
21
|
-
namespace = pobject.namespace("test_namespace") # creates a new namespace (new subdict in json)
|
|
22
|
-
namespace.third_value = [10, 5]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
print(pobject.test_namespace.third_value) # alternate way to access namespaces
|
|
26
|
-
|
|
27
|
-
pobject.temp_ = True # suffixed with '_' so it will not be saved and behaves like a normal attribute.
|
|
28
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistentobjects-0.1.6 → persistentobjects-0.2.0}/PersistentObjects.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{persistentobjects-0.1.6 → persistentobjects-0.2.0}/PersistentObjects.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|