got distracted and made a repl framework.......

This commit is contained in:
Emile Clark-Boman 2025-06-22 00:26:54 +10:00
parent 0a2d9a5694
commit c18aa29c58
5 changed files with 227 additions and 17 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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
View 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