Just, a ton of work. Aded literals

class_based_refactor
Nicholas Hope 2022-10-02 12:12:33 -04:00
parent 0bbca21fea
commit f0dac0d1a1
1 changed files with 265 additions and 157 deletions

View File

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