Source code for jgdv.mixins.annotate.subclasser

  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