1#!/usr/bin/env python3
2"""
3
4"""
5# Imports:
6from __future__ import annotations
7
8# ##-- stdlib imports
9import datetime
10import enum
11import functools as ftz
12import itertools as itz
13import logging as logmod
14import pathlib as pl
15import re
16import time
17import collections
18import contextlib
19import hashlib
20from copy import deepcopy
21from uuid import UUID, uuid1
22from weakref import ref
23import atexit # for @atexit.register
24import faulthandler
25# ##-- end stdlib imports
26
27from . import _interface as API # noqa: N812
28
29# ##-- types
30# isort: off
31# General
32import abc
33import collections.abc
34import typing
35import types
36from types import GenericAlias, resolve_bases
37from typing import cast, assert_type, assert_never
38from typing import Generic, NewType, Never, TypeAliasType
39from typing import no_type_check, final, override, overload, _caller # type: ignore[attr-defined]
40# Protocols and Interfaces:
41from typing import Protocol, runtime_checkable
42
43from pydantic import BaseModel, create_model
44
45if typing.TYPE_CHECKING:
46 from typing import Final, ClassVar, Any, Self
47 from typing import Literal, LiteralString
48 from typing import TypeGuard
49 from collections.abc import Iterable, Iterator, Callable, Generator
50 from collections.abc import Sequence, Mapping, MutableMapping, Hashable
51
52 from jgdv import Maybe
53
54# isort: on
55# ##-- end types
56
57##-- logging
58logging = logmod.getLogger(__name__)
59##-- end logging
60
61# Vars:
62
63# Body:
64
[docs]
65class Subclasser:
66 """ A Util class for building subclasses programmatically
67
68 Subclasses can have modified mro's,
69 Also extended namespaces,
70 And preserve the base class' __slots__/__dict__ state
71
72 """
73
[docs]
74 @staticmethod
75 def decorate_name(cls:str|type, *vals:str, params:Maybe[str]=None) -> Maybe[str]: # noqa: PLW0211
76 """ Create a new name for an annotated subclass
77
78 decorate(cls, a,b,c) -> cls<+a+b+c>
79 decorate(cls, params='blah') -> cls[blah]
80 """
81 name : str
82 annotations : Maybe[str] = None
83 set_extras : set[str] = set(vals)
84
85 match cls:
86 case x if not (bool(params) or bool(vals)):
87 return None
88 case type() as x:
89 name = x.__name__
90 case str() as x:
91 name = x
92 case x:
93 raise TypeError(API.BadDecorationNameTarget, x)
94
95 match API.AnnotateRx.match(name):
96 case re.Match() as mtch:
97 set_extras.update({y for y in (mtch['extras'] or "").split("+") if bool(y)})
98 params = params or mtch['params'] or None
99 name = mtch['name'] or name
100 case _:
101 raise ValueError(API.NoNameMatch, cls)
102
103 if bool(set_extras):
104 annotations = "+".join(x for x in sorted(set_extras) if bool(x))
105
106 match annotations, params:
107 case str() as x, None:
108 return f"{name}<+{x}>"
109 case None, str() as x:
110 return f"{name}[{params}]"
111 case str() as x, str() as y:
112 return f"{name}<+{x}>[{y}]"
113 case _:
114 return None
115
[docs]
116 def annotate[T](self, cls:type[T], *params:Any) -> type[T]: # noqa: ANN401
117 """ Make a subclass of cls,
118
119 annotated to have params in getattr(cls, '_annotate_to', '_typevar')
120 """
121 p_str : str
122 def_mod : str
123 subname : str
124 namespace : dict
125 anno_target : str = getattr(cls, API.AnnotateKWD, API.AnnotationTarget)
126 anno_type : str = "ClassVar[str]"
127 match params:
128 case [NewType() as param]:
129 p_str = param.__name__ # type: ignore[attr-defined]
130 case [TypeAliasType() as param]:
131 p_str = param.__value__.__name__
132 case [type() as param]:
133 p_str = param.__name__
134 case [str() as param]:
135 p_str = param
136 case [param]:
137 p_str = str(param)
138 case [_, *_]: # type: ignore[misc]
139 raise NotImplementedError(API.MultiParamFail, params)
140 case _:
141 raise ValueError(API.BadParamFail, params)
142
143 # Get the module definer 3 frames up.
144 # So not annotate, or __class_getitem__, but where the subclass is created
145 def_mod = _caller(3)
146 match self.decorate_name(cls, params=p_str):
147 case str() as x:
148 subname = x
149 namespace = {
150 anno_target : param,
151 API.MODULE_NAME : def_mod,
152 API.ANNOTS_NAME : {anno_target : anno_type},
153 }
154 sub = self.make_subclass(subname, cls, namespace=namespace)
155 setattr(sub, anno_target , param) # type: ignore[attr-defined]
156 return sub
157 case _:
158 raise ValueError(API.NoSubName)
159
[docs]
160 def make_generic[T](self, cls:type[T], *params:Any) -> GenericAlias: # noqa: ANN401
161 return GenericAlias(cls, *params)
162
[docs]
163 def make_subclass[T](self, name:str, cls:type[T], *, namespace:Maybe[dict]=None, mro:Maybe[Iterable]=None) -> type[T]:
164 """
165 Build a dynamic subclass of cls, with name,
166 possibly with a maniplated mro and internal namespace
167 """
168 if (ispydantic:=issubclass(cls, BaseModel)) and mro is not None:
169 raise NotImplementedError(API.NoPydanticFail)
170 elif ispydantic:
171 sub = self._new_pydantic_class(name, cls, namespace=namespace)
172 return sub
173 else:
174 sub = self._new_std_class(name, cls, namespace=namespace, mro=mro)
175 return sub
176
[docs]
177 def _new_std_class[T](self, name:str, cls:type[T], *, namespace:Maybe[dict]=None, mro:Maybe[Iterable]=None) -> type[T]:
178 """
179 Dynamically creates a new class
180 """
181 assert(not issubclass(cls, BaseModel)), cls
182 mod_name : str
183 mcls : type[type] = type(cls)
184
185 match namespace:
186 case dict():
187 pass
188 case _:
189 namespace = {}
190 match mro:
191 case None:
192 mro = cls.mro()
193 case tuple() | list():
194 pass
195 case x:
196 raise TypeError(API.UnexpectedMRO, x)
197 ##--|
198 assert(namespace is not None)
199 # Expand out generics by calling __mro_entries__
200 match (mro:=tuple(resolve_bases(mro))):
201 case [x, *_]: # Use the base class module name
202 mod_name = x.__dict__[API.MODULE_NAME]
203 namespace.setdefault(API.MODULE_NAME, mod_name)
204 case _:
205 raise ValueError()
206
207 namespace.setdefault(API.SLOTS_NAME, ())
208 try:
209 return mcls(name, mro, namespace)
210 except TypeError as err:
211 err.add_note(str(mro))
212 raise
213
[docs]
214 def _new_pydantic_class(self, name:str, cls:type, *, namespace:Maybe[dict]=None) -> type:
215 assert(issubclass(cls, BaseModel)), cls
216 sub = create_model(name, __base__=cls)
217 for x,y in (namespace or {}).items():
218 setattr(sub, x, y)
219 return sub