1#!/usr/bin/env python3
2"""
3
4"""
5
6# Imports:
7from __future__ import annotations
8
9# ##-- stdlib imports
10import datetime
11import enum
12import functools as ftz
13import itertools as itz
14import logging as logmod
15import pathlib as pl
16import re
17import time
18import collections
19import contextlib
20import hashlib
21from copy import deepcopy
22from uuid import UUID, uuid1
23from weakref import ref
24# ##-- end stdlib imports
25
26from collections import ChainMap
27from jgdv import Proto, Mixin
28from jgdv.mixins.annotate import SubAlias_m
29from jgdv.structs.strang import Strang
30from ._util import _interface as ExpAPI # noqa: N812
31from ._util.expander_stack import DKeyExpanderStack
32from .processor import DKeyProcessor
33
34# ##-- types
35# isort: off
36# General
37import abc
38import collections.abc
39import typing
40import types
41from typing import cast, assert_type, assert_never
42from typing import Generic, NewType, Never
43from typing import no_type_check, final, override, overload
44# Protocols and Interfaces:
45from typing import Protocol
46from . import _interface as API # noqa: N812
47
48if typing.TYPE_CHECKING:
49 from typing import Final, ClassVar, Any, Self
50 from typing import Literal, LiteralString
51 from typing import TypeGuard
52 from collections.abc import Iterable, Iterator, Callable, Generator
53 from collections.abc import Sequence, Mapping, MutableMapping, Hashable
54
55 from jgdv import Maybe, M_
56 from ._util._interface import Expander_p
57
58# isort: on
59# ##-- end types
60
61##-- logging
62logging = logmod.getLogger(__name__)
63##-- end logging
64
65# Vars:
66CLASS_GETITEM_K : Final[str] = "__class_getitem__"
67type DP_TYPE = API.DKeyMarkAbstract_e | type
68# Body:
69
[docs]
70@Proto(API.Key_p, check=False, mod_mro=False)
71class DKey[**K](Strang, fresh_registry=True):
72 """ A facade for DKeys and variants.
73 Implements __new__ to create the correct key type, from a string, dynamically.
74
75 kwargs:
76 explicit = insists that keys in the string are wrapped in braces '{akey} {anotherkey}'.
77 mark = pre-register expansion parameters / type etc
78 check = dictate a type that expanding this key must match
79 fparams = str formatting instructions for the key
80
81 Eg:
82 DKey('blah')
83 -> SingleDKey('blah')
84 -> SingleDKey('blah').format('w')
85 -> '{blah}'
86 -> [toml] aValue = '{blah}'
87
88 Because cls.__new__ calls __init__ automatically for return values of type cls,
89 DKey is the factory, but all DKeys are subclasses of DKeyBase,
90 to allow control over __init__.
91
92 Base class for implementing actual DKeys.
93
94 init takes kwargs:
95 fmt, mark, check, ctor, help, fallback, max_exp
96
97 on class definition, can register a 'mark', 'multi', and a conversion parameter str
98 """
99 __slots__ = ("data",)
100 __match_args = ()
101 _annotate_to : ClassVar[str] = "dkey_mark"
102 _processor : ClassVar = DKeyProcessor()
103 _sections : ClassVar = API.DKEY_SECTIONS
104 _expander : ClassVar[Expander_p] = cast("Expander_p", DKeyExpanderStack())
105 _typevar : ClassVar = None
106 _extra_kwargs : ClassVar[set[str]] = set()
107 _extra_sources : ClassVar[list[ExpAPI.SourceBases]] = []
108 Marks : ClassVar[API.DKeyMarkAbstract_e] = API.DKeyMark_e # type: ignore[assignment]
109 data : API.DKey_d
110
111 ##--| Class Utils
112
[docs]
113 @final
114 @staticmethod
115 def MarkOf[T](target:T|type[T]) -> API.KeyMark|tuple[API.KeyMark, ...]: # noqa: N802
116 """ Get the mark of the key type or instance """
117 if not hasattr(target, "cls_annotation"):
118 return ()
119 match target.cls_annotation(): # type: ignore[union-attr]
120 case None:
121 return ()
122 case [x]:
123 return cast("API.KeyMark", x)
124 case xs:
125 return cast("tuple[API.KeyMark, ...]", xs)
126
[docs]
127 @classmethod
128 def add_sources(cls, *sources:dict) -> None:
129 """ register additional sources that are always included in expansion """
130 cls._extra_sources += sources
131
132 @override
133 def __init_subclass__(cls, *args, **kwargs) -> None: # noqa: ANN002, ANN003
134 super().__init_subclass__(*args, annotation=kwargs.pop("mark", None), **kwargs)
135 cls._expander.set_ctor(DKey) # type: ignore[arg-type]
136 cls._processor.register_convert_param(cls, kwargs.pop("convert", None))
137
138 ##--| Class Main
139
140 def __init__(self, *args:Any, **kwargs:Any) -> None: # noqa: ANN401
141 assert(not self.endswith(API.INDIRECT_SUFFIX)), self[:]
142 super().__init__(*args, **kwargs)
143 self.data = API.DKey_d(**kwargs)
144
145 def __call__(self, *args:Any, **kwargs:Any) -> Any: # noqa: ANN401
146 """ call expand on the key.
147 Args and kwargs are passed verbatim to expand()
148 """
149 return self.expand(*args, **kwargs)
150
151 @override
152 def __eq__(self, other:object) -> bool:
153 match other:
154 case DKey() | str():
155 return str.__eq__(self, other)
156 case _:
157 return NotImplemented
158
159 @override
160 def __hash__(self) -> int:
161 return hash(self[:])
162
163 @override
164 def __format__(self, spec:str) -> str:
165 """
166 Extends standard string format spec language:
167 [[fill]align][sign][z][#][0][width][grouping_option][. precision][type]
168 (https://docs.python.org/3/library/string.html#format-specification-mini-language)
169
170 Using the # alt form to declare keys are wrapped.
171 eg: for key = DKey('test'), ikey = DKey('test_')
172 f'{key}' -> 'test'
173 f'{key:w}' -> '{test}'
174 f'{key:i} -> 'test_'
175 f'{key:wi} -> '{test_}'
176
177 f'{ikey:d} -> 'test'
178
179 """
180 result = self[:]
181 if not bool(spec):
182 return result
183
184 rem, wrap, direct = self._processor.consume_format_params(spec)
185
186 # format
187 if not direct:
188 result = f"{result}{API.INDIRECT_SUFFIX}"
189
190 if wrap:
191 result = "".join(["{", result, "}"]) # noqa: FLY002
192
193 return result
194 ##--| Utils
195
[docs]
196 def var_name(self) -> str:
197 """ When testing the dkey for its inclusion in a decorated functions signature,
198 this gives the 'named' val if its not None, otherwise the str of the key
199 """
200 return self.data.name or str(self)
201
[docs]
202 def expand(self, *args:Any, **kwargs:Any) -> Maybe: # noqa: ANN401
203 kwargs.setdefault("limit", self.data.max_expansions)
204 assert(isinstance(self, API.Key_p))
205 match self._expander.expand(self, *args, **kwargs):
206 case ExpAPI.ExpInst_d(value=val, literal=True):
207 return val
208 case _:
209 return None
210
[docs]
211 def redirect(self, *args:Any, **kwargs:Any) -> list[API.Key_p]: # noqa: ANN401
212 assert(isinstance(self, API.Key_p))
213 result = [DKey(x.value) for x in self._expander.redirect(self, *args, **kwargs) if x is not None]
214 return result
215
216 ##--| expansion hooks