pocketutils.core.enums.CleverEnum._if_not_found()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
# SPDX-FileCopyrightText: Copyright 2020-2023, Contributors to pocketutils
2
# SPDX-PackageHomePage: https://github.com/dmyersturnbull/pocketutils
3
# SPDX-License-Identifier: Apache-2.0
4
"""
5
6
"""
7
8
import enum
9
import logging
10
from typing import Self
11
12
__all__ = ["TrueFalseEither", "DisjointEnum", "FlagEnum", "CleverEnum", "MultiTruth"]
13
14
logger = logging.getLogger("pocketutils")
15
16
17
class DisjointEnum(enum.Enum):
18
    """
19
    An enum that does not have combinations.
20
    """
21
22
    @classmethod
23
    def _fix_lookup(cls: type[Self], s: str) -> str:
24
        return s
25
26
    @classmethod
27
    def or_none(cls: type[Self], s: str | Self) -> Self | None:
28
        """
29
        Returns a choice by name (or returns `s` itself).
30
        Returns `None` if the choice is not found.
31
        """
32
        try:
33
            return cls.of(s)
34
        except KeyError:
35
            return None
36
37
    @classmethod
38
    def of(cls: type[Self], s: str | Self) -> Self:
39
        """
40
        Returns a choice by name (or returns `s` itself).
41
        """
42
        if isinstance(s, cls):
43
            return s
44
        return cls[cls._fix_lookup(s)]
45
46
47
class FlagEnum(enum.Flag):
48
    """
49
    A bit flag that behaves as a set, has a null set, and auto-sets values and names.
50
51
    Example:
52
53
        class Flavor(FlagEnum):
54
            NONE = ()
55
            BITTER = ()
56
            SWEET = ()
57
            SOUR = ()
58
            UMAMI = ()
59
60
        bittersweet = Flavor.BITTER | Flavor.SWEET
61
        print(bittersweet.value)  # 1 + 2 == 3
62
        print(bittersweet.name)  # "bitter|sweet"
63
64
    Note:
65
        The *first element* must always be the null set ("no flags")
66
        and should be named something like 'none', 'empty', or 'zero'
67
    """
68
69
    @classmethod
70
    def _fix_lookup(cls: type[Self], s: str) -> str:
71
        return s
72
73
    @classmethod
74
    def or_none(cls: type[Self], s: str | Self) -> Self | None:
75
        """
76
        Returns a choice by name (or returns `s` itself).
77
78
        Returns:
79
            `None` if the choice is not found.
80
        """
81
        try:
82
            return cls.of(s)
83
        except KeyError:
84
            return None
85
86
    @classmethod
87
    def of(cls: type[Self], s: str | Self | set[str | Self] | frozenset[str | Self]) -> Self:
88
        """
89
        Returns a choice by name (or `s` itself), or a set of those.
90
        """
91
        if isinstance(s, cls):
92
            return s
93
        if isinstance(s, str):
94
            return cls[cls._fix_lookup(s)]
95
        z = cls(0)
96
        for m in s:
97
            z |= cls.of(m)
98
        return z
99
100
101
@enum.unique
102
class TrueFalseEither(DisjointEnum):
103
    """
104
    A `DisjointEnum`[pocketutils.core.enums.DisjointEnum] of true, false, or unknown.
105
    """
106
107
    TRUE = enum.auto()
108
    FALSE = enum.auto()
109
    EITHER = enum.auto()
110
111
    @classmethod
112
    def _if_not_found(cls: type[Self], s: str | Self) -> Self:
113
        return cls.EITHER
114
115
116
@enum.unique
117
class MultiTruth(FlagEnum):
118
    """
119
    A [`FlagEnum`](pocketutils.core.enums.FlagEnum) for true, false, true+false, and neither.
120
    """
121
122
    FALSE = enum.auto()
123
    TRUE = enum.auto()
124
125
126
class CleverEnum(DisjointEnum):
127
    """
128
    An enum with a :meth:`of` method that finds values with limited string/value fixing.
129
    Replaces `" "` and `"-"` with `_` and ignores case in :meth:`of`.
130
    May support an "unmatched" type via :meth:`_if_not_found`,
131
    which can return a fallback value when there is no match.
132
133
    Example:
134
135
        class Thing(CleverEnum):
136
            BUILDING = ()
137
            OFFICE_SUPPLY = ()
138
            POWER_OUTLET = ()
139
140
        x = Thing.of("power outlet")
141
142
    Example:
143
144
        class Color(CleverEnum):
145
            RED = ()
146
            GREEN = ()
147
            BLUE = ()
148
            OTHER = ()
149
150
            @classmethod
151
            def _if_not_found(cls: type[Self], s: str) -> Self:
152
                # raise XValueError(f"No member for value '{s}'", value=s) from None
153
                #   ^
154
                #   the default implementation
155
                logger.warning(f"Color {s} unknown; using {cls.OTHER}")
156
                return cls.OTHER
157
158
    Note:
159
160
        If [`_if_not_found`](pocketutils.core.enums.CleverEnum._if_not_found`) is overridden,
161
        it should return a member value.
162
        (In particular, it should never return `None`.)
163
164
    Note:
165
166
        To use with non-uppercase enum values (e.g. `Color.red` instead of `Color.RED`),
167
        override :meth:`_fix_lookup` with this:
168
169
        ```python
170
            @classmethod
171
            def _fix_lookup(cls: type[Self], s: str) -> str:
172
                return s.strip().replace(" ", "_").replace("-", "_").lower()
173
                #                                                      ^
174
                #                                                    changed
175
        ```
176
    """
177
178
    @classmethod
179
    def of(cls: type[Self], s: str | Self) -> Self:
180
        try:
181
            return super().of(s)
182
        except KeyError:
183
            return cls._if_not_found(s)
184
185
    @classmethod
186
    def _if_not_found(cls: type[Self], s: str | Self) -> Self:
187
        msg = f"No member for value '{s}'"
188
        raise KeyError(msg) from None
189
190
    @classmethod
191
    def _fix_lookup(cls: type[Self], s: str) -> str:
192
        return s
193