tagged-enum 1.0.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,262 @@
|
|
|
1
|
+
from typing import (List, Type, Dict, ForwardRef, Self,
|
|
2
|
+
Union, Any, get_origin, get_args, ClassVar)
|
|
3
|
+
from types import UnionType, NoneType
|
|
4
|
+
from enum import Enum, EnumMeta
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaggedEnumMeta(EnumMeta):
|
|
8
|
+
@property
|
|
9
|
+
def Kind(cls) -> Type[Self]:
|
|
10
|
+
return cls
|
|
11
|
+
|
|
12
|
+
def __new__(mcls, name: str, bases, namespace: Dict[str, Any], **kw):
|
|
13
|
+
declared: Dict[str, Type[Any]] = {}
|
|
14
|
+
fresh = mcls.__prepare__(name, bases)
|
|
15
|
+
next_case = 0
|
|
16
|
+
|
|
17
|
+
for k, v in namespace.items():
|
|
18
|
+
if k.startswith('_') or not k.isupper():
|
|
19
|
+
fresh[k] = v
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
if (isinstance(v, type)
|
|
23
|
+
or get_origin(v) is not None
|
|
24
|
+
or isinstance(v, str)):
|
|
25
|
+
declared[k] = v
|
|
26
|
+
fresh[k] = next_case
|
|
27
|
+
next_case += 1
|
|
28
|
+
elif v is None:
|
|
29
|
+
declared[k] = NoneType
|
|
30
|
+
fresh[k] = next_case
|
|
31
|
+
next_case += 1
|
|
32
|
+
else:
|
|
33
|
+
fresh[k] = v
|
|
34
|
+
|
|
35
|
+
cls = super().__new__(mcls, name, bases, fresh, **kw)
|
|
36
|
+
|
|
37
|
+
def sanitize_type(
|
|
38
|
+
t: Type[Any] | ForwardRef | None
|
|
39
|
+
) -> Type[Any] | str:
|
|
40
|
+
if t is None:
|
|
41
|
+
return NoneType
|
|
42
|
+
|
|
43
|
+
# Keep self-references as strings instead of ForwardRefs
|
|
44
|
+
if isinstance(t, str) and t == name:
|
|
45
|
+
return name
|
|
46
|
+
|
|
47
|
+
# Convert ForwardRefs back to strings if they reference self
|
|
48
|
+
if isinstance(t, ForwardRef):
|
|
49
|
+
forward_name = t.__forward_arg__
|
|
50
|
+
if forward_name == name:
|
|
51
|
+
return name
|
|
52
|
+
return forward_name
|
|
53
|
+
|
|
54
|
+
# Handle regular strings (non-self references)
|
|
55
|
+
if isinstance(t, str):
|
|
56
|
+
return t # Keep as string
|
|
57
|
+
|
|
58
|
+
origin = get_origin(t)
|
|
59
|
+
if origin is not None:
|
|
60
|
+
args = get_args(t)
|
|
61
|
+
new_args = tuple(sanitize_type(arg) for arg in args)
|
|
62
|
+
|
|
63
|
+
# Reconstruct the generic type
|
|
64
|
+
if isinstance(t, UnionType):
|
|
65
|
+
return Union[*new_args]
|
|
66
|
+
else:
|
|
67
|
+
# For generic types like List, Tuple, etc.
|
|
68
|
+
return origin[new_args]
|
|
69
|
+
|
|
70
|
+
# Regular types (int, str, custom classes, etc.)
|
|
71
|
+
return t
|
|
72
|
+
|
|
73
|
+
sanitized_declared_types = {}
|
|
74
|
+
for member_name, t in declared.items():
|
|
75
|
+
sanitized_types = sanitize_type(t)
|
|
76
|
+
sanitized_declared_types[member_name] = sanitized_types
|
|
77
|
+
|
|
78
|
+
cls._declared_types = sanitized_declared_types
|
|
79
|
+
return cls
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TaggedEnum(Enum, metaclass=TaggedEnumMeta):
|
|
83
|
+
"""
|
|
84
|
+
Base class for tagged enums with payloads.
|
|
85
|
+
|
|
86
|
+
Each uppercase attribute will be considered an **enum case**.
|
|
87
|
+
Each case should be a Python type representing the payload type.
|
|
88
|
+
Collection types are allowed.
|
|
89
|
+
|
|
90
|
+
`None` denotes no payload.
|
|
91
|
+
|
|
92
|
+
### Usage:
|
|
93
|
+
```
|
|
94
|
+
class Message(TaggedEnum):
|
|
95
|
+
PING = None
|
|
96
|
+
STRING = str
|
|
97
|
+
COORDINATES = tuple[int, int]
|
|
98
|
+
|
|
99
|
+
ping_msg = Message.PING()
|
|
100
|
+
string_msg = Message.STRING("hello world")
|
|
101
|
+
coords_msg = Message.COORDINATES((1, -1))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`TaggedEnum`s can be unpacked using `match` statements:
|
|
105
|
+
```
|
|
106
|
+
match message:
|
|
107
|
+
case Message(kind=Message.PING):
|
|
108
|
+
# handle ping
|
|
109
|
+
case Message(kind=Message.STRING, payload=string):
|
|
110
|
+
# handle string
|
|
111
|
+
case Message(kind=Message.COORDINATES, payload=(x, y)):
|
|
112
|
+
# handle coords (x, y)
|
|
113
|
+
case _:
|
|
114
|
+
raise ValueError
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `.kind` property returns the static enum member (the "tag")
|
|
118
|
+
associated with an instance, allowing for simplified matching
|
|
119
|
+
when the payload is not required:
|
|
120
|
+
```
|
|
121
|
+
if message.kind is Message.Kind.STRING:
|
|
122
|
+
print("This is a string message")
|
|
123
|
+
|
|
124
|
+
match message.kind:
|
|
125
|
+
case Message.Kind.PING:
|
|
126
|
+
...
|
|
127
|
+
case Message.Kind.STRING:
|
|
128
|
+
...
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Members vs. Instances
|
|
132
|
+
**Member**: the static class attribute (e.g., `Message.STRING`)
|
|
133
|
+
representing the variant's definition, referred to as its _kind_.
|
|
134
|
+
|
|
135
|
+
**Instance**: the object created by calling a member with a data payload
|
|
136
|
+
(e.g., `Message.STRING("hello")`). The instance holds a reference to the
|
|
137
|
+
concrete `payload` value.
|
|
138
|
+
|
|
139
|
+
The `Kind` type attribute serves as a type-level representation of all
|
|
140
|
+
available members. While an instance contains specific data, its `.kind`
|
|
141
|
+
property returns the member itself, allowing for
|
|
142
|
+
type-safe dispatching and matching without inspecting the underlying
|
|
143
|
+
payload.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
Kind: ClassVar[type[Self]]
|
|
147
|
+
|
|
148
|
+
def __call__(self, *args, **kwargs) -> Self:
|
|
149
|
+
if not self.is_member:
|
|
150
|
+
raise TypeError
|
|
151
|
+
return self._construct(self, *args, **kwargs)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def _construct(cls: Type['TaggedEnum'],
|
|
155
|
+
member: 'TaggedEnum',
|
|
156
|
+
*args: List[Any]) -> 'TaggedEnum':
|
|
157
|
+
t: Type[Any] = cls._declared_types[member.name]
|
|
158
|
+
arg = args[0] if len(args) > 0 else None
|
|
159
|
+
|
|
160
|
+
# Type check
|
|
161
|
+
if not cls._typecheck(arg, t):
|
|
162
|
+
raise TypeError(f"{member.name}: expected {t}, got {type(arg)}")
|
|
163
|
+
|
|
164
|
+
# Create instance
|
|
165
|
+
inst = object.__new__(cls)
|
|
166
|
+
inst._member_ = member
|
|
167
|
+
inst._name_ = member.name
|
|
168
|
+
inst._value_ = member.value
|
|
169
|
+
|
|
170
|
+
inst._payload = arg
|
|
171
|
+
inst._kind = member
|
|
172
|
+
|
|
173
|
+
return inst
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def make(cls, kind: int, payload: Any) -> 'TaggedEnum':
|
|
177
|
+
member = cls(kind)
|
|
178
|
+
return cls._construct(member, payload)
|
|
179
|
+
|
|
180
|
+
# Pattern matching support
|
|
181
|
+
__match_args__ = ("kind", "payload")
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def _typecheck(cls, value: Any, typ: Any) -> bool:
|
|
185
|
+
# Handle string forward references - skip validation
|
|
186
|
+
if isinstance(typ, str):
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
origin = get_origin(typ)
|
|
190
|
+
args = get_args(typ)
|
|
191
|
+
|
|
192
|
+
# Handle Union types (including | syntax)
|
|
193
|
+
if origin is Union or origin is UnionType:
|
|
194
|
+
return any(cls._typecheck(value, arg) for arg in args)
|
|
195
|
+
|
|
196
|
+
# Handle Optional (which is Union[T, None])
|
|
197
|
+
if origin is Union and len(args) == 2 and type(None) in args:
|
|
198
|
+
non_none_type = args[0] if args[1] is type(None) else args[1]
|
|
199
|
+
return value is None or cls._typecheck(value, non_none_type)
|
|
200
|
+
|
|
201
|
+
# Handle parameterized generics like list[int]
|
|
202
|
+
if origin is not None:
|
|
203
|
+
# Check the container type, skip checking type parameters for now
|
|
204
|
+
return isinstance(value, origin)
|
|
205
|
+
|
|
206
|
+
# Handle regular types
|
|
207
|
+
if isinstance(typ, type):
|
|
208
|
+
return isinstance(value, typ)
|
|
209
|
+
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def payload_type(cls, kind: int) -> Type[Any]:
|
|
214
|
+
kind_name = cls._value2member_map_[kind].name
|
|
215
|
+
return cls._declared_types[kind_name]
|
|
216
|
+
|
|
217
|
+
""" Instance Methods """
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def kind(self) -> Self:
|
|
221
|
+
return self if self.is_member else self._kind
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def payload(self) -> Any:
|
|
225
|
+
return self._payload
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def is_member(self) -> bool:
|
|
229
|
+
return not hasattr(self, "_payload")
|
|
230
|
+
|
|
231
|
+
def __eq__(self, other: Any) -> bool:
|
|
232
|
+
if not isinstance(other, self.__class__):
|
|
233
|
+
return False
|
|
234
|
+
elif self.value is not other.value:
|
|
235
|
+
return False
|
|
236
|
+
elif self.is_member != other.is_member:
|
|
237
|
+
return False
|
|
238
|
+
elif not self.is_member:
|
|
239
|
+
return self.payload == other.payload
|
|
240
|
+
else:
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
def __hash__(self) -> int:
|
|
244
|
+
if not self.is_member:
|
|
245
|
+
return hash((self.value, self.payload))
|
|
246
|
+
else:
|
|
247
|
+
return hash(self.value)
|
|
248
|
+
|
|
249
|
+
def __repr__(self):
|
|
250
|
+
if not self.is_member:
|
|
251
|
+
return f"{self.__class__.__name__}.{self.name}({self.payload!r})"
|
|
252
|
+
else:
|
|
253
|
+
return f"{self.__class__.__name__}.{self.name}"
|
|
254
|
+
|
|
255
|
+
def __str__(self):
|
|
256
|
+
return repr(self)
|
|
257
|
+
|
|
258
|
+
# Ensure immutability
|
|
259
|
+
def __setattr__(self, key: str, value: Any):
|
|
260
|
+
if hasattr(self, key):
|
|
261
|
+
raise AttributeError("TaggedEnum instances are immutable")
|
|
262
|
+
super().__setattr__(key, value)
|
tagged_enum/__init__.py
ADDED
tagged_enum/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tagged-enum
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Tagged enums for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/rob-barker/tagged-enum
|
|
6
|
+
Project-URL: Repository, https://github.com/rob-barker/tagged-enum
|
|
7
|
+
Project-URL: Issues, https://github.com/rob-barker/tagged-enum/issues
|
|
8
|
+
Author-email: Rob Barker <rob.barker.official@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: discriminated-union,enum,pattern-matching,tagged-enum,tagged-union
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# tagged-enum
|
|
27
|
+
|
|
28
|
+
Tagged enums for Python ๐.
|
|
29
|
+
|
|
30
|
+
Tagged enums (a.k.a. discriminated unions, enums with associated values, etc.)
|
|
31
|
+
are enums where each case carries a typed **payload**.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from tagged_enum import TaggedEnum
|
|
35
|
+
|
|
36
|
+
# Declare a tagged enum
|
|
37
|
+
class Shape(TaggedEnum):
|
|
38
|
+
CIRCLE = float # radius
|
|
39
|
+
RECTANGLE = tuple[float, float] # width, height
|
|
40
|
+
TRIANGLE = tuple[float, float] # base, height
|
|
41
|
+
|
|
42
|
+
def area(shape: Shape) -> float:
|
|
43
|
+
# match over enum cases
|
|
44
|
+
match shape:
|
|
45
|
+
case Shape(kind=Shape.CIRCLE, payload=radius):
|
|
46
|
+
return 3.14159 * radius ** 2
|
|
47
|
+
case Shape(kind=Shape.RECTANGLE, payload=(w, h)):
|
|
48
|
+
return w * h
|
|
49
|
+
case Shape(kind=Shape.TRIANGLE, payload=(b, h)):
|
|
50
|
+
return 0.5 * b * h
|
|
51
|
+
|
|
52
|
+
shapes = [Shape.CIRCLE(2.0),
|
|
53
|
+
Shape.RECTANGLE((3.0, 4.0)),
|
|
54
|
+
Shape.TRIANGLE((6.0, 2.0))]
|
|
55
|
+
|
|
56
|
+
[area(s) for s in shapes] # [12.57, 12.0, 6.0]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is one of my favorite features from [Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations/#Associated-Values) and [Rust](https://doc.rust-lang.org/rust-by-example/custom_types/enum.html) so I brought it to Python ๐.
|
|
60
|
+
|
|
61
|
+
## โฌ Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install tagged-enum
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Python 3.11+
|
|
68
|
+
|
|
69
|
+
## ๐ค How to use
|
|
70
|
+
|
|
71
|
+
Each uppercase attribute on a `TaggedEnum` subclass declares a *case*. The
|
|
72
|
+
value assigned to it is the *type* of payload that case carries. `None`
|
|
73
|
+
means the case carries nothing.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
class GameEvent(TaggedEnum):
|
|
77
|
+
PLAYER_JOINED = str # player name
|
|
78
|
+
DAMAGE_DEALT = tuple[str, int] # target, amount
|
|
79
|
+
GAME_OVER = None # void
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Calling a case constructs an **instance**, and the payload is checked against
|
|
83
|
+
the declared type on instantiation.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
GameEvent.PLAYER_JOINED("Ada") # ๐
|
|
87
|
+
GameEvent.DAMAGE_DEALT(("Jeff", 42)) # ๐
|
|
88
|
+
GameEvent.GAME_OVER() # ๐
|
|
89
|
+
GameEvent.PLAYER_JOINED(-1) # ๐ - TypeError
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### ๐งฉ Typing
|
|
93
|
+
|
|
94
|
+
Payload declarations may be plain types, generic containers, unions, custom types, or forward references to the enclosing class for recursive structures:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
class Json(TaggedEnum):
|
|
98
|
+
NULL = None
|
|
99
|
+
NUMBER = float
|
|
100
|
+
STRING = str
|
|
101
|
+
ARRAY = list["Json"]
|
|
102
|
+
OBJECT = dict[str, "Json"]
|
|
103
|
+
CUSTOM = MyDataclass
|
|
104
|
+
|
|
105
|
+
Json.OBJECT({
|
|
106
|
+
"name": Json.STRING("Ada"),
|
|
107
|
+
"tags": Json.ARRAY([Json.STRING("math"), Json.NUMBER(1815.0)]),
|
|
108
|
+
"other": Json.CUSTOM(MyDataclass())
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`Union[...]` or `X | Y` and `Optional[...]` payloads are also valid types. Construction succeeds if the value matches any member of the union:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
class ID(TaggedEnum):
|
|
116
|
+
VALUE = int | str
|
|
117
|
+
|
|
118
|
+
ID.VALUE(42) # ๐
|
|
119
|
+
ID.VALUE("abc") # ๐
|
|
120
|
+
ID.VALUE(4.2) # ๐ - TypeError
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Type checking validates the outer container (e.g. `list`, `dict`, which
|
|
124
|
+
member of a `Union`) but does not recurse into generic type parameters, and
|
|
125
|
+
skips validation entirely for unresolved forward references like `"Json"`
|
|
126
|
+
above.
|
|
127
|
+
|
|
128
|
+
### โจ Pattern Matching
|
|
129
|
+
|
|
130
|
+
Tagged enums are unpacked with Python's native [match statement](https://docs.python.org/3/tutorial/controlflow.html#match-statements) using the `kind`/`payload` attributes:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
def handle(event: GameEvent) -> str:
|
|
134
|
+
match event:
|
|
135
|
+
case GameEvent(kind=GameEvent.PLAYER_JOINED, payload=name):
|
|
136
|
+
return f"{name} joined the game"
|
|
137
|
+
case GameEvent(kind=GameEvent.DAMAGE_DEALT, payload=(target, amount)):
|
|
138
|
+
return f"{target} took {amount} damage"
|
|
139
|
+
case GameEvent(kind=GameEvent.GAME_OVER):
|
|
140
|
+
return "Game over"
|
|
141
|
+
|
|
142
|
+
handle(GameEvent.DAMAGE_DEALT(("Grendel", 42)))
|
|
143
|
+
# 'Grendel took 42 damage'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
When you only care about which case you're looking at and not the payload, use `.kind` to return the tag itself (also called the **member**):
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
if event.kind is GameEvent.Kind.PLAYER_JOINED:
|
|
150
|
+
print("player joined")
|
|
151
|
+
|
|
152
|
+
match event.kind:
|
|
153
|
+
case GameEvent.Kind.PLAYER_JOINED:
|
|
154
|
+
...
|
|
155
|
+
case GameEvent.Kind.GAME_OVER:
|
|
156
|
+
...
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`GameEvent.PLAYER_JOINED` (the **member**) and
|
|
160
|
+
`GameEvent.PLAYER_JOINED(...)` (an **instance**) are different objects and ideas. The member is a singleton tag you compare with `is`, while the instance carries the actual data payload. Members and instances are not equal (by `==`). Instances of the same kind/case are equal if and only if their payload values are equal.
|
|
161
|
+
|
|
162
|
+
The `Kind` type attribute is a type-level alias for annotating variables that should hold a case rather than an instance.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
def describe(kind: GameEvent.Kind) -> str:
|
|
166
|
+
if kind is GameEvent.Kind.PLAYER_JOINED:
|
|
167
|
+
do_something()
|
|
168
|
+
|
|
169
|
+
describe(event.kind) # 'damage was dealt'
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## ๐ค Development
|
|
173
|
+
|
|
174
|
+
This project uses [`uv`](https://docs.astral.sh/uv/) for dependency management.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv sync
|
|
178
|
+
uv run pytest
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Building
|
|
182
|
+
|
|
183
|
+
Build the sdist/wheel into `dist/`:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
uv build
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
To try the library locally without installing it anywhere:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# drop into a REPL
|
|
193
|
+
uv run python
|
|
194
|
+
# OR run a script inside the project's environment
|
|
195
|
+
uv run python my_script.py
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### ๐งช Testing
|
|
199
|
+
|
|
200
|
+
Tests live in `tests/`, split by topic (construction, equality/hashing,
|
|
201
|
+
pattern matching, generic payloads, etc.) so a single area can be run in
|
|
202
|
+
isolation, e.g.:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
uv run pytest tests/test_generic_payloads.py
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Without `uv`, the equivalent is:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install -e ".[test]"
|
|
212
|
+
pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## ๐ ๏ธ Contribution Guidelines
|
|
216
|
+
|
|
217
|
+
This repository is open source, though maintenance is infrequent. Please be
|
|
218
|
+
patient ๐ฅบ.
|
|
219
|
+
|
|
220
|
+
- **File an issue first:** Anything beyond a small fix should be discussed in an issue
|
|
221
|
+
before you put work into a PR.
|
|
222
|
+
- **Keep PRs focused:** One change or feature per PR, branched off `Development`
|
|
223
|
+
and rebased/updated before you submit.
|
|
224
|
+
- **Branch naming:** `issues/<issue-number>`, optionally with a short
|
|
225
|
+
description, e.g. `issues/123-fix-a-bug`.
|
|
226
|
+
- **Include a description and test coverage:** Explain what changed and
|
|
227
|
+
why, add or update tests under `tests/`, and reference the issue it
|
|
228
|
+
closes.
|
|
229
|
+
|
|
230
|
+
Thanks for helping keep this repo clean and organized! ๐
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tagged_enum/TaggedEnum.py,sha256=O7nsHHT97aXu45HbDZFPMPecMG7QSycR3SPMqUek_pw,8653
|
|
2
|
+
tagged_enum/__init__.py,sha256=FFPUU9Vapxvph7oWn1ix85072QQLJdGKMsf2zHCOVS4,214
|
|
3
|
+
tagged_enum/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
tagged_enum-1.0.0.dist-info/METADATA,sha256=mKBbJBf11_nh4R34jIIbpri-9nSnwxENZHBqfK-VVaE,7187
|
|
5
|
+
tagged_enum-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
6
|
+
tagged_enum-1.0.0.dist-info/licenses/LICENSE,sha256=uIIPB9pZIhCeJFhlhTmp-L8g-8ubl3xOVMjbUX0ZAek,1091
|
|
7
|
+
tagged_enum-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Barker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|