morphic 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
morphic/typed.py ADDED
@@ -0,0 +1,1327 @@
1
+ """Enhanced base configuration class with Pydantic-like functionality."""
2
+
3
+ from dataclasses import Field, fields, MISSING
4
+ from typing import Any, ClassVar, Dict, Type, TypeVar, Union, get_args, get_origin, Callable
5
+ from functools import wraps
6
+ import inspect
7
+
8
+ T = TypeVar("T", bound="Typed")
9
+
10
+
11
+ class Typed:
12
+ """Base class for all configuration classes with enhanced dict conversion and validation.
13
+
14
+ This class provides Pydantic-like functionality for dataclasses without external dependencies.
15
+ Subclasses automatically become dataclasses - no @dataclass decorator needed!
16
+ Validation is automatically called after instance creation.
17
+
18
+ Features:
19
+ - Automatic dataclass transformation for subclasses
20
+ - Automatic type validation for all field types
21
+ - Automatic nested Typed conversion in constructor
22
+ - Automatic validation after instance creation
23
+ - Automatic type conversion from dictionaries
24
+ - **Default value validation and conversion at class definition time**
25
+ - **Automatic mutable default handling with default_factory**
26
+ - **Hierarchical default value conversion (nested Typeds, lists, dicts)**
27
+ - AutoEnum string conversion with fuzzy matching and aliases (if morphic.AutoEnum is available)
28
+ - Nested object support with validation
29
+ - Serialization/deserialization with filtering options
30
+ - Field caching for performance
31
+ - Copy with modifications
32
+
33
+ Default Value Features:
34
+ - Default values are validated and converted at class definition time
35
+ - Invalid defaults raise clear errors when the class is defined
36
+ - Convertible defaults are automatically transformed (e.g., "25" -> 25 for int fields)
37
+ - Mutable defaults (lists, dicts, Typed objects) are automatically converted to default_factory
38
+ - Hierarchical structures in defaults are recursively converted
39
+ - Supports Optional fields, Union types, and complex nested structures
40
+
41
+ Basic Usage Examples:
42
+ ```python
43
+ from morphic import Typed, AutoEnum, alias
44
+ from typing import List, Dict, Optional, Union
45
+
46
+ # Simple dataclass with automatic validation
47
+ class User(Typed):
48
+ name: str
49
+ age: int
50
+ active: bool = True
51
+
52
+ def validate(self):
53
+ if self.age < 0:
54
+ raise ValueError("Age must be non-negative")
55
+
56
+ # Validation happens automatically during creation
57
+ user = User(name="John", age=30)
58
+ print(user.name, user.age, user.active) # John 30 True
59
+
60
+ # Type validation catches mismatches immediately
61
+ try:
62
+ User(name=123, age=30) # Raises TypeError - name must be str
63
+ except TypeError as e:
64
+ print(f"Type error: {e}")
65
+
66
+ # from_dict with automatic type conversion
67
+ user = User.from_dict({"name": "John", "age": "30"}) # "30" -> int(30)
68
+ assert user.age == 30 and isinstance(user.age, int)
69
+
70
+ # Custom validation runs after type validation
71
+ try:
72
+ User(name="John", age=-5) # Raises ValueError from validate()
73
+ except ValueError as e:
74
+ print(f"Validation error: {e}")
75
+ ```
76
+
77
+ Advanced Examples:
78
+ ```python
79
+ # Nested Typed objects with automatic conversion
80
+ class Address(Typed):
81
+ street: str
82
+ city: str
83
+ zip_code: str = "00000"
84
+
85
+ class Company(Typed):
86
+ name: str
87
+ address: Address
88
+ employees: List[str] = []
89
+
90
+ # Dict automatically converted to Address object
91
+ company = Company(
92
+ name="Tech Corp",
93
+ address={"street": "123 Main St", "city": "NYC", "zip_code": "10001"}
94
+ )
95
+ assert isinstance(company.address, Address)
96
+ assert company.address.street == "123 Main St"
97
+
98
+ # Works with from_dict too
99
+ company_data = {
100
+ "name": "Tech Corp",
101
+ "address": {"street": "456 Oak Ave", "city": "SF"},
102
+ "employees": ["Alice", "Bob", "Charlie"]
103
+ }
104
+ company2 = Company.from_dict(company_data)
105
+ assert company2.address.zip_code == "00000" # Default value
106
+
107
+ # Complex nested structures
108
+ class Project(Typed):
109
+ name: str
110
+ team_lead: User
111
+ members: List[User]
112
+ settings: Dict[str, Union[str, int]]
113
+
114
+ project = Project.from_dict({
115
+ "name": "Alpha Project",
116
+ "team_lead": {"name": "Alice", "age": 30},
117
+ "members": [
118
+ {"name": "Bob", "age": 25},
119
+ {"name": "Charlie", "age": 28}
120
+ ],
121
+ "settings": {"priority": "high", "budget": 50000}
122
+ })
123
+
124
+ assert isinstance(project.team_lead, User)
125
+ assert all(isinstance(member, User) for member in project.members)
126
+ assert project.settings["budget"] == 50000
127
+ ```
128
+
129
+ Default Value Validation Examples:
130
+ ```python
131
+ # Basic default value conversion
132
+ class Config(Typed):
133
+ port: int = "8080" # String automatically converted to int
134
+ debug: bool = "true" # String automatically converted to bool
135
+ timeout: float = "30.5" # String automatically converted to float
136
+
137
+ config = Config()
138
+ assert config.port == 8080 # Converted to int
139
+ assert isinstance(config.port, int)
140
+
141
+ # Invalid defaults caught at class definition time
142
+ try:
143
+ class BadConfig(Typed):
144
+ count: int = "not_a_number" # Raises TypeError immediately
145
+ except TypeError as e:
146
+ print(f"Invalid default caught: {e}")
147
+
148
+ # Hierarchical default conversion
149
+ class Contact(Typed):
150
+ name: str
151
+ email: str
152
+
153
+ class ContactList(Typed):
154
+ # Dict converted to Contact object automatically
155
+ primary: Contact = {"name": "Admin", "email": "admin@example.com"}
156
+
157
+ # List of dicts converted to list of Contact objects
158
+ contacts: List[Contact] = [
159
+ {"name": "John", "email": "john@example.com"},
160
+ {"name": "Jane", "email": "jane@example.com"}
161
+ ]
162
+
163
+ # Dict of dicts converted to dict of Contact objects
164
+ by_role: Dict[str, Contact] = {
165
+ "admin": {"name": "Administrator", "email": "admin@company.com"},
166
+ "user": {"name": "Regular User", "email": "user@company.com"}
167
+ }
168
+
169
+ # All defaults are properly converted and validated
170
+ contacts = ContactList()
171
+ assert isinstance(contacts.primary, Contact)
172
+ assert isinstance(contacts.contacts[0], Contact)
173
+ assert isinstance(contacts.by_role["admin"], Contact)
174
+
175
+ # Each instance gets its own copy of mutable defaults
176
+ contacts2 = ContactList()
177
+ contacts.contacts.append(Contact(name="New", email="new@example.com"))
178
+ assert len(contacts.contacts) == 3 # Modified
179
+ assert len(contacts2.contacts) == 2 # Unchanged
180
+
181
+ # Optional fields with proper None handling
182
+ class OptionalConfig(Typed):
183
+ name: str
184
+ description: Optional[str] = None # None is valid for Optional types
185
+ settings: Optional[Dict[str, str]] = None
186
+
187
+ config = OptionalConfig(name="test")
188
+ assert config.description is None
189
+ assert config.settings is None
190
+ ```
191
+
192
+ Error Handling:
193
+ Default value validation provides clear error messages that include:
194
+ - The class name where the error occurred
195
+ - The specific field name with the invalid default
196
+ - The expected type and actual type/value received
197
+ - Whether the error occurred during conversion or validation
198
+
199
+ ```python
200
+ # Example error message:
201
+ # TypeError: Invalid default value for field 'port' in class 'Config':
202
+ # Default value for field 'port' in class 'Config' expected type <class 'int'>,
203
+ # got str with value 'invalid_port_number'
204
+ ```
205
+
206
+ Performance and Best Practices:
207
+ - Default value validation occurs only once at class definition time
208
+ - Converted default values are cached and reused for all instances
209
+ - Mutable defaults are automatically handled to prevent shared state issues
210
+ - Use Optional[T] for fields that can legitimately be None
211
+ - Large or complex default structures are efficiently handled via default_factory
212
+ - Type conversion follows the same rules as from_dict() for consistency
213
+
214
+ Advanced Features:
215
+ - Supports Union types: Union[int, str] defaults try conversion in declaration order
216
+ - Handles deeply nested structures: Dict[str, List[Typed]] with full conversion
217
+ - Integrates with custom validation: default values must pass validate() method
218
+ - Compatible with dataclass field() for advanced default_factory scenarios
219
+ - Works seamlessly with AutoEnum string conversion and aliases
220
+ """
221
+
222
+ # Class-level cache for field information
223
+ _field_cache: ClassVar[Dict[Type, Dict[str, Field]]] = {}
224
+
225
+ def __init_subclass__(cls, **kwargs):
226
+ """Automatically cache field information for subclasses and apply dataclass transformation."""
227
+ super().__init_subclass__(**kwargs)
228
+
229
+ # Validate and convert default values BEFORE applying dataclass transformation
230
+ # This ensures dataclass uses the converted values
231
+ cls._validate_and_convert_class_defaults()
232
+
233
+ # Automatically apply dataclass transformation if not already applied
234
+ if not hasattr(cls, "__dataclass_fields__"):
235
+ # Import dataclass here to avoid circular imports
236
+ from dataclasses import dataclass
237
+
238
+ # Apply dataclass transformation
239
+ dataclass_cls = dataclass(cls)
240
+
241
+ # Copy dataclass attributes back to the original class
242
+ # This is necessary because dataclass() returns a new class
243
+ cls.__dataclass_fields__ = dataclass_cls.__dataclass_fields__
244
+ cls.__init__ = dataclass_cls.__init__
245
+ cls.__repr__ = dataclass_cls.__repr__
246
+ cls.__eq__ = dataclass_cls.__eq__
247
+
248
+ # Copy any other dataclass-specific attributes that might exist
249
+ for attr_name in dir(dataclass_cls):
250
+ if attr_name.startswith("__dataclass") and not hasattr(cls, attr_name):
251
+ setattr(cls, attr_name, getattr(dataclass_cls, attr_name))
252
+
253
+ # Cache field information and validate default_factory after dataclass transformation
254
+ if hasattr(cls, "__dataclass_fields__"):
255
+ cls._field_cache[cls] = cls.__dataclass_fields__
256
+ cls._validate_default_factories()
257
+
258
+ def __post_init__(self) -> None:
259
+ """Automatically called after dataclass initialization to run validation."""
260
+ self._convert_field_values()
261
+ self._validate_types()
262
+ self.validate()
263
+
264
+ @classmethod
265
+ def from_dict(cls: Type[T], data: Dict[str, Any], *, strict: bool = False) -> T:
266
+ """Create config instance from dictionary with automatic type conversion.
267
+
268
+ Args:
269
+ data: Dictionary to convert
270
+ strict: If True, raise error on unknown fields
271
+
272
+ Returns:
273
+ Instance of the config class
274
+
275
+ Raises:
276
+ TypeError: If data is not a dictionary
277
+ ValueError: If strict=True and unknown fields are present
278
+ """
279
+ if not isinstance(data, dict):
280
+ raise TypeError(f"Expected dict, got {type(data)}")
281
+
282
+ # Get cached field information
283
+ field_info = cls._get_field_info()
284
+ constructor_inputs = {}
285
+
286
+ for field_name, value in data.items():
287
+ if field_name not in field_info:
288
+ if strict:
289
+ raise ValueError(f"Unknown field '{field_name}' for {cls.__name__}")
290
+ continue
291
+
292
+ field = field_info[field_name]
293
+ constructor_inputs[field_name] = cls._convert_value(field, value)
294
+
295
+ return cls(**constructor_inputs)
296
+
297
+ @classmethod
298
+ def _get_field_info(cls) -> Dict[str, Field]:
299
+ """Get field information, using cache when available."""
300
+ if cls not in cls._field_cache:
301
+ cls._field_cache[cls] = {field.name: field for field in fields(cls)}
302
+ return cls._field_cache[cls]
303
+
304
+ @classmethod
305
+ def _convert_value(cls, field: Field, value: Any) -> Any:
306
+ """Convert a value to the appropriate type for a field."""
307
+ if value is None:
308
+ return None
309
+
310
+ field_type = field.type
311
+
312
+ # Handle Union types (e.g., Optional[int] = Union[int, None])
313
+ if get_origin(field_type) is Union:
314
+ union_args = get_args(field_type)
315
+ # Try each type in the union
316
+ for arg_type in union_args:
317
+ if arg_type is type(None):
318
+ continue
319
+ try:
320
+ return cls._convert_single_type(arg_type, value)
321
+ except (ValueError, TypeError):
322
+ continue
323
+ # If no conversion worked, return as-is
324
+ return value
325
+
326
+ return cls._convert_single_type(field_type, value)
327
+
328
+ @classmethod
329
+ def _convert_single_type(cls, target_type: Type, value: Any) -> Any:
330
+ """Convert value to a single target type."""
331
+ # Handle generic types first before isinstance check
332
+ origin_type = get_origin(target_type)
333
+ if origin_type is not None:
334
+ # Handle List[Typed] or similar list structures
335
+ if origin_type is list:
336
+ type_args = get_args(target_type)
337
+ if type_args and isinstance(value, (list, tuple)):
338
+ element_type = type_args[0]
339
+ # Convert each element if it's a Typed type
340
+ if cls._is_Typed_type(element_type):
341
+ return [cls._convert_single_type(element_type, item) for item in value]
342
+ # For non-Typed types, try basic conversion
343
+ else:
344
+ return [cls._convert_single_type(element_type, item) for item in value]
345
+ return value
346
+
347
+ # Handle Dict[str, Typed] or similar dict structures
348
+ elif origin_type is dict:
349
+ type_args = get_args(target_type)
350
+ if len(type_args) >= 2 and isinstance(value, dict):
351
+ key_type, value_type = type_args[0], type_args[1]
352
+ # Convert dict values
353
+ converted_dict = {}
354
+ for k, v in value.items():
355
+ converted_key = cls._convert_single_type(key_type, k)
356
+ converted_value = cls._convert_single_type(value_type, v)
357
+ converted_dict[converted_key] = converted_value
358
+ return converted_dict
359
+ return value
360
+
361
+ # For other generic types, return as-is
362
+ return value
363
+
364
+ # If already the right type, return as-is (only for non-generic types)
365
+ try:
366
+ if isinstance(value, target_type):
367
+ return value
368
+ except TypeError:
369
+ # Some types (like subscripted generics) can't be used with isinstance
370
+ pass
371
+
372
+
373
+ # Handle AutoEnum conversion (if available in morphic)
374
+ if hasattr(target_type, "__bases__"):
375
+ try:
376
+ # Try to import from morphic package
377
+ from .autoenum import AutoEnum
378
+
379
+ if any(
380
+ issubclass(base, AutoEnum) for base in target_type.__bases__ if isinstance(base, type)
381
+ ):
382
+ if isinstance(value, str):
383
+ # Use from_str method for better conversion with fuzzy matching
384
+ return target_type.from_str(value)
385
+ return value
386
+ except ImportError:
387
+ pass
388
+
389
+ # Handle standard Python enum types
390
+ try:
391
+ import enum
392
+ if issubclass(target_type, enum.Enum):
393
+ if isinstance(value, str):
394
+ return target_type(value)
395
+ return value
396
+ except (TypeError, ImportError):
397
+ # Not an enum or enum not available, continue with other checks
398
+ pass
399
+
400
+ # Handle other enum types by looking for common enum characteristics
401
+ if (
402
+ hasattr(target_type, "_value_")
403
+ or hasattr(target_type, "value")
404
+ or any(hasattr(base, "_value_") for base in target_type.__bases__ if isinstance(base, type))
405
+ ):
406
+ if isinstance(value, str):
407
+ return target_type(value)
408
+ return value
409
+
410
+ # Handle nested Typed objects
411
+ if hasattr(target_type, "__bases__") and any(
412
+ issubclass(base, Typed) for base in target_type.__bases__ if isinstance(base, type)
413
+ ):
414
+ if isinstance(value, dict):
415
+ return target_type.from_dict(value)
416
+ return value
417
+
418
+ # Handle basic type conversions
419
+ if target_type in (int, float, str, bool):
420
+ try:
421
+ return target_type(value)
422
+ except (ValueError, TypeError):
423
+ # If conversion fails, return as-is and let dataclass validation handle it
424
+ pass
425
+
426
+ # For complex types, return as-is and let dataclass handle it
427
+ return value
428
+
429
+ def to_dict(self, *, exclude_none: bool = False, exclude_defaults: bool = False) -> Dict[str, Any]:
430
+ """Convert instance to dictionary.
431
+
432
+ Args:
433
+ exclude_none: If True, exclude fields with None values
434
+ exclude_defaults: If True, exclude fields with default values
435
+
436
+ Returns:
437
+ Dictionary representation of the instance
438
+ """
439
+ result = {}
440
+ field_info = self._get_field_info()
441
+
442
+ for field_name, field in field_info.items():
443
+ value = getattr(self, field_name)
444
+
445
+ if exclude_none and value is None:
446
+ continue
447
+
448
+ if exclude_defaults and self._is_default_value(field, value):
449
+ continue
450
+
451
+ # Convert nested Typed objects
452
+ if hasattr(value, "to_dict"):
453
+ result[field_name] = value.to_dict(
454
+ exclude_none=exclude_none, exclude_defaults=exclude_defaults
455
+ )
456
+ # Handle lists that might contain Typed objects
457
+ elif isinstance(value, list):
458
+ converted_list = []
459
+ for item in value:
460
+ if hasattr(item, "to_dict"):
461
+ converted_list.append(item.to_dict(exclude_none=exclude_none, exclude_defaults=exclude_defaults))
462
+ elif hasattr(item, "value"):
463
+ # Handle enums in lists
464
+ try:
465
+ from .autoenum import AutoEnum
466
+ if isinstance(item, AutoEnum):
467
+ converted_list.append(str(item))
468
+ else:
469
+ converted_list.append(item.value)
470
+ except ImportError:
471
+ converted_list.append(item.value if hasattr(item, "value") else str(item))
472
+ else:
473
+ converted_list.append(item)
474
+ result[field_name] = converted_list
475
+ # Handle dictionaries that might contain Typed objects
476
+ elif isinstance(value, dict):
477
+ converted_dict = {}
478
+ for k, v in value.items():
479
+ if hasattr(v, "to_dict"):
480
+ converted_dict[k] = v.to_dict(exclude_none=exclude_none, exclude_defaults=exclude_defaults)
481
+ elif hasattr(v, "value"):
482
+ # Handle enums in dict values
483
+ try:
484
+ from .autoenum import AutoEnum
485
+ if isinstance(v, AutoEnum):
486
+ converted_dict[k] = str(v)
487
+ else:
488
+ converted_dict[k] = v.value
489
+ except ImportError:
490
+ converted_dict[k] = v.value if hasattr(v, "value") else str(v)
491
+ else:
492
+ converted_dict[k] = v
493
+ result[field_name] = converted_dict
494
+ # Convert enums to their value (AutoEnum and other enums)
495
+ elif hasattr(value, "value"):
496
+ try:
497
+ # Try to import from morphic package
498
+ from .autoenum import AutoEnum
499
+
500
+ if isinstance(value, AutoEnum):
501
+ # AutoEnum stores the name as the value, just use str() representation
502
+ result[field_name] = str(value)
503
+ else:
504
+ result[field_name] = value.value
505
+ except ImportError:
506
+ result[field_name] = value.value if hasattr(value, "value") else str(value)
507
+ else:
508
+ result[field_name] = value
509
+
510
+ return result
511
+
512
+ def _is_default_value(self, field: Field, value: Any) -> bool:
513
+ """Check if a value is the default value for a field."""
514
+ if field.default is not MISSING:
515
+ return value == field.default
516
+ elif field.default_factory is not MISSING:
517
+ return value == field.default_factory()
518
+
519
+ return False
520
+
521
+ def copy(self: T, **changes) -> T:
522
+ """Create a copy of this instance with optional field changes.
523
+
524
+ Args:
525
+ **changes: Field changes to apply to the copy
526
+
527
+ Returns:
528
+ New instance with changes applied
529
+ """
530
+ current_dict = self.to_dict()
531
+ current_dict.update(changes)
532
+ return self.__class__.from_dict(current_dict)
533
+
534
+ def validate(self) -> None:
535
+ """Override in subclasses to add custom validation logic.
536
+
537
+ This method is called automatically after instance creation.
538
+ """
539
+ pass
540
+
541
+ def _convert_field_values(self) -> None:
542
+ """Convert field values to appropriate types before validation.
543
+
544
+ This enables automatic conversion of dictionaries to nested Typed objects
545
+ and basic type conversion (like string to int) in the regular constructor.
546
+ This makes the constructor behavior consistent with from_dict().
547
+ """
548
+ field_info = self._get_field_info()
549
+
550
+ for field_name, field in field_info.items():
551
+ current_value = getattr(self, field_name)
552
+
553
+ # Use full conversion logic (same as from_dict)
554
+ converted_value = self._convert_value(field, current_value)
555
+
556
+ # Update the field value if it was converted
557
+ if converted_value is not current_value:
558
+ setattr(self, field_name, converted_value)
559
+
560
+ @classmethod
561
+ def _convert_value_strict(cls, field: Field, value: Any) -> Any:
562
+ """Convert a value with strict rules (only nested Typeds and enums).
563
+
564
+ This is used in the constructor to maintain strict type validation while
565
+ still allowing dict-to-Typed conversion for nested objects.
566
+ """
567
+ if value is None:
568
+ return None
569
+
570
+ field_type = field.type
571
+
572
+ # Handle Union types (e.g., Optional[Typed])
573
+ if get_origin(field_type) is Union:
574
+ union_args = get_args(field_type)
575
+ # Try each type in the union
576
+ for arg_type in union_args:
577
+ if arg_type is type(None):
578
+ continue
579
+ try:
580
+ return cls._convert_single_type_strict(arg_type, value)
581
+ except (ValueError, TypeError):
582
+ continue
583
+ # If no conversion worked, return as-is
584
+ return value
585
+
586
+ return cls._convert_single_type_strict(field_type, value)
587
+
588
+ @classmethod
589
+ def _convert_single_type_strict(cls, target_type: Type, value: Any) -> Any:
590
+ """Convert value to a single target type with strict rules.
591
+
592
+ Only converts nested Typed objects and enums, not basic types.
593
+ Also handles hierarchical structures like List[Typed] and Dict[str, Typed].
594
+ """
595
+ # Handle generic types (e.g., List[Typed], Dict[str, Typed])
596
+ origin_type = get_origin(target_type)
597
+ if origin_type is not None:
598
+ # Handle List[Typed] or similar list structures
599
+ if origin_type is list:
600
+ type_args = get_args(target_type)
601
+ if type_args and isinstance(value, list):
602
+ element_type = type_args[0]
603
+ # Convert each element if it's a Typed type
604
+ if cls._is_Typed_type(element_type) and all(isinstance(item, dict) for item in value):
605
+ return [element_type(**item) for item in value]
606
+ # Also handle nested conversions for existing Typed instances
607
+ elif cls._is_Typed_type(element_type):
608
+ converted_items = []
609
+ for item in value:
610
+ if isinstance(item, dict):
611
+ converted_items.append(element_type(**item))
612
+ else:
613
+ converted_items.append(item)
614
+ return converted_items
615
+ return value
616
+
617
+ # Handle Dict[str, Typed] or similar dict structures
618
+ elif origin_type is dict:
619
+ type_args = get_args(target_type)
620
+ if len(type_args) >= 2 and isinstance(value, dict):
621
+ value_type = type_args[1] # Second type arg is the value type
622
+ # Convert dict values if they're Typed types
623
+ if cls._is_Typed_type(value_type):
624
+ converted_dict = {}
625
+ for k, v in value.items():
626
+ if isinstance(v, dict):
627
+ converted_dict[k] = value_type(**v)
628
+ else:
629
+ converted_dict[k] = v
630
+ return converted_dict
631
+ return value
632
+
633
+ # For other generic types, don't try to convert - return as-is
634
+ # Validation will handle checking the container type
635
+ return value
636
+
637
+ # Handle direct type match
638
+ try:
639
+ if isinstance(value, target_type):
640
+ return value
641
+ except TypeError:
642
+ # Some types (like complex generics) can't be used with isinstance
643
+ # Return as-is and let validation handle it
644
+ return value
645
+
646
+ # Handle AutoEnum conversion (if available in morphic)
647
+ if hasattr(target_type, "__bases__"):
648
+ try:
649
+ # Try to import from morphic package
650
+ from .autoenum import AutoEnum
651
+
652
+ if any(
653
+ issubclass(base, AutoEnum) for base in target_type.__bases__ if isinstance(base, type)
654
+ ):
655
+ if isinstance(value, str):
656
+ # Try conversion, but don't raise errors - let validation handle it
657
+ try:
658
+ return target_type.from_str(value)
659
+ except ValueError:
660
+ # Invalid enum value - return as-is for validation to catch
661
+ return value
662
+ return value
663
+ except ImportError:
664
+ pass
665
+
666
+ # Handle other enum types by looking for common enum characteristics
667
+ if (
668
+ hasattr(target_type, "_value_")
669
+ or hasattr(target_type, "value")
670
+ or any(hasattr(base, "_value_") for base in target_type.__bases__ if isinstance(base, type))
671
+ ):
672
+ if isinstance(value, str):
673
+ try:
674
+ return target_type(value)
675
+ except ValueError:
676
+ # Invalid enum value - return as-is for validation to catch
677
+ return value
678
+ return value
679
+
680
+ # Handle nested Typed objects
681
+ if hasattr(target_type, "__bases__") and any(
682
+ issubclass(base, Typed) for base in target_type.__bases__ if isinstance(base, type)
683
+ ):
684
+ if isinstance(value, dict):
685
+ # Create nested object directly to maintain strict validation
686
+ # The nested object's own validation will catch type errors
687
+ return target_type(**value)
688
+ return value
689
+
690
+ # Do NOT convert basic types (int, float, str, bool) - maintain strict validation
691
+ # Return value as-is and let validation catch type mismatches
692
+ return value
693
+
694
+ @classmethod
695
+ def _is_Typed_type(cls, target_type: Type) -> bool:
696
+ """Check if a type is a Typed subclass.
697
+
698
+ Args:
699
+ target_type: The type to check
700
+
701
+ Returns:
702
+ True if target_type is a subclass of Typed, False otherwise
703
+
704
+ Note:
705
+ This method safely handles types that may not be classes or may
706
+ not support isinstance/issubclass operations.
707
+ """
708
+ if not hasattr(target_type, "__bases__"):
709
+ return False
710
+ try:
711
+ return any(
712
+ issubclass(base, Typed) for base in target_type.__bases__ if isinstance(base, type)
713
+ )
714
+ except TypeError:
715
+ return False
716
+
717
+ @classmethod
718
+ def _validate_and_convert_class_defaults(cls) -> None:
719
+ """Validate and convert default values at class definition time.
720
+
721
+ This method is called during class creation (in __init_subclass__) to:
722
+ 1. Convert default values to appropriate types (e.g., "25" -> 25 for int fields)
723
+ 2. Handle hierarchical defaults (convert dicts to Typed objects)
724
+ 3. Convert mutable defaults to default_factory to prevent shared mutable state
725
+ 4. Validate that converted defaults comply with their type annotations
726
+ 5. Provide clear error messages for invalid defaults
727
+
728
+ The validation happens before dataclass transformation to ensure that
729
+ dataclass receives properly typed default values.
730
+
731
+ Raises:
732
+ TypeError: If a default value cannot be converted or is invalid for its type
733
+
734
+ Examples:
735
+ ```python
736
+ class Config(Typed):
737
+ port: int = "8080" # Converted to int(8080)
738
+ users: List[User] = [{"name": "admin"}] # Converted to default_factory
739
+
740
+ # Raises TypeError at class definition:
741
+ class BadConfig(Typed):
742
+ count: int = "invalid" # Cannot convert to int
743
+ ```
744
+ """
745
+ # Get type hints directly from the class
746
+ if not hasattr(cls, '__annotations__'):
747
+ return
748
+
749
+ annotations = cls.__annotations__
750
+ for field_name, field_type in annotations.items():
751
+ # Check if there's a class attribute with a default value
752
+ if hasattr(cls, field_name):
753
+ default_value = getattr(cls, field_name)
754
+
755
+ # Skip if this looks like a Field object or method
756
+ if hasattr(default_value, '__call__') or str(type(default_value)).startswith('<class \'dataclasses.'):
757
+ continue
758
+
759
+ try:
760
+ # Create a mock field object for conversion
761
+ mock_field = type('MockField', (), {'type': field_type})()
762
+
763
+ # Try to convert the default value
764
+ converted_default = cls._convert_value(mock_field, default_value)
765
+
766
+ # Handle mutable defaults - convert to default_factory
767
+ # Include Typed objects as they are also mutable
768
+ is_mutable = isinstance(converted_default, (list, dict, set)) or (
769
+ hasattr(converted_default, '__dict__') and
770
+ hasattr(converted_default.__class__, '__bases__') and
771
+ any(issubclass(base, Typed) for base in converted_default.__class__.__bases__ if isinstance(base, type))
772
+ )
773
+
774
+ if is_mutable:
775
+ # Import field here to avoid circular imports
776
+ from dataclasses import field
777
+
778
+ # Create a factory function that returns a copy of the converted default
779
+ def make_factory(value):
780
+ def factory():
781
+ if isinstance(value, list):
782
+ return value.copy()
783
+ elif isinstance(value, dict):
784
+ return value.copy()
785
+ elif isinstance(value, set):
786
+ return value.copy()
787
+ elif hasattr(value, 'copy'):
788
+ # For Typed objects that might have a copy method
789
+ try:
790
+ return value.copy()
791
+ except (AttributeError, TypeError):
792
+ # If copy fails, create a new instance from dict
793
+ return value.__class__.from_dict(value.to_dict())
794
+ else:
795
+ # For other Typed objects, create new instance
796
+ if hasattr(value, 'to_dict') and hasattr(value.__class__, 'from_dict'):
797
+ return value.__class__.from_dict(value.to_dict())
798
+ return value
799
+ return factory
800
+
801
+ # Replace the class attribute with a field() using default_factory
802
+ setattr(cls, field_name, field(default_factory=make_factory(converted_default)))
803
+ else:
804
+ # Update the class attribute with the converted value for immutable types
805
+ if converted_default is not default_value:
806
+ setattr(cls, field_name, converted_default)
807
+
808
+ # Basic type validation - create temp instance for validation methods
809
+ temp_instance = object.__new__(cls)
810
+ temp_instance._Typed__dict = {} # Initialize to avoid AttributeError
811
+
812
+ # Special handling for None values with Optional types
813
+ if converted_default is None and temp_instance._type_allows_none(field_type):
814
+ # None is valid for Optional types, skip validation
815
+ pass
816
+ elif not temp_instance._is_value_valid_for_type(converted_default, field_type):
817
+ raise TypeError(
818
+ f"Default value for field '{field_name}' in class '{cls.__name__}' "
819
+ f"expected type {field_type}, got {type(converted_default).__name__} "
820
+ f"with value {converted_default!r}"
821
+ )
822
+ except Exception as e:
823
+ # Re-raise with more context
824
+ raise TypeError(
825
+ f"Invalid default value for field '{field_name}' in class '{cls.__name__}': {e}"
826
+ ) from e
827
+
828
+ @classmethod
829
+ def _validate_default_factories(cls) -> None:
830
+ """Validate default_factory callables after dataclass transformation.
831
+
832
+ This method ensures that all default_factory values are callable.
833
+ It's called after dataclass transformation because some default_factory
834
+ values may be created automatically during mutable default conversion.
835
+
836
+ Raises:
837
+ TypeError: If a default_factory is not callable
838
+
839
+ Note:
840
+ This validation cannot check the return type of default_factory
841
+ functions since they are called at instance creation time, not
842
+ class definition time.
843
+ """
844
+ if cls not in cls._field_cache:
845
+ return
846
+
847
+ field_info = cls._field_cache[cls]
848
+ for field_name, field in field_info.items():
849
+ # Check default_factory values
850
+ if field.default_factory is not MISSING:
851
+ if not callable(field.default_factory):
852
+ raise TypeError(
853
+ f"default_factory for field '{field_name}' in class '{cls.__name__}' "
854
+ f"must be callable, got {type(field.default_factory).__name__}"
855
+ )
856
+
857
+ def _validate_types(self) -> None:
858
+ """Validate that all field values match their type annotations."""
859
+ field_info = self._get_field_info()
860
+
861
+ for field_name, field in field_info.items():
862
+ value = getattr(self, field_name)
863
+ field_type = field.type
864
+
865
+ # Skip validation for None values if the field type allows None
866
+ if value is None:
867
+ if self._type_allows_none(field_type):
868
+ continue
869
+ else:
870
+ raise TypeError(f"Field '{field_name}' cannot be None, expected {field_type}")
871
+
872
+ # Validate the value against the field type
873
+ if not self._is_value_valid_for_type(value, field_type):
874
+ raise TypeError(
875
+ f"Field '{field_name}' expected type {field_type}, got {type(value).__name__} with value {value!r}"
876
+ )
877
+
878
+ def _type_allows_none(self, field_type: Type) -> bool:
879
+ """Check if a type annotation allows None values."""
880
+ # Handle Union types (e.g., Optional[int] = Union[int, None])
881
+ if get_origin(field_type) is Union:
882
+ union_args = get_args(field_type)
883
+ return type(None) in union_args
884
+
885
+ return False
886
+
887
+ def _is_value_valid_for_type(self, value: Any, field_type: Type) -> bool:
888
+ """Check if a value is valid for the given type annotation."""
889
+ # Handle Union types (e.g., Optional[int] = Union[int, None])
890
+ if get_origin(field_type) is Union:
891
+ union_args = get_args(field_type)
892
+ # Value is valid if it matches any type in the union (except None, handled separately)
893
+ for arg_type in union_args:
894
+ if arg_type is type(None):
895
+ continue
896
+ if self._is_value_valid_for_single_type(value, arg_type):
897
+ return True
898
+ return False
899
+
900
+ return self._is_value_valid_for_single_type(value, field_type)
901
+
902
+ def _is_value_valid_for_single_type(self, value: Any, target_type: Type) -> bool:
903
+ """Check if a value is valid for a single target type."""
904
+ # Handle generic types (e.g., List[str], Dict[str, int])
905
+ origin_type = get_origin(target_type)
906
+ if origin_type is not None:
907
+ # For generic types, check if value is instance of the origin type
908
+ # We don't check the type parameters for simplicity - just the container type
909
+ try:
910
+ return isinstance(value, origin_type)
911
+ except TypeError:
912
+ # Some types might not work with isinstance, fallback to basic checks
913
+ return False
914
+
915
+ # Handle direct type match
916
+ try:
917
+ if isinstance(value, target_type):
918
+ return True
919
+ except TypeError:
920
+ # Some types (like complex generics) can't be used with isinstance
921
+ # In this case, we'll be permissive and allow the value
922
+ return True
923
+
924
+ # Handle AutoEnum types (if available in morphic)
925
+ if hasattr(target_type, "__bases__"):
926
+ try:
927
+ # Try to import from morphic package
928
+ from .autoenum import AutoEnum
929
+
930
+ if any(
931
+ issubclass(base, AutoEnum) for base in target_type.__bases__ if isinstance(base, type)
932
+ ):
933
+ return isinstance(value, target_type)
934
+ except ImportError:
935
+ pass
936
+
937
+ # Handle other enum types
938
+ if (
939
+ hasattr(target_type, "_value_")
940
+ or hasattr(target_type, "value")
941
+ or any(hasattr(base, "_value_") for base in target_type.__bases__ if isinstance(base, type))
942
+ ):
943
+ return isinstance(value, target_type)
944
+
945
+ # Handle nested Typed objects
946
+ if hasattr(target_type, "__bases__") and any(
947
+ issubclass(base, Typed) for base in target_type.__bases__ if isinstance(base, type)
948
+ ):
949
+ return isinstance(value, target_type)
950
+
951
+ # For basic types, only allow exact type matches for strict validation
952
+ # This means str won't auto-convert to int, etc.
953
+ try:
954
+ return isinstance(value, target_type)
955
+ except TypeError:
956
+ # If isinstance fails, be permissive
957
+ return True
958
+
959
+ def __repr__(self) -> str:
960
+ """Enhanced repr that shows all fields clearly."""
961
+ field_info = self._get_field_info()
962
+ field_strs = []
963
+
964
+ for field_name in field_info:
965
+ value = getattr(self, field_name)
966
+ field_strs.append(f"{field_name}={value!r}")
967
+
968
+ return f"{self.__class__.__name__}({', '.join(field_strs)})"
969
+
970
+
971
+ class ValidationError(ValueError):
972
+ """Exception raised when function argument validation fails."""
973
+ pass
974
+
975
+
976
+ def validate(
977
+ func: Callable = None,
978
+ /,
979
+ *,
980
+ validate_return: bool = False
981
+ ) -> Callable:
982
+ """Decorator that validates function arguments using type annotations.
983
+
984
+ This decorator provides Pydantic-like validation for function arguments,
985
+ using the same type conversion and validation system as Typed.
986
+
987
+ Args:
988
+ func: The function to decorate (when used as @validate)
989
+ validate_return: Whether to validate the return value. Default: False
990
+
991
+ Returns:
992
+ Decorated function with argument validation
993
+
994
+ Raises:
995
+ ValidationError: When function arguments don't match their type annotations
996
+
997
+ Examples:
998
+ ```python
999
+ from morphic import Typed, validate
1000
+
1001
+ # Basic usage
1002
+ @validate
1003
+ def add_numbers(a: int, b: int) -> int:
1004
+ return a + b
1005
+
1006
+ result = add_numbers("5", "10") # Strings converted to ints: 15
1007
+
1008
+ # With return validation
1009
+ @validate(validate_return=True)
1010
+ def process_data(data: Any, count: int = 10) -> str:
1011
+ return f"Processed {count} items: {data}"
1012
+
1013
+ # With return validation
1014
+ @validate(validate_return=True)
1015
+ def get_user_name(user_id: int) -> str:
1016
+ return f"user_{user_id}" # Return value validated as str
1017
+
1018
+ # With Typed types
1019
+ class User(Typed):
1020
+ name: str
1021
+ age: int
1022
+
1023
+ @validate
1024
+ def create_user(user_data: User) -> User:
1025
+ return user_data
1026
+
1027
+ # Dict automatically converted to User object
1028
+ user = create_user({"name": "John", "age": 30})
1029
+ assert isinstance(user, User)
1030
+ ```
1031
+
1032
+ Configuration:
1033
+ This decorator always uses the following configuration:
1034
+ - arbitrary_types_allowed: True - allows any type annotations
1035
+ - validate_default: True - validates default parameter values at decoration time
1036
+
1037
+ Features:
1038
+ - Automatic type conversion (e.g., "5" -> 5 for int parameters)
1039
+ - Typed object creation from dictionaries
1040
+ - AutoEnum string conversion with fuzzy matching
1041
+ - List and dict conversion for nested structures
1042
+ - Union type support (tries each type in order)
1043
+ - Optional parameter validation
1044
+ - Default value validation (if validate_default=True)
1045
+ - Return value validation (if validate_return=True)
1046
+ - Preserves original function signature and metadata
1047
+ - Works with both sync and async functions
1048
+
1049
+ Performance Notes:
1050
+ - Validation overhead occurs on every function call
1051
+ - Type conversion is cached for repeated calls with same types
1052
+ - Original function accessible via decorated_func.raw_function
1053
+ """
1054
+ # Fixed configuration with pydantic-compatible settings
1055
+ config = {
1056
+ 'arbitrary_types_allowed': True,
1057
+ 'validate_default': True
1058
+ }
1059
+
1060
+ def decorator(f: Callable) -> Callable:
1061
+ # Get function signature for parameter validation
1062
+ sig = inspect.signature(f)
1063
+
1064
+ # Validate default values (always enabled)
1065
+ _validate_function_defaults(f, sig)
1066
+
1067
+ @wraps(f)
1068
+ def wrapper(*args, **kwargs):
1069
+ # Bind arguments to parameters
1070
+ try:
1071
+ bound_args = sig.bind(*args, **kwargs)
1072
+ bound_args.apply_defaults()
1073
+ except TypeError as e:
1074
+ raise ValidationError(f"Invalid function arguments: {e}") from e
1075
+
1076
+ # Validate and convert each argument
1077
+ validated_args = {}
1078
+ for param_name, value in bound_args.arguments.items():
1079
+ param = sig.parameters[param_name]
1080
+
1081
+ # Skip validation for *args and **kwargs parameters
1082
+ if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
1083
+ validated_args[param_name] = value
1084
+ continue
1085
+
1086
+ # Skip if no type annotation
1087
+ if param.annotation == inspect.Parameter.empty:
1088
+ validated_args[param_name] = value
1089
+ continue
1090
+
1091
+ # Create a mock field for the Typed conversion system
1092
+ mock_field = type('MockField', (), {'type': param.annotation})()
1093
+
1094
+ try:
1095
+ # Use Typed's type conversion system
1096
+ converted_value = Typed._convert_value(mock_field, value)
1097
+
1098
+ # Validate the converted value (always using arbitrary_types_allowed=True)
1099
+ if not _is_value_valid_for_annotation(converted_value, param.annotation):
1100
+ raise ValidationError(
1101
+ f"Argument '{param_name}' expected type {param.annotation}, "
1102
+ f"got {type(converted_value).__name__} with value {converted_value!r}"
1103
+ )
1104
+
1105
+ validated_args[param_name] = converted_value
1106
+
1107
+ except Exception as e:
1108
+ if isinstance(e, ValidationError):
1109
+ raise
1110
+ raise ValidationError(
1111
+ f"Failed to validate argument '{param_name}': {e}"
1112
+ ) from e
1113
+
1114
+ # Call the original function
1115
+ result = f(**validated_args)
1116
+
1117
+ # Validate return value if requested
1118
+ if validate_return and sig.return_annotation != inspect.Parameter.empty:
1119
+ try:
1120
+ # For return validation, we're stricter - don't do automatic conversion
1121
+ # Just validate that the return value matches the expected type
1122
+ if not _is_value_valid_for_annotation(result, sig.return_annotation):
1123
+ raise ValidationError(
1124
+ f"Return value expected type {sig.return_annotation}, "
1125
+ f"got {type(result).__name__} with value {result!r}"
1126
+ )
1127
+ except Exception as e:
1128
+ if isinstance(e, ValidationError):
1129
+ raise
1130
+ raise ValidationError(f"Failed to validate return value: {e}") from e
1131
+
1132
+ return result
1133
+
1134
+ # Store original function for access
1135
+ wrapper.raw_function = f
1136
+ wrapper.__signature__ = sig
1137
+
1138
+ return wrapper
1139
+
1140
+ # Handle both @validate and @validate(...) usage
1141
+ if func is None:
1142
+ return decorator
1143
+ else:
1144
+ return decorator(func)
1145
+
1146
+
1147
+ def _validate_function_defaults(func: Callable, sig: inspect.Signature) -> None:
1148
+ """Validate default parameter values against their type annotations."""
1149
+ for param_name, param in sig.parameters.items():
1150
+ # Skip if no default value or no annotation
1151
+ if param.default == inspect.Parameter.empty or param.annotation == inspect.Parameter.empty:
1152
+ continue
1153
+
1154
+ # Skip *args and **kwargs
1155
+ if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
1156
+ continue
1157
+
1158
+ try:
1159
+ # Create mock field for validation
1160
+ mock_field = type('MockField', (), {'type': param.annotation})()
1161
+
1162
+ # Use stricter validation for default parameters
1163
+ converted_default = _convert_and_validate_default(mock_field, param.default, param.annotation)
1164
+
1165
+ # Additional validation check
1166
+ if not _is_value_valid_for_annotation(converted_default, param.annotation):
1167
+ raise ValidationError(
1168
+ f"Default value for parameter '{param_name}' in function '{func.__name__}' "
1169
+ f"expected type {param.annotation}, got {type(converted_default).__name__} "
1170
+ f"with value {converted_default!r}"
1171
+ )
1172
+
1173
+ except Exception as e:
1174
+ if isinstance(e, ValidationError):
1175
+ raise
1176
+ raise ValidationError(
1177
+ f"Invalid default value for parameter '{param_name}' in function '{func.__name__}': {e}"
1178
+ ) from e
1179
+
1180
+
1181
+ def _convert_and_validate_default(mock_field: Any, value: Any, annotation: Type) -> Any:
1182
+ """Convert and validate default parameter values with strict validation.
1183
+
1184
+ This function is stricter than Typed._convert_value and will raise
1185
+ ValidationError for any conversion that fails, ensuring default values
1186
+ are properly validated at decoration time.
1187
+ """
1188
+ if value is None:
1189
+ # Handle None for Optional types
1190
+ if get_origin(annotation) is Union:
1191
+ union_args = get_args(annotation)
1192
+ if type(None) in union_args:
1193
+ return None
1194
+ else:
1195
+ raise ValidationError(f"None not allowed for non-Optional type {annotation}")
1196
+ else:
1197
+ raise ValidationError(f"None not allowed for type {annotation}")
1198
+
1199
+ # Handle Union types
1200
+ if get_origin(annotation) is Union:
1201
+ union_args = get_args(annotation)
1202
+ last_error = None
1203
+ # Try each type in the union
1204
+ for arg_type in union_args:
1205
+ if arg_type is type(None):
1206
+ continue
1207
+ try:
1208
+ return _convert_and_validate_default_single_type(value, arg_type)
1209
+ except (ValueError, TypeError, ValidationError) as e:
1210
+ last_error = e
1211
+ continue
1212
+ # If no conversion worked, raise the last error
1213
+ raise ValidationError(f"Could not convert {value!r} to any type in {annotation}") from last_error
1214
+
1215
+ return _convert_and_validate_default_single_type(value, annotation)
1216
+
1217
+
1218
+ def _convert_and_validate_default_single_type(value: Any, target_type: Type) -> Any:
1219
+ """Convert value to a single target type with strict validation for defaults."""
1220
+ # Handle generic types first
1221
+ origin_type = get_origin(target_type)
1222
+ if origin_type is not None:
1223
+ # Handle List[Type]
1224
+ if origin_type is list:
1225
+ if not isinstance(value, (list, tuple)):
1226
+ raise ValidationError(f"Expected list for {target_type}, got {type(value).__name__}")
1227
+
1228
+ type_args = get_args(target_type)
1229
+ if type_args:
1230
+ element_type = type_args[0]
1231
+ converted_items = []
1232
+ for i, item in enumerate(value):
1233
+ try:
1234
+ converted_item = _convert_and_validate_default_single_type(item, element_type)
1235
+ converted_items.append(converted_item)
1236
+ except Exception as e:
1237
+ raise ValidationError(f"Invalid list element at index {i}: {e}") from e
1238
+ return converted_items
1239
+ return list(value)
1240
+
1241
+ # Handle Dict[KeyType, ValueType]
1242
+ elif origin_type is dict:
1243
+ if not isinstance(value, dict):
1244
+ raise ValidationError(f"Expected dict for {target_type}, got {type(value).__name__}")
1245
+
1246
+ type_args = get_args(target_type)
1247
+ if len(type_args) >= 2:
1248
+ key_type, value_type = type_args[0], type_args[1]
1249
+ converted_dict = {}
1250
+ for k, v in value.items():
1251
+ try:
1252
+ converted_key = _convert_and_validate_default_single_type(k, key_type)
1253
+ converted_value = _convert_and_validate_default_single_type(v, value_type)
1254
+ converted_dict[converted_key] = converted_value
1255
+ except Exception as e:
1256
+ raise ValidationError(f"Invalid dict entry {k!r}: {e}") from e
1257
+ return converted_dict
1258
+ return dict(value)
1259
+
1260
+ # For other generic types, return as-is
1261
+ return value
1262
+
1263
+ # If already the right type, return as-is
1264
+ try:
1265
+ if isinstance(value, target_type):
1266
+ return value
1267
+ except TypeError:
1268
+ # Some types can't be used with isinstance
1269
+ pass
1270
+
1271
+ # Handle Typed types
1272
+ if hasattr(target_type, "__bases__") and any(
1273
+ issubclass(base, Typed) for base in target_type.__bases__ if isinstance(base, type)
1274
+ ):
1275
+ if isinstance(value, dict):
1276
+ try:
1277
+ return target_type.from_dict(value)
1278
+ except Exception as e:
1279
+ raise ValidationError(f"Could not create {target_type.__name__} from dict: {e}") from e
1280
+ return value
1281
+
1282
+ # Handle basic type conversions with strict validation
1283
+ if target_type in (int, float, str, bool):
1284
+ try:
1285
+ if target_type is bool and isinstance(value, str):
1286
+ # Handle string to bool conversion more strictly
1287
+ lower_val = value.lower()
1288
+ if lower_val in ('true', '1', 'yes', 'on'):
1289
+ return True
1290
+ elif lower_val in ('false', '0', 'no', 'off', ''):
1291
+ return False
1292
+ else:
1293
+ raise ValueError(f"Cannot convert '{value}' to bool")
1294
+ else:
1295
+ converted = target_type(value)
1296
+ # Additional validation for string to number conversion
1297
+ if target_type in (int, float) and isinstance(value, str):
1298
+ # Make sure the conversion actually makes sense
1299
+ if str(converted) != str(value).strip():
1300
+ # Allow for float precision differences
1301
+ if target_type is float:
1302
+ try:
1303
+ if abs(float(value) - converted) > 1e-10:
1304
+ raise ValueError(f"Conversion changed value: '{value}' -> {converted}")
1305
+ except (ValueError, TypeError):
1306
+ raise ValueError(f"Cannot convert '{value}' to {target_type.__name__}")
1307
+ return converted
1308
+ except (ValueError, TypeError) as e:
1309
+ raise ValidationError(f"Cannot convert {value!r} to {target_type.__name__}: {e}") from e
1310
+
1311
+ # For complex types we can't handle, return as-is and let validation catch issues
1312
+ return value
1313
+
1314
+
1315
+ def _is_value_valid_for_annotation(value: Any, annotation: Type) -> bool:
1316
+ """Check if a value is valid for a type annotation (always with arbitrary_types_allowed=True)."""
1317
+ # Handle None for Optional types
1318
+ if value is None:
1319
+ if get_origin(annotation) is Union:
1320
+ union_args = get_args(annotation)
1321
+ return type(None) in union_args
1322
+ return False
1323
+
1324
+ # Use Typed's validation logic (with arbitrary types allowed)
1325
+ temp_instance = object.__new__(Typed)
1326
+ temp_instance._Typed__dict = {} # Initialize to avoid AttributeError
1327
+ return temp_instance._is_value_valid_for_type(value, annotation)