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 importlib
14import itertools as itz
15import logging as logmod
16import pathlib as pl
17import re
18import time
19import weakref
20from dataclasses import InitVar, dataclass, field
21from uuid import UUID, uuid1
22
23# ##-- end stdlib imports
24
25# ##-- 3rd party imports
26from pydantic import BaseModel, Field, field_validator, model_validator
27from jgdv import Maybe, Proto
28from jgdv.structs.chainguard import ChainGuard
29from jgdv.structs.strang import CodeReference
30from jgdv._abstract.protocols.general import SpecStruct_p, Buildable_p
31from jgdv._abstract.protocols.pydantic import ProtocolModelMeta
32# ##-- end 3rd party imports
33
34# ##-- 1st party imports
35import doot
36import doot.errors
37
38# ##-- end 1st party imports
39
40# ##-- types
41# isort: off
42import abc
43import collections.abc
44from typing import TYPE_CHECKING, cast, assert_type, assert_never
45from typing import Generic, NewType, Never, Any
46# Protocols:
47from typing import Protocol, runtime_checkable
48# Typing Decorators:
49from typing import no_type_check, final, overload, override
50# outside of type_checking for pydantic
51from jgdv import Func # noqa: TC002
52if TYPE_CHECKING:
53 from .. import _interface as API # noqa: N812
54 from jgdv import Maybe
55 from typing import Final
56 from typing import ClassVar, Any, LiteralString
57 from typing import Self, Literal
58 from typing import TypeGuard
59 from collections.abc import Iterable, Iterator, Callable, Generator
60 from collections.abc import Sequence, Mapping, MutableMapping, Hashable
61
62##--|
63
64# isort: on
65# ##-- end types
66
67##-- logging
68logging = logmod.getLogger(__name__)
69##-- end logging
70
71ALIASES = doot.aliases.on_fail([]).action
72
73##--| body
[docs]
74class ActionSpec(BaseModel, Buildable_p, metaclass=ProtocolModelMeta, arbitrary_types_allowed=True):
75 """
76 When an action isn't a full blown class, it gets wrapped in this,
77 which passes the action spec to the callable.
78
79 TODO: recogise arg prefixs and convert to correct type.
80 eg: path:a/relative/path -> Path(./a/relative/path)
81 path:/usr/bin/python -> Path(/usr/bin/python)
82
83 """
84 do : Maybe[CodeReference] = Field(default=None)
85 args : list[Any] = Field(default_factory=list)
86 kwargs : ChainGuard = Field(default_factory=ChainGuard)
87 fun : Maybe[Func] = Field(default=None)
88
[docs]
89 @override
90 @classmethod
91 def build(cls, data:dict|list|ChainGuard|ActionSpec, *, fun:Maybe[Callable]=None) -> ActionSpec: # type: ignore[override]
92 match data:
93 case ActionSpec():
94 return data
95 case list():
96 action_spec = cls(
97 args=data,
98 fun=fun if callable(fun) else None,
99 )
100 return action_spec
101
102 case dict() | ChainGuard():
103 kwargs = ChainGuard({x:y for x,y in data.items() if x not in ActionSpec.model_fields})
104 fun = data.get('fun', fun)
105 action_spec = cls(
106 do=data.get('do', None),
107 args=data.get('args',[]),
108 kwargs=kwargs,
109 fun=fun,
110 )
111 return action_spec
112 case _:
113 raise doot.errors.StructLoadError("Unrecognized specification data", data)
114
[docs]
115 @field_validator("do", mode="before")
116 def _validate_do(cls, val:Maybe[str|CodeReference|Callable]) -> Maybe[CodeReference]:
117 aliases : dict[str, str]
118 match val:
119 case None:
120 return None
121 case CodeReference():
122 return val
123 case str() if (aliases:=ALIASES()) and val in aliases:
124 alias = aliases[val]
125 return CodeReference(alias)
126 case str():
127 return CodeReference(val)
128 case x if callable(x):
129 return CodeReference(x)
130 case _:
131 raise TypeError("Unrecognized action spec do type", val)
132
133 @override
134 def __str__(self) -> str:
135 result = []
136 if isinstance(self.do, str):
137 result.append(f"do={self.do}")
138 elif self.do and hasattr(self.do, '__qualname__'):
139 result.append(f"do={self.do.__qualname__}")
140 elif self.do:
141 result.append(f"do={self.do.__class__.__qualname__}")
142
143 if self.args:
144 result.append(f"args={[str(x) for x in self.args]}")
145 if self.kwargs:
146 result.append(f"kwargs={self.kwargs}")
147 if self.fun and hasattr(self.fun, '__qualname__'):
148 result.append(f"calling={self.fun.__qualname__}")
149 elif self.fun:
150 result.append(f"calling={self.fun.__class__.__qualname__}")
151
152 return f"<ActionSpec: {' '.join(result)} >"
153
154 def __call__(self, task_state:dict) -> Any:
155 if self.fun is None:
156 raise doot.errors.StructError("Action Spec has not been finalised with a function", self)
157
158 return self.fun(self, task_state)
159
[docs]
160 @property
161 def params(self) -> ChainGuard:
162 return self.kwargs
163
[docs]
164 def set_function(self, *, fun:Maybe[API.Action_p|Func|type|ImportError]=None) -> None:
165 """
166 Sets the function of the action spec.
167 if given a class, the class is built,
168 if given a callable, that is used directly.
169
170 """
171 if fun is None:
172 assert(self.do is not None)
173 fun = self.do()
174
175 match fun:
176 case ImportError() as err:
177 raise err from None
178 case type() as x:
179 self.fun = x()
180 case x if callable(x):
181 self.fun = fun
182 case x:
183 raise doot.errors.StructError("Action Spec Given a non-callable fun: %s", fun)
184
[docs]
185 def verify(self, state:dict, *, fields:Maybe[list[str]]=None) -> None:
186 raise NotImplementedError()
187
[docs]
188 def verify_out(self, state:dict) -> None:
189 self.verify(state, fields=self.outState)