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 re
16import time
17import types
18import weakref
19from uuid import UUID, uuid1
20
21# ##-- end stdlib imports
22
23from . import _interface as API # noqa: N812
24from ._interface import INDIRECT_SUFFIX, RAWKEY_ID, DKeyMark_e, Key_p, NonKey_p
25from ._util._interface import (ExpInst_d, ExpInstChain_d, InstructionFactory_p,
26 SourceChain_d)
27from .dkey import DKey
28
29# ##-- types
30# isort: off
31import abc
32import collections.abc
33from typing import TYPE_CHECKING, Generic, cast, assert_type, assert_never, Self
34# Protocols:
35from typing import Protocol, runtime_checkable
36# Typing Decorators:
37from typing import no_type_check, final, override, overload
38from typing import Never
39from typing import Any
40from collections.abc import Mapping
41
42if TYPE_CHECKING:
43 import pathlib as pl
44 from jgdv import Maybe
45 from typing import Final
46 from typing import ClassVar, LiteralString
47 from typing import Literal
48 from typing import TypeGuard
49 from collections.abc import Iterable, Iterator, Callable, Generator
50 from collections.abc import Sequence, MutableMapping, Hashable
51 from jgdv._abstract.protocols.general import SpecStruct_p
52
53# isort: on
54# ##-- end types
55
56##-- logging
57logging = logmod.getLogger(__name__)
58##-- end logging
59
[docs]
60class SingleDKey(DKey, mark=Any, default=True):
61 """
62 A Single key with no extras.
63 ie: {x}. not {x}{y}, or {x}.blah.
64 """
65 __slots__ = ()
66
67 def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
68 super().__init__(*args, **kwargs)
69 match self.data.raw:
70 case [x] if self.data.convert is None:
71 self.data.convert = x.convert
72 self.data.format = x.format
73 case [_]:
74 pass
75 case None | []:
76 msg = "A Single Key has no raw key data"
77 raise ValueError(msg)
78 case [*xs]:
79 msg = "A Single Key got multiple raw key data"
80 raise ValueError(msg, xs)
81
[docs]
82 def _post_process_h(self, data:Maybe[dict]=None) -> None: # noqa: ARG002
83 fmt : str
84 match self.data.raw[0].format:
85 case None:
86 return
87 case str() as fmt :
88 pass
89
90 match API.EXPANSION_LIMIT_PATTERN.search(fmt):
91 case re.Match() as m:
92 self.data.max_expansions = int(m[1])
93 case None:
94 pass
95
[docs]
96class MultiDKey(DKey, mark=list):
97 """ Multi keys allow 1+ explicit subkeys.
98
99 They have additional fields:
100
101 _subkeys : parsed information about explicit subkeys
102
103 """
104 __slots__ = ("anon",)
105 anon : str
106
107 def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
108 super().__init__(*args, **kwargs)
109 match self.data.raw:
110 case [] | None:
111 msg = "Tried to build a multi key with no subkeys"
112 raise ValueError(msg, self.data.raw, kwargs)
113 case [*xs]:
114 self.anon = "".join(x.anon() for x in xs)
115
116 @override
117 def __str__(self) -> str:
118 return self[:]
119
120 @override
121 def __contains__(self, other:object) -> bool:
122 return other in self.keys()
123
124 @override
125 def __format__(self, spec:str, **kwargs:Any) -> str:
126 """ Just does normal str formatting """
127 rem, _, _= self._processor.consume_format_params(spec)
128 return super().__format__(rem, **kwargs)
129
[docs]
130 def _multi(self) -> Literal[True]:
131 return True
132
[docs]
133 def keys(self) -> list[DKey]:
134 return [DKey(x, implicit=True) for x in self.data.meta if bool(x)]
135
[docs]
136 def exp_generate_chains_h(self, root:ExpInst_d, factory:InstructionFactory_p, opts:dict) -> list[ExpInstChain_d|ExpInst_d]:
137 """ Lift subkeys to expansion instructions """
138 targets : list[ExpInstChain_d|ExpInst_d]= []
139 keys = self.keys()
140 targets.append(ExpInstChain_d(root=self, merge=len(keys))) # type: ignore[arg-type]
141 for key in keys:
142 match factory.build_inst(key, root, opts, decrement=False):
143 case None:
144 targets.append(factory.null_inst())
145 case ExpInst_d() as inst if inst.literal is True:
146 targets.append(inst)
147 case ExpInst_d() as inst if inst.value != self:
148 assert(inst.value != root.value)
149 targets += factory.build_chains(inst, opts)
150 case ExpInst_d() as inst:
151 vals = [
152 inst,
153 factory.null_inst(),
154 ]
155 targets.append(factory.build_single_chain(vals, self))
156 else:
157 return targets
158
[docs]
159 def exp_flatten_h(self, vals:list[ExpInst_d], factory:InstructionFactory_p, opts:dict) -> Maybe[ExpInst_d]: # noqa: ARG002
160 """ Flatten the multi-key expansion into a single string,
161 by using the anon-format str
162 """
163 flat : list[str] = []
164 key_meta = [x for x in self.data.meta if bool(x)]
165 if bool(vals) and not bool(key_meta):
166 return vals[0]
167 if len(vals) != len([x for x in self.data.meta if bool(x)]):
168 return None
169
170 for x in vals:
171 match x:
172 case ExpInst_d(value=IndirectDKey() as k):
173 flat.append(f"{k:wi}")
174 case ExpInst_d(value=API.Key_p() as k):
175 flat.append(f"{k:w}")
176 case ExpInst_d(value=x):
177 flat.append(str(x))
178 else:
179 return ExpInst_d(value=self.anon.format(*flat), literal=True)
180
[docs]
181class NonDKey(DKey, mark=False):
182 """ Just a string, not a key.
183
184 ::
185
186 But this lets you call no-ops for key specific methods.
187 It can coerce itself though
188 """
189 __slots__ = ()
190
191 def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
192 super().__init__(*args, **kwargs)
193 if (fb:=kwargs.get('fallback', None)) is not None and fb != self:
194 msg = "NonKeys can't have a fallback, did you mean to use an explicit key?"
195 raise ValueError(msg, self)
196
197 @override
198 def __format__(self, spec:str, **kwargs:Any) -> str:
199 """ Just does normal str formatting """
200 rem, _, _= self._processor.consume_format_params(spec)
201 return super().__format__(rem, **kwargs)
202
[docs]
203 def _nonkey(self) -> Literal[True]:
204 return True
205
[docs]
206 @override
207 def expand(self, *args, **kwargs) -> Maybe: # noqa: ANN002, ANN003
208 """ A Non-key just needs to be coerced into the correct str format """
209 assert(isinstance(self, API.Key_p))
210 val = ExpInst_d(value=self[:])
211 match self._expander.coerce_result(val, self, kwargs):
212 case None if (fallback:=kwargs.get("fallback")) is not None:
213 return ExpInst_d(value=fallback, literal=True)
214 case None:
215 return self.data.fallback
216 case ExpInst_d() as x:
217 return x.value
218 case x:
219 msg = "Nonkey coercion didn't return an ExpInst_d"
220 raise TypeError(msg, x)
221
[docs]
222class IndirectDKey(DKey, mark=Mapping, convert="I"):
223 """
224 A Key for getting a redirected key.
225 eg: RedirectionDKey(key) -> SingleDKey(value)
226
227 re_mark :
228 """
229 __slots__ = ("multi_redir", "re_mark")
230 __hash__ = SingleDKey.__hash__
231
232 def __init__(self, *, multi:bool=False, re_mark:Maybe[API.KeyMark]=None, **kwargs) -> None: # noqa: ANN003
233 assert(not self.endswith(INDIRECT_SUFFIX)), self[:]
234 super().__init__(**kwargs)
235 self.multi_redir = multi
236 self.re_mark = re_mark
237
238 @override
239 def __str__(self) -> str:
240 return f"{self:i}"
241
242 @override
243 def __eq__(self, other:object) -> bool:
244 match other:
245 case str() if other.endswith(INDIRECT_SUFFIX):
246 return f"{self:i}" == other
247 case _:
248 return super().__eq__(other)
249
[docs]
250 def _indirect(self) -> Literal[True]:
251 return True
252
[docs]
253 def exp_generate_chains_h(self, root:ExpInst_d, factory:InstructionFactory_p, opts:dict) -> list[ExpInstChain_d|ExpInst_d]:
254 """ Lookup the indirect version, the direct version, then use the fallback """
255 targets : list = [
256 factory.build_inst(self, root, opts, decrement=False),
257 factory.lift_inst(f"{self:d}", root, opts, decrement=False, implicit=True),
258 factory.null_inst(),
259 ]
260
261 return [ExpInstChain_d(*targets, root=root.value)]