|
1 | 1 | import pathlib
|
2 |
| -import sys |
3 | 2 | from abc import ABC, abstractmethod
|
4 | 3 | from dataclasses import dataclass, is_dataclass
|
5 | 4 | from typing import Any, AsyncIterator, Iterator, List, Optional, Type, TypeVar, Union
|
@@ -97,39 +96,48 @@ class Input:
|
97 | 96 | deprecated: Optional[bool] = None
|
98 | 97 |
|
99 | 98 |
|
100 |
| -# pyodide does not recognise `BaseModel` with the `__new__` keyword as a dataclass while regular python does. |
101 |
| -# to get around this, we hijack `__init__subclass` instead to make sure the subclass of a base model is recognised |
102 |
| -# as a dataclass. In addition to this, we provide this `BaseModel` only on pyodide instances, normal python still gets |
103 |
| -# the regular `BaseModel``. |
104 |
| -class _BaseModelPyodide: |
105 |
| - def __init_subclass__(cls, **kwargs): |
106 |
| - dc_keys = { |
107 |
| - 'init', |
108 |
| - 'repr', |
109 |
| - 'eq', |
110 |
| - 'order', |
111 |
| - 'unsafe_hash', |
112 |
| - 'frozen', |
113 |
| - 'match_args', |
114 |
| - 'kw_only', |
115 |
| - 'slots', |
116 |
| - 'weakref_slot', |
117 |
| - } |
118 |
| - dc_opts = {k: kwargs.pop(k) for k in list(kwargs) if k in dc_keys} |
119 |
| - super().__init_subclass__(**kwargs) |
120 |
| - if not is_dataclass(cls): |
121 |
| - dataclass(**dc_opts)(cls) |
122 |
| - |
123 |
| - |
124 |
| -class _BaseModelStd: |
125 |
| - def __new__(cls, *args, **kwargs): |
126 |
| - # This does not work with frozen=True |
127 |
| - # Also user might want to mutate the output class |
128 |
| - dcls = dataclass()(cls) |
129 |
| - return super().__new__(dcls) |
130 |
| - |
131 |
| - |
132 |
| -BaseModel = _BaseModelPyodide if 'pyodide' in sys.modules else _BaseModelStd |
| 99 | +class BaseModel: |
| 100 | + def __init_subclass__( |
| 101 | + cls, *, auto_dataclass: bool = True, init: bool = True, **kwargs |
| 102 | + ): |
| 103 | + # BaseModel is parented to `object` so we have nothing to pass up to it, we pass the kwargs to dataclass() only. |
| 104 | + super().__init_subclass__() |
| 105 | + |
| 106 | + # For sanity, the primary base class must inherit from BaseModel |
| 107 | + if not issubclass(cls.__bases__[0], BaseModel): |
| 108 | + raise TypeError( |
| 109 | + f'Primary base class of "{cls.__name__}" must inherit from BaseModel' |
| 110 | + ) |
| 111 | + elif not auto_dataclass: |
| 112 | + try: |
| 113 | + if ( |
| 114 | + cls.__bases__[0] != BaseModel |
| 115 | + and cls.__bases__[0].__auto_dataclass is True # type: ignore[attr-defined] |
| 116 | + ): |
| 117 | + raise ValueError( |
| 118 | + f'Primary base class of "{cls.__name__}" ("{cls.__bases__[0].__name__}") has auto_dataclass=True, but "{cls.__name__}" has auto_dataclass=False. This creates broken field inheritance.' |
| 119 | + ) |
| 120 | + except AttributeError: |
| 121 | + raise RuntimeError( |
| 122 | + f'Primary base class of "{cls.__name__}" is a child of a child of `BaseModel`, but `auto_dataclass` tracking does not exist. This is likely a bug or other programming error.' |
| 123 | + ) |
| 124 | + |
| 125 | + for base in cls.__bases__[1:]: |
| 126 | + if is_dataclass(base): |
| 127 | + raise TypeError( |
| 128 | + f'Cannot mixin dataclass "{base.__name__}" while inheriting from `BaseModel`' |
| 129 | + ) |
| 130 | + |
| 131 | + # Once manual dataclass handling is enabled, we never apply the auto dataclass logic again, |
| 132 | + # it becomes the responsibility of the user to ensure that all dataclass semantics are handled. |
| 133 | + if not auto_dataclass: |
| 134 | + cls.__auto_dataclass = False # type: ignore[attr-defined] |
| 135 | + return |
| 136 | + |
| 137 | + # all children should be dataclass'd, this is the only way to ensure that the dataclass inheritence |
| 138 | + # is handled properly. |
| 139 | + dataclass(init=init, **kwargs)(cls) |
| 140 | + cls.__auto_dataclass = True # type: ignore[attr-defined] |
133 | 141 |
|
134 | 142 |
|
135 | 143 | ########################################
|
|
0 commit comments