Source code for jgdv.cli._interface

  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