Skip to content

Commit

Permalink
added support to Type_Safe to nested types
Browse files Browse the repository at this point in the history
  • Loading branch information
DinisCruz committed Jul 1, 2024
1 parent aca9187 commit 7df0d38
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 38 deletions.
13 changes: 5 additions & 8 deletions osbot_utils/base_classes/Type_Safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
from osbot_utils.utils.Dev import pprint
from osbot_utils.utils.Json import json_parse
from osbot_utils.utils.Misc import list_set
from osbot_utils.utils.Objects import default_value, value_type_matches_obj_annotation_for_attr, \
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
obj_is_type_union_compatible, value_type_matches_obj_annotation_for_union_attr, \
convert_dict_to_value_from_obj_annotation

# Backport implementations of get_origin and get_args for Python 3.7
if sys.version_info < (3, 8):
Expand Down Expand Up @@ -92,13 +93,9 @@ def __setattr__(self, name, value):
if not hasattr(self, '__annotations__'): # can't do type safety checks if the class does not have annotations
return super().__setattr__(name, value)

# if self.__type_safety__:
# if self.__lock_attributes__:
# todo: this can't work on all, current hypothesis is that this will work for the values that are explicitly set
# if not hasattr(self, name):
# raise AttributeError(f"'[Object Locked] Current object is locked (with __lock_attributes__=True) which prevents new attributes allocations (i.e. setattr calls). In this case {type(self).__name__}' object has no attribute '{name}'") from None

if value is not None:
if type(value) is dict:
value = convert_dict_to_value_from_obj_annotation(self, name, value)
check_1 = value_type_matches_obj_annotation_for_attr (self, name, value)
check_2 = value_type_matches_obj_annotation_for_union_attr(self, name, value)
if (check_1 is False and check_2 is None or
Expand Down
14 changes: 14 additions & 0 deletions osbot_utils/utils/Objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def base_classes(cls):
target = type(cls)
return type_base_classes(target)

def base_classes_names(cls):
return [cls.__name__ for cls in base_classes(cls)]

def class_functions_names(target):
return list_set(class_functions(target))

Expand All @@ -89,6 +92,17 @@ def class_full_name(target):
type_name = type_target.__name__
return f'{type_module}.{type_name}'

def convert_dict_to_value_from_obj_annotation(target, attr_name, value):
if target is not None and attr_name is not None:
if hasattr(target, '__annotations__'):
obj_annotations = target.__annotations__
if hasattr(obj_annotations,'get'):
attribute_annotation = obj_annotations.get(attr_name)
if 'Type_Safe' in base_classes_names(attribute_annotation):
return attribute_annotation(**value)
return value


def default_value(target : type):
try:
return target() # try to create the object using the default constructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from osbot_utils.utils.Dev import pprint


class test_Kwargs_To_Self__bugs(TestCase):
class test_Type_Safe__bugs(TestCase):

def test__bug__check_type_safety_assignments__on_ctor(self):
an_bool_value = True
Expand Down Expand Up @@ -113,30 +113,3 @@ class Should_Raise_Exception(Type_Safe): # a class that uses
assert type(should_raise_exception.an_int) is bool # BUG: confirming that an_int is a bool
assert should_raise_exception.__annotations__ == {'an_int': int } # confirm that the an_int annotation is int

def test_bug__should_create_nested_objects_when_loading_dicts(self): # Test method to verify the creation of nested objects from dictionaries.
class Class_A(Type_Safe): # Define a nested class Class_A inheriting from Type_Safe.
an_int: int # Define an attribute 'an_int' of type int.
an_str: str # Define an attribute 'an_str' of type str.

class Class_B(Type_Safe): # Define another nested class Class_B inheriting from Type_Safe.
an_bool: bool # Define an attribute 'an_bool' of type bool.
an_bytes: bytes # Define an attribute 'an_bytes' of type bytes.

class Class_C(Type_Safe): # Define the main class Class_C inheriting from Type_Safe.
an_class_a: Class_A # Define an attribute 'an_class_a' of type Class_A.
an_class_b: Class_B # Define an attribute 'an_class_b' of type Class_B.

class_c = Class_C() # Instantiate an object of Class_C.
class_c_as_dict = { 'an_class_a': {'an_int' : 0 , 'an_str' : '' }, # Define a dictionary representing class_c with nested dictionaries.
'an_class_b': {'an_bool': False, 'an_bytes': b''}} # Define attributes for nested Class_B in the dictionary.

assert class_c.json() == class_c_as_dict # Assert that the JSON representation of class_c matches the dictionary.
with self.assertRaises(ValueError) as context: # Assert that ValueError is raised during class instantiation with invalid types.
Class_C(**class_c_as_dict) # BUG: Attempt to create a Class_C instance with the dictionary.

assert context.exception.args[0] == ("Invalid type for attribute 'an_class_a'. Expected '<class " # Verify the exception message for invalid type of 'an_class_a'.
"'test_Kwargs_To_Self__bugs.test_Kwargs_To_Self__bugs."
"test_bug__should_create_nested_objects_when_loading_dicts."
"<locals>.Class_A'>' "
"but got '<class 'dict'>'") # Complete the verification of the exception message.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
from osbot_utils.base_classes.Type_Safe import Type_Safe
from osbot_utils.base_classes.Type_Safe__List import Type_Safe__List
from osbot_utils.decorators.methods.cache_on_self import cache_on_self
from osbot_utils.graphs.mermaid.Mermaid import Mermaid
Expand All @@ -16,7 +17,7 @@
from osbot_utils.utils.Objects import default_value, obj_attribute_annotation


class test_Kwargs_To_Self__regression(TestCase):
class test_Type_Safe__regression(TestCase):

def test_regression_base_class_attributes_set_to_null_when_super_is_used(self):

Expand Down Expand Up @@ -449,4 +450,44 @@ def an_local_function():

# with self.assertRaises(Exception) as context:
# An_Class(an_callable=an_local_function)
# assert context.exception.args[0] == ("Invalid type for attribute 'an_callable'. Expected '<built-in function callable>' but got '<class 'function'>'")
# assert context.exception.args[0] == ("Invalid type for attribute 'an_callable'. Expected '<built-in function callable>' but got '<class 'function'>'")

def test_regression__should_create_nested_objects_when_loading_dicts(self): # Test method to verify the creation of nested objects from dictionaries.
class Class_A(Type_Safe): # Define a nested class Class_A inheriting from Type_Safe.
an_int: int # Define an attribute 'an_int' of type int.
an_str: str # Define an attribute 'an_str' of type str.

class Class_B(Type_Safe): # Define another nested class Class_B inheriting from Type_Safe.
an_bool: bool # Define an attribute 'an_bool' of type bool.
an_bytes: bytes # Define an attribute 'an_bytes' of type bytes.

class Class_C(Type_Safe): # Define the main class Class_C inheriting from Type_Safe.
an_class_a: Class_A # Define an attribute 'an_class_a' of type Class_A.
an_class_b: Class_B # Define an attribute 'an_class_b' of type Class_B.

class_c = Class_C() # Instantiate an object of Class_C.
class_c_as_dict = { 'an_class_a': {'an_int' : 0 , 'an_str' : '' }, # Define a dictionary representing class_c with nested dictionaries.
'an_class_b': {'an_bool': False, 'an_bytes': b''}} # Define attributes for nested Class_B in the dictionary.

assert class_c.json() == class_c_as_dict # Assert that the JSON representation of class_c matches the dictionary.
assert Class_C(**class_c_as_dict).json() == class_c_as_dict # FIXED: now exception is not raised

class Class_D(Type_Safe):
an_class_a: dict
an_class_b: dict
assert Class_D(**class_c_as_dict).json() == class_c_as_dict # added use case of when both variables are dict

class Class_F(Type_Safe):
an_class_a: dict
an_class_b: Class_B
assert Class_F(**class_c_as_dict).json() == class_c_as_dict # added use case of when we have a mix

# with self.assertRaises(ValueError) as context: # Assert that ValueError is raised during class instantiation with invalid types.
# Class_C(**class_c_as_dict) # BUG: Attempt to create a Class_C instance with the dictionary.
#
# assert context.exception.args[0] == ("Invalid type for attribute 'an_class_a'. Expected '<class " # Verify the exception message for invalid type of 'an_class_a'.
# "'test_Type_Safe__bugs.test_Type_Safe__bugs."
# "test_bug__should_create_nested_objects_when_loading_dicts."
# "<locals>.Class_A'>' "
# "but got '<class 'dict'>'") # Complete the verification of the exception message.

File renamed without changes.
File renamed without changes.

0 comments on commit 7df0d38

Please sign in to comment.