Create argparse type "existing_dir[_empty]"
[pyi2ncommon] / src / argparse_helpers.py
1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
3 #
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
6 #
7 # As a special exception, if other files instantiate templates or use macros
8 # or inline functions from this file, or you compile this file and link it
9 # with other works to produce a work based on this file, this file
10 # does not by itself cause the resulting work to be covered
11 # by the GNU General Public License.
12 #
13 # However the source code for this file must still be made available
14 # in accordance with section (3) of the GNU General Public License.
15 #
16 # This exception does not invalidate any other reasons why a work based
17 # on this file might be covered by the GNU General Public License.
18
19 """
20 argparse_helpers: Some convenience helpers for argparse
21
22 Featuring
23 - NonExitingParser: a subclass of ArgumentParser that does not do sys.exit(2)
24   on parse error but instead raises a ArgParserWantsExit exception.
25 - function existing_file which can be used as a type in add_argument() calls
26
27 .. codeauthor:: Intra2net
28 """
29
30 from argparse import ArgumentParser, ArgumentTypeError
31 from os.path import isfile, isdir
32
33
34 class ArgParserWantsExit(Exception):
35     """
36     Exception raised from NonExitingParser instead of calling sys.exit(2).
37     """
38
39     def __init__(self, message):
40         super(ArgParserWantsExit, self).__init__("Error parsing args: " +
41                                                  message)
42
43
44 class NonExitingParser(ArgumentParser):
45     """ArgumentParser that does not call sys.exit(2) on parse failure.
46
47     Calling `sys.exit` also just raises a SystemExit exception. But that is not
48     a subclass of Exception and not as explicit and specific as this one.
49
50     Convenient e.g. for global try-except blocks e.g. in a daemon::
51
52         def main():
53             log = None
54             try:
55                log = create_log()
56                parser = NonExitingParser()
57                # ... add args ...
58                args = parser.parse_args()
59                # ... rest of your program ...
60             except Exception as exc:
61                 log.error('uncaught exception: exc')  # will always show in log
62
63     """
64     def error(self, message):
65         """Called when error occurred parsing args. Raise error, not exit."""
66         raise ArgParserWantsExit(message)
67
68
69 def existing_file(filename):
70     """
71     Function that raises ArgumentTypeError if argument is not an existing file.
72
73     Returns filename if it is valid.  Can be used as `type` argument in
74     :py:meth:`argparse.ArgumentParser.add_argument`
75
76     If you want to open the file, you can as well use the python built-in
77     :py:class:`argparse.FileType` instead.
78     """
79     if isfile(filename):
80         return filename
81     raise ArgumentTypeError('{} is not an existing file'.format(filename))
82
83
84 def existing_file_or_empty(filename=''):
85     """
86     Like :py:func:`existing_file` but accepts empty filename (returns '' then).
87     """
88     if not filename.strip():
89         return ''
90     return existing_file(filename)
91
92
93 def existing_dir(path):
94     """
95     Function that raises ArgumentTypeError if argument is not an existing directory.
96
97     .. seealso:: :py:func:`existing_file`, :py:func:`existing_dir_or_empty`
98     """
99     if isdir(path):
100         return path
101     raise ArgumentTypeError('{} is not an existing directory'.format(path))
102
103
104 def existing_dir_or_empty(path=''):
105     """
106     Like :py:func:`existing_dir` but accepts empty path (returns '' then).
107     """
108     if not path.strip():
109         return ''
110     return existing_dir(path)