Skip to content

Commit d958f0b

Browse files
committed
fixed edge case on type_safe on how type annotations are resolved
1 parent cdb9f1f commit d958f0b

File tree

4 files changed

+210
-19
lines changed

4 files changed

+210
-19
lines changed

osbot_utils/type_safe/type_safe_core/steps/Type_Safe__Step__Class_Kwargs.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,23 @@ def handle_defined_var(self, base_cls : Type ,
100100
type_safe_validation.validate_variable_type(base_cls, var_name, var_type, var_value)
101101
type_safe_validation.validate_type_immutability(var_name, var_type)
102102

103-
def process_annotation(self, cls : Type , # Process single annotation
103+
def process_annotation(self, cls : Type ,
104104
base_cls : Type ,
105105
kwargs : Dict[str, Any] ,
106106
var_name : str ,
107-
var_type : Type )\
108-
-> None:
109-
if not hasattr(base_cls, var_name): # Handle undefined variables
107+
var_type : Type ):
108+
109+
class_declares_annotation = var_name in getattr(base_cls, '__annotations__', {})
110+
111+
if not hasattr(base_cls, var_name):
110112
self.handle_undefined_var(cls, kwargs, var_name, var_type)
111-
else: # Handle defined variables
113+
elif class_declares_annotation and base_cls is cls:
114+
origin = type_safe_cache.get_origin(var_type) # Only recalculate default for Type[T] annotations
115+
if origin is type:
116+
self.handle_undefined_var(cls, kwargs, var_name, var_type)
117+
else:
118+
self.handle_defined_var(base_cls, var_name, var_type)
119+
else:
112120
self.handle_defined_var(base_cls, var_name, var_type)
113121

114122
def process_annotations(self, cls : Type , # Process all annotations
@@ -119,6 +127,7 @@ def process_annotations(self, cls : Type ,
119127
for var_name, var_type in type_safe_cache.get_class_annotations(base_cls):
120128
self.process_annotation(cls, base_cls, kwargs, var_name, var_type)
121129

130+
122131
def process_mro_class(self, base_cls : Type , # Process class in MRO chain
123132
kwargs : Dict[str, Any] )\
124133
-> None:

tests/unit/type_safe/test_Type_Safe.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,17 +1085,6 @@ def label(self, value):
10851085
test_class.data = 123 # confirm that type safety is still working on the main class
10861086

10871087
def test_validate_type_immutability(self): # Tests type immutability validation
1088-
# class Simple_Type(Type_Safe):
1089-
# valid_int : int = 42 # valid immutable type
1090-
# valid_str : str = 'abc' # valid immutable type
1091-
# valid_bool : bool = True # valid immutable type
1092-
# valid_tuple : tuple = (1,2) # valid immutable type
1093-
#
1094-
# simple = Simple_Type() # Should work fine with valid types
1095-
# assert simple.valid_int == 42
1096-
# assert simple.valid_str == 'abc'
1097-
# assert simple.valid_bool == True
1098-
# assert simple.valid_tuple == (1,2)
10991088

11001089
with pytest.raises(ValueError, match= "variable 'invalid_list' is defined as type '<class 'list'>' which is not supported by Type_Safe" ): # Test invalid mutable type
11011090
class Invalid_Type(Type_Safe):

tests/unit/type_safe/type_safe_core/_bugs/test_Type_Safe__bugs.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import re
2-
from typing import Type
3-
42
import pytest
3+
from typing import Type
54
from unittest import TestCase
65
from osbot_utils.type_safe.Type_Safe import Type_Safe
76

@@ -71,3 +70,32 @@ def __init__(self): # this will make the __annotations__ to
7170

7271
assert an_class.an_str == 'new_value'
7372
assert an_class.an_bool == False
73+
74+
def test__bug__type_annotation__non_none_parent_default(self):
75+
# What happens when parent has a non-None default?
76+
# This combines BOTH bugs:
77+
# 1. Subclass inherits parent's value (Base_Handler) instead of auto-assigning Extended_Handler
78+
# 2. Then validation fails because Base_Handler is not a subclass of Extended_Handler
79+
80+
class Base_Handler(Type_Safe):
81+
pass
82+
83+
class Extended_Handler(Base_Handler):
84+
pass
85+
86+
class Base_Config(Type_Safe):
87+
handler_type: Type[Base_Handler] = Base_Handler # Non-None default
88+
89+
class Extended_Config(Base_Config):
90+
handler_type: Type[Extended_Handler] # Re-declare with more specific type
91+
92+
# Parent default is Base_Handler
93+
with Base_Config() as _:
94+
assert _.handler_type is Base_Handler # Correct
95+
96+
# BUG: Subclass inherits parent's value (Base_Handler), then validation fails
97+
# because Base_Handler is not a subclass of Extended_Handler
98+
error_message = "On Extended_Config, invalid type for attribute 'handler_type'. Expected 'typing.Type["
99+
with pytest.raises(ValueError, match=re.escape(error_message)):
100+
Extended_Config() # BUG: should auto-assign Extended_Handler
101+

tests/unit/type_safe/type_safe_core/_regression/test_Type_Safe__regression.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1834,4 +1834,169 @@ class Child(Parent):
18341834

18351835
# This works (because Child is subclass of incorrectly-resolved Child)
18361836
child = Child(ref=Child)
1837-
assert child.ref is Child # Works (accidentally correct)
1837+
assert child.ref is Child # Works (accidentally correct)
1838+
1839+
def test__regression__type_annotation__expected_behavior_after_fix(self):
1840+
# Documents what SHOULD work after the bug is fixed
1841+
1842+
class Animal(Type_Safe):
1843+
name: str = ''
1844+
1845+
class Dog(Animal):
1846+
breed: str = ''
1847+
1848+
class AnimalRegistry(Type_Safe):
1849+
animal_type: Type[Animal] = None
1850+
1851+
class DogRegistry(AnimalRegistry):
1852+
animal_type: Type[Dog] # Should auto-default to Dog
1853+
1854+
# After fix, this should work:
1855+
# with DogRegistry() as _:
1856+
# assert _.animal_type is Dog # Auto-assigned default
1857+
1858+
# Currently:
1859+
with DogRegistry() as _:
1860+
#assert _.animal_type is None # BUG: inherits None
1861+
assert _.animal_type is Dog
1862+
1863+
def test__regression__type_annotation__workaround_comparison(self):
1864+
# Compare buggy vs workaround patterns
1865+
1866+
class Processor(Type_Safe):
1867+
pass
1868+
1869+
class FastProcessor(Processor):
1870+
pass
1871+
1872+
class Config_Base(Type_Safe):
1873+
processor_type: Type[Processor] = None
1874+
1875+
# BUGGY: No explicit value
1876+
class Config_Fast_Buggy(Config_Base):
1877+
processor_type: Type[FastProcessor]
1878+
1879+
# WORKAROUND: Explicit value
1880+
class Config_Fast_Fixed(Config_Base):
1881+
processor_type: Type[FastProcessor] = FastProcessor
1882+
1883+
# Buggy version gets None
1884+
#assert Config_Fast_Buggy().processor_type is None # BUG
1885+
assert Config_Fast_Buggy().processor_type is FastProcessor
1886+
1887+
# Workaround version gets correct value
1888+
assert Config_Fast_Fixed().processor_type is FastProcessor # Works
1889+
1890+
1891+
def test__regression__type_annotation__subclass_redeclaration__no_auto_default(self):
1892+
# Bug: When a subclass re-declares a Type[T] annotation with a more specific type,
1893+
# Type_Safe should auto-assign the type as the default value.
1894+
# Instead, it inherits the parent's None value.
1895+
#
1896+
# Root cause: In Type_Safe__Step__Class_Kwargs.process_annotation(),
1897+
# hasattr(base_cls, var_name) returns True (from parent),
1898+
# so handle_undefined_var() is never called to calculate default.
1899+
1900+
class Base_Node(Type_Safe):
1901+
value: str = ''
1902+
1903+
class Extended_Node(Base_Node):
1904+
extra: int = 0
1905+
1906+
class Base_Types(Type_Safe):
1907+
node_type: Type[Base_Node] = None # Explicit None default
1908+
1909+
class Extended_Types(Base_Types):
1910+
node_type: Type[Extended_Node] # Re-declared with specific type, no explicit value
1911+
1912+
# Base class works as expected
1913+
with Base_Types() as _:
1914+
assert _.node_type is None # Explicit None default
1915+
1916+
# BUG: Subclass should auto-assign Extended_Node as default
1917+
with Extended_Types() as _:
1918+
#assert _.node_type is None # BUG: inherits parent's None
1919+
assert _.node_type is Extended_Node
1920+
# EXPECTED: _.node_type should be Extended_Node
1921+
1922+
def test__regression__type_annotation__subclass_redeclaration__with_explicit_value_works(self):
1923+
# Document that explicit value assignment DOES work (workaround)
1924+
1925+
class Base_Node(Type_Safe):
1926+
value: str = ''
1927+
1928+
class Extended_Node(Base_Node):
1929+
extra: int = 0
1930+
1931+
class Base_Types(Type_Safe):
1932+
node_type: Type[Base_Node] = None
1933+
1934+
class Extended_Types__Fixed(Base_Types):
1935+
node_type: Type[Extended_Node] = Extended_Node # Explicit assignment works
1936+
1937+
# This workaround works correctly
1938+
with Extended_Types__Fixed() as _:
1939+
assert _.node_type is Extended_Node # Works with explicit value
1940+
1941+
def test__regression__type_annotation__multiple_type_fields(self):
1942+
# Bug affects multiple Type[T] fields
1943+
1944+
class Model_Node(Type_Safe):
1945+
pass
1946+
1947+
class Model_Edge(Type_Safe):
1948+
pass
1949+
1950+
class Simple_Node(Model_Node):
1951+
pass
1952+
1953+
class Simple_Edge(Model_Edge):
1954+
pass
1955+
1956+
class Base_Model_Types(Type_Safe):
1957+
node_model_type: Type[Model_Node] = None
1958+
edge_model_type: Type[Model_Edge] = None
1959+
1960+
class Simple_Model_Types(Base_Model_Types):
1961+
node_model_type: Type[Simple_Node] # Re-declared, no explicit value
1962+
edge_model_type: Type[Simple_Edge] # Re-declared, no explicit value
1963+
1964+
# BUG: Both fields inherit None instead of auto-assigning the type
1965+
with Simple_Model_Types() as _:
1966+
# assert _.node_model_type is None # BUG: should be Simple_Node
1967+
# assert _.edge_model_type is None # BUG: should be Simple_Edge
1968+
assert _.node_model_type is Simple_Node
1969+
assert _.edge_model_type is Simple_Edge
1970+
1971+
def test__regression__type_annotation__deep_inheritance_chain(self):
1972+
# Bug affects deeper inheritance chains
1973+
1974+
class Node_L0(Type_Safe):
1975+
pass
1976+
1977+
class Node_L1(Node_L0):
1978+
pass
1979+
1980+
class Node_L2(Node_L1):
1981+
pass
1982+
1983+
class Types_L0(Type_Safe):
1984+
node_type: Type[Node_L0] = None
1985+
1986+
class Types_L1(Types_L0):
1987+
node_type: Type[Node_L1] # Re-declare for L1
1988+
1989+
class Types_L2(Types_L1):
1990+
node_type: Type[Node_L2] # Re-declare for L2
1991+
1992+
# All levels inherit None instead of auto-assigning
1993+
with Types_L0() as _:
1994+
assert _.node_type is None # Correct (explicit None)
1995+
1996+
with Types_L1() as _:
1997+
#assert _.node_type is None # BUG: should be Node_L1
1998+
assert _.node_type is Node_L1
1999+
2000+
with Types_L2() as _:
2001+
#assert _.node_type is None # BUG: should be Node_L2
2002+
assert _.node_type is Node_L2

0 commit comments

Comments
 (0)