1919import shutil
2020import sys
2121import warnings
22+ from collections .abc import Mapping
2223from copy import copy , deepcopy
2324from dataclasses import dataclass
2425from pathlib import Path
2526from textwrap import indent
2627from typing import (
28+ AbstractSet ,
2729 Any ,
2830 Callable ,
2931 get_args ,
@@ -762,19 +764,30 @@ class is created. Without effect if :attr:`obj` is a type.
762764 """
763765 from dataclasses import asdict , make_dataclass
764766
767+ def _field_specs (source , type_hints ):
768+ return [
769+ (
770+ field .name ,
771+ type_hints .get (field .name , field .type ),
772+ copy (field ),
773+ )
774+ for field in source .__dataclass_fields__ .values ()
775+ ]
776+
765777 if isinstance (obj , type ):
766778 if is_tensorclass (obj ):
767779 return obj
780+ type_hints = get_type_hints (obj )
768781 if not inplace :
769782 cls = make_dataclass (
770783 obj .__name__ + "_tc" ,
771- fields = obj . __dataclass_fields__ ,
784+ fields = _field_specs ( obj , type_hints ) ,
772785 bases = obj .__bases__ ,
773786 )
774787 else :
775788 cls = obj
776789 clz = _tensorclass (cls , frozen = frozen , shadow = shadow , tensor_only = tensor_only )
777- clz . _type_hints = get_type_hints ( obj )
790+ _set_tensorclass_type_hints ( clz , type_hints )
778791 clz ._autocast = autocast
779792 clz ._nocast = nocast
780793 clz ._shadow = shadow
@@ -790,20 +803,37 @@ class is created. Without effect if :attr:`obj` is a type.
790803 raise TypeError (
791804 "tensor_only and autocast or nocast are exclusive features."
792805 )
806+ try :
807+ type_hints = get_type_hints (type (obj ))
808+ except (NameError , TypeError ):
809+ # Preserve the existing fallback for unresolved annotations.
810+ type_hints = None
793811 clz = _tensorclass (
794- make_dataclass (name , fields = obj .__dataclass_fields__ ),
812+ make_dataclass (
813+ name ,
814+ fields = _field_specs (type (obj ), type_hints or {}),
815+ ),
795816 frozen = frozen ,
796817 shadow = shadow ,
797818 tensor_only = tensor_only ,
798819 )
820+ if type_hints is not None :
821+ _set_tensorclass_type_hints (clz , type_hints )
799822 clz ._autocast = autocast
800823 clz ._nocast = nocast
801824 clz ._shadow = shadow
802825 clz ._frozen = frozen
803826 clz ._tensor_only = tensor_only
804827 else :
805828 clz = dest_cls
806- result = clz (** asdict (obj ), batch_size = batch_size , device = device )
829+ data = asdict (obj )
830+ if clz ._tensor_only :
831+ # ``asdict`` recursively deep-copies values, which would discard the
832+ # identity and lock state of TensorDicts. Keep TensorDict-annotated
833+ # fields intact and let the tensor-only constructor normalize mappings.
834+ for key in clz ._tensordict_fields :
835+ data [key ] = getattr (obj , key )
836+ result = clz (** data , batch_size = batch_size , device = device )
807837 if auto_batch_size :
808838 if batch_size is not None :
809839 raise TypeError (
@@ -1442,10 +1472,15 @@ def wrapper(
14421472 _td_dict = _td ._tensordict
14431473 _non_td = self ._non_tensordict
14441474 _validate = _td ._validate_value
1475+ _tensordict_fields = type (self )._tensordict_fields
14451476 for key , value in kwargs .items ():
14461477 if value is None :
14471478 _non_td [key ] = None
14481479 else :
1480+ if _tensordict_fields :
1481+ value , _ = _convert_mapping_for_field (
1482+ key , value , _tensordict_fields
1483+ )
14491484 _td_dict [key ] = _validate (
14501485 value , check_shape = True , non_blocking = False
14511486 )
@@ -1552,12 +1587,14 @@ def get_parent_locals(cls, localns=localns):
15521587
15531588 globalns = None
15541589
1590+ cls ._tensordict_fields = frozenset ()
15551591 try :
1556- cls . _type_hints = get_type_hints (
1592+ type_hints = get_type_hints (
15571593 cls ,
15581594 localns = localns ,
15591595 # globalns=globals(),
15601596 )
1597+ _set_tensorclass_type_hints (cls , type_hints )
15611598 if tensor_only :
15621599
15631600 def is_tensor_or_optional_tensor (type_hint ):
@@ -1632,6 +1669,52 @@ def is_tensor_or_optional_tensor(type_hint):
16321669 cls ._type_hints = None
16331670
16341671
1672+ def _is_tensordict_annotation (type_hint : Any ) -> bool :
1673+ """Return whether an annotation contains a TensorDictBase type."""
1674+ origin = get_origin (type_hint )
1675+ if origin in (Union , UnionType ):
1676+ return any (_is_tensordict_annotation (arg ) for arg in get_args (type_hint ))
1677+ if origin is not None :
1678+ type_hint = origin
1679+ return isinstance (type_hint , type ) and issubclass (type_hint , TensorDictBase )
1680+
1681+
1682+ def _set_tensorclass_type_hints (cls : type , type_hints : dict [str , Any ]) -> None :
1683+ """Store resolved hints and cache fields with TensorDict-like annotations."""
1684+ cls ._tensordict_fields = frozenset (
1685+ key
1686+ for key , val in type_hints .items ()
1687+ if key in cls .__expected_keys__ and _is_tensordict_annotation (val )
1688+ )
1689+ cls ._type_hints = type_hints
1690+
1691+
1692+ def _normalize_nested_mapping (
1693+ mapping : Mapping [NestedKey , Any ],
1694+ ) -> dict [NestedKey , Any ] | TensorDictBase :
1695+ """Materialize nested mappings while preserving TensorDictBase values."""
1696+ if isinstance (mapping , TensorDictBase ):
1697+ return mapping
1698+ return {
1699+ key : _normalize_nested_mapping (value ) if isinstance (value , Mapping ) else value
1700+ for key , value in mapping .items ()
1701+ }
1702+
1703+
1704+ def _convert_mapping_for_field (
1705+ key : str , value : Any , tensordict_fields : AbstractSet [str ]
1706+ ) -> tuple [Any , bool ]:
1707+ """Normalize a Mapping assigned to a TensorDict-annotated field.
1708+
1709+ Returns the normalized value and whether the field/value pair required
1710+ Mapping handling. Existing TensorDict values are returned unchanged.
1711+ """
1712+ is_mapping_field = key in tensordict_fields and isinstance (value , Mapping )
1713+ if is_mapping_field :
1714+ value = _normalize_nested_mapping (value )
1715+ return value , is_mapping_field
1716+
1717+
16351718def _from_tensordict (
16361719 cls ,
16371720 tensordict : TensorDictBase ,
@@ -2023,6 +2106,7 @@ def _setattr_tensor_only(self, key: str, value: Any) -> None: # noqa: D417
20232106 if value is None :
20242107 self ._non_tensordict [key ] = None
20252108 return
2109+ value , _ = _convert_mapping_for_field (key , value , type (self )._tensordict_fields )
20262110 out = self ._set_str (key , value , inplace = False , validated = False , ignore_lock = False )
20272111 if out is not self :
20282112 raise RuntimeError (
@@ -2654,6 +2738,13 @@ def set_tensor(
26542738 def _is_castable (datatype ):
26552739 return issubclass (datatype , (int , float , np .ndarray ))
26562740
2741+ if cls ._tensor_only :
2742+ value , is_mapping_field = _convert_mapping_for_field (
2743+ key , value , cls ._tensordict_fields
2744+ )
2745+ if is_mapping_field :
2746+ return set_tensor (value = value )
2747+
26572748 if cls ._autocast :
26582749 type_hints = cls ._type_hints
26592750 if type_hints is not None :
0 commit comments