spdx_tools.common.typing.dataclass_with_properties

 1# SPDX-FileCopyrightText: 2022 spdx contributors
 2#
 3# SPDX-License-Identifier: Apache-2.0
 4from dataclasses import dataclass
 5
 6from beartype import beartype
 7from beartype.roar import BeartypeCallHintException
 8
 9
10def dataclass_with_properties(cls):
11    """Decorator to generate a dataclass with properties out of the class' value:type list.
12    Their getters and setters will be subjected to the @typechecked decorator to ensure type conformity."""
13    data_cls = dataclass(cls)
14    for field_name, field_type in data_cls.__annotations__.items():
15        set_field = make_setter(field_name, field_type)
16        get_field = make_getter(field_name, field_type)
17
18        setattr(data_cls, field_name, property(get_field, set_field))
19
20    return data_cls
21
22
23def make_setter(field_name, field_type):
24    """helper method to avoid late binding when generating functions in a for loop"""
25
26    @beartype
27    def set_field(self, value: field_type):
28        setattr(self, f"_{field_name}", value)
29
30    def set_field_with_error_conversion(self, value: field_type):
31        try:
32            set_field(self, value)
33        except BeartypeCallHintException as err:
34            error_message: str = f"SetterError {self.__class__.__name__}: {err}"
35
36            # As setters are created dynamically, their argument name is always "value". We replace it by the
37            # actual name so the error message is more helpful.
38            raise TypeError(error_message.replace("value", field_name, 1) + f": {value}")
39
40    return set_field_with_error_conversion
41
42
43def make_getter(field_name, field_type):
44    """helper method to avoid late binding when generating functions in a for loop"""
45
46    def get_field(self) -> field_type:
47        return getattr(self, f"_{field_name}")
48
49    return get_field
def dataclass_with_properties(cls):
11def dataclass_with_properties(cls):
12    """Decorator to generate a dataclass with properties out of the class' value:type list.
13    Their getters and setters will be subjected to the @typechecked decorator to ensure type conformity."""
14    data_cls = dataclass(cls)
15    for field_name, field_type in data_cls.__annotations__.items():
16        set_field = make_setter(field_name, field_type)
17        get_field = make_getter(field_name, field_type)
18
19        setattr(data_cls, field_name, property(get_field, set_field))
20
21    return data_cls

Decorator to generate a dataclass with properties out of the class' value:type list. Their getters and setters will be subjected to the @typechecked decorator to ensure type conformity.

def make_setter(field_name, field_type):
24def make_setter(field_name, field_type):
25    """helper method to avoid late binding when generating functions in a for loop"""
26
27    @beartype
28    def set_field(self, value: field_type):
29        setattr(self, f"_{field_name}", value)
30
31    def set_field_with_error_conversion(self, value: field_type):
32        try:
33            set_field(self, value)
34        except BeartypeCallHintException as err:
35            error_message: str = f"SetterError {self.__class__.__name__}: {err}"
36
37            # As setters are created dynamically, their argument name is always "value". We replace it by the
38            # actual name so the error message is more helpful.
39            raise TypeError(error_message.replace("value", field_name, 1) + f": {value}")
40
41    return set_field_with_error_conversion

helper method to avoid late binding when generating functions in a for loop

def make_getter(field_name, field_type):
44def make_getter(field_name, field_type):
45    """helper method to avoid late binding when generating functions in a for loop"""
46
47    def get_field(self) -> field_type:
48        return getattr(self, f"_{field_name}")
49
50    return get_field

helper method to avoid late binding when generating functions in a for loop