flinventory 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,552 @@
1
+ #! /usr/bin/env python3
2
+ """Data structures for objects with fallback.
3
+
4
+ This module provides a dict-like data structure, maybe later
5
+ also other data structures, that can provide information
6
+ from a different default data storage if it does not provide
7
+ it itself but is still editable.
8
+
9
+ To keep the special cases at bay, the supported data types are limited:
10
+ - keys: str, tuple(str, language key (which is a str as well))
11
+ - values: str, int, float, bool, None, list of the simple ones
12
+ - when list is accessed, always tuple is returned
13
+ - list-altering functions are supplied
14
+ """
15
+
16
+ import logging
17
+
18
+ import collections
19
+
20
+ from typing import Any, Iterable, Union, Iterator, cast, Callable
21
+
22
+ from . import constant
23
+
24
+ IMMUTABLE_TYPES = (str, int, float, bool, type(None))
25
+ Immutable = Union[*IMMUTABLE_TYPES]
26
+ Key = Union[str, tuple[str, str]]
27
+ """Type of keys of DefaultedDict"""
28
+ Value = Union[Immutable, list[Immutable]]
29
+ """Type of values of DefaultedDict"""
30
+ Data = collections.abc.Mapping[Key, Value]
31
+ """Type of data that can be used as default and starting data."""
32
+ SuperDict = collections.abc.MutableMapping[Key, Value]
33
+ """Super type of DefaultedDict."""
34
+
35
+
36
+ class DefaultedDict(SuperDict):
37
+ """A dict that returns information from a default dict if needed.
38
+
39
+ The DefaultedDict is not subclassed from dict or UserDict so that
40
+ we have to think for every functionality how it should be implemented.
41
+
42
+ For simplicity only some value types are allowed:
43
+ - immutable values
44
+ - lists of immutable values
45
+
46
+ For simplicity only some key types are allowed:
47
+ - str
48
+ - tuple[str, str|int] for translated keys (where ints are converted to language keys)
49
+
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ data: collections.abc.Mapping[Key, Value],
55
+ default: collections.abc.Mapping[Key, Value],
56
+ non_defaulted: Iterable[str],
57
+ translated: Iterable[str],
58
+ lists: Iterable[str],
59
+ default_order: Iterable[str],
60
+ options: constant.Options,
61
+ ):
62
+ """Creates a defaulted dict.
63
+
64
+ Args:
65
+ data: data at the beginning. Is converted as if all elements where inserted one by one.
66
+ default: other dict where to draw information from where this dict has none.
67
+ Use empty dict if no default values exist.
68
+ non_defaulted: keys that should not use data from default
69
+ translated: keys that are used as a tuple (this_key, language) for translated values
70
+ lists: keys (and tuples of (this_key, language) of list value
71
+ default_order: default order of keys when iterating or turning into regular dict
72
+ """
73
+ self._logger = logging.getLogger(__name__)
74
+ self._default = default
75
+ self._non_defaulted = tuple(non_defaulted)
76
+ self._translated = tuple(translated)
77
+ self._lists = tuple(lists)
78
+ self._default_order = tuple(default_order)
79
+ self._options = options
80
+ self._data = {}
81
+ for key, value in data.items():
82
+ self[key] = value # type checking and so on
83
+ if isinstance(data, DefaultedDict):
84
+ logging.getLogger(__name__).debug(
85
+ f"Check that it's correctly saved: {data!r} -> {self!r}."
86
+ )
87
+
88
+ @property
89
+ def default(self):
90
+ """The default values."""
91
+ return self._default
92
+
93
+ @property
94
+ def non_defaulted(self) -> tuple[str, ...]:
95
+ """Tuple of keys that do not use default."""
96
+ return tuple(self._non_defaulted)
97
+
98
+ @property
99
+ def translated(self) -> tuple[str, ...]:
100
+ """Tuple of keys that should be called as a (key, language_key)."""
101
+ return tuple(self._translated)
102
+
103
+ @property
104
+ def lists(self) -> tuple[str, ...]:
105
+ """Tuple of keys that hold lists."""
106
+ return tuple(self._lists)
107
+
108
+ @property
109
+ def default_order(self) -> tuple[str]:
110
+ """Default order of keys. Not changable."""
111
+ return tuple(self._default_order)
112
+
113
+ def best(self, translated_key, **backup):
114
+ """For a translated key, get the 'best' value.
115
+
116
+ That is: get the value for the language that is highest in the options.languages list.
117
+
118
+ Args:
119
+ translated_key: key for which a value is looked for. Must be in self.translated.
120
+ **backup: if the key-word-only argument 'backup' is given, use this as the default value
121
+ If no such argument is given, raise KeyError
122
+ Raises:
123
+ KeyError: if no arguments are given and no value could be found
124
+ """
125
+ assert translated_key in self.translated
126
+ for language in self._options.languages:
127
+ try:
128
+ return self[translated_key, language]
129
+ except KeyError:
130
+ pass
131
+ for key in self:
132
+ # also searches in default
133
+ if isinstance(key, tuple) and len(key) == 2 and key[0] == translated_key:
134
+ return self[key]
135
+ if "backup" in backup:
136
+ return backup["backup"]
137
+ raise KeyError(f"No {translated_key} in {self!r} known.")
138
+
139
+ @staticmethod
140
+ def _key_in_category(key: Key, category: Iterable[str]) -> bool:
141
+ """Is the key in one of the categories (lists, non_defaulted, translated)?
142
+
143
+ In case of tuple only regard the first element since the second is a language key.
144
+ """
145
+ return key in category or (isinstance(key, tuple) and key[0] in category)
146
+
147
+ def interpret_number_language(self, key: Union[Key, tuple[str, int]]) -> Key:
148
+ """Transform key in case of type (translatable, number). Otherwise return as is."""
149
+ try:
150
+ return (key[0], self._options.languages[key[1]])
151
+ # checks implicitly:
152
+ # and isinstance(key, tuple)
153
+ # and len(key) > 1
154
+ # and isinstance(key[1], int)
155
+ except (IndexError, TypeError):
156
+ return key
157
+
158
+ def get_conversion(self, key: Key) -> Callable[[Any], Any]:
159
+ """Choose the correct conversion from saved value to given value based on key.
160
+
161
+ Can be tuple for lists or identity for everything else.
162
+ """
163
+ return (
164
+ tuple if DefaultedDict._key_in_category(key, self._lists) else lambda x: x
165
+ )
166
+
167
+ def __getitem__(self, key: Union[Key, tuple[str, int]]) -> Value:
168
+ """Give item for ["key"] syntax. Use default is needed.
169
+
170
+ Returns:
171
+ in order of preference:
172
+ - internally saved value
173
+ - value in default
174
+ - in case of translated keys: dictionary language_key : value, also including default
175
+ values. Note that changing this dictionary does not change the value of self.
176
+ Does not include translated values for language codes that are not listed
177
+ in options.
178
+ Raises:
179
+ KeyError: if key does not exist and default has not key either
180
+ or key is in self.non_defaulted. Also raises KeyError if a translated key
181
+ is queried and no translations are available (no empty dict is given in this case).
182
+ """
183
+ key = self.interpret_number_language(key)
184
+ conversion = self.get_conversion(key)
185
+
186
+ try:
187
+ return conversion(self._data[key])
188
+ except KeyError as key_error:
189
+ if DefaultedDict._key_in_category(key, self._non_defaulted):
190
+ raise KeyError(f"Default not used for {key}") from key_error
191
+ try:
192
+ return conversion(self._default[key])
193
+ except KeyError:
194
+ if key in self._translated:
195
+ translations = {}
196
+ for lang_key in self._options.languages:
197
+ try:
198
+ translations[lang_key] = conversion(self[(key, lang_key)])
199
+ except KeyError:
200
+ # does not exist
201
+ pass
202
+ if translations:
203
+ return translations
204
+ # else:
205
+ raise KeyError(f"{key} has no value.") from key_error
206
+
207
+ def get_undefaulted(self, key: Key, **backup: Value) -> Value:
208
+ """Get a value but without resorting to "backup".
209
+
210
+ Args:
211
+ key: the key for which the value is looked for.
212
+ Can be (translatable, nr) which is interpreted as in __getitem__.
213
+ Cannot be translatable without language key.
214
+ (If that is necessary in the same way as for [key], it needs to be implemented.)
215
+ backup: optional keyword-only argument "backup". Used if no value is saved.
216
+ If not given, KeyError is raised.
217
+ Returns:
218
+ internalData.get(key, backup) or internalData[key]
219
+ """
220
+ key = self.interpret_number_language(key)
221
+ conversion = self.get_conversion(key)
222
+ try:
223
+ return conversion(self._data[key])
224
+ except KeyError:
225
+ if "backup" in backup:
226
+ return backup["backup"]
227
+ raise
228
+
229
+ def __setitem__(
230
+ self, key: Union[Key, tuple[str, int]], value: Union[Value, dict[str, Value]]
231
+ ) -> None:
232
+ """Sets a value.
233
+
234
+ Checks the type: if it should be a list, then check that it is.
235
+ Otherwise, check that it is Immutable.
236
+
237
+ If language is needed but not given,
238
+ use first language in options if a list or single value is given,
239
+ set all languages if dict is given.
240
+ If language is given as an integer, use the language at this index.
241
+ Args:
242
+ key: new key
243
+ value: new value. If key in self.translated, must be dict.
244
+ Raises:
245
+ AssertionError: in case of wrong types. This is chosen to give
246
+ a confident caller the possibility to optimize this away.
247
+ TypeError: if value should be a list but is not iterable
248
+ Value Error: if value is not a dict for key in translated.
249
+ Setting individual values is probably preferable.
250
+ """
251
+ if isinstance(key, str) and any(
252
+ key.endswith("_" + (found_language := language))
253
+ for language in self._options.languages
254
+ ):
255
+ main_key = key[: -len("_" + found_language)]
256
+ if main_key in self._translated:
257
+ new_key = (main_key, found_language)
258
+ key = new_key
259
+ else:
260
+ self._logger.warning(
261
+ f"debug: found {key} ({found_language}) but {main_key} "
262
+ "should not be translated"
263
+ )
264
+ ### up until here could be deleted when data is converted
265
+
266
+ if key in self._translated:
267
+ assert isinstance(key, str)
268
+ # must be str, not (str, language) since _translated has only str
269
+ # therefore the following recursive calls do not create an infinite
270
+ # recursion
271
+ if isinstance(value, dict):
272
+ for language, subvalue in value.items():
273
+ self[key, language] = subvalue
274
+ else:
275
+ self[key, self._options.languages[0]] = value
276
+ return # otherwise the original key would be used below
277
+
278
+ if DefaultedDict._key_in_category(key, self._lists) and isinstance(
279
+ value, IMMUTABLE_TYPES
280
+ ):
281
+ value = [value]
282
+
283
+ ##### Datatype assertions
284
+ if DefaultedDict._key_in_category(key, self._lists):
285
+ # str is iterable, so to forbid it, it needs to be handled differently
286
+ assert not isinstance(
287
+ value, str
288
+ ), f"Key {key} is only storing lists, not strings like {value}."
289
+ try:
290
+ value = list(value)
291
+ except TypeError:
292
+ assert (
293
+ False
294
+ ), f"Key {key} is only taking storing lists, so supply some iterable, not {value}."
295
+ assert all(
296
+ isinstance(mutable := element, IMMUTABLE_TYPES) for element in value
297
+ ), f"{mutable} of type {type(mutable)} is not allowed in list in our dicts."
298
+ else:
299
+ assert isinstance(
300
+ value, IMMUTABLE_TYPES
301
+ ), f"Values for key {key} must be immutable, not {type(value)} as {value} is."
302
+ ###### Datatype assertions end
303
+
304
+ if DefaultedDict._key_in_category(key, self._translated):
305
+ # cannot be simple str since we checked that at beginning
306
+ assert isinstance(
307
+ key, tuple
308
+ ), "If a translatable key is given, it must be a tuple of length 2."
309
+ assert (
310
+ len(key) == 2
311
+ ), "If a translatable key is given, it must be a tuple of length 2."
312
+ try:
313
+ key = key[0], self._options.languages[cast(int, key[1])]
314
+ except TypeError: # probably key[1] is str, not int.
315
+ pass # it's fine
316
+ except IndexError:
317
+ assert False, f"There are not {cast(int, key[1]) + 1} many languages."
318
+ assert isinstance(key[1], str), "Second part of key must be integer or str"
319
+ else:
320
+ assert isinstance(key, str), f"Key {key} has to be a string but."
321
+ self._data[key] = value
322
+
323
+ def to_jsonable_data(self) -> dict[str, Any]:
324
+ """Returns values without resorting to default.
325
+
326
+ Intended to be used for saving to file.
327
+
328
+ Returns:
329
+ dict with
330
+ key: value for non-translated keys
331
+ key: { lang: value } for translated keys
332
+ """
333
+ # make sure that all keys in saveable will be strings:
334
+ assert all(
335
+ ((non_str := key) not in self._translated and isinstance(key, str))
336
+ or (
337
+ isinstance(key, tuple)
338
+ and len(key) == 2
339
+ and isinstance(key[0], str)
340
+ and isinstance(key[1], str)
341
+ and key[0] in self._translated
342
+ )
343
+ for key in self._data
344
+ ), f"{non_str} is an invalid key. Only strings (and tuple for translated keys) allowed."
345
+
346
+ saveable = {}
347
+ for key, value in self._data.items():
348
+ if isinstance(key, tuple):
349
+ assert (
350
+ len(key) == 2
351
+ ), f"Somehow a weird tuple key was introduced: {key}."
352
+ if key[0] in saveable:
353
+ saveable[key[0]][key[1]] = value
354
+ else:
355
+ saveable[key[0]] = {key[1]: value}
356
+ else:
357
+ saveable[key] = value
358
+ assert all(
359
+ isinstance(saveable[mutable := key], IMMUTABLE_TYPES)
360
+ or key in self._lists
361
+ or key in self._translated
362
+ for key in saveable
363
+ ), (
364
+ f"Value {saveable[mutable]} of type {type(saveable[mutable])}"
365
+ f" for key {mutable} is mutable but should not be."
366
+ )
367
+ assert all(
368
+ key not in self._lists
369
+ or key in self._translated
370
+ or (
371
+ isinstance(saveable[not_list := key], list)
372
+ and all(isinstance(element, IMMUTABLE_TYPES) for element in value)
373
+ )
374
+ for key, value in saveable.items()
375
+ ), (
376
+ f"Value {saveable[not_list]} for key {not_list} "
377
+ "is not a list or some element is mutable."
378
+ )
379
+ assert all(
380
+ (wrong := key) in self._lists
381
+ or key not in self._translated
382
+ or (
383
+ isinstance(value, dict)
384
+ and all(isinstance(value[subkey], IMMUTABLE_TYPES) for subkey in value)
385
+ )
386
+ for key, value in saveable.items()
387
+ ), (
388
+ f"Value {saveable[wrong]} for key {wrong} is somehow wrong. "
389
+ "Should be dict of immutable values."
390
+ )
391
+ assert all(
392
+ (wrong := key) not in self._lists
393
+ or key not in self._translated
394
+ or (
395
+ isinstance(value, dict)
396
+ and all(
397
+ isinstance(value[subkey], list)
398
+ and all(
399
+ isinstance(value, IMMUTABLE_TYPES) for value in value[subkey]
400
+ )
401
+ for subkey in value
402
+ )
403
+ )
404
+ for key, value in saveable.items()
405
+ ), (
406
+ f"Value {saveable[wrong]} for key {wrong} is somehow wrong. "
407
+ "Should be dict of lists of immutable values."
408
+ )
409
+
410
+ assert all(
411
+ isinstance(wrong_key := element, str)
412
+ and (
413
+ isinstance(wrong_type := value, IMMUTABLE_TYPES)
414
+ or (
415
+ isinstance(value, list)
416
+ and all(isinstance(subvalue, IMMUTABLE_TYPES) for subvalue in value)
417
+ )
418
+ or (
419
+ isinstance(value, dict)
420
+ and all(
421
+ isinstance(subkey, str)
422
+ and (
423
+ isinstance(sub_value, IMMUTABLE_TYPES)
424
+ or (
425
+ isinstance(sub_value, list)
426
+ and all(
427
+ isinstance(sub_sub_value, IMMUTABLE_TYPES)
428
+ for sub_sub_value in sub_value
429
+ )
430
+ )
431
+ )
432
+ for subkey, sub_value in value.items()
433
+ )
434
+ )
435
+ )
436
+ for element, value in saveable.items()
437
+ ), f"{wrong_key} = {wrong_type} occurs in json output. Not allowed."
438
+
439
+ # todo: ensure order of self._default_order
440
+ return saveable
441
+
442
+ def __delitem__(self, key: Key):
443
+ """Removes an item.
444
+
445
+ Allows removing all translated values.
446
+
447
+ Note that [key] might still return something since default values are not deleted.
448
+ Note that (str, int)-type keys as in getitem and setitem are not supported.
449
+
450
+ Raises:
451
+ KeyError: if key is not valid.
452
+ """
453
+ assert (
454
+ not isinstance(key, tuple) or len(key) == 2 and isinstance(key[1], str)
455
+ ), f"(str, int) as {key} keys are not supported by del yet."
456
+ if key in self._translated:
457
+ del_something = False
458
+ for other_key in set(
459
+ k for k in self._data if isinstance(k, tuple) and k[0] == key
460
+ ):
461
+ # set() copies keys. If we directly iterate
462
+ # over self._data, we get RuntimeError: dictionary changed size during iteration
463
+ del self._data[other_key]
464
+ del_something = True
465
+ if not del_something:
466
+ raise KeyError(key)
467
+ else:
468
+ del self._data[key]
469
+
470
+ def items(self) -> Iterable[tuple[Key, Value]]:
471
+ """Iterate over all directly saved key-value pairs.
472
+
473
+ Do not include values only saved in the default.
474
+ """
475
+ # custom __getitem__ is not used but that's fine because we
476
+ # do not want default values
477
+ # and also giving the original lists is fine since they are
478
+ # not from the default and therefore fine to edit
479
+ return self._data.items()
480
+
481
+ def all_items(self):
482
+ """Iterator over all key-value pairs saved in self and default.
483
+
484
+ No performance improvement over iterating over self and using [key].
485
+ """
486
+ for key in iter(self):
487
+ yield key, self[key]
488
+
489
+ def get(self, key, default=None):
490
+ """Get value without KeyError, instead with default.
491
+
492
+ Returns:
493
+ value saved for key itself if it exists
494
+ otherwise value saved in objects default for key if it exists
495
+ otherwise argument default
496
+ """
497
+ try:
498
+ return self[key]
499
+ except KeyError:
500
+ return default
501
+
502
+ def __contains__(self, key: Key):
503
+ """Return if self[key] would give something, possibly from default."""
504
+ try:
505
+ _ = self[key]
506
+ except KeyError:
507
+ return False
508
+ return True
509
+
510
+ def __iter__(self) -> Iterator[Key]:
511
+ """Iterate over keys with values in self and defaults.
512
+
513
+ Each translation of a translated key is given separately.
514
+ """
515
+ already_sent = set()
516
+ for key in self._data:
517
+ already_sent.add(key)
518
+ yield key
519
+ for key in self.default:
520
+ if key not in already_sent and not self._key_in_category(
521
+ key, self._non_defaulted
522
+ ):
523
+ already_sent.add(key) # should not be necessary, but to make sure
524
+ yield key
525
+ # else: go to next
526
+
527
+ def __keys__(self) -> set[Key]:
528
+ """iter(self) as a set."""
529
+ return set(iter(self))
530
+
531
+ def __str__(self):
532
+ """Give string representation of data directly saved. (Without default values)"""
533
+ return f"{self._data}"
534
+
535
+ def __repr__(self):
536
+ """Give string representation of data including special keys and defaults."""
537
+ return f"""DefaultedDict(
538
+ data={self._data!r},
539
+ lists={self.lists!r},
540
+ translated={self.translated!r},
541
+ non_defaulted={self.non_defaulted!r},
542
+ default={repr(self.default).replace('\n', '\n ')},
543
+ languages={self._options.languages!r}
544
+ """
545
+
546
+ def __len__(self):
547
+ """Give amount of keys with values directly in self.
548
+
549
+ Ignore keys available in default.
550
+ Translated keys are counted as often as they have translations.
551
+ """
552
+ return len(self._data)