persidict 0.34.2__tar.gz → 0.35.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.

Potentially problematic release.


This version of persidict might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: persidict
3
- Version: 0.34.2
3
+ Version: 0.35.0
4
4
  Summary: Simple persistent key-value store for Python. Values are stored as files on a disk or as S3 objects on AWS cloud.
5
5
  Keywords: persistence,dicts,distributed,parallel
6
6
  Author: Vlad (Volodymyr) Pavlov
@@ -158,10 +158,9 @@ that simultaneously work with the same instance of a dictionary.
158
158
  * Insertion order is not preserved.
159
159
  * You cannot assign initial key-value pairs to a dictionary in its constructor.
160
160
  * `PersiDict` API has additional methods `delete_if_exists()`, `timestamp()`,
161
- `get_subdict()`, `subdicts()`, `random_keys()`, `newest_keys()`,
162
- `oldest_keys()`, `newest_values()`, `oldest_values()`,
163
- `get_params()`, `get_metaparams()`, and `get_default_metaparams()`,
164
- which are not available in native Python dicts.
161
+ `get_subdict()`, `subdicts()`, `random_key()`, `newest_keys()`,
162
+ `oldest_keys()`, `newest_values()`, `oldest_values()`, and
163
+ `get_params()`, which are not available in native Python dicts.
165
164
  * You can use KEEP_CURRENT constant as a fake new value
166
165
  to avoid actually setting/updating a value. Or DELETE_CURRENT as
167
166
  a fake new value to delete the previous value from a dictionary.
@@ -124,10 +124,9 @@ that simultaneously work with the same instance of a dictionary.
124
124
  * Insertion order is not preserved.
125
125
  * You cannot assign initial key-value pairs to a dictionary in its constructor.
126
126
  * `PersiDict` API has additional methods `delete_if_exists()`, `timestamp()`,
127
- `get_subdict()`, `subdicts()`, `random_keys()`, `newest_keys()`,
128
- `oldest_keys()`, `newest_values()`, `oldest_values()`,
129
- `get_params()`, `get_metaparams()`, and `get_default_metaparams()`,
130
- which are not available in native Python dicts.
127
+ `get_subdict()`, `subdicts()`, `random_key()`, `newest_keys()`,
128
+ `oldest_keys()`, `newest_values()`, `oldest_values()`, and
129
+ `get_params()`, which are not available in native Python dicts.
131
130
  * You can use KEEP_CURRENT constant as a fake new value
132
131
  to avoid actually setting/updating a value. Or DELETE_CURRENT as
133
132
  a fake new value to delete the previous value from a dictionary.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "persidict"
7
- version = "0.34.2"
7
+ version = "0.35.0"
8
8
  description = "Simple persistent key-value store for Python. Values are stored as files on a disk or as S3 objects on AWS cloud."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,11 +1,11 @@
1
- """ Persistent dictionaries that store key-value pairs on local disks.
2
-
3
- This functionality is implemented by the class FileDirDict
4
- (inherited from PersiDict): a dictionary that
5
- stores key-value pairs as files on a local hard-drive.
6
- A key is used to compose a filename, while a value is stored in the file
7
- as a binary, or as a json object, or as a plain text
8
- (depends on configuration parameters).
1
+ """Persistent dictionary implementation backed by local files.
2
+
3
+ FileDirDict stores each key-value pair in a separate file under a base
4
+ directory. Keys determine directory structure and filename; values are
5
+ serialized depending on ``file_type``.
6
+
7
+ - file_type="pkl" or "json": arbitrary Python objects via pickle/jsonpickle.
8
+ - any other value: strings are stored as plain text.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
@@ -56,29 +56,35 @@ class FileDirDict(PersiDict):
56
56
  , immutable_items:bool = False
57
57
  , digest_len:int = 8
58
58
  , base_class_for_values: Optional[type] = None):
59
- """A constructor defines location of the store and file format to use.
60
-
61
- _base_dir is a directory that will contain all the files in
62
- the FileDirDict. If the directory does not exist, it will be created.
63
-
64
- base_class_for_values constraints the type of values that can be
65
- stored in the dictionary. If specified, it will be used to
66
- check types of values in the dictionary. If not specified,
67
- no type checking will be performed and all types will be allowed.
68
-
69
- file_type is extension, which will be used for all files in the dictionary.
70
- If file_type has one of two values: "pkl" or "json", it defines
71
- which file format will be used by FileDirDict to store values.
72
- For all other values of file_type, the file format will always be plain
73
- text. "pkl" and "json" allow to store arbitrary Python objects,
74
- while all other file_type-s only work with str objects.
59
+ """Initialize a filesystem-backed persistent dictionary.
60
+
61
+ Args:
62
+ base_dir (str): Base directory where all files are stored. Created
63
+ if it does not exist.
64
+ file_type (str): File extension/format to use for stored values.
65
+ - "pkl" or "json": arbitrary Python objects are supported.
66
+ - any other value: only strings are supported and stored as text.
67
+ immutable_items (bool): If True, existing items cannot be modified
68
+ or deleted.
69
+ digest_len (int): Length of a hash suffix appended to each key path
70
+ element to avoid case-insensitive collisions. Use 0 to disable.
71
+ base_class_for_values (Optional[type]): Optional base class that all
72
+ stored values must be instances of. If provided and not ``str``,
73
+ then file_type must be either "pkl" or "json".
74
+
75
+ Raises:
76
+ ValueError: If base_dir points to a file; if file_type is "__etag__";
77
+ if file_type contains unsafe characters; or if configuration is
78
+ inconsistent (e.g., non-str values with unsupported file_type).
79
+ RuntimeError: If base_dir cannot be created or is not a directory.
75
80
  """
76
81
 
77
82
  super().__init__(immutable_items = immutable_items
78
83
  ,digest_len = digest_len
79
84
  ,base_class_for_values = base_class_for_values)
80
85
 
81
- assert file_type == replace_unsafe_chars(file_type, "")
86
+ if file_type != replace_unsafe_chars(file_type, ""):
87
+ raise ValueError("file_type contains unsafe characters")
82
88
  self.file_type = file_type
83
89
  if self.file_type == "__etag__":
84
90
  raise ValueError(
@@ -87,8 +93,8 @@ class FileDirDict(PersiDict):
87
93
 
88
94
  if (base_class_for_values is None or
89
95
  not issubclass(base_class_for_values,str)):
90
- assert file_type in {"json", "pkl"}, ("For non-string values"
91
- + " file_type must be either 'pkl' or 'json'.")
96
+ if file_type not in {"json", "pkl"}:
97
+ raise ValueError("For non-string values file_type must be either 'pkl' or 'json'.")
92
98
 
93
99
  base_dir = str(base_dir)
94
100
 
@@ -96,7 +102,8 @@ class FileDirDict(PersiDict):
96
102
  raise ValueError(f"{base_dir} is a file, not a directory.")
97
103
 
98
104
  os.makedirs(base_dir, exist_ok=True)
99
- assert os.path.isdir(base_dir)
105
+ if not os.path.isdir(base_dir):
106
+ raise RuntimeError(f"Failed to create or access directory: {base_dir}")
100
107
 
101
108
  # self.base_dir_param = _base_dir
102
109
  self._base_dir = os.path.abspath(base_dir)
@@ -105,8 +112,12 @@ class FileDirDict(PersiDict):
105
112
  def get_params(self):
106
113
  """Return configuration parameters of the dictionary.
107
114
 
108
- This method is needed to support Parameterizable API.
109
- The method is absent in the original dict API.
115
+ This method is needed to support the Parameterizable API and is absent
116
+ in the standard dict API.
117
+
118
+ Returns:
119
+ dict: A mapping of parameter names to values including base_dir and
120
+ file_type merged with the base PersiDict parameters.
110
121
  """
111
122
  params = PersiDict.get_params(self)
112
123
  additional_params = dict(
@@ -122,6 +133,9 @@ class FileDirDict(PersiDict):
122
133
  """Return dictionary's URL.
123
134
 
124
135
  This property is absent in the original dict API.
136
+
137
+ Returns:
138
+ str: URL of the underlying storage in the form "file://<abs_path>".
125
139
  """
126
140
  return f"file://{self._base_dir}"
127
141
 
@@ -131,16 +145,25 @@ class FileDirDict(PersiDict):
131
145
  """Return dictionary's base directory.
132
146
 
133
147
  This property is absent in the original dict API.
148
+
149
+ Returns:
150
+ str: Absolute path to the base directory used by this dictionary.
134
151
  """
135
152
  return self._base_dir
136
153
 
137
154
 
138
155
  def __len__(self) -> int:
139
- """ Get the number of key-value pairs in the dictionary.
156
+ """Return the number of key-value pairs in the dictionary.
157
+
158
+ This performs a recursive traversal of the base directory.
140
159
 
141
- WARNING: This operation can be slow on large dictionaries as it
142
- needs to recursively walk the entire base directory.
143
- Avoid using it in performance-sensitive code.
160
+ Returns:
161
+ int: Count of stored items.
162
+
163
+ Note:
164
+ This operation can be slow on large dictionaries as it walks the
165
+ entire directory tree. Avoid using it in performance-sensitive
166
+ code paths.
144
167
  """
145
168
 
146
169
  suffix = "." + self.file_type
@@ -149,7 +172,11 @@ class FileDirDict(PersiDict):
149
172
 
150
173
 
151
174
  def clear(self) -> None:
152
- """ Remove all elements from the dictionary."""
175
+ """Remove all elements from the dictionary.
176
+
177
+ Raises:
178
+ KeyError: If immutable_items is True.
179
+ """
153
180
 
154
181
  if self.immutable_items:
155
182
  raise KeyError("Can't clear a dict that contains immutable items")
@@ -172,7 +199,26 @@ class FileDirDict(PersiDict):
172
199
  , key:SafeStrTuple
173
200
  , create_subdirs:bool=False
174
201
  , is_file_path:bool=True) -> str:
175
- """Convert a key into a filesystem path."""
202
+ """Convert a key into an absolute filesystem path.
203
+
204
+ Transforms a SafeStrTuple into either a directory path or a file path
205
+ inside this dictionary's base directory. When is_file_path is True, the
206
+ final component is treated as a filename with the configured file_type
207
+ extension. When create_subdirs is True, missing intermediate directories
208
+ are created.
209
+
210
+ Args:
211
+ key (SafeStrTuple): The key to convert. It will be temporarily
212
+ signed according to digest_len to produce collision-safe names.
213
+ create_subdirs (bool): If True, create any missing intermediate
214
+ directories.
215
+ is_file_path (bool): If True, return a file path ending with
216
+ ".{file_type}"; otherwise return just the directory path for
217
+ the key prefix.
218
+
219
+ Returns:
220
+ str: An absolute path within base_dir corresponding to the key.
221
+ """
176
222
 
177
223
  key = sign_safe_str_tuple(key, self.digest_len)
178
224
  key = [self._base_dir] + list(key.strings)
@@ -190,7 +236,22 @@ class FileDirDict(PersiDict):
190
236
 
191
237
 
192
238
  def _build_key_from_full_path(self, full_path:str)->SafeStrTuple:
193
- """Convert a filesystem path back into a key."""
239
+ """Convert an absolute filesystem path back into a SafeStrTuple key.
240
+
241
+ This function reverses _build_full_path, stripping base_dir, removing the
242
+ file_type extension if the path points to a file, and unsigning the key
243
+ components according to digest_len.
244
+
245
+ Args:
246
+ full_path (str): Absolute path within the dictionary's base
247
+ directory.
248
+
249
+ Returns:
250
+ SafeStrTuple: The reconstructed (unsigned) key.
251
+
252
+ Raises:
253
+ ValueError: If full_path is not located under base_dir.
254
+ """
194
255
 
195
256
  # Ensure we're working with absolute paths
196
257
  full_path = os.path.abspath(full_path)
@@ -225,8 +286,15 @@ class FileDirDict(PersiDict):
225
286
  """Get a subdictionary containing items with the same prefix key.
226
287
 
227
288
  For non-existing prefix key, an empty sub-dictionary is returned.
228
-
229
289
  This method is absent in the original dict API.
290
+
291
+ Args:
292
+ key (PersiDictKey): Prefix key (string or sequence of strings) that
293
+ identifies the subdirectory.
294
+
295
+ Returns:
296
+ FileDirDict: A new FileDirDict instance rooted at the specified
297
+ subdirectory, sharing the same parameters as this dictionary.
230
298
  """
231
299
  key = SafeStrTuple(key)
232
300
  full_dir_path = self._build_full_path(
@@ -240,7 +308,14 @@ class FileDirDict(PersiDict):
240
308
 
241
309
 
242
310
  def _read_from_file_impl(self, file_name:str) -> Any:
243
- """Read a value from a file. """
311
+ """Read a value from a single file without retries.
312
+
313
+ Args:
314
+ file_name (str): Absolute path to the file to read.
315
+
316
+ Returns:
317
+ Any: The deserialized value according to file_type.
318
+ """
244
319
 
245
320
  if self.file_type == "pkl":
246
321
  with open(file_name, 'rb') as f:
@@ -255,7 +330,22 @@ class FileDirDict(PersiDict):
255
330
 
256
331
 
257
332
  def _read_from_file(self,file_name:str) -> Any:
258
- """Read a value from a file. """
333
+ """Read a value from a file with retry/backoff for concurrency.
334
+
335
+ Validates that the configured file_type is compatible with the allowed
336
+ value types, then attempts to read the file using an exponential backoff
337
+ to better tolerate concurrent writers.
338
+
339
+ Args:
340
+ file_name (str): Absolute path of the file to read.
341
+
342
+ Returns:
343
+ Any: The deserialized value according to file_type.
344
+
345
+ Raises:
346
+ ValueError: If file_type is incompatible with non-string values.
347
+ Exception: Propagates the last exception if all retries fail.
348
+ """
259
349
 
260
350
  if not (self.file_type in {"pkl", "json"} or issubclass(
261
351
  self.base_class_for_values, str)):
@@ -275,7 +365,15 @@ class FileDirDict(PersiDict):
275
365
 
276
366
 
277
367
  def _save_to_file_impl(self, file_name:str, value:Any) -> None:
278
- """Save a value to a file. """
368
+ """Write a single value to a file atomically (no retries).
369
+
370
+ Uses a temporary file and atomic rename to avoid partial writes and to
371
+ reduce the chance of readers observing corrupted data.
372
+
373
+ Args:
374
+ file_name (str): Absolute destination file path.
375
+ value (Any): Value to serialize and save.
376
+ """
279
377
 
280
378
  dir_name = os.path.dirname(file_name)
281
379
  # Use a temporary file and atomic rename to prevent data corruption
@@ -313,7 +411,20 @@ class FileDirDict(PersiDict):
313
411
  raise
314
412
 
315
413
  def _save_to_file(self, file_name:str, value:Any) -> None:
316
- """Save a value to a file. """
414
+ """Save a value to a file with retry/backoff.
415
+
416
+ Ensures the configured file_type is compatible with value types and then
417
+ writes the value using an exponential backoff to better tolerate
418
+ concurrent readers/writers.
419
+
420
+ Args:
421
+ file_name (str): Absolute destination file path.
422
+ value (Any): Value to serialize and save.
423
+
424
+ Raises:
425
+ ValueError: If file_type is incompatible with non-string values.
426
+ Exception: Propagates the last exception if all retries fail.
427
+ """
317
428
 
318
429
  if not (self.file_type in {"pkl", "json"} or issubclass(
319
430
  self.base_class_for_values, str)):
@@ -334,14 +445,36 @@ class FileDirDict(PersiDict):
334
445
 
335
446
 
336
447
  def __contains__(self, key:PersiDictKey) -> bool:
337
- """True if the dictionary has the specified key, else False. """
448
+ """Check whether a key exists in the dictionary.
449
+
450
+ Args:
451
+ key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
452
+
453
+ Returns:
454
+ bool: True if a file for the key exists; False otherwise.
455
+ """
338
456
  key = SafeStrTuple(key)
339
457
  filename = self._build_full_path(key)
340
458
  return os.path.isfile(filename)
341
459
 
342
460
 
343
461
  def __getitem__(self, key:PersiDictKey) -> Any:
344
- """ Implementation for x[y] syntax. """
462
+ """Retrieve the value stored for a key.
463
+
464
+ Equivalent to obj[key]. Reads the corresponding file from the disk and
465
+ deserializes according to file_type.
466
+
467
+ Args:
468
+ key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
469
+
470
+ Returns:
471
+ Any: The stored value.
472
+
473
+ Raises:
474
+ KeyError: If the file for the key does not exist.
475
+ TypeError: If the deserialized value does not match base_class_for_values
476
+ when it is set.
477
+ """
345
478
  key = SafeStrTuple(key)
346
479
  filename = self._build_full_path(key)
347
480
  if not os.path.isfile(filename):
@@ -356,7 +489,22 @@ class FileDirDict(PersiDict):
356
489
 
357
490
 
358
491
  def __setitem__(self, key:PersiDictKey, value:Any):
359
- """Set self[key] to value."""
492
+ """Store a value for a key on the disk.
493
+
494
+ Interprets joker values KEEP_CURRENT and DELETE_CURRENT accordingly.
495
+ Validates value type if base_class_for_values is set, then serializes
496
+ and writes to a file determined by the key and file_type.
497
+
498
+ Args:
499
+ key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
500
+ value (Any): Value to store, or a joker command.
501
+
502
+ Raises:
503
+ KeyError: If attempting to modify an existing item when
504
+ immutable_items is True.
505
+ TypeError: If the value is a PersiDict or does not match
506
+ base_class_for_values when it is set.
507
+ """
360
508
 
361
509
  if value is KEEP_CURRENT:
362
510
  return
@@ -384,9 +532,17 @@ class FileDirDict(PersiDict):
384
532
 
385
533
 
386
534
  def __delitem__(self, key:PersiDictKey) -> None:
387
- """Delete self[key]."""
535
+ """Delete the stored value for a key.
536
+
537
+ Args:
538
+ key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
539
+
540
+ Raises:
541
+ KeyError: If immutable_items is True or if the key does not exist.
542
+ """
388
543
  key = SafeStrTuple(key)
389
- assert not self.immutable_items, "Can't delete immutable items"
544
+ if self.immutable_items:
545
+ raise KeyError("Can't delete immutable items")
390
546
  filename = self._build_full_path(key)
391
547
  if not os.path.isfile(filename):
392
548
  raise KeyError(f"File {filename} does not exist")
@@ -394,22 +550,54 @@ class FileDirDict(PersiDict):
394
550
 
395
551
 
396
552
  def _generic_iter(self, result_type: set[str]):
397
- """Underlying implementation for .items()/.keys()/.values() iterators"""
398
- assert isinstance(result_type, set)
399
- assert 1 <= len(result_type) <= 3
400
- assert len(result_type | {"keys", "values", "timestamps"}) == 3
401
- assert 1 <= len(result_type & {"keys", "values", "timestamps"}) <= 3
553
+ """Underlying implementation for .items()/.keys()/.values() iterators.
554
+
555
+ Produces generators over keys, values, and/or timestamps by traversing
556
+ the directory tree under base_dir. Keys are converted back from paths by
557
+ removing the file extension and unsigning according to digest_len.
558
+
559
+ Args:
560
+ result_type (set[str]): Any non-empty subset of {"keys", "values",
561
+ "timestamps"} specifying which fields to yield.
562
+
563
+ Returns:
564
+ Iterator: A generator yielding:
565
+ - SafeStrTuple if result_type == {"keys"}
566
+ - Any if result_type == {"values"}
567
+ - tuple[SafeStrTuple, Any] if result_type == {"keys", "values"}
568
+ - tuple[..., float] including POSIX timestamp if "timestamps" is requested.
569
+
570
+ Raises:
571
+ TypeError: If result_type is not a set.
572
+ ValueError: If result_type is empty or contains unsupported labels.
573
+ """
574
+ if not isinstance(result_type, set):
575
+ raise TypeError("result_type must be a set")
576
+ if not (1 <= len(result_type) <= 3):
577
+ raise ValueError("result_type must be a non-empty subset of {'keys','values','timestamps'}")
578
+ allowed = {"keys", "values", "timestamps"}
579
+ invalid = result_type - allowed
580
+ if invalid:
581
+ raise ValueError(f"Unsupported result_type entries: {sorted(invalid)}; allowed: {sorted(allowed)}")
402
582
 
403
583
  walk_results = os.walk(self._base_dir)
404
584
  ext_len = len(self.file_type) + 1
405
585
 
406
586
  def splitter(dir_path: str):
407
- """Transform a dirname into a PersiDictKey key"""
587
+ """Transform a relative dirname into SafeStrTuple components.
588
+
589
+ Args:
590
+ dir_path (str): Relative path under base_dir (e.g., "a/b").
591
+
592
+ Returns:
593
+ list[str]: List of safe string components (may be empty).
594
+ """
408
595
  if dir_path == ".":
409
596
  return []
410
597
  return dir_path.split(os.sep)
411
598
 
412
599
  def step():
600
+ """Generator that yields entries based on result_type."""
413
601
  suffix = "." + self.file_type
414
602
  for dir_name, _, files in walk_results:
415
603
  for f in files:
@@ -448,6 +636,15 @@ class FileDirDict(PersiDict):
448
636
  """Get last modification time (in seconds, Unix epoch time).
449
637
 
450
638
  This method is absent in the original dict API.
639
+
640
+ Args:
641
+ key (PersiDictKey): Key whose timestamp to return.
642
+
643
+ Returns:
644
+ float: POSIX timestamp of the underlying file.
645
+
646
+ Raises:
647
+ FileNotFoundError: If the key does not exist.
451
648
  """
452
649
  key = SafeStrTuple(key)
453
650
  filename = self._build_full_path(key)
@@ -455,6 +652,15 @@ class FileDirDict(PersiDict):
455
652
 
456
653
 
457
654
  def random_key(self) -> PersiDictKey | None:
655
+ """Return a uniformly random key from the dictionary, or None if empty.
656
+
657
+ Performs a full directory traversal using reservoir sampling
658
+ (k=1) to select a random file matching the configured file_type without
659
+ loading all keys into memory.
660
+
661
+ Returns:
662
+ PersiDictKey | None: A random key if any items exist; otherwise None.
663
+ """
458
664
  # canonicalise extension once
459
665
  ext = None
460
666
  if self.file_type:
@@ -0,0 +1,91 @@
1
+ """Special singleton markers used to modify values in PersiDict without data payload.
2
+
3
+ This module defines two singleton flags used as "joker" values when writing to
4
+ persistent dictionaries:
5
+
6
+ - KEEP_CURRENT: keep the current value unchanged.
7
+ - DELETE_CURRENT: delete the current value if it exists.
8
+
9
+ These flags are intended to be passed as the value part in dict-style
10
+ assignments (e.g., d[key] = KEEP_CURRENT) and are interpreted by PersiDict
11
+ implementations.
12
+
13
+ Examples:
14
+ >>> from persidict.jokers import KEEP_CURRENT, DELETE_CURRENT
15
+ >>> d[key] = KEEP_CURRENT # Do not alter existing value
16
+ >>> d[key] = DELETE_CURRENT # Remove key if present
17
+ """
18
+ from typing import Any
19
+
20
+ from parameterizable import (
21
+ ParameterizableClass
22
+ , register_parameterizable_class)
23
+
24
+
25
+ class Joker(ParameterizableClass):
26
+ """Base class for singleton joker flags.
27
+
28
+ Implements a per-subclass singleton pattern and integrates with the
29
+ parameterizable framework. Subclasses represent value-less commands that
30
+ alter persistence behavior when assigned to a key.
31
+
32
+ Returns:
33
+ Joker: The singleton instance for the subclass when instantiated.
34
+ """
35
+ _instances = {}
36
+
37
+ def get_params(self) -> dict[str, Any]:
38
+ """Return parameters for parameterizable API.
39
+
40
+ Returns:
41
+ dict[str, Any]: Always an empty dict for joker flags.
42
+ """
43
+ return {}
44
+
45
+ def __new__(cls):
46
+ """Create or return the singleton instance for the subclass."""
47
+ if cls not in Joker._instances:
48
+ Joker._instances[cls] = super().__new__(cls)
49
+ return Joker._instances[cls]
50
+
51
+
52
+ class KeepCurrentFlag(Joker):
53
+ """Flag instructing PersiDict to keep the current value unchanged.
54
+
55
+ Usage:
56
+ Assign this flag instead of a real value to indicate that an existing
57
+ value should not be modified.
58
+
59
+ Examples:
60
+ >>> d[key] = KEEP_CURRENT
61
+
62
+ Note:
63
+ This is a singleton class; constructing it repeatedly returns the same
64
+ instance.
65
+ """
66
+ pass
67
+
68
+ class DeleteCurrentFlag(Joker):
69
+ """Flag instructing PersiDict to delete the current value for a key.
70
+
71
+ Usage:
72
+ Assign this flag instead of a real value to remove the key if it
73
+ exists. If the key is absent, implementations will typically no-op.
74
+
75
+ Examples:
76
+ >>> d[key] = DELETE_CURRENT
77
+
78
+ Note:
79
+ This is a singleton class; constructing it repeatedly returns the same
80
+ instance.
81
+ """
82
+ pass
83
+
84
+ register_parameterizable_class(KeepCurrentFlag)
85
+ register_parameterizable_class(DeleteCurrentFlag)
86
+
87
+ KeepCurrent = KeepCurrentFlag()
88
+ KEEP_CURRENT = KeepCurrentFlag()
89
+
90
+ DeleteCurrent = DeleteCurrentFlag()
91
+ DELETE_CURRENT = DeleteCurrentFlag()