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
22import atexit # for @atexit.register
23import faulthandler
24# ##-- end stdlib imports
25
26# ##-- types
27# isort: off
28import abc
29import collections.abc
30from typing import TYPE_CHECKING, cast, assert_type, assert_never
31from typing import Generic, NewType
32# Protocols:
33from typing import Protocol, runtime_checkable
34# Typing Decorators:
35from typing import no_type_check, final, override, overload
36
37from dataclasses import dataclass, field, InitVar
38from typing import Any
39
40if TYPE_CHECKING:
41 import types
42 from jgdv import Maybe, Rx
43 from typing import Final
44 from typing import ClassVar, LiteralString
45 from typing import Never, Self, Literal
46 from typing import TypeGuard
47 from collections.abc import Iterable, Iterator, Callable, Generator
48 from collections.abc import Sequence, Mapping, MutableMapping, Hashable
49
50##--|
51
52# isort: on
53# ##-- end types
54
55##-- logging
56logging = logmod.getLogger(__name__)
57##-- end logging
58
59# Vars:
60DEFAULT_PREFIX : Final[str] = "-"
61END_SEP : Final[str] = "--"
62FULLNAME_RE : Final[Rx] = re.compile(r"(?:<(?P<pos>\d*)>|(?P<prefix>\W+))?(?P<name>.+?)(?P<assign>=)?$")
63DEFAULT_DOC : Final[str] = "A Base Parameter"
64""" The Regexp for parsing string descriptions of parameters """
65
66##--|
67EMPTY_CMD : Final[str] = "_cmd_"
68EXTRA_KEY : Final[str] = "_extra_"
69NON_DEFAULT_KEY : Final[str] = "_non_default_"
70DEFAULT_COUNT : Final[int] = 1
71UNRESTRICTED_COUNT : Final[int] = -1
72##--|
73TYPE_CONV_MAPPING: Final[dict[str|type|types.GenericAlias, type|Callable]] = {
74 "int" : int,
75 "float" : float,
76 "bool" : bool,
77 "str" : str,
78 "list" : list,
79}
80
81# Body:
82
[docs]
83class SectionType_e(enum.Enum):
84 """ The different types of section a parse machine can process """
85 prog = enum.auto()
86 cmd = enum.auto()
87 sub = enum.auto()
88
[docs]
89class ParseResult_d:
90 """ Simple container for parsed cli information
91
92 Args:
93 name : the name of the spec that parsed this data
94 args : mapping of {arg -> data}, including default values it nothing parsing for it
95 non_default : set[arg, ...] that actually parsed
96 ref : the name of a linked parse result this object extends
97
98 """
99 __slots__ = ("args", "name", "non_default", "ref")
100 name : str
101 args : dict
102 non_default : set[str]
103 ref : Maybe[str]
104
105 def __init__(self, name:str, args:Maybe[dict]=None, ref:Maybe[str]=None) -> None:
106 self.name = name
107 self.args = args or {}
108 self.non_default = set()
109 self.ref = ref
110
111 @override
112 def __repr__(self) -> str:
113 return f"<ParseResult: {self.name}, args:{self.args}>"
114
[docs]
115 def to_dict(self) -> dict:
116 return {"name":self.name, "args":self.args, NON_DEFAULT_KEY:self.non_default}
117
[docs]
118class ParseReport_d:
119 """ The returned data of parsing cli args
120
121 Args:
122 raw : the raw args that were used. ieg: sys.argv[:]
123 remaining : anything not parsed
124 prog : ParseResult_d of base program arguments
125 cmds : mapping(cmdName -> [ParseResult_d])
126 subs : mapping(subName -> [ParseResult_d])
127 help : bool
128
129 """
130 type CmdName = str
131 type SubName = str
132 __slots__ = ("cmds", "help", "prog", "raw", "remaining", "subs")
133 raw : tuple[str, ...]
134 remaining : tuple[str, ...]
135 prog : ParseResult_d
136 cmds : dict[CmdName, tuple[ParseResult_d]]
137 subs : dict[SubName, tuple[ParseResult_d]]
138 help : bool
139
140 def __init__(self, *, raw:Iterable[str], remaining:Iterable[str], prog:ParseResult_d, _help:bool) -> None:
141 self.raw = tuple(raw)
142 self.remaining = tuple(remaining)
143 self.help = _help
144 self.prog = prog
145 self.cmds = {}
146 self.subs = {}
147
[docs]
148 def to_dict(self) -> dict:
149 return {
150 "prog" : self.prog.to_dict(),
151 "cmds" : {x: [y.to_dict() for y in ys] for x,ys in self.cmds.items()},
152 "subs" : {x: [y.to_dict() for y in ys] for x,ys in self.subs.items()},
153 "help" : self.help,
154 }
155
156##--| Params
157
[docs]
158@runtime_checkable
159class ParamSpec_p(Protocol):
160 """ Base class for CLI param specs, for type matching
161 when 'consume' is given a list of strs,
162 it can match on the args,
163 and return an updated diction and a list of values it didn't consume
164
165 """
166
[docs]
167 @classmethod
168 def key_func(cls, x:ParamSpec_i) -> tuple: ...
169
[docs]
170 def consume(self, args:list[str], *, offset:int=0) -> Maybe[tuple[dict, int]]:
171 pass
172
173 ##--| properties
174
[docs]
175 @property
176 def short(self) -> str: ...
177
[docs]
178 @property
179 def inverse(self) -> str: ...
180
[docs]
181 @property
182 def repeatable(self) -> bool: ...
183
[docs]
184 @property
185 def key_str(self) -> str: ...
186
[docs]
187 @property
188 def short_key_str(self) -> Maybe[str]: ...
189
[docs]
190 @property
191 def key_strs(self) -> list[str]: ...
192
[docs]
193 @property
194 def default_value(self) -> Any: ... # noqa: ANN401
195
[docs]
196 @property
197 def default_tuple(self) -> tuple[str, Any]: ...
198
[docs]
199 def help_str(self, *, force:bool=False) -> str: ...
200
[docs]
201class ParamSpec_i(ParamSpec_p, Protocol):
202 _processor : ClassVar
203
204 name : str
205 type_ : Maybe[type]
206 insist : bool
207 default : Any|Callable
208 desc : str
209 count : int
210 prefix : int|str
211 separator : str|Literal[False]
212 implicit : bool
213
214##--| Param Subtypes
215
[docs]
216@runtime_checkable
217class PositionalParam_p(Protocol):
218 """ Interface to mark a parameter as positional """
219
[docs]
220 def _positional(self) -> Literal[True]: ...
221
[docs]
222@runtime_checkable
223class AssignmentParam_p(Protocol):
224 """ Interface to mark a parameter as an assignment """
225
[docs]
226 def _assignment(self) -> Literal[True]: ...
227
[docs]
228@runtime_checkable
229class KeyParam_p(Protocol):
230 """ Interface to mark a parameter as a key value pair """
231
[docs]
232 def _keyval(self) -> Literal[True]: ...
233
[docs]
234@runtime_checkable
235class ToggleParam_p(Protocol):
236 """ Interface to mark a parameter as a boolean toggle """
237
[docs]
238 def _toggle(self) -> Literal[True]: ...
239##--| Parsing
240
[docs]
241@runtime_checkable
242class ArgParserModel_p(Protocol):
243 """ The Model used in a jgdv.cli.arg_parser:ParseMachine to implement specific parsing logic """
244
[docs]
245 def prepare_for_parse(self, *, prog:ParamSource_p, cmds:list[ParamSource_p], subs:list[tuple[tuple[str, ...], ParamSource_p]], raw_args:list[str]) -> None: ...
246
[docs]
247@runtime_checkable
248class ParamSource_p(Protocol):
249 """ Param Sources are anything that can provide a name and a set of parameters """
250
[docs]
251 @property
252 def name(self) -> str:
253 raise NotImplementedError()
254
[docs]
255 def param_specs(self) -> list[ParamSpec_i]:
256 raise NotImplementedError()
257
[docs]
258@runtime_checkable
259class CLIParamProvider_p(Protocol):
260 """
261 Things that can provide parameter specs for CLI parsing
262 """
263
[docs]
264 @classmethod
265 def param_specs(cls) -> list[ParamSpec_i]:
266 """ make class parameter specs """
267 pass