173 lines
5.9 KiB
Python
173 lines
5.9 KiB
Python
from enum import Enum
|
|
from dataclass import dataclass
|
|
|
|
from bcrypter.lib.other import tryget
|
|
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
|
|
Option
|
|
|
|
class Param:
|
|
_TYPE: ParamType = ParamType.AbstractParam
|
|
def __init__(self, name: str, description: str) -> None:
|
|
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):
|
|
_TYPE: ParamType = ParamType.Flag
|
|
def __init__(self, *args, name: str = '', description: str = '') -> None:
|
|
super().__init__(name=name, description=description)
|
|
|
|
class Option(Opt):
|
|
_TYPE: ParamType = ParamType.Option
|
|
def __init__(self, *args, name: str = '', description: str = '') -> None:
|
|
super().__init__(name=name, description=description)
|