Skip to content

quast_decisiontree.core.problem_data

quast_decisiontree.core.problem_data

logger module-attribute

logger = logging.getLogger('dt_logger')

DecisiontreeProblemData

Bases: dict

dict-like class intended for storing the decision tree data.

The main differences are - availability of the self.check(key) method that checks the validity and existence of an entry - availability of logic to perform that check depending on the key - a flag to indicate whether the validity of the entry should be checked when getting or setting an item

Source code in src/quast_decisiontree/core/problem_data.py
 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
class DecisiontreeProblemData(dict):
    """dict-like class intended for storing the decision tree data.

    The main differences are
    - availability of the self.check(key) method that checks the validity and existence
    of an entry
    - availability of logic to perform that check depending on the key
    - a flag to indicate whether the validity of the entry should be checked when getting
    or setting an item
    """

    known_keys = basic_keys
    check_on_set = False
    check_on_get = True
    prevent_wild_keys = False

    @classmethod
    def update_known_keys(cls, keys: dict, collision_policy: str = "discard") -> None:
        """Updates the list of known keys from the keys dictionary. The collision_policy argument
        specifies what to do when encountering a key with already existing checks. Allowed values:
        - "discard" : keep the old checks (default)
        - "overwrite" : replace with the new checks
        - "raise" : raise a ProblemDataKeyCollisionError

        Args:
            keys (dict): A dictionary {key_name : checks} where key_name is the name of the
                associated problem data entry, and checks a list of checks that determine the
                validity of an entry (connected via AND)
            collision_policy (str, optional): See above. Defaults to "discard".

        Raises:
            ProblemDataKeyCollisionError: When collision_policy is "raise" and cls.checks already
                contains an entry of the same name as a key contained in keys.
        """
        for key, val in keys.items():
            if key in cls.known_keys:
                if collision_policy == "discard":
                    continue
                elif collision_policy == "overwrite":
                    cls.known_keys[key] = val
                elif collision_policy == "raise":
                    raise ProblemDataKeyCollisionError(
                        f"Trying to set checks for {key}, but checks are already defined for it."
                    )
            else:
                cls.known_keys[key] = val

    def _coerce_and_check(self, key: Any, val: Any) -> tuple[bool, Any]:
        """Validate `val` for `key`, coercing to the expected type when possible.

        If the expected type is a tuple, the value is accepted unchanged when it
        is already an instance of any listed type; otherwise coercion is attempted
        against each candidate in order, and the first successful cast is used.

        Returns (is_valid, possibly_coerced_value). On coercion failure the
        original value is returned with is_valid=False.
        """
        expected_type, checks = self.known_keys[key]

        if expected_type is not None and not isinstance(val, expected_type):
            coerced_ok, coerced = self._coerce_to_type(expected_type, val)
            if not coerced_ok:
                return False, val
            val = coerced

        return all(check(val) for check in checks), val

    @staticmethod
    def _coerce_to_type(expected_type: Any, val: Any) -> tuple[bool, Any]:
        """Attempt to coerce `val` to `expected_type` (a single type or tuple of types).

        Returns (success, value). On failure, success is False and the original
        value is returned unchanged.
        """
        candidates = expected_type if isinstance(expected_type, tuple) else (expected_type,)
        for target in candidates:
            try:
                return True, target(val)
            except (TypeError, ValueError):
                continue
        return False, val

    def check_entry(self, key: Any, strict: bool = False) -> bool | None:
        """Checks whether the entry indexed by key is valid.

        Args:
            key (Any): A dictionary key.
            strict (optional, bool): Whether to raise an error if there is no
            check known for the given key.

        Returns:
            Union[bool, None]: When the key doesn't exist, returns None.
            When the key exists, returns True if the entry is valid, and
            False if it is not.
        """
        try:
            val = super().__getitem__(key)
        except KeyError:
            logger.debug("Key %r not found in dictionary.", key)
            return None

        if key not in self.known_keys.keys():
            if strict:
                raise ProblemDataUnknownKeyError(
                    f"Key {key!r} does not seem to be valid for problem data dictionary."
                )
            logger.debug("Key %r not known, no strict checking though. Returning true.", key)
            return True

        logger.debug("Found checks for key %r.", key)
        return self._coerce_and_check(key, val)[0]

    def __setitem__(self, key, val):
        """differs from the dict method in that it checks whether an input is valid
        if self.check_on_set is True. Values are stored in coerced form when a cast
        was required to satisfy the expected type.

        Note that this method is only called when assigning items directly, not in a constructor
        or in an update() method.
        """
        if not self.check_on_set:
            super().__setitem__(key, val)
            return

        if key in self.known_keys:
            valid, coerced = self._coerce_and_check(key, val)
            if valid:
                super().__setitem__(key, coerced)
            else:
                raise ProblemDataInvalidEntryError(
                    f"Value {val!r} for key {key!r} didn't pass its checks."
                )
        elif self.prevent_wild_keys:
            raise ProblemDataUnknownKeyError(
                f"Tried to set entry for unknown key: {key!r}. Value: {val!r}"
            )
        else:
            logger.warning(
                "Inserting unknown key %r with value %r.\n"
                "To eliminate this warning, add the key to the known problem data keys.\n"
                "To raise an error instead, set prevent_wild_keys property to True.",
                key,
                val,
            )
            super().__setitem__(key, val)

    def __getitem__(self, key) -> Any:
        """differs from the dict method in that it checks whether an input is valid
        if self.check_on_get is True. When the value required coercion to satisfy the
        expected type, the coerced value is returned (the stored value is left unchanged).

        Note that this method is only called when fetching items directly.
        """
        if not self.check_on_get:
            return super().__getitem__(key)

        if key not in self.known_keys:
            if self.prevent_wild_keys:
                raise ProblemDataUnknownKeyError(f"Tried to get entry from unknown key: {key!r}")
            logger.warning(
                "Retrieving unknown key %r.\n"
                "To eliminate this warning, add the key to the known problem data keys.\n"
                "To raise an error instead, set prevent_wild_keys property to True.",
                key,
            )
            return super().__getitem__(key)

        val = super().__getitem__(key)
        valid, coerced = self._coerce_and_check(key, val)
        if valid:
            return coerced
        raise ProblemDataInvalidEntryError(
            f"Value {val!r} for key {key!r} didn't pass its checks."
        )

    def extract(self, key_list, exclude=None):
        if exclude is None:
            exclude = []
        out = dict()
        for key in key_list:
            if key not in exclude:
                try:
                    out[key] = self[key]
                except KeyError as exc:
                    logger.debug(
                        "Tried to access key %s, but the access failed due to an %s.",
                        key,
                        type(exc).__name__,
                    )
                    logger.debug("Error message was %s", exc)
                    pass
        return out

known_keys class-attribute instance-attribute

known_keys = basic_keys

check_on_set class-attribute instance-attribute

check_on_set = False

check_on_get class-attribute instance-attribute

check_on_get = True

prevent_wild_keys class-attribute instance-attribute

prevent_wild_keys = False

update_known_keys classmethod

update_known_keys(keys, collision_policy='discard')

Updates the list of known keys from the keys dictionary. The collision_policy argument specifies what to do when encountering a key with already existing checks. Allowed values: - "discard" : keep the old checks (default) - "overwrite" : replace with the new checks - "raise" : raise a ProblemDataKeyCollisionError

Parameters:

Name Type Description Default
keys dict

A dictionary {key_name : checks} where key_name is the name of the associated problem data entry, and checks a list of checks that determine the validity of an entry (connected via AND)

required
collision_policy str

See above. Defaults to "discard".

'discard'

Raises:

Type Description
ProblemDataKeyCollisionError

When collision_policy is "raise" and cls.checks already contains an entry of the same name as a key contained in keys.

Source code in src/quast_decisiontree/core/problem_data.py
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
@classmethod
def update_known_keys(cls, keys: dict, collision_policy: str = "discard") -> None:
    """Updates the list of known keys from the keys dictionary. The collision_policy argument
    specifies what to do when encountering a key with already existing checks. Allowed values:
    - "discard" : keep the old checks (default)
    - "overwrite" : replace with the new checks
    - "raise" : raise a ProblemDataKeyCollisionError

    Args:
        keys (dict): A dictionary {key_name : checks} where key_name is the name of the
            associated problem data entry, and checks a list of checks that determine the
            validity of an entry (connected via AND)
        collision_policy (str, optional): See above. Defaults to "discard".

    Raises:
        ProblemDataKeyCollisionError: When collision_policy is "raise" and cls.checks already
            contains an entry of the same name as a key contained in keys.
    """
    for key, val in keys.items():
        if key in cls.known_keys:
            if collision_policy == "discard":
                continue
            elif collision_policy == "overwrite":
                cls.known_keys[key] = val
            elif collision_policy == "raise":
                raise ProblemDataKeyCollisionError(
                    f"Trying to set checks for {key}, but checks are already defined for it."
                )
        else:
            cls.known_keys[key] = val

check_entry

check_entry(key, strict=False)

Checks whether the entry indexed by key is valid.

Parameters:

Name Type Description Default
key Any

A dictionary key.

required
strict (optional, bool)

Whether to raise an error if there is no

False

Returns:

Type Description
bool | None

Union[bool, None]: When the key doesn't exist, returns None.

bool | None

When the key exists, returns True if the entry is valid, and

bool | None

False if it is not.

Source code in src/quast_decisiontree/core/problem_data.py
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
def check_entry(self, key: Any, strict: bool = False) -> bool | None:
    """Checks whether the entry indexed by key is valid.

    Args:
        key (Any): A dictionary key.
        strict (optional, bool): Whether to raise an error if there is no
        check known for the given key.

    Returns:
        Union[bool, None]: When the key doesn't exist, returns None.
        When the key exists, returns True if the entry is valid, and
        False if it is not.
    """
    try:
        val = super().__getitem__(key)
    except KeyError:
        logger.debug("Key %r not found in dictionary.", key)
        return None

    if key not in self.known_keys.keys():
        if strict:
            raise ProblemDataUnknownKeyError(
                f"Key {key!r} does not seem to be valid for problem data dictionary."
            )
        logger.debug("Key %r not known, no strict checking though. Returning true.", key)
        return True

    logger.debug("Found checks for key %r.", key)
    return self._coerce_and_check(key, val)[0]

__setitem__

__setitem__(key, val)

differs from the dict method in that it checks whether an input is valid if self.check_on_set is True. Values are stored in coerced form when a cast was required to satisfy the expected type.

Note that this method is only called when assigning items directly, not in a constructor or in an update() method.

Source code in src/quast_decisiontree/core/problem_data.py
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
def __setitem__(self, key, val):
    """differs from the dict method in that it checks whether an input is valid
    if self.check_on_set is True. Values are stored in coerced form when a cast
    was required to satisfy the expected type.

    Note that this method is only called when assigning items directly, not in a constructor
    or in an update() method.
    """
    if not self.check_on_set:
        super().__setitem__(key, val)
        return

    if key in self.known_keys:
        valid, coerced = self._coerce_and_check(key, val)
        if valid:
            super().__setitem__(key, coerced)
        else:
            raise ProblemDataInvalidEntryError(
                f"Value {val!r} for key {key!r} didn't pass its checks."
            )
    elif self.prevent_wild_keys:
        raise ProblemDataUnknownKeyError(
            f"Tried to set entry for unknown key: {key!r}. Value: {val!r}"
        )
    else:
        logger.warning(
            "Inserting unknown key %r with value %r.\n"
            "To eliminate this warning, add the key to the known problem data keys.\n"
            "To raise an error instead, set prevent_wild_keys property to True.",
            key,
            val,
        )
        super().__setitem__(key, val)

__getitem__

__getitem__(key)

differs from the dict method in that it checks whether an input is valid if self.check_on_get is True. When the value required coercion to satisfy the expected type, the coerced value is returned (the stored value is left unchanged).

Note that this method is only called when fetching items directly.

Source code in src/quast_decisiontree/core/problem_data.py
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
def __getitem__(self, key) -> Any:
    """differs from the dict method in that it checks whether an input is valid
    if self.check_on_get is True. When the value required coercion to satisfy the
    expected type, the coerced value is returned (the stored value is left unchanged).

    Note that this method is only called when fetching items directly.
    """
    if not self.check_on_get:
        return super().__getitem__(key)

    if key not in self.known_keys:
        if self.prevent_wild_keys:
            raise ProblemDataUnknownKeyError(f"Tried to get entry from unknown key: {key!r}")
        logger.warning(
            "Retrieving unknown key %r.\n"
            "To eliminate this warning, add the key to the known problem data keys.\n"
            "To raise an error instead, set prevent_wild_keys property to True.",
            key,
        )
        return super().__getitem__(key)

    val = super().__getitem__(key)
    valid, coerced = self._coerce_and_check(key, val)
    if valid:
        return coerced
    raise ProblemDataInvalidEntryError(
        f"Value {val!r} for key {key!r} didn't pass its checks."
    )

extract

extract(key_list, exclude=None)
Source code in src/quast_decisiontree/core/problem_data.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def extract(self, key_list, exclude=None):
    if exclude is None:
        exclude = []
    out = dict()
    for key in key_list:
        if key not in exclude:
            try:
                out[key] = self[key]
            except KeyError as exc:
                logger.debug(
                    "Tried to access key %s, but the access failed due to an %s.",
                    key,
                    type(exc).__name__,
                )
                logger.debug("Error message was %s", exc)
                pass
    return out