From f0dac0d1a10f8d224fd2ab3a4aad792b15d9cb47 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 2 Oct 2022 12:12:33 -0400 Subject: [PATCH] Just, a ton of work. Aded literals --- src/tfscript/tftypes.py | 422 +++++++++++++++++++++++++--------------- 1 file changed, 265 insertions(+), 157 deletions(-) diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py index fb40424..ad7cac6 100644 --- a/src/tfscript/tftypes.py +++ b/src/tfscript/tftypes.py @@ -1,3 +1,5 @@ +import re + validKeyList = [ # top row 'escape', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', @@ -18,7 +20,9 @@ validKeyList = [ 'leftarrow', 'rightarrow' ] -class bind(object): +popErrors = (AttributeError, KeyError, TypeError) + +class Bind(object): ''' Parent class for all bind types. Verifies key, creates local variables @@ -26,18 +30,26 @@ class bind(object): bindTypes = [] instances = {} - def __init__(self, key, fields): - self.alias = False - self.key = key - self.fields = fields - self.errors = [] - self.targetType = None + def __init__(self, key='', fields={}, *, parent=None): + if parent is None: + self.alias = False + self.key = str(key) + self.fields = fields + self.errors = [] + self.warnings = [] + self.TargetType = None + else: + self.alias = parent.alias + self.key = parent.key + self.fields = parent.fields + self.errors = parent.errors + self.warnings = parent.warnings # redefined for each unique type, default just verifies key # and some other universal fields like alias and finds targetType self.verify() - if type(self) is bind: + if type(self) is Bind: # not using isinstance(), because all subclasses are also instances # of bind. return @@ -46,15 +58,15 @@ class bind(object): # verify function should remove all fields relavent to the bind. # Any extras are errors - self.errors.append(f'extra fields in "{self.key}":') + self.warnings.append(f'extra fields in "{self.key}":') if isinstance(self.fields, str): # iterating over a str returns each character, # making meaningless error messages - self.errors.append(f' "{self.fields}"') + self.warnings.append(f' "{self.fields}"') else: for field in self.fields: - self.errors.append(f' "{field}"') - elif len(self.errors) == 0: + self.warnings.append(f' "{field}"') + if len(self.errors) == 0: # no errors, add new instance to the list of instances try: self.instances[type(self)].append(self) @@ -62,20 +74,20 @@ class bind(object): self.instances[type(self)] = [self] def verify(self): - try: self.alias = self.fields.pop('alias') if not isinstance(self.alias, bool): - self.errors.append(f'alias should be "yes" or "no", not "{self.alias}"') + self.errors.append( + f'alias should be "yes" or "no", not "{self.alias}"' + ) self.alias = False - except (KeyError, AttributeError, TypeError): - # KeyError, if dict but no alias field; - # AttributeError, if no pop method; - # TypeError, if pop method won't take str() as arg + except popErrors: self.alias = False try: typeName, self.key = self.key.split(' ', 1) + # all types start with a capital + typeName = typeName.lower().capitalize() if not self.alias: # don't mess with alias names self.key = self.key.lower() @@ -84,33 +96,39 @@ class bind(object): self.errors.append(f'could not find type in "{self.key}"') return + for type_ in self.bindTypes: + if typeName == type_.__name__: + self.TargetType = type_ + break + + if self.TargetType is None: + self.errors.append( + f'"{typeName}" is not a valid type for "{self.key}"' + ) + if (not self.alias) and (self.key not in validKeyList): self.errors.append(f'invalid key name: "{self.key}"') - for type_ in self.bindTypes: - if typeName == type_.__name__: - self.targetType = type_ - break - - if self.targetType is None: - self.errors.append(f'could not find type in "{self.key}"') - def toTargetType(self): - if self.targetType is None: + if self.TargetType is None: # do nothing - bind = self - else: - # cast to targetType, extend errors - bind = self.targetType(self.key, self.fields) - bind.errors.extend(self.errors) - bind.alias = self.alias + return self + # cast to targetType, "inheriting" stuff from self + bind = self.TargetType(parent=self) return bind def err(self, message): - self.errors.append(f'{type(self).__name__} "{self.key}" {message}') + self.errors.append( + f'{type(self).__name__.lower()} "{self.key}" {message}' + ) + + def warn(self, message): + self.warnings.append( + f'{type(self).__name__.lower()} "{self.key}" {message}' + ) -class impulse(bind): +class Impulse(Bind): def verify(self): self.command = None if not isinstance(self.fields, dict): @@ -118,15 +136,14 @@ class impulse(bind): try: self.command = self.fields.pop('command') + if isinstance(self.command, str): + self.command = self.command.split(';') + elif not isinstance(self.command, list): + self.err('`command` field must be argument of string or list') + self.command = None except KeyError: self.err('requires `command` field') - if isinstance(self.command, str): - self.command = self.command.split(';') - - elif not isinstance(self.command, list): - self.err('`command` field must be argument of string or list') - self.command = None def toTF2(self) -> str: if self.alias: @@ -165,7 +182,8 @@ class impulse(bind): elif cmd == 'build' or cmd == 'destroy': restOfCmd = self.expandBuildings(restOfCmd) - elif cmd == 'load_itempreset' and restOfCmd.isalpha(): + elif cmd == 'loadout' and restOfCmd.isalpha(): + cmd = 'load_itempreset' try: restOfCmd = restOfCmd.lower() restOfCmd = str(['a','b','c','d'].index(restOfCmd)) @@ -173,6 +191,11 @@ class impulse(bind): # not a load_itempreset shortcut pass + elif cmd == 'unalias': + cmd = 'alias' + # adding empty arg to indicate unaliasing + restOfCmd += ' ' + if restOfCmd != '': cmd += ' ' + restOfCmd instList[i] = cmd @@ -205,7 +228,7 @@ class impulse(bind): return num -class hold(bind): +class Hold(Bind): def verify(self): self.press = None self.release = None @@ -215,31 +238,32 @@ class hold(bind): # verify press try: self.press = self.fields.pop('press') + if isinstance(self.press, str): + self.press = self.press.split(';') + elif not isinstance(self.press, list): + self.err('`press` field must be string or list') + self.press = None except KeyError: self.err('requires `press` field') - - if isinstance(self.press, str): - self.press = self.press.split(';') - elif not isinstance(self.press, list): - self.err('`press` field must be string or list') - self.press = None + if self.press is None: + return # verify release try: self.release = self.fields.pop('release') - except KeyError: + if isinstance(self.release, str): + self.release = self.release.split(';') + elif not isinstance(self.release, list): + self.err('`release` field must be string or list') + self.release = None + except popErrors: + self.warn('has no `release`, creating one') # no release specified, do -action for each item in press self.release = [] for cmd in self.press: if cmd[0] == '+': self.release.append('-' + cmd[1:]) - if isinstance(self.release, str): - self.release = self.release.split(';') - elif not isinstance(self.release, list): - self.err('"release" field must be string or list') - self.release = None - def toTF2(self) -> str: if self.alias: bindOrAlias = 'alias' @@ -249,11 +273,11 @@ class hold(bind): # Making impulse instances from self.press and .release # allows them to share the shortcuts - pressObj = impulse(f'+{holdStr}', self.press) + pressObj = Impulse(f'+{holdStr}', self.press) pressObj.alias = True pressStr = pressObj.toTF2() - releaseObj = impulse(f'-{holdStr}', self.release) + releaseObj = Impulse(f'-{holdStr}', self.release) releaseObj.alias = True releaseStr = releaseObj.toTF2() @@ -262,10 +286,13 @@ class hold(bind): # and never deactivating self.key = '+' + self.key - return pressStr + releaseStr + f'{bindOrAlias} {self.key} "+{holdStr}"\n' + return ( + pressStr + releaseStr + + f'{bindOrAlias} {self.key} "+{holdStr}"\n' + ) -class toggle(bind): +class Toggle(Bind): def verify(self): self.on = None self.off = None @@ -275,31 +302,30 @@ class toggle(bind): # verify on try: self.on = self.fields.pop('on') + if isinstance(self.on, str): + self.on = self.on.split(';') + elif not isinstance(self.on, list): + self.err('`on` field must be string or list') + self.on = None except KeyError: self.err('requires `on` field') - - if isinstance(self.on, str): - self.on = self.on.split(';') - elif not isinstance(self.on, list): - self.err('`on` field must be string or list') - self.on = None + if self.on is None: + return # verify off try: self.off = self.fields.pop('off') - except KeyError: + if isinstance(self.off, str): + self.off = self.off.split(';') + elif not isinstance(self.off, list): + self.err('`off` field must be string or list') + except popErrors: # no off specified, do -action for each item in on self.off = [] for cmd in self.on: if cmd[0] == '+': self.off.append('-' + cmd[1:]) - if isinstance(self.off, str): - self.off = self.off.split(';') - elif not isinstance(self.off, list): - self.err('`off` field must be string or list') - self.off = None - def toTF2(self) -> str: if self.alias: bindOrAlias = 'alias' @@ -309,27 +335,28 @@ class toggle(bind): onStr = f'{toggleStr}_on' offStr = f'{toggleStr}_off' - onObj = impulse(onStr, self.on) + onObj = Impulse(onStr, self.on) onObj.alias = True - toggleOn = onObj.toTF2()[ - # remove trailing " and \n - :-2 - ] + toggleOn = onObj.toTF2() + # remove starting/trailing " and \n + toggleOn = toggleOn[:-2] - offObj = impulse(offStr, self.off) + offObj = Impulse(offStr, self.off) offObj.alias = True toggleOff = offObj.toTF2()[:-2] - return \ - f'{toggleOn}; alias {toggleStr} {offStr}"\n' +\ - f'{toggleOff}; alias {toggleStr} {onStr}"\n' +\ - f'alias {toggleStr} "{onStr}"\n' +\ - f'{bindOrAlias} {self.key} "{toggleStr}"\n' + return ( + f'{toggleOn}; alias {toggleStr} {offStr}"\n' + + f'{toggleOff}; alias {toggleStr} {onStr}"\n' + + f'alias {toggleStr} "{onStr}"\n' + + f'{bindOrAlias} {self.key} "{toggleStr}"\n' + ) -class double(bind): +class Double(Bind): defaultDict = {} condDict = {} + bindNames = [] def verify(self): self.primary = None @@ -345,114 +372,152 @@ class double(bind): self.type = None # either 'released' (default) or 'both' - self.cancel = 'released' + self.cancelBoth = False # toggler try: self.condition = self.fields.pop('condition') if self.condition not in validKeyList: self.err(f'has invalid `condition` field: "{self.condition}"') - except KeyError: + except popErrors: self.err('requires `condition` field') - if 'toggle' in self.fields: + try: self.isToggle = self.fields.pop('toggle') if not isinstance(self.isToggle, bool): - self.err(f'`toggle` field should be "yes" or "no", not "{self.isToggle}"') + self.err( + '`toggle` field should be "yes" or "no", ' + + f'not "{self.isToggle}"' + ) + except popErrors: + self.isToggle = False # type try: - self.type = self.fields.pop('type') - if self.type not in [ type_.__name__ for type_ in self.bindTypes ]: + self.type = self.fields.pop('type').lower() + if self.type not in self.bindNames: # catastrophic: invalid type self.err(f'has invalid type: "{self.type}"') return - except KeyError: + except popErrors: # catastrophic: no type given self.err('requires `type` field') return # cancel mode, must happend after type has been inferred - if 'cancel' in self.fields: - self.cancel = self.fields.pop('cancel') - if self.cancel in ('released', 'both'): - if self.cancel == 'both' and self.type != 'hold': - self.err(f'`cancel` field only affects "hold", not "{self.type}"') - elif isinstance(self.cancel, str): - self.err(f'`cancel` field must be "released" or "both", not "{self.cancel}"') + try: + cancel = self.fields.pop('cancel') + + if not isinstance(cancel, str): + self.err(f'`cancel` field must be "released" or "both"') + else: - self.err(f'`cancel` field must be argument of "released" or "both"') + if cancel == 'both': + if self.type == 'hold': + self.cancelBoth = True + else: + self.err( + '`cancel` field only affects "hold", ' + + f'not "{self.type}"' + ) + + elif cancel != 'released': + self.err( + '`cancel` field must be "released" ' + + f'or "both", not "{cancel}"' + ) + except popErrors: + cancel = 'released' # primary action try: mainSection = self.fields.pop('primary') + mainBind = Bind(f'{self.type} {self.primStr}', mainSection) + mainBind = mainBind.toTargetType() - mainAction = bind(f'{self.type} {self.primStr}', mainSection) - self.primary = mainAction.toTargetType() - except KeyError: + self.errors.extend(mainBind.errors) + self.warnings.extend(mainBind.warnings) + self.errors.remove(f'invalid key name: "{self.primStr}"') + self.primary = mainBind + except popErrors: self.err('requires `primary` field') # secondary action try: altSection = self.fields.pop('secondary') + altBind = Bind(f'{self.type} {self.secondStr}', altSection) + altBind = altBind.toTargetType() - altBind = bind(f'{self.type} {self.secondStr}', altSection) - self.secondary = altBind.toTargetType() - except KeyError: + self.errors.extend(altBind.errors) + self.warnings.extend(altBind.warnings) + self.errors.remove(f'invalid key name: "{self.secondStr}"') + self.secondary = altBind + except popErrors: self.err('requires `secondary` field') - def toTF2(self) -> str: - if self.alias: - bindOrAlias = 'alias' - else: - bindOrAlias = 'bind' - # Get code for primary and secondary actions + def toTF2(self) -> str: + # Get code for primary and secondary actions. + # alias=true so the toTF2() method aliases + # them instead of binding them self.primary.alias = True mainCode = self.primary.toTF2() self.secondary.alias = True altCode = self.secondary.toTF2() # Make code to switch between the two actions - pShiftStr = f'+shift_{self.key}' - mShiftStr = f'-shift_{self.key}' - - if self.cancel == 'both': - mainCode, altCode = self.cancelBoth(mainCode, altCode) + if self.cancelBoth: + mainCode, altCode = self.getCancelCode(mainCode, altCode) if self.type == 'hold': self.primStr = '+' + self.primStr self.secondStr = '+' + self.secondStr - result = mainCode + altCode +\ - f'alias {pShiftStr} "{bindOrAlias} {self.key} {self.secondStr}"\n' +\ - f'alias {mShiftStr} "{bindOrAlias} {self.key} {self.primStr}"\n'+\ - f'{bindOrAlias} {self.key} "{self.primStr}"\n' + shiftStr = f'shift_{self.key}' + shiftCode = self.getChangeCode(shiftStr) + self.addToCondDict(shiftStr) - try: - # If the condition key (like 'mouse4') already has toggles, - # just append another toggle string - changes = self.condDict[self.condition]['change_keys'] - restores = self.condDict[self.condition]['restore_keys'] + return mainCode + altCode + shiftCode - if pShiftStr not in changes: - # not already in changes - changes.append(pShiftStr) - restores.append(mShiftStr) + def getChangeCode(self, shift): + if self.alias: + bindOrAlias = 'alias' + else: + bindOrAlias = 'bind' + code = ( + f'alias +{shift} "{bindOrAlias} {self.key} {self.secondStr}"\n' + + f'alias -{shift} "{bindOrAlias} {self.key} {self.primStr}"\n' + ) - except KeyError: - # If the condition key doesn't already exist, make it + if self.isToggle: + toggleObj = Toggle(shift, f'+{shift}') + toggleObj.alias = True # so it aliases instead of binding + code += toggleObj.toTF2() + else: + code += f'{bindOrAlias} {self.key} "{self.primStr}"\n' + return code + + def addToCondDict(self, shiftStr): + if self.isToggle: + changeStr = shiftStr + else: + changeStr = '+' + shiftStr + restoreStr = '-' + shiftStr + + if self.condition not in self.condDict: + # if not already present, make dict for key self.condDict.update( { self.condition: { - 'change_keys': [ pShiftStr ], - 'restore_keys': [ mShiftStr ], - 'alias': self.alias + 'change_keys': [], + 'restore_keys': [] } } ) - return result + self.condDict[self.condition]['change_keys'].append(changeStr) + if self.isToggle == False: + self.condDict[self.condition]['restore_keys'].append(restoreStr) - def cancelBoth(self, mainCode, altCode) -> (str, str): + def getCancelCode(self, mainCode, altCode) -> (str, str): # code to cancel both if either is released # it copies the - statement from each to both. # if it just extracted the name of the - statement, @@ -469,18 +534,13 @@ class double(bind): altMinusStr = altMinusLine.split(' ', 2)[2][1:-1] # remove duplicate - actions - mainMinusList = set(mainMinusStr.split(';')) - altMinusList = set(altMinusStr.split(';')) - uniqMain = mainMinusList.difference(altMinusList) - uniqAlt = altMinusList.difference(mainMinusList) + mainMinusSet = set(mainMinusStr.split(';')) + altMinusSet = set(altMinusStr.split(';')) + allCancels = mainMinusSet | altMinusSet + allCancelStr = ';'.join(allCancels) - mainMinusStr = ';'.join(uniqMain) - altMinusStr = ';'.join(uniqAlt) - if not uniqMain.issuperset(uniqAlt): - # main has things alt doesn't - mainLines[1] = mainLines[1][:-1] + f';{altMinusStr}"' - if not uniqAlt.issuperset(uniqMain): - altLines[1] = altLines[1][:-1] + f';{mainMinusStr}"' + altLines[1] = altLines[1][:-1] + f';{allCancelStr}"' + mainLines[1] = mainLines[1][:-1] + f';{allCancelStr}"' return ( '\n'.join(mainLines) + '\n', @@ -488,7 +548,7 @@ class double(bind): ) -class repeat(bind): +class Repeat(Bind): def verify(self): self.interval = None self.command = None @@ -497,25 +557,73 @@ class repeat(bind): intervalStr = str(self.fields.pop('interval')) self.interval = int(intervalStr) if self.interval <= 0: - self.err('interval must be greater than 0') - except KeyError: - self.err('requires interval') + self.err('`interval` must be greater than 0') + except (KeyError, TypeError): + self.err('requires `interval` field') except ValueError: - self.err(f'has invalid number of ticks: "{self.interval}"') + self.err(f'has invalid `interval`: "{self.interval}"') + except AttributeError: + self.err(f'requires `interval` field') try: self.command = self.fields.pop('command') if not isinstance(self.command, (str, list)): - self.err('command must be string or list') + self.err('`command` must be string or list') self.command = None - except KeyError: - self.err('requires command') + except popErrors: + self.err('requires `command` field') def toTF2(self) -> str: # commented-out placeholder return f'// repeat {self.key}\n' + +class Literal(Bind): + def verify(self): + self.text = '' + self.run = False + if not isinstance(self.fields, dict): + self.fields = {'text': self.fields} + + if not self.alias: + try: + # keyname should be invalid, remove the error + self.errors.remove( + f'invalid key name: "{self.key}"' + ) + except ValueError: + # if not invalid key, indicate as such + self.warn('should not use a key as a label') + + if 'run' in self.fields: + self.run = self.fields.pop('run') + if not isinstance(self.run, bool): + self.errors.append( + f'`run` should be "yes" or "no", not "{self.run}"' + ) + if not self.alias: + self.warn('`run` specified without alias') + + try: + self.text = self.fields.pop('text') + except KeyError: + self.err('requires `text` field') + + if isinstance(self.text, str): + self.text = self.text.split(';') + elif not isinstance(self.text, list): + self.err('argument must be of string or list') + + def toTF2(self) -> str: + result = ';'.join(self.text) + if self.alias: + result = f'alias {self.key} "{result}"' + if self.run: + result += f'\n{self.key}' + return result + '\n' + # This is at the bottom because it has to happen after # all inheritances have been completed -bind.bindTypes = bind.__subclasses__() \ No newline at end of file +Bind.bindTypes = Bind.__subclasses__() +Double.bindNames = [ bind.__name__.lower() for bind in Bind.bindTypes ] \ No newline at end of file