ArgParse

Many people use Python’s argparse command line argument parser. This parser can handle sub-commands like git commit -m "message" where commit is a sub-command and -m <message> is an argument of this sub-command parser. It’s possible to assign a callback function to each individual sub-command parser.

Advantages

  • Declarative description instead of imperative form.

  • All options from argparse can be used.

  • Declare accepted command-line arguments close to the responsible handler method

  • Complex parsers can be distributed accross multiple classes and merged via multiple inheritance.

  • Pre-defined argument templates like switch parameters (--help).

Classic argparse Example

tests/example/OldStyle.py
  1import textwrap
  2from argparse import ArgumentParser, RawDescriptionHelpFormatter
  3from typing import List, Dict
  4
  5
  6class ProgramBase():
  7  HeadLine = "Simple ArgParse Test Program"
  8
  9  def __init__(self):
 10    pass
 11
 12  def PrintHeadline(self):
 13    print("{line}".format(line="=" * 80))
 14    print("{headline: ^80s}".format(headline=self.HeadLine))
 15    print("{line}".format(line="=" * 80))
 16
 17
 18class Program(ProgramBase):
 19  MainParser: ArgumentParser = None
 20  SubParsers: Dict[str, ArgumentParser] = None
 21
 22  def __init__(self):
 23    super().__init__()
 24
 25    self._ConstructParser()
 26
 27  def _ConstructParser(self):
 28    # create a commandline argument parser
 29    self.MainParser = ArgumentParser(
 30      description=textwrap.dedent('''\
 31        This is the test program.
 32        '''),
 33      epilog=textwrap.fill("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam."),
 34      formatter_class=RawDescriptionHelpFormatter,
 35      add_help=False
 36    )
 37
 38    self.MainParser.add_argument("-q", "--quiet",   dest="quiet",   action="store_const", const=True, default=False, help="Reduce messages to a minimum.")
 39    self.MainParser.add_argument("-v", "--verbose", dest="verbose", action="store_const", const=True, default=False, help="Print out detailed messages.")
 40    self.MainParser.add_argument("-d", "--debug",   dest="debug",   action="store_const", const=True, default=False, help="Enable debug mode.")
 41
 42    # create subparsers
 43    subParsers = self.MainParser.add_subparsers(help='sub-command help')
 44    self.SubParsers = {}
 45
 46    # Default handler
 47    self.MainParser.set_defaults(func=self.HandleDefault)
 48
 49    # Help handler
 50    HelpParser = subParsers.add_parser("help", help = "Display help page(s) for the given command name.")
 51    HelpParser.add_argument(metavar = "Command", dest = "Command", type = str, nargs = "?", help = "Print help page(s) for a command.")
 52    HelpParser.set_defaults(func=self.HandleHelp)
 53    self.SubParsers["help"] = HelpParser
 54
 55    # UserManagement commands
 56    CreateUserParser = subParsers.add_parser("new-user", help="Create a new user.")
 57    CreateUserParser.add_argument(metavar='<UserID>', dest="UserID", type=str, help="UserID - unique identifier")
 58    CreateUserParser.add_argument(metavar='<Name>', dest="Name", type=str, help="The user's display name.")
 59    CreateUserParser.set_defaults(func=self.HandleNewUser)
 60    self.SubParsers["new-user"] = CreateUserParser
 61
 62    RemoveUserParser = subParsers.add_parser("delete-user", help="Delete a user.")
 63    RemoveUserParser.add_argument(metavar='<UserID>', dest="UserID", type=str, help="UserID - unique identifier")
 64    RemoveUserParser.set_defaults(func=self.HandleDeleteUser)
 65    self.SubParsers["delete-user"] = RemoveUserParser
 66
 67    RemoveUserParser = subParsers.add_parser("list-user", help="List users.")
 68    RemoveUserParser.add_argument('--all', dest="all", help='List all users.')
 69    RemoveUserParser.set_defaults(func=self.HandleListUser)
 70    self.SubParsers["list-user"] = RemoveUserParser
 71
 72  def Run(self):
 73    args = self.MainParser.parse_args()
 74    args.func(args)
 75
 76  def HandleDefault(self, args):
 77    self.PrintHeadline()
 78    # TODO: use dedent() and multi-line string
 79    print(f"HandleDefault:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}")
 80
 81  def HandleHelp(self, args):
 82    self.PrintHeadline()
 83
 84    # TODO: use dedent() and multi-line string
 85    print(f"HandleHelp:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  command={args.Command!s}\n\n")
 86
 87    if (args.Command is None):
 88      self.MainParser.print_help()
 89    elif (args.Command == "help"):
 90      print("This is a recursion ...")
 91    else:
 92      try:
 93        self.SubParsers[args.Command].print_help()
 94      except KeyError:
 95        print(f"Command {args.Command} is unknown.")
 96
 97  def HandleNewUser(self, args):
 98    self.PrintHeadline()
 99
100    # TODO: use dedent() and multi-line string
101    print(f"HandleHelp:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  UserID={args.UserID!s}  Name={args.Name!s}")
102
103  def HandleDeleteUser(self, args):
104    self.PrintHeadline()
105
106    # TODO: use dedent() and multi-line string
107    print(f"HandleHelp:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  UserID={args.UserID!s}")
108
109  def HandleListUser(self, args):
110    self.PrintHeadline()
111
112    # TODO: use dedent() and multi-line string
113    print(f"HandleHelp:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  all={args.all!s}")
114
115
116if __name__ == "__main__":
117  prog = Program()
118  prog.Run()

New pyAttributes Approach

A better and more descriptive solution could look like this:

tests/example/UserManager.py
 1from argparse import ArgumentDefaultsHelpFormatter
 2
 3from pyAttributes.ArgParseAttributes import ArgParseMixin, CommonSwitchArgumentAttribute, DefaultAttribute, CommandAttribute, CommonArgumentAttribute, ArgumentAttribute, SwitchArgumentAttribute
 4
 5
 6class ProgramBase():
 7  HeadLine = "Simple ArgParse Test Program"
 8
 9  def __init__(self):
10    pass
11
12  def PrintHeadline(self) -> None:
13    print("{line}".format(line="="*80))
14    print("{headline: ^80s}".format(headline=self.HeadLine))
15    print("{line}".format(line="="*80))
16
17
18class Program(ProgramBase, ArgParseMixin):
19  def __init__(self):
20    import textwrap
21
22    # call constructor of the main inheritance tree
23    super().__init__()
24
25    # Call the constructor of the ArgParseMixin
26    ArgParseMixin.__init__(
27      self,
28      description=textwrap.dedent('''\
29        This is the test program.
30        '''),
31      epilog=textwrap.fill("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam."),
32      formatter_class=ArgumentDefaultsHelpFormatter,
33      add_help=False
34    )
35
36  @CommonSwitchArgumentAttribute("-q", "--quiet",   dest="quiet",   help="Reduce messages to a minimum.")
37  @CommonSwitchArgumentAttribute("-v", "--verbose", dest="verbose", help="Print out detailed messages.")
38  @CommonSwitchArgumentAttribute("-d", "--debug",   dest="debug",   help="Enable debug mode.")
39  def Run(self) -> None:
40    ArgParseMixin.Run(self)
41
42
43  @DefaultAttribute()
44  def HandleDefault(self, args) -> None:
45    self.PrintHeadline()
46
47    print(f"HandleDefault:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}")
48
49
50  @CommandAttribute("help", help="Display help page(s) for the given command name.")
51  @ArgumentAttribute(metavar="Command", dest="Command", type=str, nargs="?", help="Print help page(s) for a command.")
52  def HandleHelp(self, args) -> None:
53    self.PrintHeadline()
54
55    print(f"HandleHelp:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  command={args.Command!s}\n\n")
56
57    if (args.Command is None):
58      self.MainParser.print_help()
59    elif (args.Command == "help"):
60      print("This is a recursion ...")
61    else:
62      try:
63        self.SubParsers[args.Command].print_help()
64      except KeyError:
65        print(f"Command {args.Command} is unknown.")
66
67
68  @CommandAttribute("new-user", help="Create a new user.")
69  @ArgumentAttribute(metavar='<UserID>', dest="UserID", type=str, help="UserID - unique identifier")
70  @ArgumentAttribute(metavar='<Name>', dest="Name", type=str, help="The user's display name.")
71  def HandleNewUser(self, args) -> None:
72    self.PrintHeadline()
73
74    print(f"HandleNewUser:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  UserID={args.UserID!s}  Name={args.Name!s}  Limit={args.Limit!s}")
75
76
77  @CommandAttribute("delete-user", help="Delete a user.")
78  @ArgumentAttribute(metavar='<UserID>', dest="UserID", type=str, help="UserID - unique identifier")
79  def HandleDeleteUser(self, args) -> None:
80    self.PrintHeadline()
81
82    print(f"HandleDeleteUser:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  UserID={args.UserID!s}")
83
84
85  @CommandAttribute("list-user", help="List users.")
86  @SwitchArgumentAttribute('--all', dest="all", help='List all users.')
87  def HandleListUser(self, args) -> None:
88    self.PrintHeadline()
89
90    print(f"HandleListUser:\n  quiet={args.quiet!s}\n  verbose={args.verbose!s}\n  debug={args.debug!s}\n\n  all={args.all!s}")
91
92
93if __name__ == "__main__":
94  prog = Program()
95  prog.Run()