Elegant command line argument parsing for PyQt

2019-04-21 11:17发布

问题:

I'm writing a new PyQt app. I'm trying to do everything related to the program and ui using as much of the PyQt APIs as possible as a means to improve my knowledge of PyQt and Qt in general.

The question I have is, is there an API within PyQt/Qt to handle command line argument parsing elegantly?

My research so far has turned up:

  • an example of how to make it play nice with python's opt_parser module, except it doesn't handle QApplication's built in arg parsing.
  • PyKDE's KCmdLineArgs (which introduces an unwanted KDE dependency)
  • it looks like KCmdLineArgs is being ported upstream for Qt5.1 as QCommandLineParser, which is cool, but I'd like to be able to use it now, not 18 months from now.

So how do PyQt applications normally handle this? or is opt_parser/argparse the way to go?

This is far from a nice solution...

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys, argparse
from PyQt4 import QtGui

def main(argv):

  app = QtGui.QApplication(argv) # QApplication eats argv in constructor

  # We can get a QStringList out of QApplication of those arguments it 
  # didn't decide were reserved by Qt.
  argv2 = app.arguments()   

  # now we need to turn them back into something that optparse/argparse 
  # can understand, since a QStringList is not what it wants
  argv3 = []
  for i in argv2:
    argv3.append(str(i))

  # now we can pass this to optparse/argparse
  process_args(argv3)

  # dummy app
  mw = QtGui.QMainWindow()
  mw.show()
  sys.exit(app.exec_())

def process_args(argv):
  parser = argparse.ArgumentParser(description='PyQt4 argstest', 
                                   add_help=False)

  # we now have to add all of the options described at 
  # http://qt-project.org/doc/qt-4.8/qapplication.html#QApplication
  # but have them do nothing - in order to have them show up in the help list

  # add this to the list if Qt is a debug build (How to detect this?)
  parser.add_argument("-nograb", action=ignore,
                      help="don't grab keyboard/mouse for debugging")

  # add these to the list if Qt is a debug build for X11
  parser.add_argument("-dograb", action=ignore,
                      help="grab keyboard/mouse for debugging")
  parser.add_argument("-sync", action=ignore,
                      help="run in synchronous mode for debugging")

  # add all the standard args that Qt will grab on all platforms
  parser.add_argument("-reverse", action=ignore,
                      help="run program in Right-to-Left mode")
  # an example -- there are 10 such items in the docs for QApplication

  # then we need to figure out if we're running on X11 and add these
  parser.add_argument("-name", action=ignore,
                      help="sets the application name")
  # an example -- there are 13 such items in the docs

  # reimplement help (which we disabled above) so that -help works rather 
  # than --help; done to be consistent with the style of args Qt wants
  parser.add_argument("-h", "-help", action='help',
                      help="show this help message and exit")

  parser.parse_args(argv[1:])

class ignore(argparse.Action):
  # we create an action that does nothing, so the Qt args do nothing
  def __call__(self, parser, namespace, values, option_string=None):
    pass

if __name__ == "__main__":
  main(sys.argv)

回答1:

Use argparse if you're using Python 2.7 (optparse if < 2.7), the package doesn't have to be specific to PyQt for you to handle commandline options.



回答2:

The best solution here is using the argparse module's parse_known_args() method (docs) to process the non-Qt command line options first. It's no more work in configuring the ArgumentParser—it just changes which method you call, and gives you a tuple instead of a single object in the return. That gives you the best of both worlds.

A simplified example that catches only a couple of the Qt 4.8 arguments and a few others, but gives the general idea.

# my_script.py

import argparse
from PyQt4 import QtGui  # this will work with PySide.QtGui, too

def process_cl_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('-s', '--swallow', action='store')  # optional flag
    parser.add_argument('holy_hand_grenade', action='store')  # positional argument

    parsed_args, unparsed_args = parser.parse_known_args()
    return parsed_args, unparsed_args

if __name__ == '__main__':
    parsed_args, unparsed_args = process_cl_args()
    # QApplication expects the first argument to be the program name.
    qt_args = sys.argv[:1] + unparsed_args
    app = QtGui.QApplication(qt_args)
    # ... the rest of your handling: `sys.exit(app.exec_())`, etc.

Assume you were to run this like so:

$ python my_script.py -dograb --swallow=unladen 3 -style cde

Then parsed_args would have the usual Namespace with holy_hand_grenade set to 3 and --swallow set to 'unladen', while unparsed_args would have a simple list: ['-dograb', '-style,' 'cde']. This in turn can be passed normally to the QApplication with the inclusion of the program name from sys.argv[0] (thanks to marcin for pointing this out). We use sys.argv[:1] to get an array for concatenation with unparsed_args; you could just as well do [sys.argv[0]]

Among other things, this lets you set up your application to specify whether to launch the Qt UI, to run as a command line, to run unit tests, and so forth instead if you so desired. Handling the non-Qt (or anything else) arguments first is better because it does not leave argparse dependent on the setup you're using, but the other way around.



回答3:

You should really try docopt. It doesn't need any argument definitions. You have "Usage: " string and unparsed arguments as input, pass them into docopt() function, and you have parsed arguments as output.