class ParserContext(object): """ Parsing context with knowledge of flags & their format. Generally associated with the core program or a task. When run through a parser, will also hold runtime values filled in by the parser. """ def __init__(self, name=None, aliases=(), args=()): """ Create a new ``ParserContext`` named ``name``, with ``aliases``. ``name`` is optional, and should be a string if given. It's used to tell ParserContext objects apart, and for use in a Parser when determining what chunk of input might belong to a given ParserContext. ``aliases`` is also optional and should be an iterable containing strings. Parsing will honor any aliases when trying to "find" a given context in its input. May give one or more ``args``, which is a quick alternative to calling ``for arg in args: self.add_arg(arg)`` after initialization. """ self.args = Lexicon() self.positional_args = [] self.flags = Lexicon() self.inverse_flags = {} # No need for Lexicon here self.name = name self.aliases = aliases for arg in args: self.add_arg(arg) def __repr__(self): aliases = "" if self.aliases: aliases = " ({0})".format(', '.join(self.aliases)) name = (" {0!r}{1}".format(self.name, aliases)) if self.name else "" args = (": {0!r}".format(self.args)) if self.args else "" return "<parser/Context{0}{1}>".format(name, args) def add_arg(self, *args, **kwargs): """ Adds given ``Argument`` (or constructor args for one) to this context. The Argument in question is added to the following dict attributes: * ``args``: "normal" access, i.e. the given names are directly exposed as keys. * ``flags``: "flaglike" access, i.e. the given names are translated into CLI flags, e.g. ``"foo"`` is accessible via ``flags['--foo']``. * ``inverse_flags``: similar to ``flags`` but containing only the "inverse" versions of boolean flags which default to True. This allows the parser to track e.g. ``--no-myflag`` and turn it into a False value for the ``myflag`` Argument. """ # Normalize if len(args) == 1 and isinstance(args[0], Argument): arg = args[0] else: arg = Argument(*args, **kwargs) # Uniqueness constraint: no name collisions for name in arg.names: if name in self.args: msg = "Tried to add an argument named {0!r} but one already exists!" # noqa raise ValueError(msg.format(name)) # First name used as "main" name for purposes of aliasing main = arg.names[0] # NOT arg.name self.args[main] = arg # Note positionals in distinct, ordered list attribute if arg.positional: self.positional_args.append(arg) # Add names & nicknames to flags, args self.flags[to_flag(main)] = arg for name in arg.nicknames: self.args.alias(name, to=main) self.flags.alias(to_flag(name), to=to_flag(main)) # Add attr_name to args, but not flags if arg.attr_name: self.args.alias(arg.attr_name, to=main) # Add to inverse_flags if required if arg.kind == bool and arg.default is True: # Invert the 'main' flag name here, which will be a dashed version # of the primary argument name if underscore-to-dash transformation # occurred. inverse_name = to_flag("no-{0}".format(main)) self.inverse_flags[inverse_name] = to_flag(main) @property def needs_positional_arg(self): return any(x.value is None for x in self.positional_args) @property def as_kwargs(self): """ This context's arguments' values keyed by their ``.name`` attribute. Results in a dict suitable for use in Python contexts, where e.g. an arg named ``foo-bar`` becomes accessible as ``foo_bar``. """ ret = {} for arg in self.args.values(): ret[arg.name] = arg.value return ret def names_for(self, flag): # TODO: should probably be a method on Lexicon/AliasDict return list(set([flag] + self.flags.aliases_of(flag))) def help_for(self, flag): """ Return 2-tuple of ``(flag-spec, help-string)`` for given ``flag``. """ # Obtain arg obj if flag not in self.flags: err = "{0!r} is not a valid flag for this context! Valid flags are: {1!r}" # noqa raise ValueError(err.format(flag, self.flags.keys())) arg = self.flags[flag] # Determine expected value type, if any value = { str: 'STRING', }.get(arg.kind) # Format & go full_names = [] for name in self.names_for(flag): if value: # Short flags are -f VAL, long are --foo=VAL # When optional, also, -f [VAL] and --foo[=VAL] if len(name.strip('-')) == 1: value_ = ("[{0}]".format(value)) if arg.optional else value valuestr = " {0}".format(value_) else: valuestr = "={0}".format(value) if arg.optional: valuestr = "[{0}]".format(valuestr) else: # no value => boolean # check for inverse if name in self.inverse_flags.values(): name = "--[no-]{0}".format(name[2:]) valuestr = "" # Tack together full_names.append(name + valuestr) namestr = ", ".join(sorted(full_names, key=len)) helpstr = arg.help or "" return namestr, helpstr def help_tuples(self): """ Return sorted iterable of help tuples for all member Arguments. Sorts like so: * General sort is alphanumerically * Short flags win over long flags * Arguments with *only* long flags and *no* short flags will come first. * When an Argument has multiple long or short flags, it will sort using the most favorable (lowest alphabetically) candidate. This will result in a help list like so:: --alpha, --zeta # 'alpha' wins --beta -a, --query # short flag wins -b, --argh -c """ # TODO: argument/flag API must change :( # having to call to_flag on 1st name of an Argument is just dumb. # To pass in an Argument object to help_for may require moderate # changes? # Cast to list to ensure non-generator on Python 3. return list(map( lambda x: self.help_for(to_flag(x.name)), sorted(self.flags.values(), key=flag_key) )) def flag_names(self): """ Similar to `help_tuples` but returns flag names only, no helpstrs. Specifically, all flag names, flattened, in rough order. """ # Regular flag names flags = sorted(self.flags.values(), key=flag_key) names = [self.names_for(to_flag(x.name)) for x in flags] # Inverse flag names sold separately names.append(self.inverse_flags.keys()) return tuple(itertools.chain.from_iterable(names))
class ParserContext(object): """ Parsing context with knowledge of flags & their format. Generally associated with the core program or a task. When run through a parser, will also hold runtime values filled in by the parser. .. versionadded:: 1.0 """ def __init__(self, name=None, aliases=(), args=()): """ Create a new ``ParserContext`` named ``name``, with ``aliases``. ``name`` is optional, and should be a string if given. It's used to tell ParserContext objects apart, and for use in a Parser when determining what chunk of input might belong to a given ParserContext. ``aliases`` is also optional and should be an iterable containing strings. Parsing will honor any aliases when trying to "find" a given context in its input. May give one or more ``args``, which is a quick alternative to calling ``for arg in args: self.add_arg(arg)`` after initialization. """ self.args = Lexicon() self.positional_args = [] self.flags = Lexicon() self.inverse_flags = {} # No need for Lexicon here self.name = name self.aliases = aliases for arg in args: self.add_arg(arg) def __repr__(self): aliases = "" if self.aliases: aliases = " ({})".format(', '.join(self.aliases)) name = (" {!r}{}".format(self.name, aliases)) if self.name else "" args = (": {!r}".format(self.args)) if self.args else "" return "<parser/Context{}{}>".format(name, args) def add_arg(self, *args, **kwargs): """ Adds given ``Argument`` (or constructor args for one) to this context. The Argument in question is added to the following dict attributes: * ``args``: "normal" access, i.e. the given names are directly exposed as keys. * ``flags``: "flaglike" access, i.e. the given names are translated into CLI flags, e.g. ``"foo"`` is accessible via ``flags['--foo']``. * ``inverse_flags``: similar to ``flags`` but containing only the "inverse" versions of boolean flags which default to True. This allows the parser to track e.g. ``--no-myflag`` and turn it into a False value for the ``myflag`` Argument. .. versionadded:: 1.0 """ # Normalize if len(args) == 1 and isinstance(args[0], Argument): arg = args[0] else: arg = Argument(*args, **kwargs) # Uniqueness constraint: no name collisions for name in arg.names: if name in self.args: msg = "Tried to add an argument named {!r} but one already exists!" # noqa raise ValueError(msg.format(name)) # First name used as "main" name for purposes of aliasing main = arg.names[0] # NOT arg.name self.args[main] = arg # Note positionals in distinct, ordered list attribute if arg.positional: self.positional_args.append(arg) # Add names & nicknames to flags, args self.flags[to_flag(main)] = arg for name in arg.nicknames: self.args.alias(name, to=main) self.flags.alias(to_flag(name), to=to_flag(main)) # Add attr_name to args, but not flags if arg.attr_name: self.args.alias(arg.attr_name, to=main) # Add to inverse_flags if required if arg.kind == bool and arg.default is True: # Invert the 'main' flag name here, which will be a dashed version # of the primary argument name if underscore-to-dash transformation # occurred. inverse_name = to_flag("no-{}".format(main)) self.inverse_flags[inverse_name] = to_flag(main) @property def missing_positional_args(self): return [x for x in self.positional_args if x.value is None] @property def as_kwargs(self): """ This context's arguments' values keyed by their ``.name`` attribute. Results in a dict suitable for use in Python contexts, where e.g. an arg named ``foo-bar`` becomes accessible as ``foo_bar``. .. versionadded:: 1.0 """ ret = {} for arg in self.args.values(): ret[arg.name] = arg.value return ret def names_for(self, flag): # TODO: should probably be a method on Lexicon/AliasDict return list(set([flag] + self.flags.aliases_of(flag))) def help_for(self, flag): """ Return 2-tuple of ``(flag-spec, help-string)`` for given ``flag``. .. versionadded:: 1.0 """ # Obtain arg obj if flag not in self.flags: err = "{!r} is not a valid flag for this context! Valid flags are: {!r}" # noqa raise ValueError(err.format(flag, self.flags.keys())) arg = self.flags[flag] # Determine expected value type, if any value = { str: 'STRING', int: 'INT', }.get(arg.kind) # Format & go full_names = [] for name in self.names_for(flag): if value: # Short flags are -f VAL, long are --foo=VAL # When optional, also, -f [VAL] and --foo[=VAL] if len(name.strip('-')) == 1: value_ = ("[{}]".format(value)) if arg.optional else value valuestr = " {}".format(value_) else: valuestr = "={}".format(value) if arg.optional: valuestr = "[{}]".format(valuestr) else: # no value => boolean # check for inverse if name in self.inverse_flags.values(): name = "--[no-]{}".format(name[2:]) valuestr = "" # Tack together full_names.append(name + valuestr) namestr = ", ".join(sorted(full_names, key=len)) helpstr = arg.help or "" return namestr, helpstr def help_tuples(self): """ Return sorted iterable of help tuples for all member Arguments. Sorts like so: * General sort is alphanumerically * Short flags win over long flags * Arguments with *only* long flags and *no* short flags will come first. * When an Argument has multiple long or short flags, it will sort using the most favorable (lowest alphabetically) candidate. This will result in a help list like so:: --alpha, --zeta # 'alpha' wins --beta -a, --query # short flag wins -b, --argh -c .. versionadded:: 1.0 """ # TODO: argument/flag API must change :( # having to call to_flag on 1st name of an Argument is just dumb. # To pass in an Argument object to help_for may require moderate # changes? # Cast to list to ensure non-generator on Python 3. return list(map( lambda x: self.help_for(to_flag(x.name)), sorted(self.flags.values(), key=flag_key) )) def flag_names(self): """ Similar to `help_tuples` but returns flag names only, no helpstrs. Specifically, all flag names, flattened, in rough order. .. versionadded:: 1.0 """ # Regular flag names flags = sorted(self.flags.values(), key=flag_key) names = [self.names_for(to_flag(x.name)) for x in flags] # Inverse flag names sold separately names.append(self.inverse_flags.keys()) return tuple(itertools.chain.from_iterable(names))
class AnalizadorDeContexto(object): """ Analizando contexto con conocimiento de banderas y su formato. Generalmente asociado con el programa central o un artefacto. Cuando se ejecuta a través de un analizador, también se mantendrán los valores de tiempoej rellenados por el analizador. .. versionadded:: 1.0 """ def __init__(self, nombre=None, alias=(), args=()): """ Crea un nuevo `` AnalizadorDeContexto llamado ``nombre``, con ``alias``. ``nombre`` es opcional y debería ser una cadena si se proporciona. Se usa para diferenciar los objetos AnalizadorDeContexto, y para usarlos en un Analizador al determinar qué porción de entrada podría pertenecer a un AnalizadorDeContexto dado. ``alias`` también es opcional y debería ser un iterable que contenga cadenas. El análisis respetará cualquier alias cuando intente "encontrar" un contexto dado en su entrada. Puede dar uno o más ``args``, que es una alternativa rápida a llamar a ``para arg en args: self.agregar_arg (arg)`` después de la inicialización. """ self.args = Lexicon() self.args_posicionales = [] self.banderas = Lexicon() self.banderas_inversas = {} # No need for Lexicon here self.nombre = nombre self.alias = alias for arg in args: self.agregar_arg(arg) def __repr__(self): alias = "" if self.alias: alias = " ({})".format(", ".join(self.alias)) nombre = (" {!r}{}".format(self.nombre, alias)) if self.nombre else "" args = (": {!r}".format(self.args)) if self.args else "" return "<analizador/Contexto{}{}>".format(nombre, args) def agregar_arg(self, *args, **kwargs): """ Adds given ``Argumento`` (or constructor args for one) to this contexto. Agrega el ``Argumento`` dado (o los argumentos del constructor para uno) a este contexto. El Argumento en cuestión se agrega a los siguientes atributos de dict: * ``args``: acceso "normal", es decir, los nombres dados se exponen directamente como claves. * ``banderas``: acceso "banderalike", es decir, los nombres dados se traducen a banderas CLI, p. ej. Se puede acceder a ``"foo"`` a través de ``banderas['--foo']``. * ``banderas_inversas``: similar a ``banderas`` pero que contiene solo las versiones "inversas" de las banderas booleanas que por defecto son True. Esto permite que el analizador rasarbol, por ejemplo, ``--no-mibandera`` y convertirlo en un valor False para el Argumento ``mibandera``. .. versionadded:: 1.0 """ # Normalize if len(args) == 1 and isinstance(args[0], Argumento): arg = args[0] else: arg = Argumento(*args, **kwargs) # Restricción de unicidad: sin colisiones de nombres for nombre in arg.nombres: if nombre in self.args: msj = "Intenté agregar un argumento llamado {!r} pero uno ya existe!" # noqa raise ValueError(msj.format(nombre)) # Nombre utilizado como nombre "principal" para fines de alias principal = arg.nombres[0] # NOT arg.nombre self.args[principal] = arg # Observe las posiciones en un atributo de lista ordenada y distinta if arg.posicional: self.args_posicionales.append(arg) # Agregar nombres y nicknombres a banderas, args self.banderas[a_bandera(principal)] = arg for nombre in arg.nicknombres: self.args.alias(nombre, to=principal) self.banderas.alias(a_bandera(nombre), to=a_bandera(principal)) # Agregar nombre_de_atributo a args, pero no a banderas if arg.nombre_de_atributo: self.args.alias(arg.nombre_de_atributo, to=principal) # Agregar a banderas_inversas si es necesario if arg.tipo == bool and arg.default is True: # Invierta aquí el nombre de la bandera 'principal', que será # una versión discontinua del nombre del argumento principal si # se produjo una transformación de guión bajo a guión. nombre_inverso = a_bandera("no-{}".format(principal)) self.banderas_inversas[nombre_inverso] = a_bandera(principal) @property def faltan_argumentos_posicionales(self): return [x for x in self.args_posicionales if x.valor is None] @property def como_kwargs(self): """ This contexto's arguments' values keyed by their ``.nombre`` attribute. como kwargs Los valores de los argumentos de este contexto codificados por su atributo ``.nombre``. Da como resultado un dicc adecuado para su uso en contextos de Python, donde p. Ej. un argumento llamado ``foo-bar`` se vuelve accesible como ``foo_bar``. .. versionadded:: 1.0 """ ret = {} for arg in self.args.valores(): ret[arg.nombre] = arg.valor return ret def nombres_para(self, bandera): # TODO: probablemente debería ser un método en Lexicon/AliasDict return list(set([bandera] + self.banderas.aliases_of(bandera))) def ayuda_para(self, bandera): """ Devuelve 2-tuplas de ``(bandera-spec, help-string)`` para la ``bandera`` dada. ..versionadded:: 1.0 """ # Obtener arg obj if bandera not in self.banderas: err = "{!r} ¡No es una bandera válida para este contexto! Las banderas válidas son: {!r}" # noqa raise ValueError(err.format(bandera, self.banderas.claves())) arg = self.banderas[bandera] # Determine el tipo de valor esperado, si lo hubiera valor = {str: "CADENA", int: "INT"}.get(arg.tipo) # Formatear y listo full_nombres = [] for nombre in self.nombres_para(bandera): if valor: # Las banderas cortas son -f VAL, largos son --foo=VAL # Cuando es opcional, también, -f [VAL] y --foo[=VAL] if len(nombre.strip("-")) == 1: valor_ = ("[{}]".format(valor)) if arg.opcional else valor valorcadena = " {}".format(valor_) else: valorcadena = "={}".format(valor) if arg.opcional: valorcadena = "[{}]".format(valorcadena) else: # sin valor => booleano # comprobar la inversa if nombre in self.banderas_inversas.values(): nombre = "--[no-]{}".format(nombre[2:]) valorcadena = "" # virar juntos full_nombres.append(nombre + valorcadena) nombrecadena = ", ".join(sorted(full_nombres, key=len)) helpcadena = arg.help or "" return nombrecadena, helpcadena def help_tuplas(self): """ Devuelve el iterable ordenado de las tuplas de ayuda para todos los Argumentos miembro. Clasifica así: * La clasificación general es alfanumérica * Banderas cortas triunfan sobre banderas largas * Los argumentos con banderas *only* largas y banderas *no* cortas vendrán primero. * Cuando un Argumento tiene varias banderas largas o cortas, se clasificará utilizando el candidato más favorable (el más bajo alfabéticamente). Esto resultará en una lista de ayuda como la siguiente:: --alfa, --zeta # 'alfa' gana --beta -a, --query # bandera corta gana -b, --argh -c .. versionadded:: 1.0 """ # TODO: argumento/bandera API debe cambiar :( # tener que llamar a una_bandera en el primer nombre de un argumento # es una tontería. # ¿Pasar un objeto Argumento a ayuda_para puede requerir cambios # moderados? # Transmitir a la lista para garantizar que no sea generador en # Python 3. return list( map( lambda x: self.ayuda_para(a_bandera(x.nombre)), sorted(self.banderas.valores(), key=bandera_clave), ) ) def nombres_de_banderas(self): """ Similar a `help_tuplas` pero solo devuelve los nombres de las banderas, no helpcadena. Específicamente, todos los nombres de las banderas, aplanados, en orden aproximado. .. versionadded:: 1.0 """ # Regular bandera nombres banderas = sorted(self.banderas.valores(), key=bandera_clave) nombres = [self.nombres_para(a_bandera(x.nombre)) for x in banderas] # Los nombres de las banderas inversas se venden por separado nombres.append(self.banderas_inversas.keys()) return tuple(itertools.chain.from_iterable(nombres))