Home Assistant Unofficial Reference 2024.12.1
frozen_dataclass_compat.py
Go to the documentation of this file.
1 """Utility to create classes from which frozen or mutable dataclasses can be derived.
2 
3 This module enabled a non-breaking transition from mutable to frozen dataclasses
4 derived from EntityDescription and sub classes thereof.
5 """
6 
7 from __future__ import annotations
8 
9 import dataclasses
10 import sys
11 from typing import TYPE_CHECKING, Any, cast, dataclass_transform
12 
13 if TYPE_CHECKING:
14  from _typeshed import DataclassInstance
15 
16 
17 def _class_fields(cls: type, kw_only: bool) -> list[tuple[str, Any, Any]]:
18  """Return a list of dataclass fields.
19 
20  Extracted from dataclasses._process_class.
21  """
22  cls_annotations = cls.__dict__.get("__annotations__", {})
23 
24  cls_fields: list[dataclasses.Field[Any]] = []
25 
26  _dataclasses = sys.modules[dataclasses.__name__]
27  for name, _type in cls_annotations.items():
28  # See if this is a marker to change the value of kw_only.
29  if dataclasses._is_kw_only(type, _dataclasses) or ( # type: ignore[attr-defined] # noqa: SLF001
30  isinstance(_type, str)
31  and dataclasses._is_type( # type: ignore[attr-defined] # noqa: SLF001
32  _type,
33  cls,
34  _dataclasses,
35  dataclasses.KW_ONLY,
36  dataclasses._is_kw_only, # type: ignore[attr-defined] # noqa: SLF001
37  )
38  ):
39  kw_only = True
40  else:
41  # Otherwise it's a field of some type.
42  cls_fields.append(dataclasses._get_field(cls, name, _type, kw_only)) # type: ignore[attr-defined] # noqa: SLF001
43 
44  return [(field.name, field.type, field) for field in cls_fields]
45 
46 
47 @dataclass_transform( field_specifiers=(dataclasses.field, dataclasses.Field),
48  frozen_default=True, # Set to allow setting frozen in child classes
49  kw_only_default=True, # Set to allow setting kw_only in child classes
50 )
51 class FrozenOrThawed(type):
52  """Metaclass which which makes classes which behave like a dataclass.
53 
54  This allows child classes to be either mutable or frozen dataclasses.
55  """
56 
57  def _make_dataclass(cls, name: str, bases: tuple[type, ...], kw_only: bool) -> None:
58  class_fields = _class_fields(cls, kw_only)
59  dataclass_bases = [getattr(base, "_dataclass", base) for base in bases]
60  cls._dataclass_dataclass = dataclasses.make_dataclass(
61  name, class_fields, bases=tuple(dataclass_bases), frozen=True
62  )
63 
64  def __new__(
65  mcs, # noqa: N804 ruff bug, ruff does not understand this is a metaclass
66  name: str,
67  bases: tuple[type, ...],
68  namespace: dict[Any, Any],
69  frozen_or_thawed: bool = False,
70  **kwargs: Any,
71  ) -> Any:
72  """Pop frozen_or_thawed and store it in the namespace."""
73  namespace["_FrozenOrThawed__frozen_or_thawed"] = frozen_or_thawed
74  return super().__new__(mcs, name, bases, namespace)
75 
76  def __init__(
77  cls,
78  name: str,
79  bases: tuple[type, ...],
80  namespace: dict[Any, Any],
81  **kwargs: Any,
82  ) -> None:
83  """Optionally create a dataclass and store it in cls._dataclass.
84 
85  A dataclass will be created if frozen_or_thawed is set, if not we assume the
86  class will be a real dataclass, i.e. it's decorated with @dataclass.
87  """
88  if not namespace["_FrozenOrThawed__frozen_or_thawed"]:
89  # This class is a real dataclass, optionally inject the parent's annotations
90  if all(dataclasses.is_dataclass(base) for base in bases):
91  # All direct parents are dataclasses, rely on dataclass inheritance
92  return
93  # Parent is not a dataclass, inject all parents' annotations
94  annotations: dict = {}
95  for parent in cls.__mro__[::-1]:
96  if parent is object:
97  continue
98  annotations |= parent.__annotations__
99  cls.__annotations____annotations__ = annotations
100  return
101 
102  # First try without setting the kw_only flag, and if that fails, try setting it
103  try:
104  cls._make_dataclass_make_dataclass(name, bases, False)
105  except TypeError:
106  cls._make_dataclass_make_dataclass(name, bases, True)
107 
108  def __new__(*args: Any, **kwargs: Any) -> object:
109  """Create a new instance.
110 
111  The function has no named arguments to avoid name collisions with dataclass
112  field names.
113  """
114  cls, *_args = args
115  if dataclasses.is_dataclass(cls):
116  if TYPE_CHECKING:
117  cls = cast(type[DataclassInstance], cls)
118  return object.__new__(cls)
119  return cls._dataclass_dataclass(*_args, **kwargs)
120 
121  cls.__init____init____init__ = cls._dataclass_dataclass.__init__ # type: ignore[misc]
122  cls.__new____new____new__ = __new__ # type: ignore[method-assign]
123 
Any __new__(mcs, str name, tuple[type,...] bases, dict[Any, Any] namespace, bool frozen_or_thawed=False, **Any kwargs)
None __init__(cls, str name, tuple[type,...] bases, dict[Any, Any] namespace, **Any kwargs)
None _make_dataclass(cls, str name, tuple[type,...] bases, bool kw_only)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
list[tuple[str, Any, Any]] _class_fields(type cls, bool kw_only)