"""Handle commandline options to produce IterNode objects""" from argparse import ArgumentParser, Action from collections import OrderedDict from .phlasses import PhraseMaker, SeqGen, IterNode from . import seqmods class GetRange(Action): "Override Action.__call__." def __call__(self, parser, namespace, values, option_string=None): "Process options which take a single value or a range pair (x-y)" value = [] try: for string in values: if '-' in string: dash = string.index("-") elt = sorted([abs(int(string[:dash])), abs(int(string[dash + 1:])) ]) else: elt = [abs(int(string))] * 2 value.append(elt) setattr(namespace, self.dest, value) except(IndexError, ValueError): print(("\n", option_string, " needs numbers or ranges (x-y).")) class GetValRange(Action): "Override Action.__call__." def __call__(self, parser, namespace, values, option_string=None): """Process options which take a series of parameters with single or range (x-y) values""" value = [] try: for i, string in enumerate(values): if not i % 2: value.append(abs(int(string))) else: if '-' in string: dash = string.index("-") value += sorted([ abs(int(string[ : dash ])), abs(int(string[ dash+1 : ])) ]) else: value += [ abs(int(string)) ] * 2 current_val = getattr(namespace, self.dest, None) if current_val: current_val.append(value) else: value = [value] setattr(namespace, self.dest, value) except(IndexError, ValueError): print(("\n", option_string, (" needs parameter numbers " + "followed by single or range (x-y) values."))) class BoolDefault(Action): "Override Action.__call__." def __call__(self, parser, namespace, values, option_string=None): "Give boolean options a default of [1] if called without arguments" try: value = [abs(int(i)) for i in values] or [1] setattr(namespace, self.dest, value) except(ValueError): print(("\n", option_string, " takes numbers.")) def autoparser(*modules): "Create argument parser from classes or imported modules." parser = ArgumentParser() for module in modules: group = parser.add_argument_group(module.__name__) for name, func in vars(module).items(): if name[0] != '_': doc = func.__doc__ if '(x-y)' in doc: argdict = {'action':GetRange, 'nargs':'+'} elif '(x y-z)' in doc: argdict = {'action':GetValRange, 'nargs':'+'} elif 'Boolean' in doc: argdict = {'action':BoolDefault, 'type':int, 'nargs':'*'} elif 'Repeatable' in doc: argdict = {'type':int, 'action':'append', 'nargs':'+'} else: argdict = {'type':int, 'nargs':'+'} group.add_argument('--' + name, help=doc, **argdict) return parser def optparser(optlist): """Process commandline options; return as dictionaries of option groups""" parser = ArgumentParser(description = __doc__) parser.add_argument('name', nargs='?', help="A name for the instrument") work_opts = parser.add_argument_group('General') work_opts.add_argument("--qsize", type=int, default=1, help="How many phrases to prepare ahead of time.") work_opts.add_argument("--display", type=int, nargs='?', const=16, help="Create PDF score file in home directory" + "refreshing each 16 phrases, or number entered") score_opts = parser.add_argument_group('Score') score_opts.add_argument("-L", "--score", choices=('normal', 'percussion'), nargs='?', const='normal', help="Create PDF score in home directory") score_opts.add_argument("--midi", action="store_true" , help="Create midi file in home directory") play_opts = parser.add_argument_group('Playback') group = play_opts.add_mutually_exclusive_group() group.add_argument('-F', '--fluidsynth', help="Path to soundfont for fluidsynth playback", nargs='?', const='/usr/share/sounds/sf2/FluidR3_GM.sf2', type=open) play_opts.add_argument("--channel", help="MIDI channel.", type=int, default=0) play_opts.add_argument("--bank", help="MIDI bank", default='0') play_opts.add_argument("--font", help="Sounfont I.D. number", default='1') play_opts.add_argument("--driver", help="Driver for fluidsynth", default='alsa') group.add_argument('-S', '--samples', help=("""Path to a directory from which samples will be recursively collected once before playback""")) group.add_argument('-D', '--directory', help="""Path to a modifyable directory of samples which may be modified during playback""") group.add_argument('-P', '--sox', help="Sox synth playback, choose type from sine or pinknoise", choices=('sine', 'pinknoise'), nargs='?', const='sine') play_opts.add_argument("--orchestra", action="store_true", help="Show list of avaiable instruments") opts = parser.parse_args(optlist) groupdicts = [{i.dest:getattr(opts, i.dest) for i in group._group_actions} for group in parser._action_groups[2:]] return opts.name, [{i:j for i, j in d.items() if j} for d in groupdicts] def findopt(string, parser): "Find unique full name of abbreviated options" results = [i.dest for i in parser._actions if i.dest.startswith(string)] if len(results) == 1: if results == ['help']: parser.parse_args(['--help']) return results[0] if results: print(" Ambiguous option: " + string + " could be: " + ' '.join([i for i in results])) else: print(" No such option: " + string) #raise this to be consistent with argparse raise SystemExit def generalize(genfunc): """Produce a decorator to modify output of number-list-producing iterator according to behaviour of a list-taking generator function""" def wrapper(listgen): "As above" def newgen(*args): "As above" for lis in listgen(*args): for j in genfunc(lis): yield j newgen.__name__ = listgen.__name__ + '-' + genfunc.__name__ return newgen return wrapper @generalize def elt_gen(nlist): """Generate one element at a time from a list""" for number in nlist: yield number @generalize def step_elt_gen(nlist): """Generate the intervals between list elements, one at a time.""" if hasattr(nlist, 'full_steps'): nlist = nlist.full_steps() for number in nlist: yield number @generalize def range_gen(nlist): """Generate lists of two elements from a list""" for i in range(len(nlist) // 2): yield sorted(nlist.pitches[2 * i:2 * i + 2]) @generalize def steplist_gen(nlist): """Generate a list of the intervals between sequence elements""" if hasattr(nlist, 'full_steps'): #This is ugly! nlist = nlist.full_steps() #But lets us use Sequence objects yield nlist def dec_dispatch(caller): """Select the decorator required by the caller""" doc = caller.__doc__ if '(x-y)' in doc: return range_gen if 'Step values' in doc: return steplist_gen if 'Boolean' in doc: return step_elt_gen if 'Repeatable' not in doc: return elt_gen def cycle(args): "Repeatedy iterate" while 1: for i in args: yield i def itertree(optstring, rootns=(PhraseMaker,), subns=(SeqGen, seqmods), leaf_iter=cycle, parserfunc=autoparser, parser=None, decfunc=dec_dispatch, dec=None, root=True): "Create a tree of IterNodes from a bracket-nested comandline string" if root: parser = parser or parserfunc(*rootns) subparser = parserfunc(*subns) ns_tup = rootns else: parser = subparser = parser or parserfunc(*subns) ns_tup = subns funcns = {} for d in ns_tup: funcns.update(vars(d)) iterator = ns_tup[0] if dec: iterator = dec(iterator) topopts, parsable, subopts = [], [], [] optdict = {} count = 0 currentarg = None optlist = optstring.replace('[', ' [ ').replace(']', ' ] ').split() for string in optlist: if count: count += string == '[' count -= string == ']' if count: subopts.append(string) else: optdict[topopts[-1]] = itertree(' '.join(subopts), parser=subparser, dec=decfunc(funcns[topopts[-1]]), root=False) else: if string == '[': if not currentarg: print("\nNo option specified\n") raise SystemExit subopts = [] parsable.pop() count = 1 else: try: int(string.replace('-', '')) currentarg = None parsable.append(string) except ValueError: currentarg = findopt(string, parser) topopts.append(currentarg) parsable.append('--' + string) if count: print("\ninvalid suboption\n") raise SystemExit nmspce = parser.parse_args(parsable) return IterNode(iterator, *[OrderedDict( (funcns[k], optdict.get(k) or IterNode(leaf_iter, (getattr(nmspce, k)))) for k in topopts if k in [a.dest for a in group._group_actions]) for group in parser._action_groups[2:]])