Skip to content

Commit 51fa849

Browse files
131: Move wrapping of pydantic exceptions for model validate, setattr, and model_validate_json to the S2MessageComponent mixin instead of wrapping it using the decorator to preserve classmethod on inheritence for model_validate. (#132)
* 131: Move wrapping of pydantic exceptions for model validate, setattr, and model_validate_json to the S2MessageComponent mixin instead of wrapping it using the decorator to preserve classmethod on inheritence for model_validate. * 131: Add unit test case for inheritance of message.
1 parent 4f2c033 commit 51fa849

File tree

8 files changed

+82
-27
lines changed

8 files changed

+82
-27
lines changed

src/s2python/common/power_forecast_element.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List
1+
from typing import List, Dict
22
from typing_extensions import Self
33

44
from pydantic import model_validator
@@ -29,7 +29,7 @@ class PowerForecastElement(GenPowerForecastElement, S2MessageComponent):
2929
def validate_values_at_most_one_per_commodity_quantity(self) -> Self:
3030
"""Validates the power measurement values to check that there is at most 1 PowerValue per CommodityQuantity."""
3131

32-
has_value: dict[CommodityQuantity, bool] = {}
32+
has_value: Dict[CommodityQuantity, bool] = {}
3333

3434
for value in self.power_values:
3535
if has_value.get(value.commodity_quantity, False):

src/s2python/common/power_measurement.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import uuid
2-
from typing import List
2+
from typing import List, Dict
33
from typing_extensions import Self
44

55
from pydantic import model_validator
@@ -26,7 +26,7 @@ class PowerMeasurement(GenPowerMeasurement, S2MessageComponent):
2626
def validate_values_at_most_one_per_commodity_quantity(self) -> Self:
2727
"""Validates the power measurement values to check that there is at most 1 PowerValue per CommodityQuantity."""
2828

29-
has_value: dict[CommodityQuantity, bool] = {}
29+
has_value: Dict[CommodityQuantity, bool] = {}
3030

3131
for value in self.values:
3232
if has_value.get(value.commodity_quantity, False):

src/s2python/ombc/ombc_operation_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class OMBCOperationMode(GenOMBCOperationMode, S2MessageComponent):
1717
model_config["validate_assignment"] = True
1818

1919
id: uuid.UUID = GenOMBCOperationMode.model_fields["id"] # type: ignore[assignment]
20-
power_ranges: List[PowerRange] = GenOMBCOperationMode.model_fields[
20+
power_ranges: List[PowerRange] = GenOMBCOperationMode.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
2121
"power_ranges"
2222
] # type: ignore[assignment]
2323
abnormal_condition_only: bool = GenOMBCOperationMode.model_fields[

src/s2python/ombc/ombc_system_description.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class OMBCSystemDescription(GenOMBCSystemDescription, S2MessageComponent):
1818
model_config["validate_assignment"] = True
1919

2020
message_id: uuid.UUID = GenOMBCSystemDescription.model_fields["message_id"] # type: ignore[assignment]
21-
operation_modes: List[OMBCOperationMode] = GenOMBCSystemDescription.model_fields[
21+
operation_modes: List[OMBCOperationMode] = GenOMBCSystemDescription.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
2222
"operation_modes"
2323
] # type: ignore[assignment]
2424
transitions: List[Transition] = GenOMBCSystemDescription.model_fields["transitions"] # type: ignore[assignment]

src/s2python/s2_parser.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,9 @@ def parse_as_any_message(unparsed_message: Union[dict, str, bytes]) -> S2Message
8888
None,
8989
message_json,
9090
f"Unable to parse {message_type} as an S2 message. Type unknown.",
91-
None,
9291
)
9392

94-
return TYPE_TO_MESSAGE_CLASS[message_type].model_validate(message_json)
93+
return TYPE_TO_MESSAGE_CLASS[message_type].from_dict(message_json)
9594

9695
@staticmethod
9796
def parse_as_message(
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
from dataclasses import dataclass
2-
from typing import Union, Type, Optional
3-
4-
from pydantic import ValidationError
5-
from pydantic.v1.error_wrappers import ValidationError as ValidationErrorV1
2+
from typing import Type, Optional
63

74

85
@dataclass
96
class S2ValidationError(Exception):
107
class_: Optional[Type]
118
obj: object
129
msg: str
13-
pydantic_validation_error: Union[
14-
ValidationErrorV1, ValidationError, TypeError, None
15-
]

src/s2python/validate_values_mixin.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
from typing_extensions import Self
1414

15-
from pydantic.v1.error_wrappers import display_errors # pylint: disable=no-name-in-module
16-
1715
from pydantic import ( # pylint: disable=no-name-in-module
1816
BaseModel,
1917
ValidationError,
@@ -28,25 +26,43 @@
2826

2927

3028
class S2MessageComponent(BaseModel):
29+
def __setattr__(self, name: str, value: Any) -> None:
30+
try:
31+
super().__setattr__(name, value)
32+
except (ValidationError, TypeError) as e:
33+
raise S2ValidationError(
34+
type(self), self, "Pydantic raised a validation error.",
35+
) from e
36+
3137
def to_json(self) -> str:
3238
try:
3339
return self.model_dump_json(by_alias=True, exclude_none=True)
3440
except (ValidationError, TypeError) as e:
3541
raise S2ValidationError(
36-
type(self), self, "Pydantic raised a format validation error.", e
42+
type(self), self, "Pydantic raised a validation error.",
3743
) from e
3844

3945
def to_dict(self) -> Dict[str, Any]:
4046
return self.model_dump()
4147

4248
@classmethod
4349
def from_json(cls, json_str: str) -> Self:
44-
gen_model = cls.model_validate_json(json_str)
50+
try:
51+
gen_model = cls.model_validate_json(json_str)
52+
except (ValidationError, TypeError) as e:
53+
raise S2ValidationError(
54+
type(cls), cls, "Pydantic raised a validation error.",
55+
) from e
4556
return gen_model
4657

4758
@classmethod
4859
def from_dict(cls, json_dict: Dict[str, Any]) -> Self:
49-
gen_model = cls.model_validate(json_dict)
60+
try:
61+
gen_model = cls.model_validate(json_dict)
62+
except (ValidationError, TypeError) as e:
63+
raise S2ValidationError(
64+
type(cls), cls, "Pydantic raised a validation error.",
65+
) from e
5066
return gen_model
5167

5268

@@ -61,9 +77,9 @@ def inner(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
6177
else:
6278
class_type = None
6379

64-
raise S2ValidationError(class_type, args, display_errors(e.errors()), e) from e # type: ignore[arg-type]
80+
raise S2ValidationError(class_type, args, str(e)) from e
6581
except TypeError as e:
66-
raise S2ValidationError(None, args, str(e), e) from e
82+
raise S2ValidationError(None, args, str(e)) from e
6783

6884
inner.__doc__ = f.__doc__
6985
inner.__annotations__ = f.__annotations__
@@ -76,10 +92,5 @@ def inner(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
7692

7793
def catch_and_convert_exceptions(input_class: Type[S]) -> Type[S]:
7894
input_class.__init__ = convert_to_s2exception(input_class.__init__) # type: ignore[method-assign]
79-
input_class.__setattr__ = convert_to_s2exception(input_class.__setattr__) # type: ignore[method-assign]
80-
input_class.model_validate_json = convert_to_s2exception( # type: ignore[method-assign]
81-
input_class.model_validate_json
82-
)
83-
input_class.model_validate = convert_to_s2exception(input_class.model_validate) # type: ignore[method-assign]
8495

8596
return input_class

tests/unit/inheritance_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import datetime
2+
import unittest
3+
import uuid
4+
from typing import Optional
5+
6+
from pydantic import Field
7+
8+
from s2python.frbc import FRBCStorageStatus as FRBCStorageStatusOfficial
9+
from s2python.s2_validation_error import S2ValidationError
10+
11+
12+
class FRBCStorageStatus(FRBCStorageStatusOfficial):
13+
measurement_timestamp: Optional[datetime.datetime] = Field(
14+
default=None, description="Timestamp when fill level was measured."
15+
)
16+
17+
18+
class InheritanceTest(unittest.TestCase):
19+
def test__inheritance__init(self):
20+
# Arrange / Act
21+
frbc_storage_status = FRBCStorageStatus(message_id=uuid.uuid4(),
22+
present_fill_level=0.0,
23+
measurement_timestamp=None)
24+
25+
# Assert
26+
self.assertIsInstance(frbc_storage_status, FRBCStorageStatus)
27+
self.assertIsNone(frbc_storage_status.measurement_timestamp)
28+
29+
def test__inheritance__init_wrong(self):
30+
# Arrange / Act / Assert
31+
with self.assertRaises(S2ValidationError):
32+
FRBCStorageStatus(message_id=uuid.uuid4(),
33+
present_fill_level=0.0,
34+
measurement_timestamp=False) # pyright: ignore [reportArgumentType]
35+
36+
def test__inheritance__from_json(self):
37+
# Arrange
38+
json_str = """
39+
{
40+
"message_id": "6bad8186-9ebf-4647-ac45-1c6856511a2f",
41+
"message_type": "FRBC.StorageStatus",
42+
"present_fill_level": 2443.939298819414,
43+
"measurement_timestamp": "2025-01-01T00:00:00Z"
44+
}"""
45+
46+
# Act
47+
frbc_storage_status = FRBCStorageStatus.from_json(json_str)
48+
49+
# Assert
50+
self.assertIsInstance(frbc_storage_status, FRBCStorageStatus)
51+
self.assertEqual(frbc_storage_status.measurement_timestamp, datetime.datetime.fromisoformat("2025-01-01T00:00:00+00:00"))

0 commit comments

Comments
 (0)