And here is a small proof of concept
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, Field, MISSING
import inspect
from typing import Annotated, get_args, get_origin, overload
@overload
def ann_dataclass(cls: None = None, **kwargs) -> Callable[[type], type]:
...
@overload
def ann_dataclass(cls: type, **kwargs) -> type:
...
def ann_dataclass(cls: type | None = None, **kwargs) -> type | Callable[[type], type]:
# actual decorator for when cls is not None
def _annotify(cls: type) -> type:
# Fetch the annotations using latest best practices
# and from __future__ import annotations mey the default in the future
ann = inspect.get_annotations(cls, eval_str=True)
for name, thint in ann.items():
if get_origin(thint) is not Annotated:
continue
# It is an Annotated type hint, see if there is any Field metainfo
_type, *metainfos = get_args(thint)
for metainfo in metainfos:
if not isinstance(metainfo, Field):
continue # not the use case, let it go
try:
default = getattr(cls, name) # check if default value exists
except AttributeError:
pass
else:
# standard dc check for both default and default_factory
if (
default is not MISSING
and metainfo.default_factory is not MISSING
):
raise ValueError(
"cannot specify both default and default_factory"
)
metainfo.default = default # can be safely assigned
# record the actual type defined in Annotated
metainfo.type = _type
# put the "Field" as default value for dataclass decorator
setattr(cls, name, metainfo)
break # only 1 Field to be processed ... break out
return dataclass(cls, **kwargs) # class ready for further processing
if cls is None:
return _annotify # -> Callable[[type], type]
return _annotify(cls) # -> type
# Small test
if __name__ == '__main__':
from dataclasses import field, fields
from typing import ClassVar
@ann_dataclass
class A:
cv: ClassVar[str] = 'classvar'
a: int = 5
b: Annotated[str, field(init=False)] = 'annotated field'
a = A()
print(f"{a.__annotations__ = }")
print(f"{a.cv = }")
print(f"{a.a = }")
print(f"{a.b = }")
print(f"{fields(a) = }")
The output
a.__annotations__ = {'cv': 'ClassVar[str]', 'a': 'int', 'b': 'Annotated[str, field(init=False)]'}
a.cv = 'classvar'
a.a = 5
a.b = 'annotated field'
fields(a) = (Field(name='a',type='int',default=5,default_factory=<dataclasses._MISSING_TYPE object at 0x000001E104BAA6E0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), Field(name='b',type='Annotated[str, field(init=False)]',default='annotated field',default_factory=<dataclasses._MISSING_TYPE object at 0x000001E104BAA6E0>,init=False,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD))
Edit: recording the actual type defined in Annotated
in the resulting Field