firstclass 0.0.3__tar.gz → 0.1.0__tar.gz
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.
- {firstclass-0.0.3/firstclass.egg-info → firstclass-0.1.0}/PKG-INFO +8 -1
- firstclass-0.0.3/PKG-INFO → firstclass-0.1.0/README.md +7 -7
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass/_core.py +6 -1
- firstclass-0.0.3/README.md → firstclass-0.1.0/firstclass.egg-info/PKG-INFO +14 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass.egg-info/SOURCES.txt +1 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/pyproject.toml +1 -1
- firstclass-0.1.0/tests/test_compare.py +221 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_edge_cases.py +1 -1
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass/__init__.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass/_header.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass/_specs.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass.egg-info/dependency_links.txt +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/firstclass.egg-info/top_level.txt +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/setup.cfg +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_ctypes_types.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_enum_bitfield.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_example.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_firstclass.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_header.py +0 -0
- {firstclass-0.0.3 → firstclass-0.1.0}/tests/test_header_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firstclass
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: dataclass with bytes, ctypes, C struct, JSON and XML serialization
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -112,6 +112,13 @@ bitfields, inheritance, virtual members, unknown types (`#include`d headers are
|
|
|
112
112
|
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
113
113
|
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
114
114
|
|
|
115
|
+
## Comparison
|
|
116
|
+
|
|
117
|
+
Like a dataclass, `==` / `!=` compare field by field. Unlike a plain dataclass, ordering is on by
|
|
118
|
+
default: `<`, `<=`, `>`, `>=` compare fields in declaration order, like tuples, so you can `sorted()`,
|
|
119
|
+
`min()` and `max()` instances. Comparing with another class raises `TypeError` (`==` is just `False`).
|
|
120
|
+
Switch it off with `@firstclass(order=False)`; `eq=False` turns off both.
|
|
121
|
+
|
|
115
122
|
## Layout options
|
|
116
123
|
|
|
117
124
|
```python
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: firstclass
|
|
3
|
-
Version: 0.0.3
|
|
4
|
-
Summary: dataclass with bytes, ctypes, C struct, JSON and XML serialization
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
|
|
8
1
|
# firstclass
|
|
9
2
|
|
|
10
3
|
`@firstclass` works like `@dataclass` (same keyword args) and adds conversion to and from
|
|
@@ -112,6 +105,13 @@ bitfields, inheritance, virtual members, unknown types (`#include`d headers are
|
|
|
112
105
|
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
113
106
|
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
114
107
|
|
|
108
|
+
## Comparison
|
|
109
|
+
|
|
110
|
+
Like a dataclass, `==` / `!=` compare field by field. Unlike a plain dataclass, ordering is on by
|
|
111
|
+
default: `<`, `<=`, `>`, `>=` compare fields in declaration order, like tuples, so you can `sorted()`,
|
|
112
|
+
`min()` and `max()` instances. Comparing with another class raises `TypeError` (`==` is just `False`).
|
|
113
|
+
Switch it off with `@firstclass(order=False)`; `eq=False` turns off both.
|
|
114
|
+
|
|
115
115
|
## Layout options
|
|
116
116
|
|
|
117
117
|
```python
|
|
@@ -279,6 +279,9 @@ class Schema:
|
|
|
279
279
|
def firstclass(cls=None, /, *, byteorder: str = "little", align: bool = False, **dataclass_kwargs):
|
|
280
280
|
"""Like `@dataclass` (same keyword args), plus binary/JSON/XML/ctypes/C codecs.
|
|
281
281
|
|
|
282
|
+
Unlike a plain dataclass, `order=True` is the default (compare field by field, like tuples);
|
|
283
|
+
pass `order=False` to turn it off.
|
|
284
|
+
|
|
282
285
|
byteorder: 'little' (default) or 'big'.
|
|
283
286
|
align: False (default) = packed layout, no padding; True = natural C alignment padding.
|
|
284
287
|
"""
|
|
@@ -286,7 +289,9 @@ def firstclass(cls=None, /, *, byteorder: str = "little", align: bool = False, *
|
|
|
286
289
|
def wrap(cls):
|
|
287
290
|
if not isinstance(cls, type):
|
|
288
291
|
raise TypeError(f"@firstclass needs a class, got {type(cls).__name__}")
|
|
289
|
-
|
|
292
|
+
kwargs = dict(dataclass_kwargs)
|
|
293
|
+
kwargs.setdefault("order", kwargs.get("eq", True)) # ==, <, <=, >, >= by field order
|
|
294
|
+
dc = dataclasses.dataclass(cls, **kwargs)
|
|
290
295
|
clash = [f.name for f in dataclasses.fields(dc) if f.name in _METHODS or f.name == "__firstclass__"]
|
|
291
296
|
if clash:
|
|
292
297
|
raise TypeError(f"{dc.__name__}: field names clash with firstclass methods: {clash}")
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firstclass
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: dataclass with bytes, ctypes, C struct, JSON and XML serialization
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
1
8
|
# firstclass
|
|
2
9
|
|
|
3
10
|
`@firstclass` works like `@dataclass` (same keyword args) and adds conversion to and from
|
|
@@ -105,6 +112,13 @@ bitfields, inheritance, virtual members, unknown types (`#include`d headers are
|
|
|
105
112
|
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
106
113
|
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
107
114
|
|
|
115
|
+
## Comparison
|
|
116
|
+
|
|
117
|
+
Like a dataclass, `==` / `!=` compare field by field. Unlike a plain dataclass, ordering is on by
|
|
118
|
+
default: `<`, `<=`, `>`, `>=` compare fields in declaration order, like tuples, so you can `sorted()`,
|
|
119
|
+
`min()` and `max()` instances. Comparing with another class raises `TypeError` (`==` is just `False`).
|
|
120
|
+
Switch it off with `@firstclass(order=False)`; `eq=False` turns off both.
|
|
121
|
+
|
|
108
122
|
## Layout options
|
|
109
123
|
|
|
110
124
|
```python
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from firstclass import (
|
|
6
|
+
array_t, bitfield, bytes_t, char_array, enum_t, firstclass, float64_t, from_header, uint8_t, uint16_t,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Level(enum.IntEnum):
|
|
11
|
+
LOW = 1
|
|
12
|
+
HIGH = 5
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@firstclass
|
|
16
|
+
class Version:
|
|
17
|
+
major: uint8_t
|
|
18
|
+
minor: uint8_t
|
|
19
|
+
patch: uint16_t = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@firstclass
|
|
23
|
+
class Rich:
|
|
24
|
+
name: char_array[4]
|
|
25
|
+
level: enum_t[Level, uint8_t]
|
|
26
|
+
items: array_t[uint8_t, 2]
|
|
27
|
+
blob: bytes_t[2]
|
|
28
|
+
ratio: float64_t
|
|
29
|
+
bits: bitfield[uint8_t, 3]
|
|
30
|
+
inner: Version
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rich(**kw):
|
|
34
|
+
base = dict(name="a", level=Level.LOW, items=[1, 2], blob=b"\x00\x01", ratio=1.0, bits=1,
|
|
35
|
+
inner=Version(1, 0))
|
|
36
|
+
base.update(kw)
|
|
37
|
+
return Rich(**base)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Equality(unittest.TestCase):
|
|
41
|
+
def test_eq_and_ne(self):
|
|
42
|
+
self.assertEqual(Version(1, 2, 3), Version(1, 2, 3))
|
|
43
|
+
self.assertNotEqual(Version(1, 2, 3), Version(1, 2, 4))
|
|
44
|
+
self.assertTrue(Version(1, 2) == Version(1, 2, 0))
|
|
45
|
+
self.assertFalse(Version(1, 2) != Version(1, 2))
|
|
46
|
+
|
|
47
|
+
def test_equality_survives_a_bytes_roundtrip(self):
|
|
48
|
+
r = rich()
|
|
49
|
+
self.assertEqual(Rich.from_bytes(r.to_bytes()), r)
|
|
50
|
+
|
|
51
|
+
def test_different_classes_are_never_equal(self):
|
|
52
|
+
@firstclass
|
|
53
|
+
class Other:
|
|
54
|
+
major: uint8_t
|
|
55
|
+
minor: uint8_t
|
|
56
|
+
patch: uint16_t = 0
|
|
57
|
+
self.assertNotEqual(Version(1, 2), Other(1, 2))
|
|
58
|
+
self.assertFalse(Version(1, 2) == (1, 2, 0))
|
|
59
|
+
|
|
60
|
+
def test_not_hashable_unless_frozen(self):
|
|
61
|
+
with self.assertRaises(TypeError):
|
|
62
|
+
hash(Version(1, 2))
|
|
63
|
+
|
|
64
|
+
@firstclass(frozen=True)
|
|
65
|
+
class Frozen:
|
|
66
|
+
a: uint8_t
|
|
67
|
+
self.assertEqual(hash(Frozen(1)), hash(Frozen(1)))
|
|
68
|
+
self.assertEqual({Frozen(1), Frozen(1), Frozen(2)}, {Frozen(1), Frozen(2)})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Ordering(unittest.TestCase):
|
|
72
|
+
def test_all_four_operators(self):
|
|
73
|
+
a, b = Version(1, 2, 3), Version(1, 3, 0)
|
|
74
|
+
self.assertTrue(a < b)
|
|
75
|
+
self.assertTrue(a <= b)
|
|
76
|
+
self.assertTrue(b > a)
|
|
77
|
+
self.assertTrue(b >= a)
|
|
78
|
+
self.assertFalse(a > b)
|
|
79
|
+
self.assertFalse(a >= b)
|
|
80
|
+
self.assertFalse(b < a)
|
|
81
|
+
self.assertFalse(b <= a)
|
|
82
|
+
|
|
83
|
+
def test_equal_values(self):
|
|
84
|
+
a, b = Version(1, 2, 3), Version(1, 2, 3)
|
|
85
|
+
self.assertTrue(a <= b)
|
|
86
|
+
self.assertTrue(a >= b)
|
|
87
|
+
self.assertFalse(a < b)
|
|
88
|
+
self.assertFalse(a > b)
|
|
89
|
+
|
|
90
|
+
def test_lexicographic_in_field_order(self):
|
|
91
|
+
self.assertTrue(Version(2, 0, 0) > Version(1, 255, 65535))
|
|
92
|
+
self.assertTrue(Version(1, 1, 0) > Version(1, 0, 65535))
|
|
93
|
+
self.assertTrue(Version(0, 0, 1) > Version(0, 0, 0))
|
|
94
|
+
|
|
95
|
+
def test_sorted_min_max(self):
|
|
96
|
+
vs = [Version(1, 2, 0), Version(0, 9, 9), Version(1, 0, 5), Version(1, 2, 1)]
|
|
97
|
+
self.assertEqual(sorted(vs), [Version(0, 9, 9), Version(1, 0, 5), Version(1, 2, 0), Version(1, 2, 1)])
|
|
98
|
+
self.assertEqual(max(vs), Version(1, 2, 1))
|
|
99
|
+
self.assertEqual(min(vs), Version(0, 9, 9))
|
|
100
|
+
self.assertEqual(sorted(vs, reverse=True)[0], Version(1, 2, 1))
|
|
101
|
+
|
|
102
|
+
def test_ordering_matches_after_roundtrip(self):
|
|
103
|
+
vs = [Version(3, 1), Version(1, 4), Version(2, 2)]
|
|
104
|
+
back = [Version.from_bytes(v.to_bytes()) for v in vs]
|
|
105
|
+
self.assertEqual(sorted(back), sorted(vs))
|
|
106
|
+
|
|
107
|
+
def test_comparing_with_other_types_raises(self):
|
|
108
|
+
with self.assertRaises(TypeError):
|
|
109
|
+
Version(1, 2) < (1, 2, 0)
|
|
110
|
+
with self.assertRaises(TypeError):
|
|
111
|
+
Version(1, 2) >= 5
|
|
112
|
+
|
|
113
|
+
@firstclass
|
|
114
|
+
class Other:
|
|
115
|
+
major: uint8_t
|
|
116
|
+
minor: uint8_t
|
|
117
|
+
patch: uint16_t = 0
|
|
118
|
+
with self.assertRaises(TypeError):
|
|
119
|
+
Version(1, 2) < Other(1, 3)
|
|
120
|
+
|
|
121
|
+
def test_every_field_kind_participates(self):
|
|
122
|
+
base = rich()
|
|
123
|
+
self.assertTrue(rich(name="b") > base)
|
|
124
|
+
self.assertTrue(rich(level=Level.HIGH) > base)
|
|
125
|
+
self.assertTrue(rich(items=[1, 3]) > base)
|
|
126
|
+
self.assertTrue(rich(blob=b"\x00\x02") > base)
|
|
127
|
+
self.assertTrue(rich(ratio=1.5) > base)
|
|
128
|
+
self.assertTrue(rich(bits=2) > base)
|
|
129
|
+
self.assertTrue(rich(inner=Version(1, 1)) > base)
|
|
130
|
+
self.assertTrue(base < rich(inner=Version(2, 0)))
|
|
131
|
+
|
|
132
|
+
def test_first_differing_field_decides(self):
|
|
133
|
+
self.assertTrue(rich(name="b", ratio=0.0) > rich(name="a", ratio=99.0))
|
|
134
|
+
|
|
135
|
+
def test_nested_classes_are_orderable(self):
|
|
136
|
+
@firstclass
|
|
137
|
+
class Pair:
|
|
138
|
+
lo: Version
|
|
139
|
+
hi: Version
|
|
140
|
+
self.assertTrue(Pair(Version(1, 0), Version(9, 9)) < Pair(Version(1, 1), Version(0, 0)))
|
|
141
|
+
|
|
142
|
+
def test_works_for_every_byte_order_and_alignment(self):
|
|
143
|
+
for kw in ({}, {"byteorder": "big"}, {"align": True}):
|
|
144
|
+
with self.subTest(kw):
|
|
145
|
+
@firstclass(**kw)
|
|
146
|
+
class V:
|
|
147
|
+
a: uint8_t
|
|
148
|
+
b: uint16_t
|
|
149
|
+
self.assertTrue(V(1, 2) < V(1, 3))
|
|
150
|
+
self.assertTrue(V(2, 0) > V(1, 9))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Options(unittest.TestCase):
|
|
154
|
+
def test_order_false_disables_ordering_only(self):
|
|
155
|
+
@firstclass(order=False)
|
|
156
|
+
class NoOrder:
|
|
157
|
+
a: uint8_t
|
|
158
|
+
self.assertEqual(NoOrder(1), NoOrder(1))
|
|
159
|
+
with self.assertRaises(TypeError):
|
|
160
|
+
NoOrder(1) < NoOrder(2)
|
|
161
|
+
|
|
162
|
+
def test_eq_false_means_no_ordering_either(self):
|
|
163
|
+
@firstclass(eq=False)
|
|
164
|
+
class NoEq:
|
|
165
|
+
a: uint8_t
|
|
166
|
+
x, y = NoEq(1), NoEq(1)
|
|
167
|
+
self.assertNotEqual(x, y) # identity comparison
|
|
168
|
+
self.assertEqual(x, x)
|
|
169
|
+
with self.assertRaises(TypeError):
|
|
170
|
+
x < y
|
|
171
|
+
|
|
172
|
+
def test_eq_false_with_explicit_order_is_a_dataclass_error(self):
|
|
173
|
+
with self.assertRaises(ValueError):
|
|
174
|
+
@firstclass(eq=False, order=True)
|
|
175
|
+
class Bad:
|
|
176
|
+
a: uint8_t
|
|
177
|
+
|
|
178
|
+
def test_order_true_explicit_is_fine(self):
|
|
179
|
+
@firstclass(order=True)
|
|
180
|
+
class Explicit:
|
|
181
|
+
a: uint8_t
|
|
182
|
+
self.assertTrue(Explicit(1) < Explicit(2))
|
|
183
|
+
|
|
184
|
+
def test_field_compare_false_is_ignored(self):
|
|
185
|
+
@firstclass
|
|
186
|
+
class Skip:
|
|
187
|
+
a: uint8_t
|
|
188
|
+
noise: uint8_t = dataclasses.field(default=0, compare=False)
|
|
189
|
+
self.assertEqual(Skip(1, 5), Skip(1, 9))
|
|
190
|
+
self.assertFalse(Skip(1, 9) < Skip(1, 5))
|
|
191
|
+
self.assertTrue(Skip(1, 0) < Skip(2, 0))
|
|
192
|
+
|
|
193
|
+
def test_functools_total_ordering_not_needed(self):
|
|
194
|
+
for name in ("__lt__", "__le__", "__gt__", "__ge__", "__eq__"):
|
|
195
|
+
self.assertIn(name, vars(Version))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class HeaderGenerated(unittest.TestCase):
|
|
199
|
+
def test_generated_classes_compare(self):
|
|
200
|
+
t = from_header(text="""
|
|
201
|
+
enum class Lvl : uint8_t { A = 1, B };
|
|
202
|
+
struct Ver { uint8_t major; uint8_t minor; };
|
|
203
|
+
struct Msg { struct Ver ver; Lvl lvl; char tag[4]; };
|
|
204
|
+
""")
|
|
205
|
+
a = t.Msg(t.Ver(1, 0), t.Lvl.A, "x")
|
|
206
|
+
b = t.Msg(t.Ver(1, 0), t.Lvl.B, "a")
|
|
207
|
+
self.assertTrue(a < b)
|
|
208
|
+
self.assertTrue(t.Ver(1, 9) < t.Ver(2, 0))
|
|
209
|
+
self.assertEqual(sorted([b, a]), [a, b])
|
|
210
|
+
self.assertEqual(t.Msg.from_bytes(a.to_bytes()), a)
|
|
211
|
+
|
|
212
|
+
def test_generated_classes_from_two_parses_are_different_types(self):
|
|
213
|
+
src = "struct Ver { uint8_t major; };"
|
|
214
|
+
a, b = from_header(text=src), from_header(text=src)
|
|
215
|
+
self.assertNotEqual(a.Ver(1), b.Ver(1))
|
|
216
|
+
with self.assertRaises(TypeError):
|
|
217
|
+
a.Ver(1) < b.Ver(2)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
unittest.main()
|
|
@@ -323,7 +323,7 @@ class DataclassBehaviour(unittest.TestCase):
|
|
|
323
323
|
self.assertEqual(s.to_bytes(), b"\xc8\x05")
|
|
324
324
|
|
|
325
325
|
def test_decorator_forms(self):
|
|
326
|
-
self.assertTrue(dataclasses.is_dataclass(firstclass(
|
|
326
|
+
self.assertTrue(dataclasses.is_dataclass(firstclass(type("Fresh", (), {"__annotations__": {"a": uint8_t}}))))
|
|
327
327
|
with self.assertRaises(TypeError):
|
|
328
328
|
firstclass(5)
|
|
329
329
|
with self.assertRaises(ValueError):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|