Beispiel #1
0
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))
Beispiel #2
0
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))
Beispiel #3
0
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))