diff --git a/src/unitxt/dataclass.py b/src/unitxt/dataclass.py index afb92bcf0e..2156a6a02e 100644 --- a/src/unitxt/dataclass.py +++ b/src/unitxt/dataclass.py @@ -2,6 +2,7 @@ import dataclasses import functools import inspect +import sys from abc import ABCMeta from inspect import Parameter, Signature from typing import Any, Dict, List, Optional, final @@ -152,6 +153,38 @@ def is_possible_field(field_name, field_value): return True +def _namespace_annotations(attrs): + """Return the annotations defined in a class namespace. + + From Python 3.14 (PEP 649/749) the namespace holds an ``__annotate__`` + function instead of ``__annotations__``. + """ + if "__annotations__" in attrs: + return dict(attrs["__annotations__"]) + if sys.version_info >= (3, 14): + import annotationlib + + annotate = annotationlib.get_annotate_from_class_namespace(attrs) + if annotate is not None: + return dict( + annotationlib.call_annotate_function( + annotate, annotationlib.Format.FORWARDREF + ) + ) + return {} + + +def _own_annotations(cls): + """Return the annotations a class (or the class of an instance) defines itself. + + From Python 3.14 an instance no longer exposes its class' ``__annotations__``. + """ + owner = cls if isinstance(cls, type) else type(cls) + if sys.version_info >= (3, 10): + return inspect.get_annotations(owner) + return owner.__dict__.get("__annotations__", {}) + + def get_fields(cls, attrs): """Get the fields for a class based on its attributes. @@ -165,7 +198,7 @@ def get_fields(cls, attrs): fields = {} for base in cls.__bases__: fields = {**getattr(base, _FIELDS, {}), **fields} - annotations = {**attrs.get("__annotations__", {})} + annotations = _namespace_annotations(attrs) for attr_name, attr_value in attrs.items(): if attr_name not in annotations and is_possible_field(attr_name, attr_value): @@ -381,7 +414,7 @@ def update_init_signature(cls): else: default_value = Parameter.empty - if isinstance(default_value, dataclasses._MISSING_TYPE): + if default_value is dataclasses.MISSING: default_value = Parameter.empty param = Parameter( name, @@ -593,7 +626,7 @@ def to_dict(self, classes: Optional[List] = None, keep_empty: bool = True): else: attributes = [] for cls in classes: - attributes += list(cls.__annotations__.keys()) + attributes += list(_own_annotations(cls).keys()) attributes_dict = { attribute: getattr(self, attribute) for attribute in attributes } diff --git a/src/unitxt/type_utils.py b/src/unitxt/type_utils.py index dd98c656e0..0dfd58c189 100644 --- a/src/unitxt/type_utils.py +++ b/src/unitxt/type_utils.py @@ -3,6 +3,7 @@ import io import itertools import re +import sys import typing from functools import lru_cache from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union @@ -804,6 +805,9 @@ def eval_forward_ref(ref, forward_refs=None): if hasattr(typing, "_eval_type"): # python3.8 & python 3.9 _eval_type = typing._eval_type + if sys.version_info >= (3, 13): + # type_params was added in 3.13 and is required from 3.15 + return _eval_type(ref, globals(), localns, type_params=()) return _eval_type(ref, globals(), localns) if hasattr(ref, "_eval_type"): # python3.6 diff --git a/tests/library/test_dataclass.py b/tests/library/test_dataclass.py index fcbc373198..efb8e4830b 100644 --- a/tests/library/test_dataclass.py +++ b/tests/library/test_dataclass.py @@ -404,3 +404,15 @@ def my_instance_method(self): # self.assertTrue(is_class_method(MyClass.my_instance_method)) # self.assertFalse(is_class_method(MyClass.my_lambda)) # self.assertFalse(is_class_method(MyClass.my_func)) + + def test_fields_from_annotations(self): + class Base(Dataclass): + a: int + b: str = "b" + + class Child(Base): + c: float = 1.0 + + self.assertEqual(fields_names(Child), ["a", "b", "c"]) + child = Child(a=1) + self.assertEqual((child.a, child.b, child.c), (1, "b", 1.0)) diff --git a/tests/library/test_type_utils.py b/tests/library/test_type_utils.py index d500b9b011..282705d077 100644 --- a/tests/library/test_type_utils.py +++ b/tests/library/test_type_utils.py @@ -3,6 +3,7 @@ from unitxt.type_utils import ( UnsupportedTypeError, + eval_forward_ref, format_type_string, infer_type, infer_type_string, @@ -613,3 +614,11 @@ def __verify_type__(cls, object): self.assertTrue(isoftype([1, 2], CustomValidation)) self.assertFalse(isoftype([1], CustomValidation)) + + def test_eval_forward_ref(self): + self.assertEqual( + eval_forward_ref(typing.ForwardRef("List[int]")), typing.List[int] + ) + self.assertEqual( + eval_forward_ref(typing.ForwardRef("Custom"), {"Custom": int}), int + )