2015-07-09 10 views
5

Quando si utilizza il modulo argparse in Python sto cercando un modo per intercettare le opzioni non valide e riportarle meglio. La documentazione a https://docs.python.org/3/library/argparse.html#invalid-arguments fornisce un esempio:segnala prima le opzioni non valide (o usa espressioni regolari) con il modulo argparse python

parser = argparse.ArgumentParser(prog='PROG' 
parser.add_argument('--foo', type=int) 
parser.add_argument('bar', nargs='?') 

# invalid option 
parser.parse_args(['--bar']) 
usage: PROG [-h] [--foo FOO] [bar] 
PROG: error: no such option: --bar 

Tuttavia è abbastanza facile da scattare questo come cattive opzioni non vengono segnalati prima. Per esempio:

import argparse 
import datetime 

def convertIsoTime(timestamp): 
    """read ISO-8601 time-stamp using the AMS conventional format YYYY-MM-DDThh:mm:ssUTC""" 
    try: 
     return datetime.datetime.strptime(timestamp,"%Y-%m-%dT%H:%M:%SUTC") 
    except: 
     raise argparse.ArgumentTypeError("'{}' is not a valid ISO-8601 time-stamp".format(timestamp)) 

parser = argparse.ArgumentParser() 
parser.add_argument('startTime', type=convertIsoTime) 
parser.add_argument('--good', type=int, 
        help='foo') 

args = parser.parse_args(['--gold','5','2015-01-01T00:00:00UTC']) 

riporterà:

error: argument startTime: '5' is not a valid ISO-8601 time-stamp 

Quando io preferisco per segnalare il più utile:

error: no such option: --gold 

E 'possibile raggiungere questo obiettivo? Mi sembra un caso d'uso abbastanza semplice. Quando si scrive direttamente un parser di argomenti, in genere utilizzo un modello in modo tale che qualsiasi cosa che inizia con un prefisso di opzione - che non è un'opzione nota viene immediatamente rifiutata. Per esempio in bash

# Process command-line arguments 
while [ $# -gt 0 ]; do 
    case "$1" in 
    --debug) 
     DEBUGOPTION="--debug" 
     shift 
     break;; 
    --) 
     shift 
     break;; 
    --*) 
     handleUsageError "$1" 
     shift;; 
    *) 
     break;; 
    esac 
done 

Credo argparse utilizza espressioni regolari internamente, ma non credo che siano accessibili tramite add_argument()

Esiste un modo per fare l'equivalente facilmente con argparse?

risposta

1

La risposta breve è che parse_args utilizza parse_known_args. Questo metodo consente di gestire argomenti sconosciuti come --gold. Di conseguenza, gli errori di tipo argomento vengono generati prima degli errori unknown arguments.

Ho aggiunto una soluzione che include la sottoclasse ArgumentParser e la modifica di un metodo nel suo stack chiamante.


Cercherò di delineare parse_args applicata al tuo esempio.

La prima cosa che fa è categorizzare le stringhe come O o A. In parole semplici, quelli che iniziano con - sono O, altri A. Prova anche a far corrispondere i O con un argomento definito.

Nel tuo esempio, trova OAA. Regex è usato per abbinare questa stringa ai pattern definiti dall'argomento nargs. (se necessario, posso spiegare più dettagliatamente questo passaggio)

--gold non corrisponde; ad un certo punto (in questo ciclo iniziale o successivo) viene inserito in un elenco extras. (Controllerò il codice per i dettagli).

Per il 2 ° ciclo attraverso le stringhe, esso tenta alternativamente di posizionare postionals e optionals.

È quando si cerca di far corrispondere lo 5 con starttime che la classe Action solleva l'errore di tipo, che si propaga fino a stamparne l'utilizzo e l'uscita. Poiché --gold non è definito, 5 non viene utilizzato come argomento facoltativo.Quindi viene analizzato come la prima stringa posizionale. (Alcuni tipi di optionals accettano 0 argomenti, quindi non assume nulla che segua uno --... è un argomento opzionale).

Penso che senza lo 5, l'ultima stringa corrispondesse. parse_known_args restituirebbe con --gold nel termine extras. parse_args utilizza parse_known_args ma genera un errore quando extras non è vuoto.

Quindi in un certo senso il parser rileva entrambi gli errori, ma è lo starttime quello che attiva il messaggio di errore. Aspetta fino alla fine per lamentarsi di non riconosciuto --gold.

Come filosofia generale, argparse non tenta di rilevare e presentare tutti gli errori. Non raccoglie un elenco di errori da presentare in un messaggio completo finale.

Esaminerò il codice per verificare i dettagli. Non penso che tu possa facilmente cambiare il modello di analisi di base. Se penso a un modo per forzare un precedente errore unrecognized option, modifico questa risposta.


def _parse_optional(self, arg_string): cerca di classificare in una stringa argv. Se la stringa è simile a positional restituisce None. Se corrisponde a una stringa_opzione Action, restituisce una tupla '(action, option_string, None) `con l'azione corrispondente. Infine, se non corrispondono, restituisce:

# it was meant to be an optional but there is no such option 
    # in this parser (though it might be a valid option in a subparser) 
    return None, arg_string, None 

Credo che questo sia ciò che accade con il vostro --gold. Nota il motivo per cui potrebbe essere ancora un'opzione valida.

Questa funzione viene richiamata da

def _parse_known_args(self, arg_strings, namespace): 
    ... 
    for i, arg_string in enumerate(arg_strings_iter): 
     .... 
     option_tuple = self._parse_optional(arg_string) 
     if option_tuple is None: 
     pattern = 'A' 
     else: 
     option_string_indices[i] = option_tuple 
     pattern = 'O' 
     arg_string_pattern_parts.append(pattern) 
    ... 
    # at the end 
    # return the updated namespace and the extra arguments 
    return namespace, extras 

raccolta che 'AOO' modello, nonché un elenco di tali tuple.

Durante il 2 ° ciclo si alterna tra posizioni di consumo e opzioni. La funzione che consuma un optional è:

def consume_optional(start_index): 
    option_tuple = option_string_indices[start_index] 
    action, option_string, explicit_arg = option_tuple 
    if action is None: 
     extras.append(arg_strings[start_index]) 
    ...otherwise... 
     take_action(action, args, option_string) 

Come ho scritto in precedenza, il vostro --gold viene messo sulla lista extras, mentre 5 rimane sulla lista di argomenti che possono essere analizzati come positionals.

Il namespace e extras sono passati attraverso parse_known_args a voi, l'utente, o per parse_args.

È plausibile che sia possibile creare una sottoclasse di ArgumentParser e definire un metodo _parse_optional modificato. Potrebbe generare un errore invece di restituire la tupla (None, arg_string, None).

import argparse 
import datetime 

class MyParser(argparse.ArgumentParser): 
    def _parse_optional(self, arg_string): 
     arg_tuple = super(MyParser, self)._parse_optional(arg_string) 
     if arg_tuple is None: 
      return arg_tuple # positional 
     else: 
      if arg_tuple[0] is not None: 
       return arg_tuple # valid optional 
      else: 
       msg = 'error: no such option: %s'%arg_string 
       self.error(msg) 

def convertIsoTime(timestamp): 
    """read ISO-8601 time-stamp using the AMS conventional format YYYY-MM-DDThh:mm:ssUTC""" 
    try: 
     return datetime.datetime.strptime(timestamp,"%Y-%m-%dT%H:%M:%SUTC") 
    except: 
     raise argparse.ArgumentTypeError("'{}' is not a valid ISO-8601 time-stamp".format(timestamp)) 

# parser = argparse.ArgumentParser() 
parser = MyParser() 
parser.add_argument('startTime', type=convertIsoTime) 
parser.add_argument('--good', type=int, 
        help='foo') 

args = parser.parse_args(['--good','5','2015-01-01T00:00:00UTC']) 
print(args) 

args = parser.parse_args(['--gold','5','2015-01-01T00:00:00UTC']) 

produce

1505:~/mypy$ python3 stack31317166.py 
Namespace(good=5, startTime=datetime.datetime(2015, 1, 1, 0, 0)) 
usage: stack31317166.py [-h] [--good GOOD] startTime 
stack31317166.py: error: error: no such option: --gold 

Subclassing per fornire un'azione personalizzata è buono argparse (e Python) la pratica.

Se si desidera maggiore considerazione di questo caso da parte degli sviluppatori Python, prendere in considerazione la scrittura di un bug/issue (a PEP è per idee formali più sviluppate). Ma c'è abbastanza arretrato di errori/patch argparse e molta prudenza sulla retrocompatibilità.


http://bugs.python.org/issue?%40columns=id%2Cactivity%2Ctitle%2Ccreator%2Cassignee%2Cstatus%2Ctype&%40sort=-activity&%40filter=status&%40action=searchid&ignore=file%3Acontent&%40search_text=_parse_optional&submit=search&status=-1%2C1%2C2%2C3

è un elenco di bug/problemi che fanno riferimento _parse_optional. Le possibili modifiche includono come vengono gestiti gli optionals ambigui. (Le scansionerò per vedere se dimentico qualcosa. Alcune patch sono mie.) Ma usando super, la mia modifica suggerita non è influenzata dalle modifiche all'interno della funzione. È influenzato solo dai cambiamenti nel modo in cui viene chiamata la funzione e da ciò che viene restituito, che è molto meno probabile che si verifichi. Presentando il tuo problema, devi almeno avvisare gli sviluppatori che qualcuno dipende da questa interfaccia.

+0

Grazie per una spiegazione dettagliata e utile. La mia unica preoccupazione sarebbe che potrebbe rompersi se Argparse viene aggiornato. Aggiungerò una richiesta al backlog argparse. –

+0

Ho aggiunto una nota su problemi esistenti. – hpaulj

Problemi correlati