Skip to content

Commit

Permalink
Fixed bug in Type_Safe where types in List[type] where not being enfo…
Browse files Browse the repository at this point in the history
…rced
  • Loading branch information
DinisCruz committed Nov 19, 2024
1 parent 13584eb commit 8ea793d
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 2 deletions.
11 changes: 10 additions & 1 deletion osbot_utils/base_classes/Type_Safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
from osbot_utils.utils.Objects import default_value, value_type_matches_obj_annotation_for_attr, \
raise_exception_on_obj_type_annotation_mismatch, obj_is_attribute_annotation_of_type, enum_from_value, \
obj_is_type_union_compatible, value_type_matches_obj_annotation_for_union_attr, \
convert_dict_to_value_from_obj_annotation, dict_to_obj, convert_to_value_from_obj_annotation
convert_dict_to_value_from_obj_annotation, dict_to_obj, convert_to_value_from_obj_annotation, \
obj_attribute_annotation

# Backport implementations of get_origin and get_args for Python 3.7
if sys.version_info < (3, 8): # pragma: no cover
Expand Down Expand Up @@ -290,6 +291,14 @@ def deserialize_from_dict(self, data, raise_on_not_found=False):
continue
if obj_is_attribute_annotation_of_type(self, key, dict): # handle the case when the value is a dict
value = self.deserialize_dict__using_key_value_annotations(key, value)
elif obj_is_attribute_annotation_of_type(self, key, list): # handle the case when the value is a list
attribute_annotation = obj_attribute_annotation(self, key) # get the annotation for this variable
expected_type = get_args(attribute_annotation)[0] # get the first arg (which is the type)
type_safe_list = Type_Safe__List(expected_type) # create a new instance of Type_Safe__List
for item in value: # next we need to convert all items (to make sure they all match the type)
new_item = expected_type(**item) # create new object
type_safe_list.append(new_item) # and add it to the new type_safe_list obejct
value = type_safe_list # todo: refactor out this create list code, maybe to an deserialize_from_list method
else:
if value is not None:
if obj_is_attribute_annotation_of_type(self, key, EnumMeta): # Handle the case when the value is an Enum
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/base_classes/test_Type_Safe__List.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@

class test_Type_Safe__List(TestCase):

def test__list_from_json__enforces_type_safety(self):
class An_Class__Item(Type_Safe):
an_str: str

class An_Class(Type_Safe):
items : List[An_Class__Item]

json_data = {'items': [{'an_str': 'abc'}]}

an_class = An_Class.from_json(json_data)
assert type(an_class.items) is Type_Safe__List
assert type(an_class.items[0]) is An_Class__Item


def test__type_safe_list_with_simple_types(self):
if sys.version_info < (3, 10):
pytest.skip("Skipping test that doesn't work on 3.9 or lower")
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/base_classes/test_Type_Safe__bugs.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import sys

import pytest
from typing import Optional, Union, Dict
from typing import Optional, Union, Dict, List
from unittest import TestCase
from osbot_utils.base_classes.Type_Safe import Type_Safe
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
from osbot_utils.helpers.Random_Guid import Random_Guid
from osbot_utils.utils.Dev import pprint
from osbot_utils.utils.Objects import __


class test_Type_Safe__bugs(TestCase):
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/base_classes/test_Type_Safe__regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,54 @@

class test_Type_Safe__regression(TestCase):

def test__regression__list_from_json_not_enforcing_type_safety(self):
class An_Class__Item(Type_Safe):
an_str: str

class An_Class(Type_Safe):
items : List[An_Class__Item]

json_data = {'items': [{'an_str': 'abc'}]}

an_class_1 = An_Class()
an_class_1.items.append(An_Class__Item(an_str='abc'))
assert type(an_class_1.items) == Type_Safe__List
assert type(an_class_1.items[0]) is An_Class__Item
assert an_class_1.json() == json_data
assert an_class_1.obj () == __(items=[__(an_str='abc')])

an_class_2 = An_Class.from_json(json_data)
assert an_class_2.json() == an_class_1.json()
assert an_class_2.obj() == an_class_1.obj()

#assert type(an_class_2.items[0]) is dict # BUG: should be An_Class__Item

assert type(an_class_2.items ) is Type_Safe__List # FIXED
assert type(an_class_2.items[0]) is An_Class__Item # FIXED


# confirm that the type safety is enforced on the objects created via the ctor
with pytest.raises(TypeError, match ="Invalid type for item: Expected 'An_Class__Item', but got 'str'"):
an_class_1.items.append('abc')

with pytest.raises(TypeError, match ="Invalid type for item: Expected 'An_Class__Item', but got 'int'"):
an_class_1.items.append(123)

# BUG, but it is not enforced in the object created using from_json
#an_class_2.items.append('abc') # BUG, should have raised exception
#an_class_2.items.append(123) # BUG, should have raised exception

with pytest.raises(TypeError, match ="Invalid type for item: Expected 'An_Class__Item', but got 'str'"):
an_class_2.items.append('abc')

with pytest.raises(TypeError, match ="Invalid type for item: Expected 'An_Class__Item', but got 'int'"):
an_class_2.items.append(123)

#assert an_class_2.obj() == __(items=[__(an_str='abc'), 'abc', 123]) # BUG new values should have not been added
assert an_class_2.obj() == __(items=[__(an_str='abc')]) # correct, values did not change
assert an_class_2.obj() == __(items=[__(an_str='abc')]) # correct, values did not change


def test__regression__from_json__pure__Dict__objects_raise_exception(self):
if sys.version_info < (3, 9):
pytest.skip("this doesn't work on 3.8 or lower")
Expand Down

0 comments on commit 8ea793d

Please sign in to comment.