Manera correcta de obtener argumentos permitidos de ArgumentParser

Pregunta: ¿Cuál es la forma deseada / oficial de acceder a los posibles argumentos de un objeto argparse.ArgumentParser existente?

Ejemplo: Supongamos el siguiente contexto:

 import argparse parser = argparse.ArgumentParser() parser.add_argument('--foo', '-f', type=str) 

Aquí me gustaría obtener la siguiente lista de argumentos permitidos:

 ['-h', '--foo', '--help', '-f'] 

He encontrado la siguiente solución que hace el truco para mí

 parser._option_string_actions.keys() 

Pero no estoy contento con eso, ya que implica acceder a un miembro que no está documentado oficialmente. ¿Cuál es la alternativa correcta para esta tarea?

No creo que haya una “mejor” forma de lograr lo que quieres.


Si realmente no desea utilizar el atributo _option_string_actions , podría procesar el parser.format_usage() para recuperar las opciones, pero al hacer esto, solo obtendrá los nombres de las opciones cortas.

Si desea nombres de opciones cortos y largos, podría procesar el parser.format_help() lugar.

Este proceso se puede hacer con una expresión regular muy simple: -+\w+

 import re OPTION_RE = re.compile(r"-+\w+") PARSER_HELP = """usage: test_args_2.py [-h] [--foo FOO] [--bar BAR] optional arguments: -h, --help show this help message and exit --foo FOO, -f FOO a random options --bar BAR, -b BAR a more random option """ options = set(OPTION_RE.findall(PARSER_HELP)) print(options) # set(['-f', '-b', '--bar', '-h', '--help', '--foo']) 

O primero puede hacer un diccionario que contenga la configuración del analizador de argumentos y luego construir el analizador argmuent a partir de él. Dicho diccionario podría tener los nombres de las opciones como clave y la configuración de la opción como valor. Al hacer esto, puede acceder a la lista de opciones a través de las teclas de dictionnary aplanadas con itertools.chain :

 import argparse import itertools parser_config = { ('--foo', '-f'): {"help": "a random options", "type": str}, ('--bar', '-b'): {"help": "a more random option", "type": int, "default": 0} } parser = argparse.ArgumentParser() for option, config in parser_config.items(): parser.add_argument(*option, **config) print(parser.format_help()) # usage: test_args_2.py [-h] [--foo FOO] [--bar BAR] # # optional arguments: # -h, --help show this help message and exit # --foo FOO, -f FOO a random options # --bar BAR, -b BAR a more random option print(list(itertools.chain(*parser_config.keys()))) # ['--foo', '-f', '--bar', '-b'] 

Esta última forma es lo que haría si me resistiera a usar _option_string_actions .

Esto comenzó como una respuesta de broma, pero he aprendido algo desde entonces, así que lo publicaré.

Supongamos que sabemos la longitud máxima permitida de una opción. Aquí hay una buena respuesta a la pregunta en esta situación:

 from itertools import combinations def parsable(option): try: return len(parser.parse_known_args(option.split())[1]) != 2 except: return False def test(tester, option): return any([tester(str(option) + ' ' + str(v)) for v in ['0', '0.0']]) def allowed_options(parser, max_len=3, min_len=1): acceptable = [] for l in range(min_len, max_len + 1): for option in combinations([c for c in [chr(i) for i in range(33, 127)] if c != '-'], l): option = ''.join(option) acceptable += [p + option for p in ['-', '--'] if test(parsable, p + option)] return acceptable 

Por supuesto, esto es muy pedante ya que la pregunta no requiere un tiempo de ejecución específico. Así que voy a ignorar eso aquí. También descartaré, que la versión anterior produce un desorden de salida porque uno puede deshacerse de ella fácilmente .

Pero lo que es más importante, este método detectó las siguientes “características” interesantes de argparse :

  • En el ejemplo de OP, argparse también permitiría --fo . Esto tiene que ser un error.
  • Pero además, en el ejemplo OP nuevamente, argparse también permitiría -fo (es decir, configurar foo en o sin espacio ni nada). Esto está documentado y destinado, pero no lo sabía.

Debido a esto, una solución correcta es un poco más larga y se vería como esto (solo los cambios parsable , omitiré los otros métodos):

 def parsable(option): try: default = vars(parser.parse_known_args(['--' + '0' * 200])[0]) parsed, remaining = parser.parse_known_args(option.split()) if len(remaining) == 2: return False parsed = vars(parsed) for k in parsed.keys(): try: if k in default and default[k] != parsed[k] and float(parsed[k]) != 0.0: return False # Filter '-fx' cases where '-f' is the argument and 'x' the value. except: return False return True except: return False 

Resumen : Además de todas las restricciones (tiempo de ejecución y duración máxima fija de la opción), esta es la única respuesta que respeta correctamente el comportamiento del parser real, sin importar el problema que tenga. Así que aquí estás, una respuesta perfecta que es absolutamente inútil.

Tengo que estar de acuerdo con la respuesta de Tryph.

No es bonito, pero puede recuperarlos desde parser.format_help() :

 import argparse parser = argparse.ArgumentParser() parser.add_argument('--foo', '-f', type=str) goal = parser._option_string_actions.keys() def get_allowed_arguments(parser): lines = parser.format_help().split('\n') line_index = 0 number_of_lines = len(lines) found_optional_arguments = False # skip the first lines until the section 'optional arguments' while line_index < number_of_lines: if lines[line_index] == 'optional arguments:': found_optional_arguments = True line_index += 1 break line_index += 1 result_list = [] if found_optional_arguments: while line_index < number_of_lines: arg_list = get_arguments_from_line(lines[line_index]) if len(arg_list) == 0: break result_list += arg_list line_index += 1 return result_list def get_arguments_from_line(line): if line[:2] != ' ': return [] arg_list = [] i = 2 N = len(line) inside_arg = False arg_start = 2 while i < N: if line[i] == '-' and not inside_arg: arg_start = i inside_arg = True elif line[i] in [',',' '] and inside_arg: arg_list.append(line[arg_start:i+1]) inside_arg = False i += 1 return arg_list answer = get_allowed_arguments(parser) 

Probablemente hay una expresión regular alternativa al lío anterior ...

Primero, una nota sobre los documentos argparse : es básicamente un documento de uso, no una API formal. El estándar para lo que argparse hace es el código en sí, las pruebas unitarias ( test/test_argparse.py ) y una preocupación paralizante por la compatibilidad con versiones anteriores.

No hay una forma ‘oficial’ de acceder a los allowed arguments , porque los usuarios generalmente no necesitan saber eso (aparte de leer la help/usage ).

Pero déjame ilustrar con un analizador simple en una sesión interactiva:

 In [247]: parser=argparse.ArgumentParser() In [248]: a = parser.add_argument('pos') In [249]: b = parser.add_argument('-f','--foo') 

add_argument devuelve el objeto Action que creó. Esto no está documentado, pero es obvio para cualquiera que haya creado un analizador de forma interactiva.

El objeto parser tiene un método de repr que muestra los parámetros principales. Pero tiene muchos más atributos, que puedes ver con vars(parser) , o parser. en Ipython.

 In [250]: parser Out[250]: ArgumentParser(prog='ipython3', usage=None, description=None, formatter_class=, conflict_handler='error', add_help=True) 

Las acciones también tienen repr ; La subclase de acción está determinada por el parámetro de action .

 In [251]: a Out[251]: _StoreAction(option_strings=[], dest='pos', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None) In [252]: b Out[252]: _StoreAction(option_strings=['-f', '--foo'], dest='foo', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None) 

vars(a) etc. pueden usarse para ver todos los atributos.

Un atributo del parser clave es _actions , una lista de todas las Acciones definidas. Esta es la base para todo el análisis. Tenga en cuenta que incluye la acción de help que se creó automáticamente. Mira las option_strings de option_strings ; Eso determina si la Acción es posicional u opcional.

 In [253]: parser._actions Out[253]: [_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None), _StoreAction(option_strings=[], dest='pos',....), _StoreAction(option_strings=['-f', '--foo'], dest='foo', ...)] 

_option_string_actions es un diccionario, que se mapea de option_strings a Actions (los mismos objetos que aparecen en _actions ). Las referencias a esos objetos de acción aparecen en todo el lugar en código argparse .

 In [255]: parser._option_string_actions Out[255]: {'--foo': _StoreAction(option_strings=['-f', '--foo'],....), '--help': _HelpAction(option_strings=['-h', '--help'],...), '-f': _StoreAction(option_strings=['-f', '--foo'], dest='foo',...), '-h': _HelpAction(option_strings=['-h', '--help'], ....)} In [256]: list(parser._option_string_actions.keys()) Out[256]: ['-f', '--help', '-h', '--foo'] 

Tenga en cuenta que hay una clave para cada cadena, larga o corta; pero no hay nada para pos , el posicional tiene un parámetro option_strings vacío.

Si esa lista de claves es lo que desea, úsela y no se preocupe por la _ . No tiene un alias ‘público’.

Puedo entender el análisis de la help para descubrir el mismo; pero eso es mucho trabajo para simplemente evitar usar un atributo ‘privado’. Si le preocupa que se cambie el atributo no documentado, también debe preocuparse por el cambio del formato de ayuda. Eso tampoco es parte de los documentos.

help diseño de la help está controlado por parser.format_help . El usage se crea a partir de la información en self._actions . Líneas de ayuda a partir de información en

  for action_group in self._action_groups: formatter.add_arguments(action_group._group_actions) 

(no quieres entrar en action groups ¿verdad?).

Hay otra forma de obtener las option_strings : option_strings de las _actions :

 In [258]: [a.option_strings for a in parser._actions] Out[258]: [['-h', '--help'], [], ['-f', '--foo']] 

===================

Profundizando en los detalles del código un poco:

parser.add_argument crea una Acción, y luego la pasa a parser._add_action . Este es el método que rellena tanto .actions como action.option_strings .

  self._actions.append(action) for option_string in action.option_strings: self._option_string_actions[option_string] = action