From 7905b449ca4c0e1702c015212568266b5dbf4a8c Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Wed, 8 Oct 2025 17:55:35 -0400 Subject: [PATCH 1/7] Allow more general callables instead of strictly lambdas --- src/smbprotocol/structure.py | 37 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/smbprotocol/structure.py b/src/smbprotocol/structure.py index 9646ce6..bd8b320 100644 --- a/src/smbprotocol/structure.py +++ b/src/smbprotocol/structure.py @@ -3,6 +3,7 @@ import copy import datetime +import inspect import math import struct import textwrap @@ -148,7 +149,7 @@ def __init__(self, little_endian=True, default=None, size=None): field_type = self.__class__.__name__ self.little_endian = little_endian - if not (size is None or isinstance(size, int) or isinstance(size, types.LambdaType)): + if not (size is None or isinstance(size, int) or callable(size)): raise InvalidFieldDefinition(f"{field_type} size for field must be an int or None for a variable length") self.size = size self.default = default @@ -270,7 +271,7 @@ def _get_calculated_value(self, value): :param value: The value to calculate/expand :return: The final value """ - if isinstance(value, types.LambdaType): + if callable(value): expanded_value = value(self.structure) return self._get_calculated_value(expanded_value) else: @@ -294,7 +295,7 @@ def _get_calculated_size(self, size, data): # is None (last field value) if size is None: return len(data) - elif isinstance(size, types.LambdaType): + elif callable(size): expanded_size = size(self.structure) return self._get_calculated_size(expanded_size, data) else: @@ -309,7 +310,7 @@ def _get_struct_format(self, size, unsigned=True): :param size: The size as an int :return: The struct format specifier for the size specified """ - if isinstance(size, types.LambdaType): + if callable(size): size = size(self.structure) struct_format = {1: "B", 2: "H", 4: "L", 8: "Q"} @@ -345,7 +346,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: int_value = 0 - elif isinstance(value, types.LambdaType): + elif callable(value): int_value = value elif isinstance(value, bytes): struct_string = self._endian_prefix + self._get_struct_format(self.size, self.unsigned) @@ -376,7 +377,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: bytes_value = b"" - elif isinstance(value, types.LambdaType): + elif callable(value): bytes_value = value elif isinstance(value, int): struct_string = self._endian_prefix + self._get_struct_format(self.size) @@ -425,7 +426,7 @@ def __init__(self, list_count=None, list_type=BytesField(), unpack_func=None, ** used when the list contains variable length values. :param kwargs: Any other kwarg to be sent to Field() """ - if list_count is not None and not (isinstance(list_count, int) or isinstance(list_count, types.LambdaType)): + if list_count is not None and not (isinstance(list_count, int) or callable(list_count)): raise InvalidFieldDefinition( "ListField list_count must be an int, lambda, or None for a variable list length" ) @@ -435,7 +436,7 @@ def __init__(self, list_count=None, list_type=BytesField(), unpack_func=None, ** raise InvalidFieldDefinition("ListField list_type must be a Field definition") self.list_type = list_type - if unpack_func is not None and not isinstance(unpack_func, types.LambdaType): + if unpack_func is not None and not callable(unpack_func): raise InvalidFieldDefinition("ListField unpack_func must be a lambda function or None") elif unpack_func is None and (list_count is None or list_type.size is None): raise InvalidFieldDefinition( @@ -456,7 +457,7 @@ def get_value(self): # Override default get_value() so we return a list with the actual # value, not the Field definition list_value = [] - if isinstance(self.value, types.LambdaType): + if callable(self.value): value = self._get_calculated_value(self.value) else: value = self.value @@ -474,9 +475,9 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: list_value = [] - elif isinstance(value, types.LambdaType): + elif callable(value): return value - elif isinstance(value, bytes) and isinstance(self.unpack_func, types.LambdaType): + elif isinstance(value, bytes) and callable(self.unpack_func): # use the lambda function to parse the bytes to a list list_value = self.unpack_func(self.structure, value) elif isinstance(value, bytes): @@ -527,7 +528,7 @@ def _to_string(self): def _create_list_from_bytes(self, list_count, list_type, value): # calculate the list_count and rerun method if a lambda - if isinstance(list_count, types.LambdaType): + if callable(list_count): list_count = list_count(self.structure) return self._create_list_from_bytes(list_count, list_type, value) @@ -576,7 +577,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: structure_value = b"" - elif isinstance(value, types.LambdaType): + elif callable(value): structure_value = value elif isinstance(value, bytes): structure_value = value @@ -586,7 +587,7 @@ def _parse_value(self, value): raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a structure") if isinstance(structure_value, bytes) and self.structure_type and structure_value != b"": - if isinstance(self.structure_type, types.LambdaType): + if callable(self.structure_type) and not inspect.isclass(self.structure_type): structure_type = self.structure_type(self.structure) else: structure_type = self.structure_type @@ -650,7 +651,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: datetime_value = datetime.datetime.today() - elif isinstance(value, types.LambdaType): + elif callable(value): datetime_value = value elif isinstance(value, bytes): struct_string = self._endian_prefix + self._get_struct_format(8) @@ -713,7 +714,7 @@ def _parse_value(self, value): uuid_value = uuid.UUID(int=value) elif isinstance(value, uuid.UUID): uuid_value = value - elif isinstance(value, types.LambdaType): + elif callable(value): uuid_value = value else: raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a uuid") @@ -823,7 +824,7 @@ def _parse_value(self, value): bool_value = value elif isinstance(value, bytes): bool_value = value == b"\x01" - elif isinstance(value, types.LambdaType): + elif callable(value): bool_value = value else: raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a bool") @@ -854,7 +855,7 @@ def _parse_value(self, value): text_value = to_text(value, encoding=self.encoding) elif isinstance(value, str): text_value = value - elif isinstance(value, types.LambdaType): + elif callable(value): text_value = value else: raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a text string") From 87fa2ebf7e23b460f9ff0bd22bb5f49ab920f5fa Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Wed, 8 Oct 2025 18:03:38 -0400 Subject: [PATCH 2/7] Remove retired windows 2019 from CI --- .github/workflows/ci.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68197e3..2fbc41a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,6 @@ jobs: os: - ubuntu-latest - macOS-latest - - windows-2019 - windows-2022 python-version: - 3.8 @@ -72,10 +71,6 @@ jobs: python-arch: x86 - os: ubuntu-latest python-arch: arm64 - - os: windows-2019 - python-arch: x86 - - os: windows-2019 - python-arch: arm64 - os: windows-2022 python-arch: arm64 - os: macOS-latest @@ -86,16 +81,6 @@ jobs: # macOS arm64 is supported from 3.9 - os: macOS-latest python-version: 3.8 - - os: windows-2019 - python-version: 3.8 - - os: windows-2019 - python-version: 3.9 - - os: windows-2019 - python-version: '3.10' - - os: windows-2019 - python-version: '3.11' - - os: windows-2019 - python-version: '3.12' steps: - uses: actions/checkout@v4 From 404d4a327062be2b57355167e39845a6cc63d184 Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Thu, 9 Oct 2025 11:05:32 -0400 Subject: [PATCH 3/7] Add non-lambda callable tests --- tests/test_structure.py | 264 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index b723478..8f1d081 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -378,6 +378,37 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 4 + def test_set_function(self): + def field_resolver(s): + return 8765 + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = 8765 + actual = field.get_value() + assert isinstance(field.value, types.FunctionType) + assert actual == expected + assert len(field) == 4 + + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return 5678 + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = 5678 + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 4 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x12\x34\x00\x00") @@ -401,6 +432,22 @@ def test_set_invalid(self): field.set_value([]) assert str(exc.value) == "Cannot parse value for field field of type list to an int" + def test_builtin_default(self): + class LengthStructure(Structure): + def __init__(self): + self.fields = OrderedDict([ + ("field1", IntField(size=4, default=b"\x01\x03\x05\x07")), + ("field2", IntField(size=2, default=len)), + ]) + super().__init__() + + structure = LengthStructure() + field = structure["field2"] + expected = b"\x06\x00" + actual = field.pack() + assert actual == expected + assert structure.pack() == b"\x01\x03\x05\x07\x06\x00" + def test_byte_order(self): class ByteOrderStructure(Structure): def __init__(self): @@ -470,6 +517,37 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 4 + def test_set_function(self): + def field_resolver(s): + return b"\x11\x12\x13\x14" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = b"\x11\x12\x13\x14" + actual = field.get_value() + assert isinstance(field.value, types.FunctionType) + assert actual == expected + assert len(field) == 4 + + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return b"\x16\x17\x18\x19" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = b"\x16\x17\x18\x19" + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 4 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x78\x00\x77\x00") @@ -486,6 +564,23 @@ def test_set_int(self): assert isinstance(field.value, bytes) assert actual == expected + def test_function_default(self): + def field_resolver(s): + return b"\x0d\x0e" + + class TestStructure(Structure): + def __init__(self): + self.fields = OrderedDict([ + ("field", BytesField(size=2, default=field_resolver)), + ]) + super().__init__() + + structure = TestStructure() + field = structure["field"] + expected = b"\x0d\x0e" + actual = field.pack() + assert actual == expected + def test_set_structure(self): field = self.StructureTest()["field"] field.size = 8 @@ -609,6 +704,25 @@ def __init__(self): assert len(field) == 7 assert actual == expected + def test_class_func(self): + class Unpacker: + def __new__(cls, s, d): + return [b"\x01\x02", b"\x03\x04\x05\x06", b"\07"] + + class UnpackListStructure(Structure): + def __init__(self): + self.fields = OrderedDict( + [("field", ListField(size=7, unpack_func=Unpacker))] + ) + super().__init__() + + field = UnpackListStructure()["field"] + field.unpack(b"\x00") + expected = [b"\x01\x02", b"\x03\x04\x05\x06", b"\07"] + actual = field.get_value() + assert len(field) == 7 + assert actual == expected + def test_set_none(self): field = self.StructureTest()["field"] field.set_value(None) @@ -631,6 +745,21 @@ def test_set_lambda_as_bytes(self): assert actual == expected assert len(field) == 4 + def test_set_function_as_bytes(self): + def field_resolver(s): + return b"\x10\x11\x12\x13" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = [b"\x10\x11", b"\x12\x13"] + actual = field.get_value() + assert isinstance(field.value, types.LambdaType) + assert actual == expected + assert len(field) == 4 + def test_set_lambda_as_list(self): structure = self.StructureTest() field = structure["field"] @@ -643,6 +772,21 @@ def test_set_lambda_as_list(self): assert actual == expected assert len(field) == 4 + def test_set_function_as_list(self): + def field_resolver(s): + return [b"\x11\x12", b"\x13\x14"] + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = [b"\x11\x12", b"\x13\x14"] + actual = field.get_value() + assert isinstance(field.value, types.LambdaType) + assert actual == expected + assert len(field) == 4 + def test_set_bytes_fixed(self): field = self.StructureTest()["field"] field.set_value(b"\x78\x00\x77\x00") @@ -823,6 +967,22 @@ def test_set_lambda(self): assert isinstance(actual, Structure2) assert len(field) == 8 + def test_set_function(self): + def field_resolver(s): + return b"\x10\x07\x04\x01\x03\x05\x07\x09" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = b"\x10\x07\x04\x01\x03\x05\x07\x09" + actual = field.get_value() + assert isinstance(field.value, types.FunctionType) + assert actual.pack() == expected + assert isinstance(actual, Structure2) + assert len(field) == 8 + def test_set_lambda_without_type(self): structure = self.StructureTest() field = structure["field"] @@ -837,6 +997,23 @@ def test_set_lambda_without_type(self): assert isinstance(actual, bytes) assert len(field) == 8 + def test_set_function_without_type(self): + def field_resolver(s): + return b"\x10\x07\x04\x01\x03\x05\x07\x09" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.structure_type = None + field.set_value(field_resolver) + expected = b"\x10\x07\x04\x01\x03\x05\x07\x09" + actual = field.get_value() + assert isinstance(field.value, types.LambdaType) + assert actual == expected + assert isinstance(actual, bytes) + assert len(field) == 8 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x7d\x00\x00\x00\x14\x15\x16\x17") @@ -979,6 +1156,22 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 16 + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return uuid.UUID(bytes=b"\x10" * 16) + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = uuid.UUID(bytes=b"\x10" * 16) + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 16 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x22" * 16) @@ -1150,6 +1343,40 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 8 + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return datetime( + year=2022, + month=5, + day=27, + hour=12, + minute=36, + second=14, + microsecond=313481, + tzinfo=timezone.utc, + ) + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = datetime( + year=2022, + month=5, + day=27, + hour=12, + minute=36, + second=14, + microsecond=313481, + tzinfo=timezone.utc, + ) + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 8 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x00\x67\x7b\x21\x3d\x5d\xd3\x01") @@ -1525,6 +1752,22 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 1 + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return True + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = True + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 1 + def test_set_invalid(self): field = self.StructureTest()["field"] field.name = "field" @@ -1592,6 +1835,25 @@ def test_set_lambda(self): assert actual == expected assert len(field) == actual_length + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return self.STRING_VALUE + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = self.STRING_VALUE + actual = field.get_value() + actual_length = 19 + if field.null_terminated: + actual_length += len("\x00".encode(field.encoding)) + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == actual_length + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(self.STRING_VALUE.encode("utf-8")) @@ -1651,4 +1913,4 @@ def __init__(self): ) ] ) - super().__init__() + super().__init__() \ No newline at end of file From 4098f5639e42de56c80c5edca6b679f4f45e9d17 Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Thu, 9 Oct 2025 11:40:53 -0400 Subject: [PATCH 4/7] Run black on test_structure.py --- tests/test_structure.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index 8f1d081..8375bfb 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -435,10 +435,12 @@ def test_set_invalid(self): def test_builtin_default(self): class LengthStructure(Structure): def __init__(self): - self.fields = OrderedDict([ - ("field1", IntField(size=4, default=b"\x01\x03\x05\x07")), - ("field2", IntField(size=2, default=len)), - ]) + self.fields = OrderedDict( + [ + ("field1", IntField(size=4, default=b"\x01\x03\x05\x07")), + ("field2", IntField(size=2, default=len)), + ] + ) super().__init__() structure = LengthStructure() @@ -570,9 +572,11 @@ def field_resolver(s): class TestStructure(Structure): def __init__(self): - self.fields = OrderedDict([ - ("field", BytesField(size=2, default=field_resolver)), - ]) + self.fields = OrderedDict( + [ + ("field", BytesField(size=2, default=field_resolver)), + ] + ) super().__init__() structure = TestStructure() @@ -711,9 +715,7 @@ def __new__(cls, s, d): class UnpackListStructure(Structure): def __init__(self): - self.fields = OrderedDict( - [("field", ListField(size=7, unpack_func=Unpacker))] - ) + self.fields = OrderedDict([("field", ListField(size=7, unpack_func=Unpacker))]) super().__init__() field = UnpackListStructure()["field"] @@ -1913,4 +1915,4 @@ def __init__(self): ) ] ) - super().__init__() \ No newline at end of file + super().__init__() From 0ddc813a7325df9ec74d4104cf910cff1d6d4d62 Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Wed, 8 Oct 2025 17:55:35 -0400 Subject: [PATCH 5/7] Allow more general callables instead of strictly lambdas --- src/smbprotocol/structure.py | 37 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/smbprotocol/structure.py b/src/smbprotocol/structure.py index 9646ce6..bd8b320 100644 --- a/src/smbprotocol/structure.py +++ b/src/smbprotocol/structure.py @@ -3,6 +3,7 @@ import copy import datetime +import inspect import math import struct import textwrap @@ -148,7 +149,7 @@ def __init__(self, little_endian=True, default=None, size=None): field_type = self.__class__.__name__ self.little_endian = little_endian - if not (size is None or isinstance(size, int) or isinstance(size, types.LambdaType)): + if not (size is None or isinstance(size, int) or callable(size)): raise InvalidFieldDefinition(f"{field_type} size for field must be an int or None for a variable length") self.size = size self.default = default @@ -270,7 +271,7 @@ def _get_calculated_value(self, value): :param value: The value to calculate/expand :return: The final value """ - if isinstance(value, types.LambdaType): + if callable(value): expanded_value = value(self.structure) return self._get_calculated_value(expanded_value) else: @@ -294,7 +295,7 @@ def _get_calculated_size(self, size, data): # is None (last field value) if size is None: return len(data) - elif isinstance(size, types.LambdaType): + elif callable(size): expanded_size = size(self.structure) return self._get_calculated_size(expanded_size, data) else: @@ -309,7 +310,7 @@ def _get_struct_format(self, size, unsigned=True): :param size: The size as an int :return: The struct format specifier for the size specified """ - if isinstance(size, types.LambdaType): + if callable(size): size = size(self.structure) struct_format = {1: "B", 2: "H", 4: "L", 8: "Q"} @@ -345,7 +346,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: int_value = 0 - elif isinstance(value, types.LambdaType): + elif callable(value): int_value = value elif isinstance(value, bytes): struct_string = self._endian_prefix + self._get_struct_format(self.size, self.unsigned) @@ -376,7 +377,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: bytes_value = b"" - elif isinstance(value, types.LambdaType): + elif callable(value): bytes_value = value elif isinstance(value, int): struct_string = self._endian_prefix + self._get_struct_format(self.size) @@ -425,7 +426,7 @@ def __init__(self, list_count=None, list_type=BytesField(), unpack_func=None, ** used when the list contains variable length values. :param kwargs: Any other kwarg to be sent to Field() """ - if list_count is not None and not (isinstance(list_count, int) or isinstance(list_count, types.LambdaType)): + if list_count is not None and not (isinstance(list_count, int) or callable(list_count)): raise InvalidFieldDefinition( "ListField list_count must be an int, lambda, or None for a variable list length" ) @@ -435,7 +436,7 @@ def __init__(self, list_count=None, list_type=BytesField(), unpack_func=None, ** raise InvalidFieldDefinition("ListField list_type must be a Field definition") self.list_type = list_type - if unpack_func is not None and not isinstance(unpack_func, types.LambdaType): + if unpack_func is not None and not callable(unpack_func): raise InvalidFieldDefinition("ListField unpack_func must be a lambda function or None") elif unpack_func is None and (list_count is None or list_type.size is None): raise InvalidFieldDefinition( @@ -456,7 +457,7 @@ def get_value(self): # Override default get_value() so we return a list with the actual # value, not the Field definition list_value = [] - if isinstance(self.value, types.LambdaType): + if callable(self.value): value = self._get_calculated_value(self.value) else: value = self.value @@ -474,9 +475,9 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: list_value = [] - elif isinstance(value, types.LambdaType): + elif callable(value): return value - elif isinstance(value, bytes) and isinstance(self.unpack_func, types.LambdaType): + elif isinstance(value, bytes) and callable(self.unpack_func): # use the lambda function to parse the bytes to a list list_value = self.unpack_func(self.structure, value) elif isinstance(value, bytes): @@ -527,7 +528,7 @@ def _to_string(self): def _create_list_from_bytes(self, list_count, list_type, value): # calculate the list_count and rerun method if a lambda - if isinstance(list_count, types.LambdaType): + if callable(list_count): list_count = list_count(self.structure) return self._create_list_from_bytes(list_count, list_type, value) @@ -576,7 +577,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: structure_value = b"" - elif isinstance(value, types.LambdaType): + elif callable(value): structure_value = value elif isinstance(value, bytes): structure_value = value @@ -586,7 +587,7 @@ def _parse_value(self, value): raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a structure") if isinstance(structure_value, bytes) and self.structure_type and structure_value != b"": - if isinstance(self.structure_type, types.LambdaType): + if callable(self.structure_type) and not inspect.isclass(self.structure_type): structure_type = self.structure_type(self.structure) else: structure_type = self.structure_type @@ -650,7 +651,7 @@ def _pack_value(self, value): def _parse_value(self, value): if value is None: datetime_value = datetime.datetime.today() - elif isinstance(value, types.LambdaType): + elif callable(value): datetime_value = value elif isinstance(value, bytes): struct_string = self._endian_prefix + self._get_struct_format(8) @@ -713,7 +714,7 @@ def _parse_value(self, value): uuid_value = uuid.UUID(int=value) elif isinstance(value, uuid.UUID): uuid_value = value - elif isinstance(value, types.LambdaType): + elif callable(value): uuid_value = value else: raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a uuid") @@ -823,7 +824,7 @@ def _parse_value(self, value): bool_value = value elif isinstance(value, bytes): bool_value = value == b"\x01" - elif isinstance(value, types.LambdaType): + elif callable(value): bool_value = value else: raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a bool") @@ -854,7 +855,7 @@ def _parse_value(self, value): text_value = to_text(value, encoding=self.encoding) elif isinstance(value, str): text_value = value - elif isinstance(value, types.LambdaType): + elif callable(value): text_value = value else: raise TypeError(f"Cannot parse value for field {self.name} of type {type(value).__name__} to a text string") From 4123a92d331520cbd9e19ae60a91ca765bcde519 Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Thu, 9 Oct 2025 11:05:32 -0400 Subject: [PATCH 6/7] Add non-lambda callable tests --- tests/test_structure.py | 264 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index 7d25a83..3fe0110 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -378,6 +378,37 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 4 + def test_set_function(self): + def field_resolver(s): + return 8765 + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = 8765 + actual = field.get_value() + assert isinstance(field.value, types.FunctionType) + assert actual == expected + assert len(field) == 4 + + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return 5678 + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = 5678 + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 4 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x12\x34\x00\x00") @@ -401,6 +432,22 @@ def test_set_invalid(self): field.set_value([]) assert str(exc.value) == "Cannot parse value for field field of type list to an int" + def test_builtin_default(self): + class LengthStructure(Structure): + def __init__(self): + self.fields = OrderedDict([ + ("field1", IntField(size=4, default=b"\x01\x03\x05\x07")), + ("field2", IntField(size=2, default=len)), + ]) + super().__init__() + + structure = LengthStructure() + field = structure["field2"] + expected = b"\x06\x00" + actual = field.pack() + assert actual == expected + assert structure.pack() == b"\x01\x03\x05\x07\x06\x00" + def test_byte_order(self): class ByteOrderStructure(Structure): def __init__(self): @@ -470,6 +517,37 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 4 + def test_set_function(self): + def field_resolver(s): + return b"\x11\x12\x13\x14" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = b"\x11\x12\x13\x14" + actual = field.get_value() + assert isinstance(field.value, types.FunctionType) + assert actual == expected + assert len(field) == 4 + + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return b"\x16\x17\x18\x19" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = b"\x16\x17\x18\x19" + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 4 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x78\x00\x77\x00") @@ -486,6 +564,23 @@ def test_set_int(self): assert isinstance(field.value, bytes) assert actual == expected + def test_function_default(self): + def field_resolver(s): + return b"\x0d\x0e" + + class TestStructure(Structure): + def __init__(self): + self.fields = OrderedDict([ + ("field", BytesField(size=2, default=field_resolver)), + ]) + super().__init__() + + structure = TestStructure() + field = structure["field"] + expected = b"\x0d\x0e" + actual = field.pack() + assert actual == expected + def test_set_structure(self): field = self.StructureTest()["field"] field.size = 8 @@ -609,6 +704,25 @@ def __init__(self): assert len(field) == 7 assert actual == expected + def test_class_func(self): + class Unpacker: + def __new__(cls, s, d): + return [b"\x01\x02", b"\x03\x04\x05\x06", b"\07"] + + class UnpackListStructure(Structure): + def __init__(self): + self.fields = OrderedDict( + [("field", ListField(size=7, unpack_func=Unpacker))] + ) + super().__init__() + + field = UnpackListStructure()["field"] + field.unpack(b"\x00") + expected = [b"\x01\x02", b"\x03\x04\x05\x06", b"\07"] + actual = field.get_value() + assert len(field) == 7 + assert actual == expected + def test_set_none(self): field = self.StructureTest()["field"] field.set_value(None) @@ -631,6 +745,21 @@ def test_set_lambda_as_bytes(self): assert actual == expected assert len(field) == 4 + def test_set_function_as_bytes(self): + def field_resolver(s): + return b"\x10\x11\x12\x13" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = [b"\x10\x11", b"\x12\x13"] + actual = field.get_value() + assert isinstance(field.value, types.LambdaType) + assert actual == expected + assert len(field) == 4 + def test_set_lambda_as_list(self): structure = self.StructureTest() field = structure["field"] @@ -643,6 +772,21 @@ def test_set_lambda_as_list(self): assert actual == expected assert len(field) == 4 + def test_set_function_as_list(self): + def field_resolver(s): + return [b"\x11\x12", b"\x13\x14"] + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = [b"\x11\x12", b"\x13\x14"] + actual = field.get_value() + assert isinstance(field.value, types.LambdaType) + assert actual == expected + assert len(field) == 4 + def test_set_bytes_fixed(self): field = self.StructureTest()["field"] field.set_value(b"\x78\x00\x77\x00") @@ -823,6 +967,22 @@ def test_set_lambda(self): assert isinstance(actual, Structure2) assert len(field) == 8 + def test_set_function(self): + def field_resolver(s): + return b"\x10\x07\x04\x01\x03\x05\x07\x09" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(field_resolver) + expected = b"\x10\x07\x04\x01\x03\x05\x07\x09" + actual = field.get_value() + assert isinstance(field.value, types.FunctionType) + assert actual.pack() == expected + assert isinstance(actual, Structure2) + assert len(field) == 8 + def test_set_lambda_without_type(self): structure = self.StructureTest() field = structure["field"] @@ -837,6 +997,23 @@ def test_set_lambda_without_type(self): assert isinstance(actual, bytes) assert len(field) == 8 + def test_set_function_without_type(self): + def field_resolver(s): + return b"\x10\x07\x04\x01\x03\x05\x07\x09" + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.structure_type = None + field.set_value(field_resolver) + expected = b"\x10\x07\x04\x01\x03\x05\x07\x09" + actual = field.get_value() + assert isinstance(field.value, types.LambdaType) + assert actual == expected + assert isinstance(actual, bytes) + assert len(field) == 8 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x7d\x00\x00\x00\x14\x15\x16\x17") @@ -979,6 +1156,22 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 16 + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return uuid.UUID(bytes=b"\x10" * 16) + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = uuid.UUID(bytes=b"\x10" * 16) + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 16 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x22" * 16) @@ -1150,6 +1343,40 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 8 + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return datetime( + year=2022, + month=5, + day=27, + hour=12, + minute=36, + second=14, + microsecond=313481, + tzinfo=timezone.utc, + ) + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = datetime( + year=2022, + month=5, + day=27, + hour=12, + minute=36, + second=14, + microsecond=313481, + tzinfo=timezone.utc, + ) + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 8 + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(b"\x00\x67\x7b\x21\x3d\x5d\xd3\x01") @@ -1525,6 +1752,22 @@ def test_set_lambda(self): assert actual == expected assert len(field) == 1 + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return True + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = True + actual = field.get_value() + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == 1 + def test_set_invalid(self): field = self.StructureTest()["field"] field.name = "field" @@ -1592,6 +1835,25 @@ def test_set_lambda(self): assert actual == expected assert len(field) == actual_length + def test_set_class(self): + class FieldResolver: + def __new__(cls, s): + return self.STRING_VALUE + + structure = self.StructureTest() + field = structure["field"] + field.name = "field" + field.structure = self.StructureTest + field.set_value(FieldResolver) + expected = self.STRING_VALUE + actual = field.get_value() + actual_length = 19 + if field.null_terminated: + actual_length += len("\x00".encode(field.encoding)) + assert isinstance(field.value, type) + assert actual == expected + assert len(field) == actual_length + def test_set_bytes(self): field = self.StructureTest()["field"] field.set_value(self.STRING_VALUE.encode("utf-8")) @@ -1651,4 +1913,4 @@ def __init__(self): ) ] ) - super().__init__() + super().__init__() \ No newline at end of file From 2d9ceb7758b2aad23e62a70173a7a072be86a548 Mon Sep 17 00:00:00 2001 From: Jayson Fong Date: Thu, 9 Oct 2025 11:40:53 -0400 Subject: [PATCH 7/7] Run black on test_structure.py --- tests/test_structure.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index 3fe0110..9114f2b 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -435,10 +435,12 @@ def test_set_invalid(self): def test_builtin_default(self): class LengthStructure(Structure): def __init__(self): - self.fields = OrderedDict([ - ("field1", IntField(size=4, default=b"\x01\x03\x05\x07")), - ("field2", IntField(size=2, default=len)), - ]) + self.fields = OrderedDict( + [ + ("field1", IntField(size=4, default=b"\x01\x03\x05\x07")), + ("field2", IntField(size=2, default=len)), + ] + ) super().__init__() structure = LengthStructure() @@ -570,9 +572,11 @@ def field_resolver(s): class TestStructure(Structure): def __init__(self): - self.fields = OrderedDict([ - ("field", BytesField(size=2, default=field_resolver)), - ]) + self.fields = OrderedDict( + [ + ("field", BytesField(size=2, default=field_resolver)), + ] + ) super().__init__() structure = TestStructure() @@ -711,9 +715,7 @@ def __new__(cls, s, d): class UnpackListStructure(Structure): def __init__(self): - self.fields = OrderedDict( - [("field", ListField(size=7, unpack_func=Unpacker))] - ) + self.fields = OrderedDict([("field", ListField(size=7, unpack_func=Unpacker))]) super().__init__() field = UnpackListStructure()["field"] @@ -1913,4 +1915,4 @@ def __init__(self): ) ] ) - super().__init__() \ No newline at end of file + super().__init__()