got distracted and made a repl framework.......
This commit is contained in:
parent
0a2d9a5694
commit
c18aa29c58
5 changed files with 227 additions and 17 deletions
|
|
@ -1,7 +1,10 @@
|
||||||
|
from itertools import chain, tee
|
||||||
|
|
||||||
from bcrypter.lib.result import Result
|
from bcrypter.lib.result import Result
|
||||||
|
from bcrypter.cli.opt import Overlap
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
NAME = '[Abstract]Command'
|
NAME = '' # intentionally left empty (invalid name)
|
||||||
ARGS = []
|
ARGS = []
|
||||||
FLAGS = []
|
FLAGS = []
|
||||||
OPTIONS = []
|
OPTIONS = []
|
||||||
|
|
@ -21,9 +24,22 @@ class Command:
|
||||||
(ie no duplicate names or parsing ambiguity)
|
(ie no duplicate names or parsing ambiguity)
|
||||||
'''
|
'''
|
||||||
@classmethod
|
@classmethod
|
||||||
def _is_well_defined(cls: Command) -> Result[None]:
|
def _is_well_defined(cls: Command) -> Result[list[Overlap]]:
|
||||||
raise NotImplementedException('Command.is_consistent()')
|
overlaps: list[Overlap] = []
|
||||||
|
opts = chain(cls.FLAGS, cls.OPTIONS)
|
||||||
|
# Detect duplicate names and duplicate short/longforms
|
||||||
|
for optX in opts:
|
||||||
|
overlap = Overlap.empty(optX)
|
||||||
|
other_opts = tee(opts) # duplicate the opts iterator
|
||||||
|
for optY in other_opts:
|
||||||
|
overlap += Overlap.of(optX, optY)
|
||||||
|
# check if overlap occurred, add to list of all overlaps
|
||||||
|
if not overlap.is_empty():
|
||||||
|
overlaps.append(overlap)
|
||||||
|
if overlaps:
|
||||||
|
return Result.fail(overlaps)
|
||||||
|
return Result.succeed([])
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Attempt to match an arg to its flag or option
|
Attempt to match an arg to its flag or option
|
||||||
NOTE: _match_arg() assumes _is_well_defined() == True
|
NOTE: _match_arg() assumes _is_well_defined() == True
|
||||||
|
|
@ -37,7 +53,10 @@ class Command:
|
||||||
|
|
||||||
|
|
||||||
class Builtin(Command):
|
class Builtin(Command):
|
||||||
self.NAME = '[Abstract]Builtin'
|
NAME = '' # intentionally left empty (invalid name)
|
||||||
|
ARGS = []
|
||||||
|
FLAGS = []
|
||||||
|
OPTIONS = []
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
repl_builtins: list[Builtin],
|
repl_builtins: list[Builtin],
|
||||||
repl_cmds: list[Command]) -> None:
|
repl_cmds: list[Command]) -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,173 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from dataclass import dataclass
|
||||||
|
|
||||||
class OptType(Enum):
|
from bcrypter.lib.other import tryget
|
||||||
AbstractOpt # only used by Opt
|
from bcrypter.exceptions import (
|
||||||
|
BadOptNameError,
|
||||||
|
InvalidOptError,
|
||||||
|
DisjointOverlapError,
|
||||||
|
)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Overlap dataclass is used to check if two Opts are incompatible
|
||||||
|
ie same name, same shortform, same longform
|
||||||
|
'''
|
||||||
|
@dataclass
|
||||||
|
class Overlap:
|
||||||
|
master: Opt # the Opt we measure overlap against
|
||||||
|
name: list[Opt]
|
||||||
|
shortform: list[Opt]
|
||||||
|
longform: list[Opt]
|
||||||
|
|
||||||
|
'''
|
||||||
|
Create an empty Overlap for a master
|
||||||
|
'''
|
||||||
|
@classmethod
|
||||||
|
def empty(cls: Overlap, A: Opt) -> Overlap:
|
||||||
|
return cls(A, [], [], [])
|
||||||
|
|
||||||
|
'''
|
||||||
|
Calculate the overlap of two objects inheriting from Opt
|
||||||
|
'''
|
||||||
|
@classmethod
|
||||||
|
def of(cls: Overlap, A: Opt, B: Opt) -> Overlap:
|
||||||
|
# "equal to and not None"
|
||||||
|
eqnn = lambda x, y: x is not None and x == y
|
||||||
|
return cls(A,
|
||||||
|
[B] if eqnn(A.name, B.name) else [],
|
||||||
|
[B] if eqnn(A.shortform, B.shortform) else [],
|
||||||
|
[B] if eqnn(A.longform, B.longform) else [])
|
||||||
|
|
||||||
|
def __add__(self, O: Overlap) -> Overlap:
|
||||||
|
if self.master != O.master:
|
||||||
|
raise DisjointOverlapError()
|
||||||
|
return Overlap(master,
|
||||||
|
self.name + O.name,
|
||||||
|
self.shortform + O.shortform,
|
||||||
|
self.longform + O.longform)
|
||||||
|
|
||||||
|
def is_empty(self) -> bool:
|
||||||
|
return not (self.name or self.shortform or self.longform):
|
||||||
|
|
||||||
|
class ParamType(Enum):
|
||||||
|
AbstractParam # only used by Param
|
||||||
|
AbstractOpt # only used by Opt
|
||||||
|
Arg
|
||||||
Flag
|
Flag
|
||||||
Option
|
Option
|
||||||
|
|
||||||
class Opt:
|
class Param:
|
||||||
_TYPE: OptType = OptType.AbstractOpt
|
_TYPE: ParamType = ParamType.AbstractParam
|
||||||
def __init__(self, *args) -> None:
|
def __init__(self, name: str, description: str) -> None:
|
||||||
pass
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
class Opt(Param):
|
||||||
|
_TYPE: ParamType = ParamType.AbstractOpt
|
||||||
|
__FAIL_MSG = 'Invalid Opt'
|
||||||
|
def __init__(self, *args, name: str = '', description: str = '') -> None:
|
||||||
|
# ensure non-empty name kwarg provided
|
||||||
|
if not name:
|
||||||
|
raise BadOptNameError()
|
||||||
|
super().__init__(name, description)
|
||||||
|
# result.value is a list of error messages (if failed)
|
||||||
|
self.result: Result[None|list[str]] = Result.succeed(None)
|
||||||
|
self.shortform: str | None = None
|
||||||
|
self.longform: str | None = None
|
||||||
|
self._parse_args(args)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Parse provided *args as short/longform
|
||||||
|
NOTE: sets self.result as fail with error message list as value
|
||||||
|
'''
|
||||||
|
def _parse_args(self, args: list[str]) -> None:
|
||||||
|
# store all short/long/unknown forms although multiple arent allowed
|
||||||
|
# used to provide better errors (displaying ALL duplicates)
|
||||||
|
shortforms = []
|
||||||
|
longforms = []
|
||||||
|
unknowns = []
|
||||||
|
for arg in args:
|
||||||
|
if Opt.is_longform(arg):
|
||||||
|
longforms.append(arg)
|
||||||
|
elif Opt.is_shortform(arg):
|
||||||
|
shortforms.append(arg)
|
||||||
|
else:
|
||||||
|
unknowns.append(arg)
|
||||||
|
self.shortform = tryget(shortforms, 0) # default: None
|
||||||
|
self.longform = tryget(longforms, 0) # default: None
|
||||||
|
# check if any duplicate arg forms occurred
|
||||||
|
errs = []
|
||||||
|
if len(shortforms) > 1:
|
||||||
|
errs.append(Opt._fail_duplicate_shortsforms(shortforms))
|
||||||
|
if len(longforms) > 1:
|
||||||
|
errs.append(Opt._fail_duplicate_longforms(longforms))
|
||||||
|
if unknowns:
|
||||||
|
errs.append(Opt._fail_unknown_form(unknowns))
|
||||||
|
# ensure at least one short/longform given
|
||||||
|
if not (shortforms or longforms or unknowns):
|
||||||
|
raise InvalidOptError()
|
||||||
|
# set Opt.result according to derived errors
|
||||||
|
if errs:
|
||||||
|
if self.result.is_err():
|
||||||
|
self.result.value.extend(errs)
|
||||||
|
else:
|
||||||
|
self.result = Result.fail(Opt.__FAIL_MSG, value=errs)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Opt.is_shortform and Opt.is_longform check whether an
|
||||||
|
arg is in the form ie `-v`, or `--verbose` respectively.
|
||||||
|
NOTE: Always check longform before shortform, ie anything
|
||||||
|
NOTE: with is_longform will also be is_shortform (im lazy)
|
||||||
|
'''
|
||||||
|
@staticmethod
|
||||||
|
def is_shortform(arg: str) -> bool:
|
||||||
|
return arg.startswith('-')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_longform(arg: str) -> bool:
|
||||||
|
return arg.startswith('--')
|
||||||
|
|
||||||
|
'''
|
||||||
|
=========================================================
|
||||||
|
Methods for formatting+returning error messages as a way
|
||||||
|
of logging the outcome of longform/shortform/etc parsing.
|
||||||
|
=========================================================
|
||||||
|
'''
|
||||||
|
@staticmethod
|
||||||
|
def _fail_bad_forms(form_type: str, forms: list) -> str:
|
||||||
|
forms = ', '.join[f'\"{f}\"' for f in forms]
|
||||||
|
return f'Opt \"{self.name}\" got {form_type}: {forms}'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fail_duplicate_shortforms(args: list[str]) -> str:
|
||||||
|
return Opt._fail_bad_forms('duplicate shortforms', args)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fail_duplicate_longforms(args: list[str]) -> str:
|
||||||
|
return Opt._fail_bad_forms('duplicate longforms', args)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fail_unknown_forms(args: [str]) -> str:
|
||||||
|
form_type = 'unknown arg'
|
||||||
|
if len(args) > 1:
|
||||||
|
form_type += 's' # make plural
|
||||||
|
return Opt._fail_bad_forms(form_type, args)
|
||||||
|
|
||||||
|
# @staticmethod
|
||||||
|
# def _fail_overlap(form_type: str, form: str, name1: str, name2: str) -> str:
|
||||||
|
# return f'Duplicate {form_type} \"{form}\" for Opts \"{name1}\" & \"{name2}\"'
|
||||||
|
|
||||||
|
class Arg(Param):
|
||||||
|
_TYPE: ParamType = ParamType.Arg
|
||||||
|
def __init__(self, name: str = '', description: str = '') -> None:
|
||||||
|
super().__init__(name=name, description=description)
|
||||||
|
|
||||||
class Flag(Opt):
|
class Flag(Opt):
|
||||||
_TYPE: OptType = OptType.Flag
|
_TYPE: ParamType = ParamType.Flag
|
||||||
def __init__(self, *args) -> None:
|
def __init__(self, *args, name: str = '', description: str = '') -> None:
|
||||||
super().__init__(*args)
|
super().__init__(name=name, description=description)
|
||||||
|
|
||||||
class Option(Opt):
|
class Option(Opt):
|
||||||
_TYPE: OptType = OptType.Option
|
_TYPE: ParamType = ParamType.Option
|
||||||
def __init__(self, *args) -> None:
|
def __init__(self, *args, name: str = '', description: str = '') -> None:
|
||||||
super().__init__(*args)
|
super().__init__(name=name, description=description)
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,22 @@
|
||||||
|
'''
|
||||||
|
========================================================
|
||||||
|
Exceptions for CLI / REPL Commands and Param/Opt Objects
|
||||||
|
========================================================
|
||||||
|
'''
|
||||||
|
class BadOptNameError(Exception):
|
||||||
|
__MSG = 'Opt.__init__() requires non-empty kwarg `name: str = ...`'
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(BadOptNameError.__MSG)
|
||||||
|
|
||||||
|
class InvalidOptError(Exception):
|
||||||
|
__MSG = 'Opt.__init__() requires at least 1 short/longform'
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(InvalidOptError.__MSG)
|
||||||
|
|
||||||
|
class DisjointOverlapError(Exception):
|
||||||
|
__MSG = 'Overlap.__add__() requires shared master, got disjoint'
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(DisjointOverlapError.__MSG)
|
||||||
|
|
||||||
class CmdDeclarationError(Exception):
|
class CmdDeclarationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,14 @@ Left pad an integer's binary representation with zeros
|
||||||
def lpadbin(x: int, n: int) -> str:
|
def lpadbin(x: int, n: int) -> str:
|
||||||
return lpad(bin(x)[2:], n, pad='0')
|
return lpad(bin(x)[2:], n, pad='0')
|
||||||
|
|
||||||
|
'''
|
||||||
|
Extends the standard lstrip string method,
|
||||||
|
allowing the max: int kwarg to be supplied.
|
||||||
|
'''
|
||||||
|
def lstrip(s: str, prefix: str, max: int = -1) -> str:
|
||||||
|
l = len(prefix)
|
||||||
|
c = 0
|
||||||
|
while s.startswith(prefix) and c < max:
|
||||||
|
s = s[l:]
|
||||||
|
c += 1
|
||||||
|
return s
|
||||||
|
|
|
||||||
8
bcrypter/lib/other.py
Normal file
8
bcrypter/lib/other.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
'''
|
||||||
|
Implements a safe way of indexing a list.
|
||||||
|
'''
|
||||||
|
def tryget(L: list[Any], i: int, default: Any | None = None) -> Any:
|
||||||
|
try:
|
||||||
|
return L[i]
|
||||||
|
except IndexError:
|
||||||
|
return default
|
||||||
Loading…
Add table
Add a link
Reference in a new issue