Skip to content

Commit 811f85e

Browse files
authored
Merge pull request #2 from xpodev/use-metaclass
Use metaclass
2 parents d654d47 + 5c7f42c commit 811f85e

File tree

12 files changed

+1066
-278
lines changed

12 files changed

+1066
-278
lines changed

README.md

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,56 @@ When deserializing a struct with multiple bases or if one of the fields was over
5656
the deserialization must be done through the exact type of the struct.
5757

5858

59+
# Fixed Size Structs
60+
A fixed size struct is any struct that has a known fixed time that doesn't depend on the
61+
data it holds. QuickStruct can verify a struct has a fixed size.
62+
```py
63+
# The StructFlags.FixedSize flag is used to verify the struct has a fixed size.
64+
# If the size could not be verified, a SizeError is raised.
65+
class FixedSizeStruct(DataStruct, flags=StructFlags.FixedSize):
66+
a: i32
67+
b: i8
68+
c: f32
69+
d: char
70+
e: String[10] # 10 character string
71+
f: Person[3] # 3 'person' objects
72+
# g: Array[i32] <- not a fixed size field. this will error
73+
```
74+
75+
# Struct Properties
76+
It is possible to query some information about a structure.
77+
```py
78+
from quickstruct import *
79+
class Fixed(DataStruct):
80+
s: String[10]
81+
x: i32
82+
83+
class Dynamic(DataStruct):
84+
s: String
85+
x: i32
86+
87+
print("Fixed.size:", Fixed.size) # 16 (padding automatically added)
88+
print("Dynamic.size:", Dynamic.size) # -1 (dynamic size)
89+
90+
print("Fixed.is_fixed_size:", Fixed.is_fixed_size) # True
91+
print("Dynamic.is_fixed_size:", Fixed.is_fixed_size) # False
92+
93+
print("Fixed.is_dynamic_size:", Fixed.is_dynamic_size) # False
94+
print("Dynamic.is_dynamic_size:", Fixed.is_dynamic_size) # True
95+
96+
print("Fixed.fields:", Fixed.fields) # [s: String[10], __pad_0__: Padding(2), x: i32]
97+
print("Dynamic.fields:", Dynamic.fields) # [s: String, x: i32]
98+
99+
print("Fixed.aligment:", Fixed.aligment) # 16.
100+
print("Dynamic.aligment:", Dynamic.aligment) # -1 (no alignment because dynamic struct can't be aligned).
101+
102+
print("Fixed.is_final:", Fixed.is_final) # False
103+
print("Dynamic.is_final:", Fixed.is_final) # False
104+
105+
print("Fixed.is_protected:", Fixed.is_protected) # False
106+
print("Dynamic.is_protected:", Fixed.is_protected) # False
107+
```
108+
59109
# Alignment
60110
It is also possible to add padding to the struct. There are 2 ways to do that:
61111
## Manual Alignment
@@ -82,22 +132,24 @@ class AlignedStruct(DataStruct, flags = StructFlags.Align2Bytes):
82132
```
83133

84134
## Struct Flags
85-
| Flag | Description |
86-
|-------------------|-----------------------------------------------------------------------------------------------------------------------|
87-
| NoAlignment | This is the most packed form of the struct. All fields are adjacent with no padding (unless manually added) |
88-
| Packed | Same as `NoAlignment` except that `NoAlignment` is a bit more optimized because no alignment is done. |
89-
| Align1Byte | Same as `Packed` |
90-
| Align2Bytes | Aligns the fields on 2 byte boundary. |
91-
| Align4Bytes | Aligns the fields on 4 byte boundary. |
92-
| Align8Bytes | Aligns the fields on 8 byte boundary. |
93-
| AlignAuto | Aligns the fields by their type. |
94-
| ReorderFields | Specifies the fields should be reordered in order to make the struct a little more compressed. |
95-
| ForceDataOnly | Specifies that the struct may only contain serializable fields. Data-only structs may only inherit data-only structs. |
96-
| AllowOverride | If set, fields defined in the struct may override fields that are defined in the base struct. |
97-
| ForceSafeOverride | If set, when fields are overridden, they must have the same type (which would make it pretty useless to override). |
98-
| ForceFixedSize | If set, the struct must have a fixed size. If not, an exception is raised. |
99-
| AllowInline | If set, the struct's fields will be inlined into another struct the contains this struct. |
100-
| Final | Marks the structure so it won't be inheritable by any other class. |
101-
| LockedStructure | If set, denies any overrides of that structure. This flag is not yet implemented. |
102-
103-
135+
| Flag | Description |
136+
|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
137+
| Default | The default to use if no flags are given. Same as `AllowOverride \| AlignAuto`. |
138+
| NoAlignment | This is the most packed form of the struct. All fields are adjacent with no padding (unless manually added) |
139+
| Packed | Same as `NoAlignment` except that `NoAlignment` is a bit more optimized because no alignment is done. |
140+
| Align1Byte | Same as `Packed` |
141+
| Align2Bytes | Aligns the fields on 2 byte boundary. |
142+
| Align4Bytes | Aligns the fields on 4 byte boundary. |
143+
| Align8Bytes | Aligns the fields on 8 byte boundary. |
144+
| AlignAuto | Aligns the fields by their type. |
145+
| ReorderFields | Specifies the fields should be reordered in order to make the struct a little more compressed. |
146+
| ForceDataOnly | **Deprecated**. Specifies that the struct may only contain serializable fields. Data-only structs may only inherit data-only structs. |
147+
| AllowOverride | If set, fields defined in the struct may override fields that are defined in the base struct. |
148+
| TypeSafeOverride | If set, when fields are overridden, they must have the same type (which would make it pretty useless to override). Implies `AllowOverride`. If fields have a different type, an `UnsafeOverrideError` is raised. |
149+
| ForceSafeOverride | **Deprectaed**. Same as `TypeSafeOverride`. |
150+
| FixedSize | If set, the struct must have a fixed size. If not, an exception `SizeError` is raised. |
151+
| ForceFixedSize | **Deprecated**. Same as `FixedSize`. |
152+
| AllowInline | **Deprecated**. If set, the struct's fields will be inlined into another struct the contains this struct. |
153+
| Protected | If set, denies any overrides of that structure. If a struct is trying to override a field of it, an `UnoverridableFieldError` is raised. |
154+
| LockedStructure | **Deprecated**. Same as `Protected`. |
155+
| Final | Marks the structure so it won't be inheritable by any other class. If a struct is trying to inherit it, an `InheritanceError` is raised. |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "quickstruct"
3-
version = "0.1.4"
3+
version = "0.2.0"
44
description = "A small library to ease the creation, usage, serialization and deserialization of C structs."
55
authors = ["Binyamin Y Cohen <binyamincohen555@gmail.com>"]
66
repository = "https://github.com/xpodev/quickstruct"

quickstruct/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .quickstruct import *
33
from .struct_builder import StructFlags
44

5-
__version__ = '0.1.4'
5+
__version__ = '0.2.0'
66

77

88
__all__ = [
@@ -12,6 +12,8 @@
1212
'DataStruct',
1313

1414
'String',
15+
'Array',
16+
1517
'i8',
1618
'i16',
1719
'i32',
@@ -26,5 +28,7 @@
2628

2729
'ptr',
2830
'ref',
29-
'anyptr'
31+
'anyptr',
32+
33+
'Padding'
3034
]

quickstruct/common.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,44 @@ def __repr__(self) -> str:
1717
def __instancecheck__(cls, value) -> bool:
1818
return cls.__is_instance__(value)
1919

20+
@property
21+
def is_dynamic_size(self) -> bool:
22+
return self._is_dynamic_size()
23+
24+
@property
25+
def alignment(self) -> int:
26+
return self._alignment()
27+
28+
@property
29+
def size(self) -> int:
30+
return self._size()
31+
2032

2133
class Type(typing.Generic[U], metaclass=TypeMeta):
2234
def __class_getitem__(cls: typing.Type[U], item) -> "typing.Type[Array[U]]":
2335
if isinstance(item, int):
2436
if issubclass(cls, Padding):
25-
return type(f"Padding({item})", (cls,), {
37+
return type(f"Padding[{item}]", (cls,), {
2638
"__struct__": Struct(f"{item}x"),
2739
})
2840
return _array(cls, item)
2941
return super().__class_getitem__(item)
3042

3143
@classmethod
32-
def is_dynamic_size(cls) -> bool:
33-
return cls.size() == -1
44+
def _is_dynamic_size(cls) -> bool:
45+
return cls.size == -1
3446

3547
@classmethod
36-
@abstractmethod
37-
def alignment(cls) -> int:
48+
# @abstractmethod
49+
def _alignment(cls) -> int:
3850
"""Returns the alignment of the type."""
39-
pass
51+
raise NotImplementedError
4052

4153
@classmethod
42-
@abstractmethod
43-
def size(cls) -> int:
54+
# @abstractmethod
55+
def _size(cls) -> int:
4456
"""The size of the type in bytes. If the type doesn't have a fixed size, -1 is returned."""
45-
raise NotImplementedError()
57+
raise NotImplementedError
4658

4759
@classmethod
4860
@abstractmethod
@@ -60,6 +72,7 @@ def __is_instance__(cls, instance) -> bool:
6072
"""Checks if the given instance is an instance of this type."""
6173
return type.__instancecheck__(cls, instance)
6274

75+
6376
T = typing.TypeVar('T', covariant=True, bound=Type)
6477

6578

@@ -69,11 +82,11 @@ class Primitive(Type, typing.Generic[T]):
6982
__alignment__: int
7083

7184
@classmethod
72-
def size(cls) -> int:
85+
def _size(cls) -> int:
7386
return cls.__struct__.size
7487

7588
@classmethod
76-
def alignment(cls) -> int:
89+
def _alignment(cls) -> int:
7790
return cls.__alignment__
7891

7992
@classmethod
@@ -106,19 +119,19 @@ def to_bytes(cls, _) -> bytes:
106119
def from_bytes(cls, data: typing.Union[bytes, BytesIO]) -> T:
107120
if isinstance(data, bytes):
108121
data = BytesIO(data)
109-
return cls.__struct__.unpack(data.read(cls.__struct__.size))[0]
122+
cls.__struct__.unpack(data.read(cls.__struct__.size))
110123

111124

112125
class String(Type):
113126
__length__: int = None
114127
__alignment__: int = 1
115128

116129
@classmethod
117-
def size(cls) -> int:
130+
def _size(cls) -> int:
118131
return cls.__length__ if cls.__length__ else -1
119132

120133
@classmethod
121-
def alignment(cls) -> int:
134+
def _alignment(cls) -> int:
122135
return cls.__alignment__
123136

124137
@classmethod
@@ -156,16 +169,16 @@ def __class_str__(cls) -> str:
156169

157170

158171
class Array(Type, typing.Generic[T]):
159-
__element_type__: T
172+
__element_type__: typing.Type[T]
160173
__length__: int = None
161174

162175
@classmethod
163-
def size(cls) -> int:
164-
return cls.__length__ if cls.__length__ else -1
176+
def _size(cls) -> int:
177+
return cls.__length__ * cls.__element_type__.size if cls.__length__ else -1
165178

166179
@classmethod
167-
def alignment(cls) -> int:
168-
return cls.__element_type__.alignment()
180+
def _alignment(cls) -> int:
181+
return cls.__element_type__.alignment
169182

170183
@classmethod
171184
def to_bytes(cls, values: typing.Iterable[T]) -> bytes:
@@ -286,8 +299,7 @@ def __getitem__(self, item):
286299

287300

288301
__all__ = [
289-
"Type", "Primitive", "String", "Array", "Pointer", "Reference",
302+
"Type", "Primitive", "String", "Array", "Pointer", "Reference", "Padding", "Array",
290303
"i8", "u8", "i16", "u16", "i32", "u32", "i64", "u64", "f32", "f64", "anyptr", "char",
291-
"ptr", "ref",
292-
"T"
304+
"ptr", "ref"
293305
]

quickstruct/error.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class QuickStructError(Exception):
2+
"""Base class for all QuickStruct errors."""
3+
pass
4+
5+
6+
class FieldError(QuickStructError):
7+
"""Base class for all field errors."""
8+
pass
9+
10+
11+
class OverrideError(FieldError):
12+
"""Raised when a field is override is not allowed."""
13+
pass
14+
15+
16+
class UnoverridbaleFieldError(FieldError):
17+
"""Raised when a field is overriding a locked field."""
18+
pass
19+
20+
21+
class UnsafeOverrideError(FieldError):
22+
"""Raised when a field is overriding a field with a different type when the struct is marked as SafeOverride."""
23+
pass
24+
25+
26+
class InheritanceError(QuickStructError):
27+
"""Raised when an invalid inheritance is detected."""
28+
pass
29+
30+
31+
class SizeError(QuickStructError):
32+
"""Raised when an invalid size is detected."""
33+
pass

quickstruct/field.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from enum import IntFlag
2+
import typing
3+
4+
from .common import Type
5+
6+
7+
class FieldFlags(IntFlag):
8+
NONE = 0
9+
"""No flags set."""
10+
Protected = 1 << 0
11+
"""If set, the field can't be overridden by a derived struct."""
12+
13+
14+
class StructField:
15+
_type: typing.Type[Type]
16+
_name: str
17+
18+
def __init__(self, typ: typing.Type[Type]) -> None:
19+
self._type = typ
20+
21+
def __set_name__(self, _, name):
22+
self._name = f"__field_{name}__"
23+
24+
def __get__(self, instance, _):
25+
if instance is None:
26+
return self
27+
return getattr(instance, self._name)
28+
29+
def __set__(self, instance, value):
30+
if not isinstance(value, self._type):
31+
raise TypeError(f"Expected {self._type}, got {type(value)}")
32+
setattr(instance, self._name, value)
33+
34+
35+
class StructPaddingField:
36+
_name: str
37+
38+
def __set_name__(self, _, name):
39+
self._name = f"__field_{name}__"
40+
41+
def __get__(self, instance, _):
42+
if instance is None:
43+
return self
44+
return None
45+
46+
47+
class FieldInfo:
48+
_name: str
49+
_type: Type
50+
_offset: int
51+
_flags: FieldFlags
52+
53+
def __init__(self, name: str, typ: Type, flags: FieldFlags = FieldFlags.NONE) -> None:
54+
if name is None:
55+
raise TypeError("Field name must not be None")
56+
if typ is None:
57+
raise TypeError("Field type must not be None")
58+
self._name = name
59+
self._type = typ
60+
self._offset = 0
61+
self._flags = flags
62+
63+
@property
64+
def name(self) -> str:
65+
return self._name
66+
67+
@property
68+
def type(self) -> Type:
69+
return self._type
70+
71+
@type.setter
72+
def type(self, typ: Type) -> None:
73+
self._type = typ
74+
75+
@property
76+
def offset(self) -> int:
77+
return self._offset
78+
79+
@offset.setter
80+
def offset(self, value: int) -> None:
81+
self._offset = value
82+
83+
@property
84+
def flags(self) -> FieldFlags:
85+
return self._flags
86+
87+
@flags.setter
88+
def flags(self, value: FieldFlags) -> None:
89+
self._flags = value
90+
91+
@property
92+
def is_protected(self) -> bool:
93+
return self._flags & FieldFlags.Protected
94+
95+
def __repr__(self) -> str:
96+
return f"{self._name}: {self._type}"

0 commit comments

Comments
 (0)