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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firstclass
3
- Version: 0.0.3
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
- dc = dataclasses.dataclass(cls, **dataclass_kwargs)
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
@@ -8,6 +8,7 @@ firstclass.egg-info/PKG-INFO
8
8
  firstclass.egg-info/SOURCES.txt
9
9
  firstclass.egg-info/dependency_links.txt
10
10
  firstclass.egg-info/top_level.txt
11
+ tests/test_compare.py
11
12
  tests/test_ctypes_types.py
12
13
  tests/test_edge_cases.py
13
14
  tests/test_enum_bitfield.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firstclass"
7
- version = "0.0.3"
7
+ version = "0.1.0"
8
8
  description = "dataclass with bytes, ctypes, C struct, JSON and XML serialization"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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(Small)))
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