Skip to content

Commit

Permalink
Access to Python class attributes in struct definition
Browse files Browse the repository at this point in the history
  • Loading branch information
andreax79 committed Jan 16, 2025
1 parent 1236e1e commit 5bca4a1
Show file tree
Hide file tree
Showing 15 changed files with 321 additions and 49 deletions.
9 changes: 9 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# .readthedocs.yaml
version: 2

# Set the OS and Python version
build:
os: ubuntu-22.04
tools:
python: "3.12"

mkdocs:
configuration: mkdocs.yml

# Declare the Python requirements required to build the documentation
python:
install:
- requirements: requirements-dev.txt
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,33 @@ pkg.length = 4
pkg.data = [10, 20, 30, 40]
```

### Python object attributes

In struct definition, you can access Python object attributes using `self`.
The value of expression accessing class attributes is evaluated at runtime.

```python
class RT11DirectoryEntry(cstruct.CStruct):

__byte_order__ = cstruct.LITTLE_ENDIAN
__def__ = """
struct RT11DirectoryEntry {
uint8_t type;
uint8_t clazz;
uint16_t raw_filename1;
uint16_t raw_filename2;
uint16_t raw_extension;
uint16_t length;
uint8_t job;
uint8_t channel;
uint16_t raw_creation_date;
uint16_t extra_bytes[self.extra_bytes_len]; /* The size of the array is determined at runtime */
};
"""

extra_bytes_len: int = 0
```

### Pack and Unpack

A code example illustrating how to use
Expand Down
10 changes: 10 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,13 @@
### Improved

- Python 3.12 support

## [6.0] - 2025-01-16

### Added

- access to Python class attributes in struct definition

### Improved

- Python 3.13 support
4 changes: 2 additions & 2 deletions cstruct/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand All @@ -24,7 +24,7 @@

__author__ = "Andrea Bonomi <[email protected]>"
__license__ = "MIT"
__version__ = "5.3"
__version__ = "6.0"
__date__ = "15 August 2013"

from typing import Any, Dict, Optional, Type, Union
Expand Down
20 changes: 16 additions & 4 deletions cstruct/abstract.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand Down Expand Up @@ -163,7 +161,7 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non
flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0]
if flexible_array is None:
raise CStructException("Flexible array not found in struct")
flexible_array.vlen = flexible_array_length
flexible_array.vlen_ex = flexible_array_length

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

def pack_into(self, buffer: bytearray, offset: int = 0) -> None:
"""
Pack the structure data into a buffer
Args:
buffer: target buffer (must be large enough to contain the packed structure)
offset: optional buffer offset
"""
tmp = self.pack()
buffer[offset : offset + len(tmp)] = tmp

def clear(self) -> None:
self.unpack(None)

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


class CEnumMeta(EnumMeta):
__size__: int
__native_format__: str

class WrapperDict(_EnumDict):
def __setitem__(self, key: str, value: Any) -> None:
env = None
Expand Down
2 changes: 1 addition & 1 deletion cstruct/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand Down
50 changes: 47 additions & 3 deletions cstruct/c_expr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand All @@ -23,11 +23,12 @@
#

import ast
import inspect
import operator
from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union

from .base import DEFINES, STRUCTS
from .exceptions import EvalError
from .exceptions import ContextNotFound, EvalError

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


def eval_attribute_node(node: ast.Attribute) -> Union[int, float]:
"""
Evaluate node attribute, e.g. 'self.x'
Only 'self' is allowed. The attribute must be a number.
Args:
node: attribute node
Returns:
result: the attribute value
Raises:
EvalError: expression result is not a number, or not self attribute
ContextNotFound: context is not defined
"""
if not node.value or node.value.id != "self": # type: ignore
raise EvalError("only self is allowed")
context = get_cstruct_context()
if context is None:
raise ContextNotFound("context is not defined")
result = getattr(context, node.attr)
if not isinstance(result, (int, float)):
raise EvalError("expression result is not a number")
return result


def eval_node(node: ast.stmt) -> Union[int, float]:
if isinstance(node, ast.Attribute):
return eval_attribute_node(node)

handler = OPS[type(node)]
result = handler(node)
if isinstance(result, bool): # convert bool to int
Expand Down Expand Up @@ -116,6 +146,20 @@ def eval_call(node) -> Union[int, float]:
raise KeyError(node.func.id)


def get_cstruct_context() -> Optional["AbstractCStruct"]:
"""
Get the calling CStruct instance from the stack (if any)
"""
from .abstract import AbstractCStruct

stack = inspect.stack()
for frame in stack:
caller_self = frame.frame.f_locals.get("self")
if isinstance(caller_self, AbstractCStruct):
return caller_self
return None


try:
Constant = ast.Constant
except AttributeError: # python < 3.8
Expand Down
45 changes: 31 additions & 14 deletions cstruct/c_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand All @@ -24,11 +24,21 @@

import re
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
Union,
)

from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS
from .c_expr import c_eval
from .exceptions import CStructException, ParserError
from .exceptions import CStructException, EvalError, ParserError
from .field import FieldType, Kind, calculate_padding
from .native_types import get_native_type

Expand All @@ -41,7 +51,7 @@
SPACES = [" ", "\t", "\n"]


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

def tokenize(self, text) -> List[str]:
def tokenize(self, text: str) -> List[str]:
tokens: List[str] = []
t: List[str] = []
for c in text:
Expand All @@ -72,7 +82,7 @@ def tokenize(self, text) -> List[str]:
else:
t.append(c)
if t:
tokens.append(t.getvalue())
tokens.append("".join(t))
return tokens

def pop(self) -> str:
Expand Down Expand Up @@ -101,7 +111,8 @@ def __str__(self) -> str:
return str(self.tokens)


def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: bool) -> Tuple[str, int, bool]:
def parse_length(tokens: Tokens, next_token: str, flexible_array: bool) -> Tuple[str, Union[int, Callable[[], int]], bool]:
# Extract t_vlen
t = next_token.split("[")
if len(t) != 2:
raise ParserError(f"Error parsing: `{next_token}`")
Expand All @@ -114,14 +125,19 @@ def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: boo
t_vlen = vlen_part.split("]")[0].strip()
vlen_expr.append(vlen_part.split("]")[0].strip())
t_vlen = " ".join(vlen_expr)
# Evaluate t_vlen
vlen: Union[int, Callable[[], int]]
if not t_vlen:
# If the length expression is empty, this is a flex array
flexible_array = True
vlen = 0
else:
# Evaluate the length expression
# If the length expression is not a constant, it is evaluated at runtime
try:
vlen = c_eval(t_vlen)
except (ValueError, TypeError):
vlen = int(t_vlen)
vlen = int(c_eval(t_vlen))
except EvalError:
vlen = lambda: int(c_eval(t_vlen))
return next_token, vlen, flexible_array


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

vlen = 1
vlen: Union[int, Callable[[], int]] = 1
flexible_array = False

if not c_type.endswith("{"):
Expand All @@ -148,20 +164,21 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt
c_type = "void *"
# parse length
if "[" in next_token:
next_token, vlen, flexible_array = parse_length(tokens, next_token, vlen, flexible_array)
next_token, vlen, flexible_array = parse_length(tokens, next_token, flexible_array)
tokens.push(next_token)
# resolve typedefs
while c_type in TYPEDEFS:
c_type = TYPEDEFS[c_type]

# calculate fmt
ref: Union[None, Type[AbstractCEnum], Type[AbstractCStruct]]
if c_type.startswith("struct ") or c_type.startswith("union "): # struct/union
c_type, tail = c_type.split(" ", 1)
kind = Kind.STRUCT if c_type == "struct" else Kind.UNION
if tokens.get() == "{": # Named nested struct
tokens.push(tail)
tokens.push(c_type)
ref: Union[Type[AbstractCEnum], Type[AbstractCStruct]] = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order)
ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order)
elif tail == "{": # Unnamed nested struct
tokens.push(tail)
tokens.push(c_type)
Expand Down Expand Up @@ -428,7 +445,7 @@ def parse_struct(
raise ParserError(f"Invalid reserved member name `{vname}`")
# parse length
if "[" in vname:
vname, field_type.vlen, field_type.flexible_array = parse_length(tokens, vname, 1, flexible_array)
vname, field_type.vlen_ex, field_type.flexible_array = parse_length(tokens, vname, flexible_array)
flexible_array = flexible_array or field_type.flexible_array
# anonymous nested union
if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__):
Expand Down
2 changes: 1 addition & 1 deletion cstruct/cstruct.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand Down
7 changes: 6 additions & 1 deletion cstruct/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2013-2019 Andrea Bonomi <[email protected]>
# Copyright (c) 2013-2025 Andrea Bonomi <[email protected]>
#
# Published under the terms of the MIT license.
#
Expand Down Expand Up @@ -27,6 +27,7 @@
"CStructException",
"ParserError",
"EvalError",
"ContextNotFound",
]


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

class EvalError(CStructException):
pass


class ContextNotFound(EvalError):
pass
Loading

0 comments on commit 5bca4a1

Please sign in to comment.