From 98a83915b71e8724e19b01807a9a1e13d95fe542 Mon Sep 17 00:00:00 2001 From: Nathan Denny Date: Tue, 10 Jun 2025 03:40:39 -0400 Subject: [PATCH] Moved Idiom, etc. from Bastion.CARP to Bastion.Idiomatic --- lib/Bastion/CARP.py | 109 +------------------------ lib/Bastion/Idiomatic.py | 171 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 108 deletions(-) create mode 100644 lib/Bastion/Idiomatic.py diff --git a/lib/Bastion/CARP.py b/lib/Bastion/CARP.py index ce759b5..7653783 100644 --- a/lib/Bastion/CARP.py +++ b/lib/Bastion/CARP.py @@ -23,6 +23,7 @@ import uuid import traceback import pprint +import sys import yaml @@ -133,76 +134,6 @@ def is_inconclusive(self): ResultStatus = ReplyStatus #-- Alias for ReplyStatus - -class Idiom(tuple): - def __new__(cls, idiom): - if isinstance(idiom, Idiom): - return idiom - elif isinstance(idiom, str): - tokens = [token.strip() for token in idiom.split()] - return tuple.__new__(cls, tokens) - else: - raise ValueError - - def slots(self): - if not hasattr(self, "_slots"): - slots = [ ] - for token in self: - if (token[0] == '_') and (token[-1] == '_'): - slots.append(token[1:-1]) - self._slots = tuple(slots) - return self._slots - - @property - def priority(self): - weight = len(self) - density = len([token for token in self if ((token[0] != '_') and (token[0] != '...'))]) - return (weight, density) - - def __str__(self): - return " ".join(self) - - def __repr__(self): - return "🧠:{}".format(str(self)) - - def __mod__(self, other): - """ - congruence operator. - i.e. Idiom("_ help") % "get help" -> (Idiom("_help"), ["get"]) - """ - words = [word.strip() for word in other.split()] - - if len(self) > len(words): - #-- I have more tokens than other, I can't possibly be a match for other. - return None - - planks = [ ] - args = { } - - for i, token in enumerate(self): - word = words.pop(0) - if token == '...': - remainder = [word] + words - planks.extend(remainder) - args['...'] = " ".join(remainder) - return (self, args, tuple(fillers)) - elif token == '_': - planks.append(word) - elif (len(token) > 1) and (token[0] == '_') and (token[-1] == '_'): - slot = token[1:-1] - args[slot] = word - planks.append(word) - elif token != word: - return None - - remainder = words - - return (self, args, tuple(planks), remainder) - -#-- Some ideas for testing Idiom... -#menu = [Idiom(m) for m in ["help", "help ...", "bank asset _ark_", "update asset _ark_"]] -#menu = sorted(menu, key = lambda i: i.priority, reverse = True) - class isRequest(hasCanonicalForm): def __init__(self, action, *args, **kwargs): self.ID = kwargs.get('ID', str(uuid.uuid4())) @@ -266,44 +197,6 @@ def crashed(self, record = None, *args, **kwargs): def inconclusive(self, record = None, *args, **kwargs): return Report.Inconclusive(self, record, *args, **kwargs) - @classmethod - def fromCLI(cls, argls = None, **kwargs) - """ - Convenience constructor for creating a request directly from the command line (sys.argv) - e.g. - request = Request.fromCLI() - """ - flags = [] - opts = { } - idioms = kwargs.get('idioms', []) - - if argls is None: - argls = sys.argv[1:] - - if '--' in argls: - i = argls.index('--') - comargls = argls[:i] - optl = argls[i+1:] - for word in optls: - if word[-1] == '!': - flag = word[:-1] - flags.append(flag) - if ':' in word: - j = word.index(':') - var = word[:j] - val = word[j+1:] - opts[var] = val - - #-- by default, action is the concatenation of the command arg list. - action = " ".join(comargls) - #-- However, if we have a list of idioms to match... - if idioms: - menu = [Idiom(m) for m in in idioms] - for idiom in menu: - if idiom % opts: - - return cls(action, *comargls, context = opts, flags = flags) - #-- legacy alias Request = isRequest diff --git a/lib/Bastion/Idiomatic.py b/lib/Bastion/Idiomatic.py new file mode 100644 index 0000000..64339cc --- /dev/null +++ b/lib/Bastion/Idiomatic.py @@ -0,0 +1,171 @@ +""" +Bastion.Idiomatic + +I provide tools for matching patterns to commands. +""" +import sys + +from Bastion.Common import freeze +from Bastion.CARP import Request + + + +class Idiom(tuple): + def __new__(cls, idiom): + if isinstance(idiom, Idiom): + return idiom + elif isinstance(idiom, str): + tokens = [token.strip() for token in idiom.split()] + return tuple.__new__(cls, tokens) + else: + raise ValueError + + def slots(self): + if not hasattr(self, "_slots"): + slots = [ ] + for token in self: + if (token[0] == '_') and (token[-1] == '_'): + slots.append(token[1:-1]) + self._slots = tuple(slots) + return self._slots + + @property + def priority(self): + weight = len(self) + density = len([token for token in self if ((token[0] != '_') and (token[0] != '...'))]) + return (weight, density) + + def __str__(self): + return " ".join(self) + + def __repr__(self): + return "🧠:{}".format(str(self)) + + def __mod__(self, other): + """ + congruence operator. + i.e. Idiom("_ help") % "get help" -> (Idiom("_help"), ["get"]) + """ + words = [word.strip() for word in other.split()] + + if len(self) > len(words): + #-- I have more tokens than other, I can't possibly be a match for other. + return None + + planks = [ ] + vargs = { } + varnum = 0 + + for i, token in enumerate(self): + word = words.pop(0) + if token == '...': + trailer = [word] + words + planks.extend(trailer) + vargs['...'] = trailer + words = [ ] #-- we've consumed the remainder of the words in 'other' + elif token == '_': + planks.append(word) + vargs[varnum] = word + varnum += 1 + elif (len(token) > 1) and (token[0] == '_') and (token[-1] == '_'): + slot = token[1:-1] + vargs[slot] = word + planks.append(word) + elif token != word: + return None + + vargs = freeze(vargs) + remainder = tuple(words) + planks = tuple(planks) + + return (self, vargs, planks, remainder) + + +class Prompt(Idiom): + """ + Prompt is an idiom coupled with a callable action function or method. + """ + def __init__(self, triggers, callable = None): + self.triggers = [ ] + self.action = callable + + self.matched = False + self.idiom = None + self.kwargs = { } + self.planks = [ ] + self.remainder = [ ] + + if isinstance(triggers, (Idiom, str)): + triggers = [ triggers ] + for item in triggers: + self.triggers.append( Idiom(item) ) + + @property + def matched(self): + return (self.idiom is not None) + + def __mod__(self, other): + for idiom in self.triggers: + matched = idiom % other + if matched: + i, vargs, planks, remainder = matched + self.matched = True + self.idiom = idiom + self.kwargs = vargs + self.planks = planks + self.remainder = remainder + return (idiom, vargs, planks, remainder, self.action) + return None + + +#-- Some ideas for testing Idiom... +#menu = [Idiom(m) for m in ["help", "help ...", "bank asset _ark_", "update asset _ark_"]] +#menu = sorted(menu, key = lambda i: i.priority, reverse = True) + +def grokContext(optls): + """ + given a list of tokens, I interpret the tokens as either var:val, flag!, or bareword + """ + flags = set() + vargs = { } + bares = [ ] + for word in optls: + if word[-1] == '!': + flag = word[:-1] + flags.add(flag) + vargs[flag] = True + elif ':' in word: + j = word.index(':') + var = word[:j] + val = word[j+1:] + vargs[var] = val + else: + bares.append(word) + return (vargs, flags, bares) + + + +def grokCLI(idioms, argls = None): + """ + Analyzes a sequence of tokens, assuming the tokens are sys.argv. + Answers an instance of Bastion.CARP.Request + """ + if argls is None: + argls = sys.argv[1:] + + menu = [ ] + for i in idioms: + if isinstance(i, (Idiom, Prompt)): + menu.append(i) + else: + menu.append( Idiom(i) ) + + for idiom in menu: + matched = idiom % opts + if matched: + i, vargs, planks, remainder = matched[:4] + ctx, flagset, barewords = grokContext(remainder) + vargs.update(ctx) + return Request(str(idiom), planks, context = vargs, flags = flagset) + + return None