Source code for jgdv.cli.param_spec.core

  1#!/usr/bin/env python3
  2"""
  3
  4"""
  5
  6# Imports:
  7from __future__ import annotations
  8
  9# ##-- stdlib imports
 10import builtins
 11import datetime
 12import enum
 13import functools as ftz
 14import importlib
 15import itertools as itz
 16import logging as logmod
 17import pathlib as pl
 18import re
 19import time
 20import types
 21import typing
 22import weakref
 23from dataclasses import InitVar, dataclass, field
 24from uuid import UUID, uuid1
 25
 26# ##-- end stdlib imports
 27
 28# ##-- 1st party imports
 29from jgdv import Maybe
 30from jgdv.mixins.annotate.annotate import SubAnnotate_m
 31from jgdv.structs.chainguard import ChainGuard
 32
 33# ##-- end 1st party imports
 34
 35from jgdv import Proto
 36from jgdv.cli.errors import ArgParseError
 37from .. import _interface as API # noqa: N812
 38from .param_spec import ParamSpec
 39from .._interface import ParamSpec_p
 40
 41# ##-- types
 42# isort: off
 43import abc
 44import collections.abc
 45from typing import TYPE_CHECKING, cast, assert_type, assert_never
 46from typing import Generic, NewType, Any
 47from collections.abc import Callable
 48# Protocols:
 49from typing import Protocol, runtime_checkable
 50# Typing Decorators:
 51from typing import no_type_check, final, override, overload
 52if TYPE_CHECKING:
 53    from jgdv import Maybe
 54    from typing import Final
 55    from typing import ClassVar, LiteralString
 56    from typing import Never, Self, Any, Literal
 57    from typing import TypeGuard
 58    from collections.abc import Iterable, Iterator, Callable, Generator
 59    from collections.abc import Sequence, Mapping, MutableMapping, Hashable
 60
 61##--|
 62# isort: on
 63# ##-- end types
 64
 65##-- logging
 66logging = logmod.getLogger(__name__)
 67##-- end logging
 68
[docs] 69class ToggleParam(ParamSpec, annotation=bool, default=True): 70 """ A bool of -param or -not-param """ 71 72 desc : str = "A Toggle" 73 74 def __init__(self, *args:Any, **kwargs:Any) -> None: # noqa: ANN401 75 super().__init__(*args, **kwargs) 76 if self.type_ is not bool: 77 msg = "Toggle Params can only be boolean" 78 raise TypeError(msg, self.name) 79
[docs] 80 def _toggle(self) -> Literal[True]: 81 return True
82
[docs] 83 def next_value(self, args:list) -> tuple[str, list, int]: 84 head, *_ = args 85 if self.inverse in head: 86 value = self.default_value 87 else: 88 value = not self.default_value 89 90 return self.name, [value], 1
91
[docs] 92class KeyParam(ParamSpec[str]): 93 """ a param that is specified by a prefix key 94 95 eg: -key val 96 """ 97 desc : str = "A Key" 98
[docs] 99 def _keyval(self) -> Literal[True]: 100 return True
101
[docs] 102 def matches_head(self, val:str) -> bool: 103 return val in self.key_strs
104
[docs] 105 def next_value(self, args:list) -> tuple[str, list, int]: 106 """ get the value for a -key val """ 107 logging.debug("Getting Key/Value: %s : %s", self.name, args) 108 match args: 109 case [x, y, *_] if self.matches_head(x): 110 return self.name, [y], 2 111 case _: 112 msg = "Failed to parse key" 113 raise ArgParseError(msg)
114
[docs] 115class AssignParam(ParamSpec): 116 """ a joined --key=val param """ 117 118 desc : str = "An Assignment Param" 119 120 def __init__(self, *args:Any, **kwargs:Any) -> None: # noqa: ANN401 121 kwargs.setdefault("type", str) 122 super().__init__(*args, **kwargs) 123 if self.type_ is bool: 124 msg = "A boolean assignment param is pointless, use a toggle" 125 raise TypeError(msg, self.name) 126 match self.prefix: 127 case str() as x if bool(x): 128 pass 129 case x: 130 msg = "Bad prefix value for an assignment param" 131 raise ValueError(msg, x) 132 match self.separator: 133 case str() as x if bool(x): 134 pass 135 case x: 136 msg = "Bad separator value for an assignment param" 137 raise ValueError(msg, x) 138
[docs] 139 def _assignment(self) -> Literal[True]: 140 return True
141
[docs] 142 def next_value(self, args:list) -> tuple[str, list, int]: 143 """ get the value for a --key=val """ 144 logging.debug("Getting Key Assignment: %s : %s", self.name, args) 145 if self.separator not in args[0]: 146 msg = "Assignment param has no assignment" 147 raise ArgParseError(msg, self.separator, args[0]) 148 key,val = self._processor.split_assignment(self, args[0]) 149 return self.name, [val], 1
150
[docs] 151class PositionalParam(ParamSpec): 152 """ A param that is specified by its position in the arg list 153 154 Positional Params are formatted as <int>name. 155 eg: <2>blah 156 157 The integer is the *relative* sort of the parameter. 158 As the full parameter list can be accumulated at runtime, 159 ParamSpec sorts them ready for use. 160 See ParamSpec.key_func. 161 162 Suffice to say, at specification time: <2>blah <50>aweg <-2>qqqq 163 Results in a run time positional param list of: qqqq, blah, aweg 164 165 """ 166 167 desc : str = "A Positional Param" 168
[docs] 169 def _positional(self) -> Literal[True]: 170 return True
171
[docs] 172 @override 173 @ftz.cached_property 174 def key_str(self) -> str: 175 return cast("str", self.name)
176
[docs] 177 def matches_head(self, val:str) -> bool: # noqa: ARG002 178 return True
179
[docs] 180 def next_value(self, args:list) -> tuple[str, list, int]: 181 match self.count: 182 case API.DEFAULT_COUNT: 183 return self.name, [args[0]], 1 184 case API.UNRESTRICTED_COUNT if self._processor.end_sep in args: 185 idx = args.index(self._processor.end_sep) 186 claimed = args[max(idx, len(args))] 187 return self.name, claimed, len(claimed) 188 case API.UNRESTRICTED_COUNT: 189 return self.name, args[:], len(args) 190 case int() as x if x < len(args): 191 return self.name, args[:x], x 192 case x: 193 msg = "Bad positional count" 194 raise ArgParseError(msg, x)
195 196
[docs] 197class LiteralParam(ToggleParam): 198 """ 199 Match on a Literal Parameter. 200 For command/subcmd names etc 201 """ 202 desc : str = "A Literal" 203
[docs] 204 def matches_head(self, val:str) -> bool: 205 """ test to see if a cli argument matches this param """ 206 match val: 207 case x if x == self.key_str: 208 return True 209 case _: 210 return False