Le sujet du jour concernera le traitement des paramètres en lignes de commandes, mais au lieu de mon précédant billet sur le sujet, qui traitait de OptParse, celui ci traitera de argparse et des différences que j'ai constaté pour passer de l'un à l'autre, et qui ne sont pas dans la doc de "migration"

Pour traiter un cas concret j'ai produit un lib python pour récupérer des infos d'un site en ligne telles que une timeline, la liste épisodes d'une série tv, la liste des séries tv ressemblant à une autre etc... ça c'est pour la lib, elle fonctionne ;)

Pour la tester j'ai pondu un script qui fait l'appel à toutes les méthodes de la classe de ma lib. Il y a pas moins d'une quarantaine de méthodes.
Pourquoi je vous dis ça ? parce que ça m'a fait faire une quarantaine de traitement d'arguments ;) Ah ba quand il faut il faut ;)

Optparse

Avec optparse, j'ai donc décidé de partir du postulat : faire des "groupes" : un par methode de ma classe avec les options qui matcheront les paramètres attendues par chaque méthode (classique quoi). Cela donnait ceci (je ne vous mets pas tout rassurez vous ;) :

def main():
    parser = OptionParser()

    group0 = OptionGroup(parser, "*** Search series")
    group0.add_option("--title", dest="title", type="string",
                      help="make a search by title")
    parser.add_option_group(group0)

    # group the options for handling Display of series parameters
    group1 = OptionGroup(parser, "*** Details of series")
    group1.add_option("--display", dest="display", action="store",
                     help="the name of the given serie")
    parser.add_option_group(group1)

    # group the options for handling Episodes parameters
    group2 = OptionGroup(parser, "*** Episodes",
                        "use --name  (--season ) (--episode )  
(--summary) to filter episodes you want to search")
    group2.add_option("--name", dest="name", action="store",
                     help="the name of the given serie")

    group2.add_option("--episode", dest="episode", action="store",
                     help="the number of the episode (optional)")

    group2.add_option("--season", dest="season", action="store",
                     help="the number of the season (optional)")

    group2.add_option("--summary", dest="summary", action="store_true",
                     help="boolean set to false by default,   
to only get the summary of the episode (optional)")

    parser.add_option_group(group2)
[...]
    (options, args) = parser.parse_args()
[...]

Ensuite pour avoir l'aide on tape

python go.py -h

qui affiche

Usage: go.py [options]

Options:
  -h, --help            show this help message and exit

  *** Search series:
    --title=TITLE       make a search by title

  *** Details of series:
    --display=DISPLAY   the name of the given serie

  *** Episodes:
    use --name  (--season ) (--episode ) (--summary) to
    filter episodes you want to search

    --name=NAME         the name of the given serie
    --episode=EPISODE   the number of the episode (optional)
    --season=SEASON     the number of the season (optional)
    --summary           boolean set to false by default,
                        to only get the summary of the episode (optional)

Ok c'est tout beau et cool et ça marche (en plus ;-)

Bon à présent les limitations de optparse (outre le fait qu'il est déprécié car plus maintenu) :

les paramètres obligatoires:

optparse permet de rendre des paramètres obligatoires via un required=True, mais le soucis c'est que les groupes ne sont pas exclusifs et que dans le "options" qui est ici :

 (options, args) = parser.parse_args()

"options" contient TOUS les paramètres de TOUS les groupes. Du coup si on fait un required=True sur

    group0.add_option("--title", dest="title", type="string",
                      help="make a search by title",required=True)

quand je taperai

python go.py --display --name dexter

il me sortira que j'ai oublié de renseigner le paramètre TITLE.... que je n'ai pas besoin pour l'utilisation de --display ...

Ça ça m'a dérangé car du coup j'ai dû déporter l'aspect "obligation de renseigner un paramètre" plus tard dans mon code.

paramètres en conflit
optparse ne permet pas d'utiliser 2 fois le même nom de paramètre pour deux groupes distincts, il pète une vraie exception et rien à faire pour contourner. Du coup la convention de nommage des variables (pour les rendre unique) devient vite pénible pour conserver un semblant d'homogénéité entre les noms des méthodes de la classe de ma lib et les noms des actions mises en place dans mon script.

aide trop verbeuse
Comme dit plus tôt j'ai pres de 40 groupes, du coup l'aide en devient carrément illisible au premier coup d'oeil.

Si si je vous promets, imaginer ça

Usage: go.py [options]

Options:
  -h, --help            show this help message and exit

  *** Search series:
    --title=TITLE       make a search by title

  *** Details of series:
    --display=DISPLAY   the name of the given serie

  *** Episodes:
    use --name  (--season ) (--episode ) (--summary) to
    filter episodes you want to search

    --name=NAME         the name of the given serie
    --episode=EPISODE   the number of the episode (optional)
    --season=SEASON     the number of the season (optional)
    --summary           boolean set to false by default,
                        to only get the summary of the episode (optional)

... multiplié par 10 ...

Donc fort de ces constats je me suis dit "bon hé ho ; si ça me saoule déjà rien qu'à moi personnellement moi même ; je ne serai pas le seul ; voyons argparse"

Argparse
Pour démarrer sans trop perdre de temps j'ai donc suivi la doc d'upgrade mentionnée plus haut pour passer de optparse à argparse.

Ça a vite fonctionné et j'étais plutôt content ;)

paramètres en conflits : résolu
Mais comme je suis toujours insatisfait, je suis reparti sur mon envie de mettre les mêmes noms de variables à mes actions qu'à celle des paramètres de mes méthodes de classe de ma lib.

en clair je voulais pour ça :

    def shows_episodes(self, url, season=None, episode=None, summary=False,
                       hide_notes=False, token=None):
[...]
    def shows_characters(self, url, summary=False, the_id=None):
[...]

faire un truc du genre :

python go.py shows_episodes --url ... --season ... --episode ... 
python go.py shows_characters --url ... --summary ...

Pour y parvenir argparse a une option de gestion des conflits conflict_handler qu'on passe à 'resolve'

Ainsi pourvu, taper les paramètres sera beaucoup facile à retenir ou tout du moins plus simple à taper que

python go.py --shows_episodes --shows_episodes_url ... --shows_episodes_season ... --shows_episodes_episode
python go.py --shows_characters --shows_characters_url ... --shows_characters_summary ... 

vous voyez le genre de balles dans la tete qu'on pouvait se tirer avec optparse pour avoir des var "unique" (sans conflit ;)

aide trop verbeuse : résolu
De même l'aide cette fois-ci s'est retrouvée raccourcie drastiquement.

avant on avait

Usage: go.py [options]

Options:
  -h, --help            show this help message and exit

  *** Search series:
    --title=TITLE       make a search by title

  *** Details of series:
    --display=DISPLAY   the name of the given serie

  *** Episodes:
    use --name  (--season ) (--episode ) (--summary) to
    filter episodes you want to search

    --name=NAME         the name of the given serie
    --episode=EPISODE   the number of the episode (optional)
    --season=SEASON     the number of the season (optional)
    --summary           boolean set to false by default,
                        to only get the summary of the episode (optional)

à présent ca donne :

Usage: go.py [options]

Options:
  -h, --help            show this help message and exit

  shows_search - Search series: use shows_series make a search by title
  shows_displays - Details of series: use display=DISPLAY   the name of the given serie
  shows_episodes - Episodes: use shows_episode  (--season ) (--episode )
                  (--summary) to filter episodes you want to search

La différence entre les 2 ? On n'affiche plus ici les options de chaque commande ! Mais ensuite si on veut l'aide complète de l'action shows_search on tapera un :

python go.py show_search --help

qui donnera l'aide escomptée

usage: go [options] shows_search [-h] --title TITLE

positional arguments:
  shows_search   Search series: use --shows_search --title 

optional arguments:
  -h, --help     show this help message and exit
  --title TITLE  make a search by title

C'est plus clair plus concis et on est tout joie ;)

Tout ceci est obtenu avec une particularité propre à argparse qui est la création d'un sub-parser. Oui un subparser.

Avant on avait la totalité des actions (sous la main avec optparse) comme montré dans le premier snipset

A présent avec subparser ca donne ceci :

    parser = argparse.ArgumentParser(prog="go",
                                     usage='%(prog)s [options]',
                                     description='BetaSeries API Management',
                                     conflict_handler='resolve',
                                     add_help=True)

    subparsers = parser.add_subparsers(help='sub-command help')

    group0 = subparsers.add_parser('shows_search', help='Search series:   
use --shows_search --title ')
    group0.add_argument("shows_search", action="store_true",
                    help='Search series: use --shows_search --title ')
    group0.add_argument("--title", action="store", required=True,
                        help="make a search by title")

    group1 = subparsers.add_parser("shows_display",
            help="Details of series: use --shows_display --url ")
    group1.add_argument("shows_display", action="store_true",
                    help="Details of series : use --shows_display --url ")
    group1.add_argument("--url", action="store", required=True,
                     help="the url/name of the given serie")

    group2 = subparsers.add_parser("shows_episodes", help="Show Episodes: use   
--shows_episodes --url  (--season ) (--episode )  
(--summary) to filter episodes you want to search")
    group2.add_argument("shows_episodes", action="store_true", help="Episodes:  
--shows_episodes --url  (--season ) (--episode )  
(--summary) to filter episodes you want to search")
    group2.add_argument("--url", action="store", required=True,
                     help="the url of the given serie")
    group2.add_argument("--episode", action="store",
                     help="the number of the episode (optional)")
    group2.add_argument("--season", action="store",
                     help="the number of the season (optional)")
    group2.add_argument("--summary", action="store_true",
                     help="boolean set to false by default,   
to only get the summary of the episode (optional)")
[...]
    args = parser.parse_args()
    if len(sys.argv) > 1:
        do_action(args)
    else:
        parser.error("enter -help to see the options you can use")

Une petite explication sur le parm help s'impose dans l'utilisation que j'en ai faite.

  • Quand on tape python go.py --help l'aide affichée n'est autre que le texte (help="..") qui se trouve sur ma ligne groupX = subparser.add_parser()
  • Quand on tape python go.py shows_search --help l'aide affichée est celle sur la ligne add_argument("show_search",help="..") puisque cette fois ci je veux l'aide de la commande elle même.

La différence c'est que la premiere sert pour afficher l'aide de toutes les commandes, la seconde pour l'aide de la commande elle seule.

Donc, personnellement je mets la même chose sinon je n'ai pas d'aide affichée suffisement explicite.
On pourrait se dire que argparse va réafficher quand même l'aide déjà fournie pour ici groupX = subparser.add_parser(), mais non :

voici la difference

sans le texte d'aide

usage: go [options] shows_search [-h] --title TITLE

positional arguments:
  shows_search

optional arguments:
  -h, --help     show this help message and exit

avec le texte d'aide

usage: go [options] shows_search [-h] --title TITLE

positional arguments:
  shows_search   Search series: use --shows_search --title 

optional arguments:
  -h, --help     show this help message and exit
  --title TITLE  make a search by title

Autre différence majeure entre optparse et argparse :

Récuperer les infos saisies avec optparse

on faisait à la tout fin de la definition des groupes (pour mon cas)

(options, args) = parser.parse_args()

puis dans sa fonction on testait les paramètres :

if options.title:
#traitement
elif options.name:
#traitement 

Récuperer les infos saisies avec argparse

args = parser.parse_args()

Ici args est une liste nommée Namespace qui contient uniquement les paramètres disponibles via le subparser concerné, exemple un print de args donnerait ceci :

python go_new.py shows_episodes --url dexter
Namespace(episode=None, season=None, shows_episodes=True, summary=False, url='dexter')

Ensuite donc dans sa fonction on teste si la commande est dispo dans options :

f hasattr(options, 'shows_characters'):
#traitement 
elif hasattr(options, 'shows_episodes'):
#traitement

On doit utiliser if hasattr(options, 'shows_episodes'): à la place de if options.shows_episodes sinon python produira systématiquement une erreur, puisque l'info sur options.shows_episodes n'est pas disponible quand on traitera de .... shows_characters... et inversement.

voili voilo.

Pour voir mes scripts complets avec optparse et argparse ; ils sont tous deux disponibles sur github au beau milieu d'un projet ;)

Si vous avez des remarques &/ou corrections, lâchez vous ;)

Si vous avez envie d'article sur un sujet particulier, "il y a un" billet "pour cela" (c) et je me fais fort de (tenter) d'y répondre ;)