File: SettingDefinition.py

package info (click to toggle)
uranium 5.0.0-9
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,328 kB
  • sloc: python: 31,765; sh: 132; makefile: 12
file content (765 lines) | stat: -rw-r--r-- 34,275 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
# Copyright (c) 2019 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.

import ast
import json
import enum
import collections
import re
from typing import Any, List, Dict, Callable, Match, Set, Union, Optional

from UM.Logger import Logger
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.i18n import i18nCatalog

MYPY = False
if MYPY:
    from UM.Settings.SettingRelation import SettingRelation

from . import SettingFunction
from UM.Settings.Validator import Validator


class DefinitionPropertyType(enum.IntEnum):
    """Type of definition property.

    This enum describes the possible types for a supported definition property.
    For more information about supported definition properties see SettingDefinition
    and SettingDefinition::addSupportedProperty().
    """

    Any = 1  ## Any value.
    String = 2  ## Value is always converted to string.
    TranslatedString = 3  ## Value is converted to string then passed through an i18nCatalog object to get a translated version of that string.
    Function = 4  ## Value is a python function. It is passed to SettingFunction's constructor which will parse and analyze it.


def toFloatConversion(value: str) -> float:
    """Conversion of string to float."""

    value = value.replace(",", ".")
    """Ensure that all , are replaced with . (so they are seen as floats)"""

    def stripLeading0(matchobj: Match[str]) -> str:
        return matchobj.group(0).lstrip("0")

    regex_pattern = r"(?<!\.|\w|\d)0+(\d+)"
    """Literal eval does not like "02" as a value, but users see this as "2"."""
    """We therefore look numbers with leading "0", provided they are not used in variable names"""
    """example: "test02 * 20" should not be changed, but "test * 02 * 20" should be changed (into "test * 2 * 20")"""
    value = re.sub(regex_pattern, stripLeading0, value)

    try:
        return ast.literal_eval(value)
    except:
        return 0


def toIntConversion(value):
    """Conversion from string to integer.

    :param value: The string representation of an integer.
    """

    try:
        return ast.literal_eval(value)
    except SyntaxError:
        return 0


class SettingDefinition:
    """Defines a single Setting with its properties.

    This class defines a single Setting with all its properties. This class is considered immutable,
    the only way to change it is using deserialize(). Should any state need to be stored for a definition,
    create a SettingInstance pointing to the definition, then store the value in that instance.

    == Supported Properties

    The SettingDefinition class contains a concept of "supported properties". These are properties that
    are supported when serializing or deserializing a setting. These properties are defined through the
    addSupportedProperty() method. Each property needs a name and a type. In addition, there are two
    optional boolean value to indicate whether the property is "required" and whether it is "read only".
    Currently, four types of supported properties are defined. Please DefinitionPropertyType for a description
    of these types.

    Required properties are properties that should be present when deserializing a setting. If the property
    is not present, an error will be raised. Read-only properties are properties that should never change
    after creating a SettingDefinition. This means they cannot be stored in a SettingInstance object.
    """

    def __init__(self, key: str, container: Optional[DefinitionContainerInterface] = None, parent: Optional["SettingDefinition"] = None, i18n_catalog: Optional[i18nCatalog] = None) -> None:
        """Construcutor

        :param key: :type{string} The unique, machine readable/writable key to use for this setting.
        :param container: :type{DefinitionContainerInterface} The container of this setting. Defaults to None.
        :param parent: :type{SettingDefinition} The parent of this setting. Defaults to None.
        :param i18n_catalog: :type{i18nCatalog} The translation catalog to use for this setting. Defaults to None.
        """

        super().__init__()
        self._all_keys = set()  # type: Set[str]
        self._key = key  # type: str
        self._container = container # type: Optional[DefinitionContainerInterface]
        self._parent = parent   # type:  Optional["SettingDefinition"]

        self._i18n_catalog = i18n_catalog  # type: Optional[i18nCatalog]

        self._children = []     # type: List[SettingDefinition]
        self._relations = []    # type: List[SettingRelation]

        # Cached set of keys of ancestors. Used for fast lookups of ancestors.
        self.__ancestors = set()  # type: Set[str]

        # Cached set of key - definition pairs of descendants. Used for fast lookup of descendants by key.
        self.__descendants = {}  # type: Dict[str, "SettingDefinition"]

        self.__property_values = {}  # type: Dict[str, Any]

    def __getattr__(self, name: str) -> Any:
        """Override __getattr__ to provide access to definition properties."""

        if name in self.__property_definitions:
            if name in self.__property_values:
                return self.__property_values[name]
            else:
                return self.__property_definitions[name]["default"]

        raise AttributeError("'SettingDefinition' object has no attribute '{0}'".format(name))

    def __setattr__(self, name: str, value: Any) -> None:
        """Override __setattr__ to enforce invariant status of definition properties."""

        if name in self.__property_definitions:
            raise NotImplementedError("Setting of property {0} not supported".format(name))

        super().__setattr__(name, value)

    def __hash__(self):
        """Ensure that the SettingDefinition is hashable, so it can be used in a set."""

        return hash(str(self))

    def __getstate__(self):
        """For Pickle support.

        This should be identical to Pickle's default behaviour but the default
        behaviour doesn't combine well with a non-default __getattr__.
        """

        return self.__dict__

    def __setstate__(self, state):
        """For Pickle support.

        This should be identical to Pickle's default behaviour but the default
        behaviour doesn't combine well with a non-default __getattr__.
        """

        self.__dict__.update(state)
        # For 4.0 we added the _all_keys property, but the pickling fails to restore this.
        # This is just there to prevent issues for developers, since only releases ignore caches.
        # If you're reading this after that. Remove this.
        if not hasattr(self, "_all_keys"):
            self._all_keys = set()

    @property
    def key(self) -> str:
        """The key of this setting.

        :return: :type{string}
        """

        return self._key

    @property
    def container(self) -> Optional[DefinitionContainerInterface]:
        """The container of this setting.

        :return:
        """

        return self._container

    @property
    def parent(self) -> Optional["SettingDefinition"]:
        """The parent of this setting.

        :return: :type{SettingDefinition}
        """

        return self._parent

    @property
    def children(self) -> List["SettingDefinition"]:
        """A list of children of this setting.

        :return: :type{list<SettingDefinition>}
        """

        return self._children

    @property
    def relations(self) -> List["SettingRelation"]:
        """A list of SettingRelation objects of this setting.

        :return: :type{list<SettingRelation>}
        """

        return self._relations

    def serialize(self) -> str:
        """Serialize this setting to a string.

        :return: :type{string} A serialized representation of this setting.
        """

        pass

    def getAllKeys(self) -> Set[str]:
        """Gets the key of this setting definition and of all its descendants.

        :return: A set of the key in this definition and all its descendants.
        """

        if not self._all_keys:
            # It was reset, re-calculate them
            self._all_keys = set()
            self._all_keys.add(self.key)
            for child in self.children:
                self._all_keys |= child.getAllKeys()  # Recursively get all keys of all descendants.
        return self._all_keys

    def serialize_to_dict(self) -> Dict[str, Any]:
        """Serialize this setting to a dict.

        :return: :type{dict} A representation of this setting definition.
        """

        result = {}  # type: Dict[str, Any]
        result["label"] = self.key

        result["children"] = {}
        for child in self.children:
            result["children"][child.key] = child.serialize_to_dict()

        for key, value in self.__property_values.items():
            result[key] = str(value)

        return result

    def deserialize(self, serialized: Union[str, Dict[str, Any]]) -> None:
        """Deserialize this setting from a string or dict.

        :param serialized: :type{string or dict} A serialized representation of this setting.
        """

        if isinstance(serialized, dict):
            self._deserialize_dict(serialized)
        else:
            parsed = json.loads(serialized, object_pairs_hook=collections.OrderedDict)
            self._deserialize_dict(parsed)

    def getChild(self, key: str) -> Optional["SettingDefinition"]:
        """Get a child by key

        :param key: :type{string} The key of the child to get.

        :return: :type{SettingDefinition} The child with the specified key or None if not found.
        """

        if not self.__descendants:
            self.__descendants = self._updateDescendants()

        if key in self.__descendants:
            child = self.__descendants[key]
            if child not in self._children:
                # Descendants includes children-of-children etc. so we need to make sure we only return direct children.
                return None

            return child

        return None

    def _matches1l8nProperty(self, property_name: str, value: Any, catalog) -> bool:
        try:
            property_value = getattr(self, property_name)
        except AttributeError:
            # If we do not have the attribute, we do not match
            return False

        if catalog:
            translated_key = "{key} {property_name}".format(key = self._key, property_name = property_name)
            property_value = catalog.i18nc(translated_key, property_value)

        if not isinstance(value, str):
            return False
        if value != property_value:
            if "*" not in value:
                return False

            value = value.strip("* ").lower()
            if value not in property_value.lower():
                return False

        return True

    def matchesFilter(self, **kwargs: Any) -> bool:
        """Check if this setting definition matches the provided criteria.

        :param kwargs: :type{dict} A dictionary of keyword arguments that need to match its attributes.
        """

        # First check for translated labels.
        keywords = kwargs.copy()
        if "i18n_label" in keywords:
            if not self._matches1l8nProperty("label", keywords["i18n_label"], keywords.get("i18n_catalog")):
                return False
            del keywords["i18n_label"]

        # There is a special case where we want to filter on either the label and the description.
        # For the sake of keeping this code simple, I've just hardcoded this option. If we ever want to have multiple
        # keywords that can be searched as an optional filter (eg; value matches either paramA or paramB, we should
        # consider refactoring this.
        # Note that this match will be called a lot, so keep an eye out for performance
        if "i18n_label|i18n_description" in keywords:
            matches_label = self._matches1l8nProperty("label", keywords["i18n_label|i18n_description"], keywords.get("i18n_catalog"))
            if not matches_label:
                if not self._matches1l8nProperty("description", keywords["i18n_label|i18n_description"], keywords.get("i18n_catalog")):
                    return False
            del keywords["i18n_label|i18n_description"]

        if "i18n_catalog" in keywords:
            del keywords["i18n_catalog"]

        # Normal attribute matching
        for key in keywords:
            try:
                property_value = getattr(self, key)
            except AttributeError:
                # If we do not have the attribute, we do not match
                return False

            value = kwargs[key]
            if property_value == value:
                # If the value matches with the expected value, we match for this property and should
                # continue with the other properties.
                # We do this check first so we can avoid the costly wildcard matching for situations where
                # we do not need to perform wildcard matching anyway.
                continue

            if isinstance(value, str):
                if not isinstance(property_value, str):
                    # If value is a string but the actual property value is not there is no situation where we
                    # will match.
                    return False

                if "*" not in value:
                    # If both are strings but there is no wildcard we do not match since we already checked if
                    # both are equal.
                    return False

                value = value.strip("* ").lower()
                if value not in property_value.lower():
                    return False
            else:
                return False

        return True

    def findDefinitions(self, **kwargs: Any) -> List["SettingDefinition"]:
        """Find all definitions matching certain criteria.

        This will search this definition and its children for definitions matching the search criteria.

        :param kwargs: :type{dict} A dictionary of keyword arguments that need to match properties of the children.

        :return: :type{list} A list of children matching the search criteria. The list will be empty if no children
        were found.
        """

        if not self.__descendants:
            self.__descendants = self._updateDescendants()

        key = kwargs.get("key")
        if key and not "*" in key:
            # Optimization for the most common situation: finding a setting by key
            if self._key != key and key not in self.__descendants:
                # If the mentioned key is not ourself and not in children, we will never match.
                return []

            if len(kwargs) == 1:
                # If all we are searching for is a key, return either ourself or a value from the descendants.
                if self._key == key:
                    return [self]
                return [self.__descendants[key]]

        definitions = []  # type: List["SettingDefinition"]
        if self.matchesFilter(**kwargs):
            definitions.append(self)

        for child in self._children:
            definitions.extend(child.findDefinitions(**kwargs))

        return definitions

    def isAncestor(self, key: str) -> bool:
        """Check whether a certain setting is an ancestor of this definition.

        :param key: :type{str} The key of the setting to check.

        :return: True if the specified setting is an ancestor of this definition, False if not.
        """

        if not self.__ancestors:
            self.__ancestors = self._updateAncestors()

        return key in self.__ancestors

    def isDescendant(self, key: str) -> bool:
        """Check whether a certain setting is a descendant of this definition.

        :param key: :type{str} The key of the setting to check.

        :return: True if the specified setting is a descendant of this definition, False if not.
        """

        if not self.__descendants:
            self.__descendants = self._updateDescendants()

        return key in self.__descendants

    def getAncestors(self) -> Set[str]:
        """Get a set of keys representing the setting's ancestors."""

        if not self.__ancestors:
            self.__ancestors = self._updateAncestors()

        return self.__ancestors

    def __repr__(self) -> str:
        return "<SettingDefinition (0x{0:x}) key={1} container={2}>".format(id(self), self._key, self._container)

    def __eq__(self, other: Any) -> bool:
        if other is None:
            return False

        try:
            if isinstance(other, SettingDefinition):
                return self._key == other.key
            else:
                Logger.log("w", "Trying to compare equality of SettingDefinition and something that is no SettingDefinition.")
                return False
        except:  # Has no key. Not the same type of object.
            Logger.log("w", "Trying to compare equality of SettingDefinition and something that is no SettingDefinition.")
            return False

    @classmethod
    def addSupportedProperty(cls, name: str, property_type: DefinitionPropertyType, required: bool=False, read_only: bool=False, default: Any=None, depends_on: Optional[str]=None) -> None:
        """Define a new supported property for SettingDefinitions.

        Since applications may want custom properties in their definitions, most properties are handled
        dynamically. This allows the application to define what extra properties it wants to support.
        Additionally, it can indicate whether a properties should be considered "required". When a
        required property is not missing during deserialization, an AttributeError will be raised.

        :param name: :type{string} The name of the property to define.
        :param property_type: :type{DefinitionPropertyType} The type of property.
        :param kwargs: Keyword arguments. Possible values:
        :param required:     True if missing the property indicates an error should be raised. Defaults to False.
        :param read_only:    True if the property should never be set on a SettingInstance. Defaults to False. Note
        that for Function properties this indicates whether the result of the function should be stored.
        :param default:      The default value for this property. This will be returned when the specified property
        is not defined for this definition.
        :param depends_on:   Key to another property that this property depends on; eg; if that value changes, this
        value should be re-evaluated.
        """

        cls.__property_definitions[name] = {"type": property_type, "required": required, "read_only": read_only,
                                            "default": default, "depends_on": depends_on}

    @classmethod
    def getPropertyNames(cls, def_type: DefinitionPropertyType = None) -> List[str]:
        """Get the names of all supported properties.

        :param type: :type{DefinitionPropertyType} The type of property to get the name of. Defaults to None which means all properties.

        :return: A list of all the names of supported properties.
        """

        if def_type is None:
            return list(cls.__property_definitions.keys())
        return [key for key, value in cls.__property_definitions.items() if not def_type or value["type"] == def_type]

    @classmethod
    def hasProperty(cls, name: str) -> bool:
        """Check if a property with the specified name is defined as a supported property.

        :param name: :type{string} The name of the property to check if it is supported.

        :return: True if the property is supported, False if not.
        """

        return name in cls.__property_definitions

    @classmethod
    def getPropertyType(cls, name: str) -> Optional[str]:
        """Get the type of a specified property.

        :param name: :type{str} The name of the property to find the type of.

        :return: DefinitionPropertyType corresponding to the type of the property or None if not found.
        """

        if name in cls.__property_definitions:
            return cls.__property_definitions[name]["type"]

        return None

    @classmethod
    def isRequiredProperty(cls, name: str) -> bool:
        """Check if the specified property is considered a required property.

        Required properties are checked when deserializing a SettingDefinition and if not present an error
        will be reported.

        :param name: :type{string} The name of the property to check if it is required or not.

        :return: True if the property is supported and is required, False if it is not required or is not part of the
        list of supported properties.
        """

        if name in cls.__property_definitions:
            return cls.__property_definitions[name]["required"]
        return False

    @classmethod
    def isReadOnlyProperty(cls, name: str) -> bool:
        """Check if the specified property is considered a read-only property.

        Read-only properties are properties that cannot have their value set in SettingInstance objects.

        :param name: :type{string} The name of the property to check if it is read-only or not.

        :return: True if the property is supported and is read-only, False if it is not required or is not part of the
        list of supported properties.
        """

        if name in cls.__property_definitions:
            return cls.__property_definitions[name]["read_only"]
        return False

    @classmethod
    def dependsOnProperty(cls, name: str) -> Optional[str]:
        """Check if the specified property depends on another property

        The value of certain properties can change if the value of another property changes. This is used to signify
        that relation.

        :param name: :type{string} The name of the property to check if it depends on another setting.

        :return: :type{string} The property it depends on or None if it does not depend on another property.
        """

        if name in cls.__property_definitions:
            return cls.__property_definitions[name]["depends_on"]
        return None

    @classmethod
    def addSettingType(cls, type_name: str, from_string: Optional[Callable[[str], Any]], to_string: Callable[[Any], str], validator: Optional[Validator] = None) -> None:
        """Add a new setting type to the list of accepted setting types.

        :param type_name: The name of the new setting type.
        :param from_string: A function to call that converts to a proper value of this type from a string.
        :param to_string: A function that converts a value of this type to a string.

        """

        cls.__type_definitions[type_name] = { "from": from_string, "to": to_string, "validator": validator }

    @classmethod
    def settingValueFromString(cls, type_name: str, string_value: str) -> Any:
        """Convert a string to a value according to a setting type.

        :param type_name: :type{string} The name of the type to convert to.
        :param string_value: :type{string} The string to convert.

        :return: The string converted to a proper value.

        :exception ValueError: Raised when the specified type does not exist.
        """

        if type_name not in cls.__type_definitions:
            raise ValueError("Unknown setting type {0}".format(type_name))

        convert_function = cls.__type_definitions[type_name]["to"]
        if convert_function:
            return convert_function(string_value)

        return string_value

    @classmethod
    def settingValueToString(cls, type_name: str, value: Any) -> str:
        """Convert a setting value to a string according to a setting type.

        :param type_name: :type{string} The name of the type to convert from.
        :param value: The value to convert.

        :return: :type{string} The specified value converted to a string.

        :exception ValueError: Raised when the specified type does not exist.
        """

        if type_name not in cls.__type_definitions:
            raise ValueError("Unknown setting type {0}".format(type_name))

        convert_function = cls.__type_definitions[type_name]["from"]
        if convert_function:
            try:
                return convert_function(value)
            except Exception:
                Logger.logException("w", "UM.Settings: Error converting from %s with value %s: %s", type_name, str(value))
                raise

        return value

    @classmethod
    def getValidatorForType(cls, type_name: str) -> Callable[[str],Validator]:
        """Get the validator type for a certain setting type."""

        if type_name not in cls.__type_definitions:
            raise ValueError("Unknown setting type {0}".format(type_name))

        return cls.__type_definitions[type_name]["validator"]

    def _deserialize_dict(self, serialized: Dict[str, Any]) -> None:
        """protected:

        Deserialize from a dictionary
        """

        self._children = []
        self._relations = []

        for key, value in serialized.items():
            if key == "children":
                for child_key, child_dict in value.items():
                    child = SettingDefinition(child_key, self._container, self, self._i18n_catalog)
                    child.deserialize(child_dict)
                    self._children.append(child)
                continue

            if key not in self.__property_definitions:
                Logger.log("w", "Unrecognised property %s in setting %s", key, self._key)
                continue

            if key == "type":
                if value not in self.__type_definitions:
                    raise ValueError("Type {0} is not a correct setting type".format(value))

            if self.__property_definitions[key]["type"] == DefinitionPropertyType.Any:
                self.__property_values[key] = value
            elif self.__property_definitions[key]["type"] == DefinitionPropertyType.String:
                self.__property_values[key] = str(value)
            elif self.__property_definitions[key]["type"] == DefinitionPropertyType.TranslatedString:
                self.__property_values[key] = self._i18n_catalog.i18n(str(value)) if self._i18n_catalog is not None else value
            elif self.__property_definitions[key]["type"] == DefinitionPropertyType.Function:
                self.__property_values[key] = SettingFunction.SettingFunction(str(value))
            else:
                Logger.log("w", "Unknown DefinitionPropertyType (%s) for key %s", key, self.__property_definitions[key]["type"])

        for key in filter(lambda i: self.__property_definitions[i]["required"], self.__property_definitions):
            if key not in self.__property_values:
                raise AttributeError("Setting {0} is missing required property {1}".format(self._key, key))

        self.__ancestors = self._updateAncestors()
        self.__descendants = self._updateDescendants()

    def _updateAncestors(self) -> Set[str]:
        result = set()  # type: Set[str]

        parent = self._parent
        while parent:
            result.add(parent.key)
            parent = parent.parent

        return result

    def _updateDescendants(self, definition: "SettingDefinition" = None) -> Dict[str, "SettingDefinition"]:
        result = {}
        self._all_keys = set()  # Reset the keys cache.
        if not definition:
            definition = self

        for child in definition.children:
            result[child.key] = child
            result.update(self._updateDescendants(child))

        return result

    __property_definitions = {
        # The name of the setting. Only used for display purposes.
        "label": {"type": DefinitionPropertyType.TranslatedString, "required": True, "read_only": True, "default": "", "depends_on" : None},
        # The type of setting. Can be any one of the types defined.
        "type": {"type": DefinitionPropertyType.String, "required": True, "read_only": True, "default": "", "depends_on" : None},
        # An optional icon that can be displayed for the setting.
        "icon": {"type": DefinitionPropertyType.String, "required": False, "read_only": True, "default": "", "depends_on" : None},
        # A string describing the unit used for the setting. This is only used for display purposes at the moment.
        "unit": {"type": DefinitionPropertyType.String, "required": False, "read_only": True, "default": "", "depends_on" : None},
        # A description of what the setting does. Used for display purposes.
        "description": {"type": DefinitionPropertyType.TranslatedString, "required": True, "read_only": True, "default": "", "depends_on" : None},
        # A description of what is wrong when the setting has a warning validation state. Used for display purposes.
        "warning_description": {"type": DefinitionPropertyType.TranslatedString, "required": False, "read_only": True, "default": "", "depends_on" : None},
        # A description of what is wrong when the setting has an error validation state. Used for display purposes.
        "error_description": {"type": DefinitionPropertyType.TranslatedString, "required": False, "read_only": True, "default": "", "depends_on" : None},
        # The default value of the setting. Used when no value function is defined.
        "default_value": {"type": DefinitionPropertyType.Any, "required": False, "read_only": True,  "default": 0, "depends_on" : None},
        # A function used to calculate the value of the setting.
        "value": {"type": DefinitionPropertyType.Function, "required": False, "read_only": False,  "default": None, "depends_on" : None},
        # A function that should evaluate to a boolean to indicate whether or not the setting is enabled.
        "enabled": {"type": DefinitionPropertyType.Function, "required": False, "read_only": False, "default": True, "depends_on": None},
        # A function that calculates the minimum value for this setting. If the value is less than this, validation will indicate an error.
        "minimum_value": {"type": DefinitionPropertyType.Function, "required": False, "read_only": False, "default": None, "depends_on" : None},
        # A function that calculates the maximum value for this setting. If the value is more than this, validation will indicate an error.
        "maximum_value": {"type": DefinitionPropertyType.Function, "required": False, "read_only": False, "default": None, "depends_on" : None},
        # A function that calculates the minimum warning value for this setting. If the value is less than this, validation will indicate a warning.
        "minimum_value_warning": {"type": DefinitionPropertyType.Function, "required": False, "read_only": False, "default": None, "depends_on" : None},
        # A function that calculates the maximum warning value for this setting. If the value is more than this, validation will indicate a warning.
        "maximum_value_warning": {"type": DefinitionPropertyType.Function, "required": False, "read_only": False, "default": None, "depends_on" : None},
        # A dictionary of key-value pairs that provide the options for an enum type setting. The key is the actual value, the value is a translated display string.
        "options": {"type": DefinitionPropertyType.Any, "required": False, "read_only": True, "default": {}, "depends_on" : None},
        # Optional comments that apply to the setting. Will be ignored.
        "comments": {"type": DefinitionPropertyType.String, "required": False, "read_only": True, "default": "", "depends_on" : None},
        # For string type: Indicates if this string setting is allowed to have empty value. This can only be used for string settings.
        "allow_empty": {"type": DefinitionPropertyType.Function, "required": False, "read_only": True, "default": True, "depends_on": None},
        # For string type: Indicates that this string setting should be an UUID. This can only be used for string settings.
        "is_uuid": {"type": DefinitionPropertyType.Function, "required": False, "read_only": True, "default": False, "depends_on": None},
        # For string type: If a non-empty string is provided, it will be used as a regex pattern to validate the value string. The value will be invalid if the value string matches the pattern.
        "regex_blacklist_pattern": {"type": DefinitionPropertyType.String, "required": False, "read_only": True, "default": "", "depends_on": None},
        # For bool type: if the value is the same as the warning value, the setting will be in the warning state.
        "warning_value": {"type": DefinitionPropertyType.Function, "required": False, "read_only": True, "default": None, "depends_on": None},
        # For bool type: if the value is the same as the error value, the setting will be in the error state.
        "error_value": {"type": DefinitionPropertyType.Function, "required": False, "read_only": True, "default": None, "depends_on": None},
    }   # type: Dict[str, Dict[str, Any]]

    __type_definitions = {
        # An integer value
        "int": {"from": lambda v: str(v) if v is not None else "", "to": toIntConversion, "validator": Validator},
        # A boolean value
        "bool": {"from": str, "to": ast.literal_eval, "validator": Validator},
        # Special case setting; Doesn't have a value. Display purposes only.
        "category": {"from": None, "to": None, "validator": None},
        # A string value
        "str": {"from": None, "to": None, "validator": Validator},
        # An enumeration
        "enum": {"from": None, "to": None, "validator": None},
        # A floating point value
        "float": {"from": lambda v: str(round(float(v), 4)) if v is not None else "", "to": toFloatConversion, "validator": Validator},
        # A list of 2D points
        "polygon": {"from": str, "to": ast.literal_eval, "validator": None},
        # A list of polygons
        "polygons": {"from": str, "to": ast.literal_eval, "validator": None},
        # A 3D point
        "vec3": {"from": None, "to": None, "validator": None},
    }   # type: Dict[str, Dict[str, Any]]