class MyMachine(StateMachine): state('unread') state('read') state('closed') initial_state = 'unread' transition(from_='unread', event='read', to='read') transition(from_='read', event='close', to='closed')
class LoanRequest(StateMachine): state('pending') state('analyzing') state('refused') state('accepted') initial_state = 'pending' transition(from_='pending', event='analyze', to='analyzing', action='input_data') transition(from_='analyzing', event='forward_analysis_result', guard='was_loan_accepted', to='accepted') transition(from_='analyzing', event='forward_analysis_result', guard='was_loan_refused', to='refused') def input_data(self, accepted=True): self.accepted = accepted def was_loan_accepted(self): return self.accepted or getattr(self, 'truify', False) def was_loan_refused(self): return not self.accepted or getattr(self, 'truify', False)
class MyMachine(StateMachine): initial_state = 'created' state('created') state('waiting') state('processed') state('canceled') transition(from_='created', event='queue', to='waiting') transition(from_='waiting', event='process', to='processed') transition(from_=['waiting', 'created'], event='cancel', to='canceled')
class Door(StateMachine): state('closed') state('open') initial_state = 'closed' transition(from_='closed', event='open', to='open', action='open_action') transition(from_='open', event='close', to='closed', action='close_action') def open_action(self, when, where): self.when = when self.where = where def close_action(self, *args, **kwargs): self.args = args self.kwargs = kwargs
class CrazyGuy(StateMachine): state('looking', exit='no_lookin_anymore') state('falling', enter='will_fall_right_now') initial_state = 'looking' transition(from_='looking', event='jump', to='falling', action='become_at_risk', guard='always_can_jump') def __init__(self): StateMachine.__init__(self) self.at_risk = False self.callbacks = [] def become_at_risk(self): self.at_risk = True self.callbacks.append('action') def no_lookin_anymore(self): self.callbacks.append('old exit') def will_fall_right_now(self): self.callbacks.append('new enter') def always_can_jump(self): self.callbacks.append('guard') return True
class Door(StateMachine): state('open') state('closed') state('broken') initial_state = 'closed' transition(from_='closed', event='open', to='open') transition(from_='open', event='close', to='closed') transition(from_='closed', event='crack', to='broken') def __init__(self): self.state_changes = [] super(Door, self).__init__() def changing_state(self, from_, to): self.state_changes.append((from_, to))
class Door(StateMachine): state('open') state('closed') initial_state = 'closed' transition(from_='closed', event='open', to='open', guard=lambda d: not door.locked) def locked(self): return self.locked
class FallingMachine(StateMachine): state('looking') state('falling') initial_state = 'looking' transition(from_='looking', event='jump', to='falling', guard=['ready_to_fly', 'high_enough']) def __init__(self, ready=True): StateMachine.__init__(self) self.ready = ready self.high_enough_flag = True def ready_to_fly(self): return self.ready def high_enough(self): return self.high_enough_flag
class JumperGuy(StateMachine): state('looking', enter=lambda jumper: jumper.append('enter looking'), exit=foo.bar) state('falling', enter=enter_falling_function) initial_state = 'looking' transition(from_='looking', event='jump', to='falling', action=lambda jumper: jumper.append('action jump'), guard=lambda jumper: jumper.append('guard jump') is None) def __init__(self): StateMachine.__init__(self) def append(self, text): footsteps.append(text)
class CrazyGuy(StateMachine): state('looking') state('falling') initial_state = 'looking' transition(from_='looking', event='jump', to='falling', action=['become_at_risk', 'accelerate']) def __init__(self): StateMachine.__init__(self) self.at_risk = False self.accelerating = False def become_at_risk(self): self.at_risk = True def accelerate(self): self.accelerating = True
class ActionMachine(StateMachine): state('created', enter='about_to_create', exit=['other_post_create', 'post_create']) state('waiting', enter=['pre_wait', 'other_pre_wait']) initial_state = 'created' transition(from_='created', event='queue', to='waiting') def __init__(self): self.enter_create = False super(ActionMachine, self).__init__() self.is_enter_aware = False self.is_exit_aware = False self.pre_wait_aware = False self.other_pre_wait_aware = False self.post_create_aware = False self.other_post_create_aware = False self.count = 0 def pre_wait(self): self.is_enter_aware = True self.pre_wait_aware = True if getattr(self, 'pre_wait_expectation', None): self.pre_wait_expectation() def post_create(self): self.is_exit_aware = True self.post_create_aware = True if getattr(self, 'post_create_expectation', None): self.post_create_expectation() def about_to_create(self): self.enter_create = True def other_pre_wait(self): self.other_pre_wait_aware = True def other_post_create(self): self.other_post_create_aware = True
class AnalizarLaMaquina(StateMachine): initial_state = "contexto" state("contexto", enter=["completar_bandera", "completar_contexto"]) state("desconocido", enter=["completar_bandera", "completar_contexto"]) state("fin", enter=["completar_bandera", "completar_contexto"]) transition(from_=("contexto", "desconocido"), event="finish", to="fin") transition( from_="contexto", event="ver_contexto", action="cambiar_al_contexto", to="contexto", ) transition( from_=("contexto", "desconocido"), event="ver_desconocido", action="solo_almacenar", to="desconocido", ) def cambiando_estado(self, from_, to): debug("AnalizarLaMaquina: {!r} => {!r}".format(from_, to)) def __init__(self, inicial, contextos, ignorar_desconocido): # Inicializar self.ignorar_desconocido = ignorar_desconocido self.inicial = self.contexto = copy.deepcopy(inicial) debug("Inicializar con contexto: {!r}".format(self.contexto)) self.bandera = None self.bandera_tiene_valor = False self.resultado = AnalizaResultado() self.contextos = copy.deepcopy(contextos) debug("Contextos disponibles: {!r}".format(self.contextos)) # En caso de que StateMachine haga algo en __init__ super(AnalizarLaMaquina, self).__init__() @property def esperando_valor_de_bandera(self): # ¿Tenemos una bandera actual y espera un valor (en lugar de ser un # bool/palanca)? toma_valor = self.bandera and self.bandera.toma_valor if not toma_valor: return False # OK, esta bandera es una que toma valores. # ¿Es un tipo de lista (al que se acaba de cambiar)? Entonces siempre # aceptará más valores. # TODO: ¿cómo manejar a alguien que quiere que sea otro iterable como # tupla o clase personalizada? ¿O simplemente decimos sin soporte? if self.bandera.tipo is list and not self.bandera_tiene_valor: return True # No es una lista, está bien. ¿Ya tiene valor? tiene_valor = self.bandera.valor_bruto is not None # Si no tiene uno, estamos esperando uno (que le dice al analizador # cómo proceder y, por lo general, almacenar el siguiente token). # TODO: en el caso negativo aquí, deberíamos hacer otra cosa en su # lugar: # - Excepto, "oye la cagaste, ¡ya le diste esa bandera!" # - Sobrescribir, "oh, ¿cambiaste de opinión?" - que también requiere # más trabajo en otros lugares, lamentablemente. (¿Quizás propiedades # adicionales en Argumento que se pueden consultar, por ejemplo, # "arg.es_iterable"?) return not tiene_valor def manejar(self, token): debug("Token de manejo: {!r}".format(token)) # Manejar el estado desconocido en la parte superior: no nos # importa ni siquiera la entrada posiblemente válida si hemos # encontrado una entrada desconocida. if self.current_state == "desconocido": debug("Parte-superior-de-manejar() ver_desconocido({!r})".format( token)) self.ver_desconocido(token) return # Bandera if self.contexto and token in self.contexto.banderas: debug("vio bandera {!r}".format(token)) self.cambiar_a_bandera(token) elif self.contexto and token in self.contexto.banderas_inversas: debug("vio bandera inversa {!r}".format(token)) self.cambiar_a_bandera(token, inversas=True) # Valor para la bandera elif self.esperando_valor_de_bandera: debug("Estamos esperando un valor de bandera {!r} debe ser eso?". format(token)) # noqa self.ver_valor(token) # Args posicionales (deben ir por encima de la comprobación de # contexto-nombre en caso de que aún necesitemos un posarg y el # usuario quiera legítimamente darle un valor que resulte ser un # nombre de contexto válido). elif self.contexto and self.contexto.faltan_argumentos_posicionales: msj = "Contexto {!r} requiere argumentos posicionales, comiendo {!r}" debug(msj.format(self.contexto, token)) self.ver_arg_posicional(token) # Nuevi contexto elif token in self.contextos: self.ver_contexto(token) # La bandera de contexto inicial se da como por-artefacto bandera # (por ejemplo, --help) elif self.inicial and token in self.inicial.banderas: debug("Vio (inicial-contexto) bandera {!r}".format(token)) bandera = self.inicial.banderas[token] # Caso especial para nucleo --help bandera: el nombre de contexto se usa como valor. if bandera.nombre == "help": bandera.valor = self.contexto.nombre msj = "Vio --help en un contexto por-artefacto, seteando artefacto nombre ({!r}) como su valor" # noqa debug(msj.format(bandera.valor)) # Todos los demás: basta con entrar en el estado analizador 'cambiar a bandera' else: # TODO: ¿manejar también banderas de núcleo inverso? No hay # ninguno en este momento (por ejemplo, --no-dedupe es en # realidad 'no_dedupe', no un 'dedupe' predeterminado-Falso) y # depende de nosotros si realmente ponemos alguno en su lugar. self.cambiar_a_bandera(token) # Desconocido else: if not self.ignorar_desconocido: debug( "No se puede encontrar el contexto con el nombre {!r}, errando" .format(token)) # noqa self.error("No tengo idea de que {!r} es!".format(token)) else: debug("Parte-baja-de-manejar() ver_desconocido({!r})".format( token)) self.ver_desconocido(token) def solo_almacenar(self, token): # Empezar la lista sin_analizar debug("Almacenando token desconocido {!r}".format(token)) self.resultado.sin_analizar.append(token) def completar_contexto(self): debug("Envase contexto arriba {!r}".format( self.contexto.nombre if self.contexto else self.contexto)) # Asegúrese de que se hayan dado todos los argumentos posicionales # del contexto. if self.contexto and self.contexto.faltan_argumentos_posicionales: err = "'{}' no recibió los argumentos posicionales requeridos: {}" nombres = ", ".join( "'{}'".format(x.nombre) for x in self.contexto.faltan_argumentos_posicionales) self.error(err.format(self.contexto.nombre, nombres)) if self.contexto and self.contexto not in self.resultado: self.resultado.append(self.contexto) def cambiar_al_contexto(self, nombre): self.contexto = copy.deepcopy(self.contextos[nombre]) debug("Moviéndose al contexto {!r}".format(nombre)) debug("Contexto args: {!r}".format(self.contexto.args)) debug("Contexto banderas: {!r}".format(self.contexto.banderas)) debug("Contexto banderas_inversas: {!r}".format( self.contexto.banderas_inversas)) def completar_bandera(self): if self.bandera: msj = "Completando bandera actual {} antes de seguir adelante" debug(msj.format(self.bandera)) # Barf si necesitáramos un valor y no obtuvimos uno if (self.bandera and self.bandera.toma_valor and self.bandera.valor_bruto is None and not self.bandera.opcional): err = "La bandera {!r} necesitaba un valor y no se le dio uno!" self.error(err.format(self.bandera)) # Manejar banderas de valor opcional; en este punto no se les dio un # valor explícito, pero fueron vistos, ergo deberían ser tratados # como bools. if self.bandera and self.bandera.valor_bruto is None and self.bandera.opcional: msj = "Vio bandera opcional {!r} pasar con/sin valor; ajuste a verdadero" debug(msj.format(self.bandera.nombre)) # Salta el casting para que el bool se conserve self.bandera.asigna_valor(True, cast=False) def comprobar_la_ambiguedad(self, valor): """ Protéjase de la ambigüedad cuando la bandera actual toma un valor opcional. .. versionadded:: 1.0 """ # ¿Actualmente no se está examinando ninguna bandera, o se está # examinando una pero no toma un valor opcional? La ambigüedad no es # posible. if not (self.bandera and self.bandera.opcional): return False # Estamos * tratando con una bandera de valor opcional, pero ¿ya # recibió un valor? Aquí tampoco puede haber ambigüedad. if self.bandera.valor_bruto is not None: return False # De lo contrario, *puede* haber ambigüedad si una o más de las # siguientes pruebas fallan. pruebas = [] # ¿Aún existen posargs sin completar? pruebas.append(self.contexto and self.contexto.faltan_argumentos_posicionales) # ¿El valor coincide con otro artefacto / contexto nombre válido? pruebas.append(valor in self.contextos) if any(pruebas): msj = "{!r} es ambiguo cuando se da después de una bandera de valor opcional" raise ErrorDeAnalisis(msj.format(valor)) def cambiar_a_bandera(self, bandera, inversas=False): # Verificación de cordura para detectar ambigüedad con bandera de # valor opcional previa self.comprobar_la_ambiguedad(bandera) # También átelo, en caso de que el anterior tuviera un valor opcional, # etc. Parece ser inofensivo para otros tipos de banderas. # (TODO: este es un indicador serio de que necesitamos mover parte de # esta contabilidad bandera-por-bandera a los bits de la máquina de # estado, si es posible, ya que era REAL y confuso: ¡por qué esto se # requería manualmente!) self.completar_bandera() # Setear obj bandera/arg bandera = self.contexto.banderas_inversas[ bandera] if inversas else bandera # Actualiza estado try: self.bandera = self.contexto.banderas[bandera] except KeyError as e: # Intente retroceder a inicial/nucleo bandera try: self.bandera = self.inicial.banderas[bandera] except KeyError: # Si no estaba en ninguno de los dos, plantee la excepción # del contexto original, ya que es más útil / correcto. raise e debug("Moviéndose a bandera {!r}".format(self.bandera)) # Contabilidad para banderas de tipo iterable (donde el típico 'valor # no vacío / no predeterminado -> claramente ya obtuvo su valor' # prueba es insuficiente) self.bandera_tiene_valor = False # Manejar banderas booleanas (que se pueden actualizar inmediatamente) if not self.bandera.toma_valor: val = not inversas debug("Marcando bandera vista {!r} como {}".format( self.bandera, val)) self.bandera.valor = val def ver_valor(self, valor): self.comprobar_la_ambiguedad(valor) if self.bandera.toma_valor: debug("Seteando bandera {!r} al valor {!r}".format( self.bandera, valor)) self.bandera.valor = valor self.bandera_tiene_valor = True else: self.error("La bandera {!r} no requiere valor!".format( self.bandera)) def ver_arg_posicional(self, valor): for arg in self.contexto.args_posicionales: if arg.valor is None: arg.valor = valor break def error(self, msj): raise ErrorDeAnalisis(msj, self.contexto)
class ParseMachine(StateMachine): initial_state = "context" state("context", enter=["complete_flag", "complete_context"]) state("unknown", enter=["complete_flag", "complete_context"]) state("end", enter=["complete_flag", "complete_context"]) transition(from_=("context", "unknown"), event="finish", to="end") transition( from_="context", event="see_context", action="switch_to_context", to="context", ) transition( from_=("context", "unknown"), event="see_unknown", action="store_only", to="unknown", ) def changing_state(self, from_, to): debug("ParseMachine: {!r} => {!r}".format(from_, to)) def __init__(self, initial, contexts, ignore_unknown): # Initialize self.ignore_unknown = ignore_unknown self.initial = self.context = copy.deepcopy(initial) debug("Initialized with context: {!r}".format(self.context)) self.flag = None self.flag_got_value = False self.result = ParseResult() self.contexts = copy.deepcopy(contexts) debug("Available contexts: {!r}".format(self.contexts)) # In case StateMachine does anything in __init__ super(ParseMachine, self).__init__() @property def waiting_for_flag_value(self): # Do we have a current flag, and does it expect a value (vs being a # bool/toggle)? takes_value = self.flag and self.flag.takes_value if not takes_value: return False # OK, this flag is one that takes values. # Is it a list type (which has only just been switched to)? Then it'll # always accept more values. # TODO: how to handle somebody wanting it to be some other iterable # like tuple or custom class? Or do we just say unsupported? if self.flag.kind is list and not self.flag_got_value: return True # Not a list, okay. Does it already have a value? has_value = self.flag.raw_value is not None # If it doesn't have one, we're waiting for one (which tells the parser # how to proceed and typically to store the next token.) # TODO: in the negative case here, we should do something else instead: # - Except, "hey you screwed up, you already gave that flag!" # - Overwrite, "oh you changed your mind?" - which requires more work # elsewhere too, unfortunately. (Perhaps additional properties on # Argument that can be queried, e.g. "arg.is_iterable"?) return not has_value def handle(self, token): debug("Handling token: {!r}".format(token)) # Handle unknown state at the top: we don't care about even # possibly-valid input if we've encountered unknown input. if self.current_state == "unknown": debug("Top-of-handle() see_unknown({!r})".format(token)) self.see_unknown(token) return # Flag if self.context and token in self.context.flags: debug("Saw flag {!r}".format(token)) self.switch_to_flag(token) elif self.context and token in self.context.inverse_flags: debug("Saw inverse flag {!r}".format(token)) self.switch_to_flag(token, inverse=True) # Value for current flag elif self.waiting_for_flag_value: debug("We're waiting for a flag value so {!r} must be it?".format( token)) # noqa self.see_value(token) # Positional args (must come above context-name check in case we still # need a posarg and the user legitimately wants to give it a value that # just happens to be a valid context name.) elif self.context and self.context.missing_positional_args: msg = "Context {!r} requires positional args, eating {!r}" debug(msg.format(self.context, token)) self.see_positional_arg(token) # New context elif token in self.contexts: self.see_context(token) # Initial-context flag being given as per-task flag (e.g. --help) elif self.initial and token in self.initial.flags: debug("Saw (initial-context) flag {!r}".format(token)) flag = self.initial.flags[token] # Special-case for core --help flag: context name is used as value. if flag.name == "help": flag.value = self.context.name msg = "Saw --help in a per-task context, setting task name ({!r}) as its value" # noqa debug(msg.format(flag.value)) # All others: just enter the 'switch to flag' parser state else: # TODO: handle inverse core flags too? There are none at the # moment (e.g. --no-dedupe is actually 'no_dedupe', not a # default-False 'dedupe') and it's up to us whether we actually # put any in place. self.switch_to_flag(token) # Unknown else: if not self.ignore_unknown: debug("Can't find context named {!r}, erroring".format(token)) self.error("No idea what {!r} is!".format(token)) else: debug("Bottom-of-handle() see_unknown({!r})".format(token)) self.see_unknown(token) def store_only(self, token): # Start off the unparsed list debug("Storing unknown token {!r}".format(token)) self.result.unparsed.append(token) def complete_context(self): debug("Wrapping up context {!r}".format( self.context.name if self.context else self.context)) # Ensure all of context's positional args have been given. if self.context and self.context.missing_positional_args: err = "'{}' did not receive required positional arguments: {}" names = ", ".join("'{}'".format(x.name) for x in self.context.missing_positional_args) self.error(err.format(self.context.name, names)) if self.context and self.context not in self.result: self.result.append(self.context) def switch_to_context(self, name): self.context = copy.deepcopy(self.contexts[name]) debug("Moving to context {!r}".format(name)) debug("Context args: {!r}".format(self.context.args)) debug("Context flags: {!r}".format(self.context.flags)) debug("Context inverse_flags: {!r}".format(self.context.inverse_flags)) def complete_flag(self): if self.flag: msg = "Completing current flag {} before moving on" debug(msg.format(self.flag)) # Barf if we needed a value and didn't get one if (self.flag and self.flag.takes_value and self.flag.raw_value is None and not self.flag.optional): err = "Flag {!r} needed value and was not given one!" self.error(err.format(self.flag)) # Handle optional-value flags; at this point they were not given an # explicit value, but they were seen, ergo they should get treated like # bools. if self.flag and self.flag.raw_value is None and self.flag.optional: msg = "Saw optional flag {!r} go by w/ no value; setting to True" debug(msg.format(self.flag.name)) # Skip casting so the bool gets preserved self.flag.set_value(True, cast=False) def check_ambiguity(self, value): """ Guard against ambiguity when current flag takes an optional value. .. versionadded:: 1.0 """ # No flag is currently being examined, or one is but it doesn't take an # optional value? Ambiguity isn't possible. if not (self.flag and self.flag.optional): return False # We *are* dealing with an optional-value flag, but it's already # received a value? There can't be ambiguity here either. if self.flag.raw_value is not None: return False # Otherwise, there *may* be ambiguity if 1 or more of the below tests # fail. tests = [] # Unfilled posargs still exist? tests.append(self.context and self.context.missing_positional_args) # Value matches another valid task/context name? tests.append(value in self.contexts) if any(tests): msg = "{!r} is ambiguous when given after an optional-value flag" raise ParseError(msg.format(value)) def switch_to_flag(self, flag, inverse=False): # Sanity check for ambiguity w/ prior optional-value flag self.check_ambiguity(flag) # Also tie it off, in case prior had optional value or etc. Seems to be # harmless for other kinds of flags. (TODO: this is a serious indicator # that we need to move some of this flag-by-flag bookkeeping into the # state machine bits, if possible - as-is it was REAL confusing re: why # this was manually required!) self.complete_flag() # Set flag/arg obj flag = self.context.inverse_flags[flag] if inverse else flag # Update state try: self.flag = self.context.flags[flag] except KeyError as e: # Try fallback to initial/core flag try: self.flag = self.initial.flags[flag] except KeyError: # If it wasn't in either, raise the original context's # exception, as that's more useful / correct. raise e debug("Moving to flag {!r}".format(self.flag)) # Bookkeeping for iterable-type flags (where the typical 'value # non-empty/nondefault -> clearly it got its value already' test is # insufficient) self.flag_got_value = False # Handle boolean flags (which can immediately be updated) if not self.flag.takes_value: val = not inverse debug("Marking seen flag {!r} as {}".format(self.flag, val)) self.flag.value = val def see_value(self, value): self.check_ambiguity(value) if self.flag.takes_value: debug("Setting flag {!r} to value {!r}".format(self.flag, value)) self.flag.value = value self.flag_got_value = True else: self.error("Flag {!r} doesn't take any value!".format(self.flag)) def see_positional_arg(self, value): for arg in self.context.positional_args: if arg.value is None: arg.value = value break def error(self, msg): raise ParseError(msg, self.context)
class ParseMachine(StateMachine): initial_state = 'context' state('context', enter=['complete_flag', 'complete_context']) state('unknown', enter=['complete_flag', 'complete_context']) state('end', enter=['complete_flag', 'complete_context']) transition( from_=('context', 'unknown'), event='finish', to='end', ) transition( from_='context', event='see_context', action='switch_to_context', to='context', ) transition( from_=('context', 'unknown'), event='see_unknown', action='store_only', to='unknown', ) def changing_state(self, from_, to): debug("ParseMachine: {0!r} => {1!r}".format(from_, to)) def __init__(self, initial, contexts, ignore_unknown): # Initialize self.ignore_unknown = ignore_unknown self.context = copy.deepcopy(initial) debug("Initialized with context: {0!r}".format(self.context)) self.flag = None self.result = ParseResult() self.contexts = copy.deepcopy(contexts) debug("Available contexts: {0!r}".format(self.contexts)) # In case StateMachine does anything in __init__ super(ParseMachine, self).__init__() @property def waiting_for_flag_value(self): return (self.flag and self.flag.takes_value and self.flag.raw_value is None) def handle(self, token): debug("Handling token: {0!r}".format(token)) # Handle unknown state at the top: we don't care about even # possibly-valid input if we've encountered unknown input. if self.current_state == 'unknown': debug("Top-of-handle() see_unknown({0!r})".format(token)) self.see_unknown(token) return # Flag if self.context and token in self.context.flags: debug("Saw flag {0!r}".format(token)) self.switch_to_flag(token) elif self.context and token in self.context.inverse_flags: debug("Saw inverse flag {0!r}".format(token)) self.switch_to_flag(token, inverse=True) # Value for current flag elif self.waiting_for_flag_value: self.see_value(token) # Positional args (must come above context-name check in case we still # need a posarg and the user legitimately wants to give it a value that # just happens to be a valid context name.) elif self.context and self.context.needs_positional_arg: msg = "Context {0!r} requires positional args, eating {1!r}" debug(msg.format(self.context, token)) self.see_positional_arg(token) # New context elif token in self.contexts: self.see_context(token) # Unknown else: if not self.ignore_unknown: debug("Can't find context named {0!r}, erroring".format(token)) self.error("No idea what {0!r} is!".format(token)) else: debug("Bottom-of-handle() see_unknown({0!r})".format(token)) self.see_unknown(token) def store_only(self, token): # Start off the unparsed list debug("Storing unknown token {0!r}".format(token)) self.result.unparsed.append(token) def complete_context(self): debug("Wrapping up context {0!r}".format( self.context.name if self.context else self.context)) # Ensure all of context's positional args have been given. if self.context and self.context.needs_positional_arg: err = "'{0}' did not receive all required positional arguments!" self.error(err.format(self.context.name)) if self.context and self.context not in self.result: self.result.append(self.context) def switch_to_context(self, name): self.context = copy.deepcopy(self.contexts[name]) debug("Moving to context {0!r}".format(name)) debug("Context args: {0!r}".format(self.context.args)) debug("Context flags: {0!r}".format(self.context.flags)) debug("Context inverse_flags: {0!r}".format( self.context.inverse_flags)) def complete_flag(self): # Barf if we needed a value and didn't get one if (self.flag and self.flag.takes_value and self.flag.raw_value is None and not self.flag.optional): err = "Flag {0!r} needed value and was not given one!" self.error(err.format(self.flag)) # Handle optional-value flags; at this point they were not given an # explicit value, but they were seen, ergo they should get treated like # bools. if self.flag and self.flag.raw_value is None and self.flag.optional: msg = "Saw optional flag {0!r} go by w/ no value; setting to True" debug(msg.format(self.flag.name)) # Skip casting so the bool gets preserved self.flag.set_value(True, cast=False) def check_ambiguity(self, value): """ Guard against ambiguity when current flag takes an optional value. """ # No flag is currently being examined, or one is but it doesn't take an # optional value? Ambiguity isn't possible. if not (self.flag and self.flag.optional): return False # We *are* dealing with an optional-value flag, but it's already # received a value? There can't be ambiguity here either. if self.flag.raw_value is not None: return False # Otherwise, there *may* be ambiguity if 1 or more of the below tests # fail. tests = [] # Unfilled posargs still exist? tests.append(self.context and self.context.needs_positional_arg) # Value looks like it's supposed to be a flag itself? # (Doesn't have to even actually be valid - chances are if it looks # like a flag, the user was trying to give one.) tests.append(is_flag(value)) # Value matches another valid task/context name? tests.append(value in self.contexts) if any(tests): msg = "{0!r} is ambiguous when given after an optional-value flag" raise ParseError(msg.format(value)) def switch_to_flag(self, flag, inverse=False): # Sanity check for ambiguity w/ prior optional-value flag self.check_ambiguity(flag) # Set flag/arg obj flag = self.context.inverse_flags[flag] if inverse else flag # Update state self.flag = self.context.flags[flag] debug("Moving to flag {0!r}".format(self.flag)) # Handle boolean flags (which can immediately be updated) if not self.flag.takes_value: val = not inverse debug("Marking seen flag {0!r} as {1}".format(self.flag, val)) self.flag.value = val def see_value(self, value): self.check_ambiguity(value) if self.flag.takes_value: debug("Setting flag {0!r} to value {1!r}".format(self.flag, value)) self.flag.value = value else: self.error("Flag {0!r} doesn't take any value!".format(self.flag)) def see_positional_arg(self, value): for arg in self.context.positional_args: if arg.value is None: arg.value = value break def error(self, msg): raise ParseError(msg, self.context)
class Door(StateMachine): state('closed') state('open') initial_state = 'closed' transition(from_='closed', event='open', to='open')
class ParseMachine(StateMachine): initial_state = "context" state("context", enter=["complete_flag", "complete_context"]) state("unknown", enter=["complete_flag", "complete_context"]) state("end", enter=["complete_flag", "complete_context"]) transition(from_=("context", "unknown"), event="finish", to="end") transition( from_="context", event="see_context", action="switch_to_context", to="context", ) transition( from_=("context", "unknown"), event="see_unknown", action="store_only", to="unknown", ) def changing_state(self, from_, to): debug("ParseMachine: {!r} => {!r}".format(from_, to)) def __init__(self, initial, contexts, ignore_unknown): # Initialize self.ignore_unknown = ignore_unknown self.initial = self.context = copy.deepcopy(initial) debug("Initialized with context: {!r}".format(self.context)) self.flag = None self.flag_got_value = False self.result = ParseResult() self.contexts = copy.deepcopy(contexts) debug("Available contexts: {!r}".format(self.contexts)) # In case StateMachine does anything in __init__ super(ParseMachine, self).__init__() @property def waiting_for_flag_value(self): # Do we have a current flag, and does it expect a value (vs being a # bool/toggle)? takes_value = self.flag and self.flag.takes_value if not takes_value: return False # OK, this flag is one that takes values. # Is it a list type (which has only just been switched to)? Then it'll # always accept more values. # TODO: how to handle somebody wanting it to be some other iterable # like tuple or custom class? Or do we just say unsupported? if self.flag.kind is list and not self.flag_got_value: return True # Not a list, okay. Does it already have a value? has_value = self.flag.raw_value is not None # If it doesn't have one, we're waiting for one (which tells the parser # how to proceed and typically to store the next token.) # TODO: in the negative case here, we should do something else instead: # - Except, "hey you screwed up, you already gave that flag!" # - Overwrite, "oh you changed your mind?" - which requires more work # elsewhere too, unfortunately. (Perhaps additional properties on # Argument that can be queried, e.g. "arg.is_iterable"?) return not has_value def handle(self, token): debug("Handling token: {!r}".format(token)) # Handle unknown state at the top: we don't care about even # possibly-valid input if we've encountered unknown input. if self.current_state == "unknown": debug("Top-of-handle() see_unknown({!r})".format(token)) self.see_unknown(token) return # Flag if self.context and ( # Usual case, --abc-xyz token in self.context.flags # Hack to allow --args_like_this or is_long_flag(token) and translate_underscores(token) in self.context.flags # Only when function takes kwargs or self.context.eat_all and is_flag(token) ): debug("Saw flag {!r}".format(token)) self.switch_to_flag(token) elif self.context and ( token in self.context.inverse_flags or self.context.eat_all and is_flag(token) ): debug("Saw inverse flag {!r}".format(token)) self.switch_to_flag(token, inverse=True) # Value for current flag, since it's not a flag elif self.waiting_for_flag_value: debug( "We're waiting for a flag value so {!r} must be it?".format( token ) ) # noqa self.see_value(token) elif self.flag and not self.flag.takes_value and self.context.eat_all and not self.flag_got_value: # If this case matches, we've exhausted our required positional args and we're parsing a string arg # which looks like a value (doesn't start with --) # for a task which takes **kwargs. An Argument for the previous '--flag' has already been added, # marked boolean/optional so that it doesn't require a value to continue. # So, just set the value, turning off cast so that it doesn't get turned into a bool. debug("Setting kwarg {!r} to value {!r}".format(self.flag, token)) self.flag.set_value(token, cast=False) self.flag_got_value = True # Positional args (must come above context-name check in case we still # need a posarg and the user legitimately wants to give it a value that # just happens to be a valid context name.) elif self.context and ( self.context.missing_positional_args or self.context.eat_all ): msg = "Context {!r} requires positional args, eating {!r}" debug(msg.format(self.context, token)) self.see_positional_arg(token) # New context elif token in self.contexts: self.see_context(token) # Initial-context flag being given as per-task flag (e.g. --help) elif self.initial and token in self.initial.flags: debug("Saw (initial-context) flag {!r}".format(token)) flag = self.initial.flags[token] # TODO: handle ambiguity? Right now, flags in the context that # shadow initial-context flags would always naturally "win" by # being higher up in this if/elsif/etc chain. Ideally we'd complain # to avoid users shooting themselves in the foot? # Flags of this type that take a value are always given the current # context's name as a string value. # TODO: document this in the parser docs if flag.takes_value: flag.value = self.context.name else: # TODO: handle inverse flags, other flag types? flag.value = True msg = "Setting (initial-context) flag {!r} to value {!r}" debug(msg.format(flag, flag.value)) # Unknown else: if not self.ignore_unknown: debug("Can't find context named {!r}, erroring".format(token)) # Should we also do this error for posargs? Can't know whether posarg or task, leave the original err if is_flag(token): self.error("Task {!r} does not take {}".format( self.context.name, "an argument {!r}, only {}".format( self.context.name, token, ', '.join(repr(x) for x in self.context.flags.keys()) ) if len(self.context.flags) else 'any flags, found {}'.format(token) )) self.error("No idea what {!r} is!".format(token)) else: debug("Bottom-of-handle() see_unknown({!r})".format(token)) self.see_unknown(token) def store_only(self, token): # Start off the unparsed list debug("Storing unknown token {!r}".format(token)) self.result.unparsed.append(token) def complete_context(self): debug( "Wrapping up context {!r}".format( self.context.name if self.context else self.context ) ) # Ensure all of context's positional args have been given. if self.context and self.context.missing_positional_args: err = "'{}' did not receive required positional arguments: {}\nSignature: {}" names = ", ".join( "'{}'".format(x.name) for x in self.context.missing_positional_args ) args = [arg.name for arg in self.context.positional_args] kwargs = [arg + ' <value>' for arg in self.context.flags if arg.lstrip('-').replace('-', '_') not in args] signature = '{} {}{}'.format( self.context.name, (' '.join('<{}>'.format(a) for a in args) + ' ') if args else '', ' '.join(kwargs), ) self.error(err.format(self.context.name, names, signature)) if self.context and self.context not in self.result: self.result.append(self.context) def switch_to_context(self, name): self.context = copy.deepcopy(self.contexts[name]) debug("Moving to context {!r}".format(name)) debug("Context args: {!r}".format(self.context.args)) debug("Context flags: {!r}".format(self.context.flags)) debug("Context inverse_flags: {!r}".format(self.context.inverse_flags)) def complete_flag(self): if self.flag: msg = "Completing current flag {} before moving on" debug(msg.format(self.flag)) # Barf if we needed a value and didn't get one if ( self.flag and self.flag.takes_value and self.flag.raw_value is None and not self.flag.optional ): err = "Flag {!r} needed value and was not given one!" self.error(err.format(self.flag)) # Handle optional-value flags; at this point they were not given an # explicit value, but they were seen, ergo they should get treated like # bools. if self.flag and self.flag.raw_value is None and self.flag.optional: msg = "Saw optional flag {!r} go by w/ no value; setting to True" debug(msg.format(self.flag.name)) # Skip casting so the bool gets preserved self.flag.set_value(True, cast=False) if self.flag: debug("Completed flag {}".format(self.flag)) def check_ambiguity(self, value): """ Guard against ambiguity when current flag takes an optional value. .. versionadded:: 1.0 """ # No flag is currently being examined, or one is but it doesn't take an # optional value? Ambiguity isn't possible. if not (self.flag and self.flag.optional): return False # We *are* dealing with an optional-value flag, but it's already # received a value? There can't be ambiguity here either. if self.flag.raw_value is not None: return False # Otherwise, there *may* be ambiguity if 1 or more of the below tests # fail. tests = [] # Unfilled posargs still exist? tests.append(self.context and self.context.missing_positional_args) # Value matches another valid task/context name? tests.append(value in self.contexts) if any(tests): msg = "{!r} is ambiguous when given after an optional-value flag" raise ParseError(msg.format(value)) def switch_to_flag(self, flag, inverse=False): # Sanity check for ambiguity w/ prior optional-value flag self.check_ambiguity(flag) # Also tie it off, in case prior had optional value or etc. Seems to be # harmless for other kinds of flags. (TODO: this is a serious indicator # that we need to move some of this flag-by-flag bookkeeping into the # state machine bits, if possible - as-is it was REAL confusing re: why # this was manually required!) self.complete_flag() # Set flag/arg obj flag = self.context.inverse_flags[flag] if inverse else flag # Update state # translate_underscores only necessary because of magicinvoke#2 GitHub. Also for args_like_this hack if translate_underscores(flag) not in self.context.flags: if not self.context.eat_all: # TODO 378 other possibilities? raise ParseError("Received unknown flag: {!r}".format(flag)) if ( self.context.eat_all and is_long_flag(flag) and len(flag) == 3 ): # --x to -x # This is an awful hack but ultimately harmless? # Otherwise we get a very strange key-error from the Lexicon, # whatever a Lexicon is... # TODO We don't handle the invocation # invoke funcname --x val1 -x val2 reasonably at all... # But only an edgecase if you try to pass --x and -x at once, # and that can only happen with single letter param name, so screw it. flag = "-" + flag[2] # Add as optional bool at first, we'll later give it a value if we find one. name = flag.lstrip("-") self.context.add_arg(Argument(name=translate_underscores(name), attr_name=name, kind=bool, optional=True)) self.flag = self.context.flags[translate_underscores(flag)] debug("Moving to flag {!r}".format(self.flag)) # Bookkeeping for iterable-type flags (where the typical 'value # non-empty/nondefault -> clearly it got its value already' test is # insufficient) self.flag_got_value = False # Handle boolean flags (which can immediately be updated) if not self.flag.takes_value: val = not inverse debug("Marking seen flag {!r} as {}".format(self.flag, val)) self.flag.value = val def see_value(self, value): self.check_ambiguity(value) if self.flag.takes_value: debug("Setting flag {!r} to value {!r}".format(self.flag, value)) self.flag.value = value self.flag_got_value = True else: self.error("Flag {!r} doesn't take any value!".format(self.flag)) def see_positional_arg(self, value): for arg in self.context.positional_args: if arg.value is None: arg.value = value return # Must have filled up positional_args if not self.context.eat_all: debug("Congrats, you found a parser bug! Please report!") debug("Adding vararg {} to context {}".format(value, self.context)) self.context.add_vararg(value) def error(self, msg): raise ParseError(msg, self.context)
class ParseMachine(StateMachine): initial_state = 'context' state('context', enter=['complete_flag', 'complete_context']) state('unknown', enter=['complete_flag', 'complete_context']) state('end', enter=['complete_flag', 'complete_context']) transition( from_=('context', 'unknown'), event='finish', to='end', ) transition( from_='context', event='see_context', action='switch_to_context', to='context', ) transition( from_=('context', 'unknown'), event='see_unknown', action='store_only', to='unknown', ) def changing_state(self, from_, to): debug("ParseMachine: {!r} => {!r}".format(from_, to)) def __init__(self, initial, contexts, ignore_unknown): # Initialize self.ignore_unknown = ignore_unknown self.initial = self.context = copy.deepcopy(initial) debug("Initialized with context: {!r}".format(self.context)) self.flag = None self.result = ParseResult() self.contexts = copy.deepcopy(contexts) debug("Available contexts: {!r}".format(self.contexts)) # In case StateMachine does anything in __init__ super(ParseMachine, self).__init__() @property def waiting_for_flag_value(self): # Do we have a current flag, and does it expect a value (vs being a # bool/toggle)? takes_value = (self.flag and self.flag.takes_value) if not takes_value: return False # OK, this flag is one that takes values. # Is it a list type? If so, it's always happy to accept more # regardless. # TODO: how to handle somebody wanting it to be some other iterable # like tuple or custom class? Or do we just say unsupported? if self.flag.kind is list: return True # Not a list, okay. Does it already have a value? has_value = self.flag.raw_value is not None # If it doesn't have one, we're waiting for one (which tells the parser # how to proceed and typically to store the next token.) # TODO: in the negative case here, we should do something else instead: # - Except, "hey you screwed up, you already gave that flag!" # - Overwrite, "oh you changed your mind?" - which requires more work # elsewhere too, unfortunately. (Perhaps additional properties on # Argument that can be queried, e.g. "arg.is_iterable"?) return not has_value def handle(self, token): debug("Handling token: {!r}".format(token)) # Handle unknown state at the top: we don't care about even # possibly-valid input if we've encountered unknown input. if self.current_state == 'unknown': debug("Top-of-handle() see_unknown({!r})".format(token)) self.see_unknown(token) return # Flag if self.context and token in self.context.flags: debug("Saw flag {!r}".format(token)) self.switch_to_flag(token) elif self.context and token in self.context.inverse_flags: debug("Saw inverse flag {!r}".format(token)) self.switch_to_flag(token, inverse=True) # Value for current flag elif self.waiting_for_flag_value: debug("We're waiting for a flag value so {!r} must be it?".format( token)) # noqa self.see_value(token) # Positional args (must come above context-name check in case we still # need a posarg and the user legitimately wants to give it a value that # just happens to be a valid context name.) elif self.context and self.context.needs_positional_arg: msg = "Context {!r} requires positional args, eating {!r}" debug(msg.format(self.context, token)) self.see_positional_arg(token) # New context elif token in self.contexts: self.see_context(token) # Initial-context flag being given as per-task flag (e.g. --help) elif self.initial and token in self.initial.flags: debug("Saw (initial-context) flag {!r}".format(token)) flag = self.initial.flags[token] # TODO: handle ambiguity? Right now, flags in the context that # shadow initial-context flags would always naturally "win" by # being higher up in this if/elsif/etc chain. Ideally we'd complain # to avoid users shooting themselves in the foot? # Flags of this type that take a value are always given the current # context's name as a string value. # TODO: document this in the parser docs if flag.takes_value: flag.value = self.context.name else: # TODO: handle inverse flags, other flag types? flag.value = True msg = "Setting (initial-context) flag {!r} to value {!r}" debug(msg.format(flag, flag.value)) # Unknown else: if not self.ignore_unknown: debug("Can't find context named {!r}, erroring".format(token)) self.error("No idea what {!r} is!".format(token)) else: debug("Bottom-of-handle() see_unknown({!r})".format(token)) self.see_unknown(token) def store_only(self, token): # Start off the unparsed list debug("Storing unknown token {!r}".format(token)) self.result.unparsed.append(token) def complete_context(self): debug("Wrapping up context {!r}".format( self.context.name if self.context else self.context)) # Ensure all of context's positional args have been given. if self.context and self.context.needs_positional_arg: err = "'{}' did not receive all required positional arguments!" self.error(err.format(self.context.name)) if self.context and self.context not in self.result: self.result.append(self.context) def switch_to_context(self, name): self.context = copy.deepcopy(self.contexts[name]) debug("Moving to context {!r}".format(name)) debug("Context args: {!r}".format(self.context.args)) debug("Context flags: {!r}".format(self.context.flags)) debug("Context inverse_flags: {!r}".format(self.context.inverse_flags)) def complete_flag(self): # Barf if we needed a value and didn't get one if (self.flag and self.flag.takes_value and self.flag.raw_value is None and not self.flag.optional): err = "Flag {!r} needed value and was not given one!" self.error(err.format(self.flag)) # Handle optional-value flags; at this point they were not given an # explicit value, but they were seen, ergo they should get treated like # bools. if self.flag and self.flag.raw_value is None and self.flag.optional: msg = "Saw optional flag {!r} go by w/ no value; setting to True" debug(msg.format(self.flag.name)) # Skip casting so the bool gets preserved self.flag.set_value(True, cast=False) def check_ambiguity(self, value): """ Guard against ambiguity when current flag takes an optional value. """ # No flag is currently being examined, or one is but it doesn't take an # optional value? Ambiguity isn't possible. if not (self.flag and self.flag.optional): return False # We *are* dealing with an optional-value flag, but it's already # received a value? There can't be ambiguity here either. if self.flag.raw_value is not None: return False # Otherwise, there *may* be ambiguity if 1 or more of the below tests # fail. tests = [] # Unfilled posargs still exist? tests.append(self.context and self.context.needs_positional_arg) # Value looks like it's supposed to be a flag itself? # (Doesn't have to even actually be valid - chances are if it looks # like a flag, the user was trying to give one.) tests.append(is_flag(value)) # Value matches another valid task/context name? tests.append(value in self.contexts) if any(tests): msg = "{!r} is ambiguous when given after an optional-value flag" raise ParseError(msg.format(value)) def switch_to_flag(self, flag, inverse=False): # Sanity check for ambiguity w/ prior optional-value flag self.check_ambiguity(flag) # Set flag/arg obj flag = self.context.inverse_flags[flag] if inverse else flag # Update state self.flag = self.context.flags[flag] debug("Moving to flag {!r}".format(self.flag)) # Handle boolean flags (which can immediately be updated) if not self.flag.takes_value: val = not inverse debug("Marking seen flag {!r} as {}".format(self.flag, val)) self.flag.value = val def see_value(self, value): self.check_ambiguity(value) if self.flag.takes_value: debug("Setting flag {!r} to value {!r}".format(self.flag, value)) self.flag.value = value else: self.error("Flag {!r} doesn't take any value!".format(self.flag)) def see_positional_arg(self, value): for arg in self.context.positional_args: if arg.value is None: arg.value = value break def error(self, msg): raise ParseError(msg, self.context)
class News(StateMachine): initial_state = 'setup' def __init__(self, episode_id, station): super(FileInfo, self).__init__() self.caller_list = "{0}-{1}".format('caller_list', episode_id) self.sound_url = "{}{}{}{}".format(TELEPHONY_SERVER_IP, '/~csik/sounds/programs/', episode_id, '/current.mp3') self.conference = "news_report_conference-{}".format(episode_id) self.station = station def setup(self): logger.info("News_Report: In setup") # Start caller/messager list r = redis.StrictRedis(host='localhost', port=6379, db=0) r.set(self.caller_list, []) # from now on get list as #numbers = ast.literal_eval(r.get(self.caller_list)) #numbers.append(incoming) #r.set(self.caller_list,numbers) #check soundfile import requests response = requests.head(self.sound_url) if response.status_code != 200: logger.error('No sound file available at url:'.format( self.sound_url)) #allocate outgoing line print r.llen('outgoing_unused') + " free phone lines available" number = r.rpoplpush('outgoing_unused', 'outgoing_busy') #place calls call_result = call( to_number=self.station.cloud_number, from_number=number, gateway='sofia/gateway/utl/', answered='http://127.0.0.1:5000/' + '/confer/' + episode_id + '/', extra_dial_string= "bridge_early_media=true,hangup_after_bridge=true,origination_caller_id_name=rootio,origination_caller_id_number=" + number, ) logger.info(str(call_result)) #count successful calls, plan otherwise #launch show-wide listeners -- or does station do that? def intro(self): logger.info("News_Report: In intro") #wait until intro music is finished #play sound to conference #check progress of sound file def report(self): logger.info("News_Report: In report") #play report sound #check on calls? # def outro(self): logger.info("News_Report: In outro") #hang up calls #log #play outgoing music # This among all others should be "blocking", i.e. how do we assure it has # executed before trying another show? def teardown(self): logger.info("News_Report: In teardown") #hang up calls if they have not been humg up #clear conference # Set up states state('setup', enter=setup) state('intro', enter=intro) state('report', enter=report) state('outro', enter=outro) state('teardown', enter=teardown) # Set up transitions, note they expect serial progression except for teardown transition(from_='setup', event='go_intro', to='intro') transition(from_='intro', event='go_report', to='report') transition(from_='report', event='go_outro', to='outro') transition(from_=['outro', 'report', 'intro', 'setup'], event='go_teardown', to='teardown')
#clear conference #clear is_master semaphore if self.is_master == True: r.set('is_master_'+str(self.episode_id),'none') #return numbers to stack top_gateway = self.station.outgoing_gateways[0] if top_gateway>0: logger.info("Returning phone numbers to stack.") r.lrem('outgoing_busy', 0, self.fnumber) # remove all instances for somenumber r.rpush('outgoing_unused', self.fnumber) # add them back to the queue # Set up states state('setup',enter=setup) state('intro',enter=intro) state('report',enter=report) state('outro',enter=outro) state('teardown',enter=teardown) # Set up transitions, note they expect serial progression except for teardown transition(from_='setup', event='go_intro', to='intro') transition(from_='intro', event='go_report', to='report') transition(from_='report', event='go_outro', to='outro') transition(from_=['outro','report','intro','setup'], event='go_teardown', to='teardown')
class OtherMachine(StateMachine): state('idle') state('working') initial_state = 'idle' transition(from_='idle', event='work', to='working')
class JumperGuy(StateMachine): state('looking') state('falling') initial_state = 'looking' transition(from_='looking', event='jump', to='falling')