Skip to content

Commit 5bca4a1

Browse files
committed
Access to Python class attributes in struct definition
1 parent 1236e1e commit 5bca4a1

15 files changed

+321
-49
lines changed

.readthedocs.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
# .readthedocs.yaml
22
version: 2
3+
4+
# Set the OS and Python version
5+
build:
6+
os: ubuntu-22.04
7+
tools:
8+
python: "3.12"
9+
310
mkdocs:
411
configuration: mkdocs.yml
12+
13+
# Declare the Python requirements required to build the documentation
514
python:
615
install:
716
- requirements: requirements-dev.txt

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,33 @@ pkg.length = 4
197197
pkg.data = [10, 20, 30, 40]
198198
```
199199

200+
### Python object attributes
201+
202+
In struct definition, you can access Python object attributes using `self`.
203+
The value of expression accessing class attributes is evaluated at runtime.
204+
205+
```python
206+
class RT11DirectoryEntry(cstruct.CStruct):
207+
208+
__byte_order__ = cstruct.LITTLE_ENDIAN
209+
__def__ = """
210+
struct RT11DirectoryEntry {
211+
uint8_t type;
212+
uint8_t clazz;
213+
uint16_t raw_filename1;
214+
uint16_t raw_filename2;
215+
uint16_t raw_extension;
216+
uint16_t length;
217+
uint8_t job;
218+
uint8_t channel;
219+
uint16_t raw_creation_date;
220+
uint16_t extra_bytes[self.extra_bytes_len]; /* The size of the array is determined at runtime */
221+
};
222+
"""
223+
224+
extra_bytes_len: int = 0
225+
```
226+
200227
### Pack and Unpack
201228

202229
A code example illustrating how to use

changelog.txt

+10
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,13 @@
182182
### Improved
183183

184184
- Python 3.12 support
185+
186+
## [6.0] - 2025-01-16
187+
188+
### Added
189+
190+
- access to Python class attributes in struct definition
191+
192+
### Improved
193+
194+
- Python 3.13 support

cstruct/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
33
#
44
# Published under the terms of the MIT license.
55
#
@@ -24,7 +24,7 @@
2424

2525
__author__ = "Andrea Bonomi <[email protected]>"
2626
__license__ = "MIT"
27-
__version__ = "5.3"
27+
__version__ = "6.0"
2828
__date__ = "15 August 2013"
2929

3030
from typing import Any, Dict, Optional, Type, Union

cstruct/abstract.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#!/usr/bin/env python
2-
# -*- coding: utf-8 -*-
3-
#
4-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
53
#
64
# Published under the terms of the MIT license.
75
#
@@ -163,7 +161,7 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non
163161
flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0]
164162
if flexible_array is None:
165163
raise CStructException("Flexible array not found in struct")
166-
flexible_array.vlen = flexible_array_length
164+
flexible_array.vlen_ex = flexible_array_length
167165

168166
def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool:
169167
"""
@@ -202,6 +200,17 @@ def pack(self) -> bytes: # pragma: no cover
202200
"""
203201
raise NotImplementedError
204202

203+
def pack_into(self, buffer: bytearray, offset: int = 0) -> None:
204+
"""
205+
Pack the structure data into a buffer
206+
207+
Args:
208+
buffer: target buffer (must be large enough to contain the packed structure)
209+
offset: optional buffer offset
210+
"""
211+
tmp = self.pack()
212+
buffer[offset : offset + len(tmp)] = tmp
213+
205214
def clear(self) -> None:
206215
self.unpack(None)
207216

@@ -300,6 +309,9 @@ def __setstate__(self, state: bytes) -> bool:
300309

301310

302311
class CEnumMeta(EnumMeta):
312+
__size__: int
313+
__native_format__: str
314+
303315
class WrapperDict(_EnumDict):
304316
def __setitem__(self, key: str, value: Any) -> None:
305317
env = None

cstruct/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
33
#
44
# Published under the terms of the MIT license.
55
#

cstruct/c_expr.py

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
33
#
44
# Published under the terms of the MIT license.
55
#
@@ -23,11 +23,12 @@
2323
#
2424

2525
import ast
26+
import inspect
2627
import operator
27-
from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union
28+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union
2829

2930
from .base import DEFINES, STRUCTS
30-
from .exceptions import EvalError
31+
from .exceptions import ContextNotFound, EvalError
3132

3233
if TYPE_CHECKING:
3334
from .abstract import AbstractCStruct
@@ -65,7 +66,36 @@ def c_eval(expr: str) -> Union[int, float]:
6566
raise EvalError
6667

6768

69+
def eval_attribute_node(node: ast.Attribute) -> Union[int, float]:
70+
"""
71+
Evaluate node attribute, e.g. 'self.x'
72+
Only 'self' is allowed. The attribute must be a number.
73+
74+
Args:
75+
node: attribute node
76+
77+
Returns:
78+
result: the attribute value
79+
80+
Raises:
81+
EvalError: expression result is not a number, or not self attribute
82+
ContextNotFound: context is not defined
83+
"""
84+
if not node.value or node.value.id != "self": # type: ignore
85+
raise EvalError("only self is allowed")
86+
context = get_cstruct_context()
87+
if context is None:
88+
raise ContextNotFound("context is not defined")
89+
result = getattr(context, node.attr)
90+
if not isinstance(result, (int, float)):
91+
raise EvalError("expression result is not a number")
92+
return result
93+
94+
6895
def eval_node(node: ast.stmt) -> Union[int, float]:
96+
if isinstance(node, ast.Attribute):
97+
return eval_attribute_node(node)
98+
6999
handler = OPS[type(node)]
70100
result = handler(node)
71101
if isinstance(result, bool): # convert bool to int
@@ -116,6 +146,20 @@ def eval_call(node) -> Union[int, float]:
116146
raise KeyError(node.func.id)
117147

118148

149+
def get_cstruct_context() -> Optional["AbstractCStruct"]:
150+
"""
151+
Get the calling CStruct instance from the stack (if any)
152+
"""
153+
from .abstract import AbstractCStruct
154+
155+
stack = inspect.stack()
156+
for frame in stack:
157+
caller_self = frame.frame.f_locals.get("self")
158+
if isinstance(caller_self, AbstractCStruct):
159+
return caller_self
160+
return None
161+
162+
119163
try:
120164
Constant = ast.Constant
121165
except AttributeError: # python < 3.8

cstruct/c_parser.py

+31-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
33
#
44
# Published under the terms of the MIT license.
55
#
@@ -24,11 +24,21 @@
2424

2525
import re
2626
from collections import OrderedDict
27-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
27+
from typing import (
28+
TYPE_CHECKING,
29+
Any,
30+
Callable,
31+
Dict,
32+
List,
33+
Optional,
34+
Tuple,
35+
Type,
36+
Union,
37+
)
2838

2939
from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS
3040
from .c_expr import c_eval
31-
from .exceptions import CStructException, ParserError
41+
from .exceptions import CStructException, EvalError, ParserError
3242
from .field import FieldType, Kind, calculate_padding
3343
from .native_types import get_native_type
3444

@@ -41,7 +51,7 @@
4151
SPACES = [" ", "\t", "\n"]
4252

4353

44-
class Tokens(object):
54+
class Tokens:
4555
def __init__(self, text: str) -> None:
4656
# remove the comments
4757
text = re.sub(r"//.*?$|/\*.*?\*/", "", text, flags=re.S | re.MULTILINE)
@@ -59,7 +69,7 @@ def __init__(self, text: str) -> None:
5969
text = "\n".join(lines)
6070
self.tokens = self.tokenize(text)
6171

62-
def tokenize(self, text) -> List[str]:
72+
def tokenize(self, text: str) -> List[str]:
6373
tokens: List[str] = []
6474
t: List[str] = []
6575
for c in text:
@@ -72,7 +82,7 @@ def tokenize(self, text) -> List[str]:
7282
else:
7383
t.append(c)
7484
if t:
75-
tokens.append(t.getvalue())
85+
tokens.append("".join(t))
7686
return tokens
7787

7888
def pop(self) -> str:
@@ -101,7 +111,8 @@ def __str__(self) -> str:
101111
return str(self.tokens)
102112

103113

104-
def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: bool) -> Tuple[str, int, bool]:
114+
def parse_length(tokens: Tokens, next_token: str, flexible_array: bool) -> Tuple[str, Union[int, Callable[[], int]], bool]:
115+
# Extract t_vlen
105116
t = next_token.split("[")
106117
if len(t) != 2:
107118
raise ParserError(f"Error parsing: `{next_token}`")
@@ -114,14 +125,19 @@ def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: boo
114125
t_vlen = vlen_part.split("]")[0].strip()
115126
vlen_expr.append(vlen_part.split("]")[0].strip())
116127
t_vlen = " ".join(vlen_expr)
128+
# Evaluate t_vlen
129+
vlen: Union[int, Callable[[], int]]
117130
if not t_vlen:
131+
# If the length expression is empty, this is a flex array
118132
flexible_array = True
119133
vlen = 0
120134
else:
135+
# Evaluate the length expression
136+
# If the length expression is not a constant, it is evaluated at runtime
121137
try:
122-
vlen = c_eval(t_vlen)
123-
except (ValueError, TypeError):
124-
vlen = int(t_vlen)
138+
vlen = int(c_eval(t_vlen))
139+
except EvalError:
140+
vlen = lambda: int(c_eval(t_vlen))
125141
return next_token, vlen, flexible_array
126142

127143

@@ -133,7 +149,7 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt
133149
if c_type in ["signed", "unsigned", "struct", "union", "enum"] and len(tokens) > 1:
134150
c_type = c_type + " " + tokens.pop()
135151

136-
vlen = 1
152+
vlen: Union[int, Callable[[], int]] = 1
137153
flexible_array = False
138154

139155
if not c_type.endswith("{"):
@@ -148,20 +164,21 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt
148164
c_type = "void *"
149165
# parse length
150166
if "[" in next_token:
151-
next_token, vlen, flexible_array = parse_length(tokens, next_token, vlen, flexible_array)
167+
next_token, vlen, flexible_array = parse_length(tokens, next_token, flexible_array)
152168
tokens.push(next_token)
153169
# resolve typedefs
154170
while c_type in TYPEDEFS:
155171
c_type = TYPEDEFS[c_type]
156172

157173
# calculate fmt
174+
ref: Union[None, Type[AbstractCEnum], Type[AbstractCStruct]]
158175
if c_type.startswith("struct ") or c_type.startswith("union "): # struct/union
159176
c_type, tail = c_type.split(" ", 1)
160177
kind = Kind.STRUCT if c_type == "struct" else Kind.UNION
161178
if tokens.get() == "{": # Named nested struct
162179
tokens.push(tail)
163180
tokens.push(c_type)
164-
ref: Union[Type[AbstractCEnum], Type[AbstractCStruct]] = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order)
181+
ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order)
165182
elif tail == "{": # Unnamed nested struct
166183
tokens.push(tail)
167184
tokens.push(c_type)
@@ -428,7 +445,7 @@ def parse_struct(
428445
raise ParserError(f"Invalid reserved member name `{vname}`")
429446
# parse length
430447
if "[" in vname:
431-
vname, field_type.vlen, field_type.flexible_array = parse_length(tokens, vname, 1, flexible_array)
448+
vname, field_type.vlen_ex, field_type.flexible_array = parse_length(tokens, vname, flexible_array)
432449
flexible_array = flexible_array or field_type.flexible_array
433450
# anonymous nested union
434451
if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__):

cstruct/cstruct.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
33
#
44
# Published under the terms of the MIT license.
55
#

cstruct/exceptions.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
2+
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
33
#
44
# Published under the terms of the MIT license.
55
#
@@ -27,6 +27,7 @@
2727
"CStructException",
2828
"ParserError",
2929
"EvalError",
30+
"ContextNotFound",
3031
]
3132

3233

@@ -44,3 +45,7 @@ class ParserError(CStructException):
4445

4546
class EvalError(CStructException):
4647
pass
48+
49+
50+
class ContextNotFound(EvalError):
51+
pass

0 commit comments

Comments
 (0)