sumolib.options

  1# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
  2# Copyright (C) 2012-2026 German Aerospace Center (DLR) and others.
  3# This program and the accompanying materials are made available under the
  4# terms of the Eclipse Public License 2.0 which is available at
  5# https://www.eclipse.org/legal/epl-2.0/
  6# This Source Code may also be made available under the following Secondary
  7# Licenses when the conditions for such availability set forth in the Eclipse
  8# Public License 2.0 are satisfied: GNU General Public License, version 2
  9# or later which is available at
 10# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
 11# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
 12
 13# @file    options.py
 14# @author  Jakob Erdmann
 15# @author  Michael Behrisch
 16# @date    2012-03-15
 17
 18
 19from __future__ import print_function
 20from __future__ import absolute_import
 21import os
 22import sys
 23import subprocess
 24from collections import namedtuple
 25import re
 26from xml.sax import parse, parseString, handler
 27import argparse
 28import io
 29from argparse import RawDescriptionHelpFormatter  # noqa
 30from copy import deepcopy
 31from functools import wraps
 32from .miscutils import openz, parseTime
 33from .xml import xmlescape
 34
 35
 36class ConfigurationReader(handler.ContentHandler):
 37
 38    """Reads a configuration template, storing the options in an OptionParser"""
 39
 40    def __init__(self, optParse, groups, configoptions):
 41        self._opts = optParse
 42        self._groups = groups
 43        self._options = configoptions
 44        self._group = self._opts
 45
 46    def startElement(self, name, attrs):
 47        if len(attrs) == 0:
 48            self._group = self._opts.add_argument_group(name)
 49        if self._group != self._opts and self._groups and self._group.title not in self._groups:
 50            return
 51        if 'type' in attrs and name != "help":
 52            if self._options and name not in self._options:
 53                return
 54            if name in ['configuration-file', 'save-configuration', 'save-template']:
 55                return
 56            help = attrs.get("help", "")
 57            action = None
 58            default = None
 59            oType = None
 60            if attrs["type"] == "BOOL":
 61                action = "store_true"
 62                default = False
 63            elif attrs["type"] == "TIME":
 64                oType = ArgumentParser.time
 65                if attrs["value"]:
 66                    default = float(attrs["value"])
 67            elif attrs["type"] == "FLOAT":
 68                oType = float
 69                if attrs["value"]:
 70                    default = float(attrs["value"])
 71            elif attrs["type"] == "INT":
 72                oType = int
 73                if attrs["value"]:
 74                    default = int(attrs["value"])
 75            else:
 76                default = attrs["value"]
 77            if action is None:
 78                self._group.add_argument("--" + name, help=help, default=default, type=oType)
 79            else:
 80                self._group.add_argument("--" + name, help=help, action=action, default=default)
 81
 82    def endElement(self, name):
 83        if self._group != self._opts and name == self._group.title:
 84            self._opts.add_argument_group(self._group)
 85            self._group = self._opts
 86
 87
 88def pullOptions(executable, argParser, groups=None, configoptions=None):
 89    optoutput = subprocess.check_output([executable, "--save-template", "-"])
 90    parseString(optoutput, ConfigurationReader(argParser, groups, configoptions))
 91
 92
 93def get_long_option_names(application):
 94    # @todo using option "--save-template stdout" and parsing xml would be prettier
 95    output = subprocess.check_output([application, '--help'], universal_newlines=True)
 96    reprog = re.compile(r'(--\S*)\s')
 97    result = []
 98    for line in output.splitlines():
 99        m = reprog.search(line)
100        if m:
101            result.append(m.group(1))
102    return result
103
104
105def assign_prefixed_options(args, allowed_programs):
106    prefixed_options = {}
107    remaining = []
108    consumed = False
109    for arg_index, arg in enumerate(args):
110        used = False
111        if consumed:
112            consumed = False
113            continue
114        if arg[:2] == '--':
115            separator_index = arg.find('-', 2)
116            if separator_index != -1:
117                program = arg[2:separator_index]
118                if program in allowed_programs:
119                    try:
120                        if '=' in arg:
121                            option = arg[separator_index+1:].split('=')
122                        else:
123                            if '--' in args[arg_index+1]:
124                                raise ValueError()
125                            option = [arg[separator_index+1:], args[arg_index+1]]
126                            consumed = True
127                    except (IndexError, ValueError):
128                        raise ValueError("Please amend prefixed argument %s with a value." % arg)
129                    used = True
130                    prefixed_options.setdefault(program, []).append(option)
131        if not used:
132            remaining.append(arg)
133    return prefixed_options, remaining
134
135
136def get_prefixed_options(options):
137    return options._prefixed_options
138
139
140Option = namedtuple("Option", ["name", "value", "type", "help", "category"])
141
142
143class OptionReader(handler.ContentHandler):
144
145    """Reads an option file"""
146
147    def __init__(self):
148        self.opts = []
149
150    def startElement(self, name, attrs):
151        if 'value' in attrs:
152            self.opts.append(Option(name, attrs['value'], attrs.get('type'), attrs.get('help'), attrs.get('category')))
153
154
155def readOptions(filename):
156    optionReader = OptionReader()
157    parse(filename, optionReader)
158    return optionReader.opts
159
160
161class ArgumentParser(argparse.ArgumentParser):
162    """Drop-in replacement for argparse.ArgumentParser that adds support for
163    sumo-style config files.
164    Inspired by https://github.com/bw2/ConfigArgParse
165    """
166
167    @staticmethod
168    def time(s):
169        return parseTime(s)
170
171    @staticmethod
172    def file(s):
173        return s
174
175    @staticmethod
176    def file_list(s):
177        return s
178
179    @staticmethod
180    def net_file(s):
181        return s
182
183    @staticmethod
184    def route_file(s):
185        return s
186
187    @staticmethod
188    def route_file_list(s):
189        return s
190
191    @staticmethod
192    def additional_file(s):
193        return s
194
195    @staticmethod
196    def additional_file_list(s):
197        return s
198
199    @staticmethod
200    def edgedata_file(s):
201        return s
202
203    @staticmethod
204    def edge(s):
205        return s
206
207    @staticmethod
208    def edge_list(s):
209        return s
210
211    @staticmethod
212    def data_file(s):
213        # arbitrary data file (i.e. for attributeStats.py and plotXMLAttributes.py)
214        return s
215
216    @staticmethod
217    def sumoconfig_file(s):
218        return s
219
220    @staticmethod
221    def sumoconfig_file_list(s):
222        return s
223
224    def __init__(self, *args, **kwargs):
225        self._allowed_programs = kwargs.pop("allowed_programs", [])
226        self._catch_all = None
227        argparse.ArgumentParser.__init__(self, *args, **kwargs)
228        # add common argument for loading configuration
229        self.add_argument('-c', '--configuration-file', help='read configuration from FILE', metavar="FILE")
230        # add common argument for save configuration
231        self.add_argument('-C', '--save-configuration', help='save configuration to FILE and exit', metavar="FILE")
232        # add common argument for save template
233        self.add_argument('--save-template', help='save configuration template to FILE and exit', metavar="FILE")
234        self._fix_path_args = set()
235
236    def add_argument(self, *args, **kwargs):
237        # due argparse only accept certain values (action, choices, type, help...),
238        #  we need to extract extra parameters before call add_argument
239        fix_path = kwargs.pop("fix_path", False)
240        category = kwargs.pop("category", None)
241        catch_all = kwargs.pop("catch_all", False)
242        # get action
243        action = kwargs.get("action")
244        # parse argument
245        a = argparse.ArgumentParser.add_argument(self, *args, **kwargs)
246        # check if fix path
247        if fix_path:
248            for s in a.option_strings:
249                if s.startswith("--"):
250                    self._fix_path_args.add(s[2:])
251        # set category
252        a.category = category
253        # set if a is a boolean
254        a.boolean = ((action == "store_true") or (action == "store_false"))
255        # the value of a.required is lost during parsing
256        a.isRequired = a.required
257        a.isPositional = args[0][0] != "-"
258        if catch_all:
259            self._catch_all = a
260
261    def add_option(self, *args, **kwargs):
262        """alias for compatibility with OptionParser"""
263        self.add_argument(*args, **kwargs)
264
265    def get_option(self, dest):
266        for action in self._actions:
267            if action.dest == dest:
268                return action
269        return None
270
271    def add_mutually_exclusive_group(self, required=False):
272        group = argparse.ArgumentParser.add_mutually_exclusive_group(self, required=required)
273        group.add_argument = handleCategoryWrapper(self, group.add_argument)
274        return group
275
276    def _write_config_file(self, namespace, toString=False):
277        if namespace.save_configuration:
278            with openz(namespace.save_configuration, "w") as out:
279                self.write_config_to_file(out, namespace, False)
280            sys.exit()
281        if namespace.save_template:
282            with openz(namespace.save_template, "w") as out:
283                self.write_config_to_file(out, namespace, True)
284            sys.exit()
285        if toString:
286            out = io.StringIO()
287            try:
288                self.write_config_to_file(out, namespace, False)
289            except Exception:
290                # python2.7
291                out = io.BytesIO()
292                self.write_config_to_file(out, namespace, False)
293            return out.getvalue()
294
295    def write_config_to_file(self, out, namespace, print_template):
296        out.write(u'<configuration>\n')
297        optionNames = vars(namespace).keys()
298        if sys.version_info.major < 3 or sys.version_info.minor < 6:
299            optionNames = sorted(optionNames)
300        for k in optionNames:
301            v = vars(namespace)[k]
302            if k not in ("save_configuration", "save_template", "configuration_file", "_parser", "_prefixed_options"):
303                key = k
304                help = ''
305                typeStr = ''
306                category = ''
307                required = ''
308                positional = ''
309                listSep = ''
310                for a in self._actions:
311                    if a.dest == k:
312                        for s in a.option_strings:
313                            if s.startswith("--"):
314                                key = s[2:]
315                                break
316                        if print_template:
317                            # default
318                            if a.default is not None:
319                                v = a.default
320                            # help
321                            if a.help is not None:
322                                help = ' help="%s"' % xmlescape(a.help)
323
324                            # note: missing time, filename, list of vehicles, edges and lanes
325                            # category
326                            category = ' category="%s"' % (a.category if a.category is not None else 'processing')
327                            if a.boolean:
328                                typeName = "bool"
329                            elif a.type is None:
330                                typeName = "string"
331                            else:
332                                typeName = a.type.__name__
333                                if typeName == 'parseTime':
334                                    typeName = 'time'
335                                knownTypes = ['bool', 'float', 'int', 'time', 'file',
336                                              'net_file', 'route_file', 'additional_file',
337                                              'additional_file_list',
338                                              'edgedata_file', 'data_file', 'file_list',
339                                              'route_file_list', 'sumoconfig_file',
340                                              'sumoconfig_file_list', 'edge', 'edge_list']
341                                if typeName not in knownTypes:
342                                    typeName = 'string'
343                                elif typeName.endswith("_list"):
344                                    typeName = typeName[:-5]
345                                    listSep = ' listSeparator=","'
346                            typeStr = ' type="%s"' % typeName
347                            if a.isRequired:
348                                required = ' required="true"'
349                            if a.isPositional:
350                                positional = ' positional="true"'
351                            if a.nargs:
352                                listSep = ' listSeparator=" "'
353
354                        break
355                if print_template or v != a.default:
356                    if isinstance(v, list):
357                        v = " ".join(map(str, v))
358                    out.write(u'    <%s value="%s"%s%s%s%s%s%s/>\n' % (
359                              key, xmlescape(v), typeStr, help, category,
360                              required, positional, listSep))
361        out.write(u'</configuration>\n')
362
363    def parse_args(self, args=None, namespace=None):
364        return self.parse_known_args(args, namespace, True)[0]
365
366    def parse_known_args(self, args=None, namespace=None, check_unknown=False):
367        if args is None:
368            args = sys.argv[1:]
369        elif isinstance(args, str):
370            args = args.split()
371        else:
372            # gracefully handle non-string args passed from another script
373            args = list(map(str, args))
374        idx = -1
375        if '-c' in args:
376            idx = args.index('-c') + 1
377        if '--configuration-file' in args:
378            idx = args.index('--configuration-file') + 1
379        if '--save-template' in args:
380            for a in self._actions:
381                a.required = False
382            for g in self._mutually_exclusive_groups:
383                g.required = False
384
385        # add each config item to the commandline unless it's there already
386        config_args = []
387        pos_args = []
388        if idx > 0:
389            act_map = {}
390            pos_map = {}
391            multi_value = set()
392            pos_idx = 0
393            for a in self._actions:
394                for s in a.option_strings:
395                    if s.startswith("--"):
396                        act_map[s[2:]] = a.option_strings
397                        if a.nargs:
398                            multi_value.add(s[2:])
399                if len(a.option_strings) == 0:
400                    pos_map[a.dest] = pos_idx
401                    pos_args.append(None)
402                    pos_idx += 1
403            for cfg_file in args[idx].split(","):
404                for option in readOptions(cfg_file):
405                    is_set = False
406                    for s in act_map.get(option.name, []):
407                        if s in args:
408                            is_set = True
409                            break
410                    value = option.value
411                    if option.name in self._fix_path_args and not value.startswith("http"):
412                        value = os.path.join(os.path.dirname(cfg_file), value)
413                    if option.name in pos_map and option.name != 'remaining_args':
414                        if ',' in value:
415                            value = value.split(',')
416                        else:
417                            value = value.split()
418                        for i, v in enumerate(value):
419                            pos_args[pos_map[option.name]] = v
420                            if i + 1 < len(value):
421                                # shift pos_map
422                                pos_args.append(None)
423                                curPos = pos_map[option.name]
424                                for o, pos in pos_map.items():
425                                    if pos >= curPos:
426                                        pos_map[o] += 1
427
428                    elif not is_set:
429                        if value == "True":
430                            config_args += ["--" + option.name]
431                        elif value != "False":
432                            if option.name == 'remaining_args':
433                                # special case: magic option name to collect remaining arguments
434                                config_args += value.split()
435                            elif option.name in multi_value:
436                                config_args += ["--" + option.name] + value.split()
437                            else:
438                                # permit negative values and empty strings in cfg files
439                                config_args += ["--" + option.name + "=" + value]
440        combined_args = args + config_args + [p for p in pos_args if p is not None]
441        namespace, unknown_args = argparse.ArgumentParser.parse_known_args(
442            self, args=combined_args, namespace=namespace)
443
444        if self._allowed_programs and unknown_args and hasattr(namespace, "remaining_args"):
445            # namespace.remaining_args are the legacy method to parse arguments
446            # for subprograms (i.e. 'duarouter--weights.random-factor 2')
447            # unknown_args are the new style # # ('--duarouter-weights.random-factor 2')
448            # the default ArgumentParser interprets the first parameter for an
449            # unknown argument as remaining_args and this also creates an
450            # invalid error message when unknown options are parsed
451            unknown_args.insert(1, namespace.remaining_args[0])
452            namespace.remaining_args = []
453
454        # print("parse_known_args:\n  args: %s\n  config_args: %s\n  pos_args: %s\n  "
455        #       "combined_args: %s\n  remaining_args: %s\n  unknown_args: %s" %
456        #       (args, config_args, pos_args, combined_args, namespace.remaining_args, unknown_args))
457
458        namespace_as_dict = deepcopy(vars(namespace))
459        namespace._prefixed_options, remaining_args = assign_prefixed_options(unknown_args, self._allowed_programs)
460
461        for program in namespace._prefixed_options:
462            prefixed_options = deepcopy(namespace._prefixed_options[program])
463            for option in prefixed_options:
464                option[0] = program + '-' + option[0]
465            namespace_as_dict.update(dict(prefixed_options))
466
467        if check_unknown and remaining_args:
468            if self._catch_all:
469                setattr(namespace, self._catch_all.dest,
470                        getattr(namespace, self._catch_all.dest) + remaining_args)
471            else:
472                self.error('unrecognized arguments: %s' % ' '.join(remaining_args))
473
474        extended_namespace = argparse.Namespace(**namespace_as_dict)
475        self._write_config_file(extended_namespace)
476        namespace.config_as_string = self._write_config_file(extended_namespace, toString=True)
477        return namespace, remaining_args
478
479
480def handleCategoryWrapper(parser, func):
481    @wraps(func)
482    def inner(*args, **kwargs):
483        # remove category from arguments and set in result
484        category = kwargs.pop("category", None)
485        fix_path = kwargs.pop("fix_path", True)
486        result = func(*args, **kwargs)
487        if fix_path:
488            for s in result.option_strings:
489                if s.startswith("--"):
490                    parser._fix_path_args.add(s[2:])
491        result.category = category
492        # set if is a boolean
493        action = kwargs.get("action")
494        result.boolean = ((action == "store_true") or (action == "store_false"))
495        result.isRequired = kwargs.get("required", False)
496        result.isPositional = args[0][0] != "-"
497        return result
498    return inner
499
500
501class SplitAction(argparse.Action):
502    def __call__(self, parser, args, values, option_string=None):
503        if len(values) == 1:
504            values = [float(x) for x in values[0].split(',')]
505        else:
506            values = [float(x) for x in values]
507        setattr(args, self.dest, values)
class ConfigurationReader(xml.sax.handler.ContentHandler):
37class ConfigurationReader(handler.ContentHandler):
38
39    """Reads a configuration template, storing the options in an OptionParser"""
40
41    def __init__(self, optParse, groups, configoptions):
42        self._opts = optParse
43        self._groups = groups
44        self._options = configoptions
45        self._group = self._opts
46
47    def startElement(self, name, attrs):
48        if len(attrs) == 0:
49            self._group = self._opts.add_argument_group(name)
50        if self._group != self._opts and self._groups and self._group.title not in self._groups:
51            return
52        if 'type' in attrs and name != "help":
53            if self._options and name not in self._options:
54                return
55            if name in ['configuration-file', 'save-configuration', 'save-template']:
56                return
57            help = attrs.get("help", "")
58            action = None
59            default = None
60            oType = None
61            if attrs["type"] == "BOOL":
62                action = "store_true"
63                default = False
64            elif attrs["type"] == "TIME":
65                oType = ArgumentParser.time
66                if attrs["value"]:
67                    default = float(attrs["value"])
68            elif attrs["type"] == "FLOAT":
69                oType = float
70                if attrs["value"]:
71                    default = float(attrs["value"])
72            elif attrs["type"] == "INT":
73                oType = int
74                if attrs["value"]:
75                    default = int(attrs["value"])
76            else:
77                default = attrs["value"]
78            if action is None:
79                self._group.add_argument("--" + name, help=help, default=default, type=oType)
80            else:
81                self._group.add_argument("--" + name, help=help, action=action, default=default)
82
83    def endElement(self, name):
84        if self._group != self._opts and name == self._group.title:
85            self._opts.add_argument_group(self._group)
86            self._group = self._opts

Reads a configuration template, storing the options in an OptionParser

ConfigurationReader(optParse, groups, configoptions)
41    def __init__(self, optParse, groups, configoptions):
42        self._opts = optParse
43        self._groups = groups
44        self._options = configoptions
45        self._group = self._opts
def startElement(self, name, attrs):
47    def startElement(self, name, attrs):
48        if len(attrs) == 0:
49            self._group = self._opts.add_argument_group(name)
50        if self._group != self._opts and self._groups and self._group.title not in self._groups:
51            return
52        if 'type' in attrs and name != "help":
53            if self._options and name not in self._options:
54                return
55            if name in ['configuration-file', 'save-configuration', 'save-template']:
56                return
57            help = attrs.get("help", "")
58            action = None
59            default = None
60            oType = None
61            if attrs["type"] == "BOOL":
62                action = "store_true"
63                default = False
64            elif attrs["type"] == "TIME":
65                oType = ArgumentParser.time
66                if attrs["value"]:
67                    default = float(attrs["value"])
68            elif attrs["type"] == "FLOAT":
69                oType = float
70                if attrs["value"]:
71                    default = float(attrs["value"])
72            elif attrs["type"] == "INT":
73                oType = int
74                if attrs["value"]:
75                    default = int(attrs["value"])
76            else:
77                default = attrs["value"]
78            if action is None:
79                self._group.add_argument("--" + name, help=help, default=default, type=oType)
80            else:
81                self._group.add_argument("--" + name, help=help, action=action, default=default)

Signals the start of an element in non-namespace mode.

The name parameter contains the raw XML 1.0 name of the element type as a string and the attrs parameter holds an instance of the Attributes class containing the attributes of the element.

def endElement(self, name):
83    def endElement(self, name):
84        if self._group != self._opts and name == self._group.title:
85            self._opts.add_argument_group(self._group)
86            self._group = self._opts

Signals the end of an element in non-namespace mode.

The name parameter contains the name of the element type, just as with the startElement event.

def pullOptions(executable, argParser, groups=None, configoptions=None):
89def pullOptions(executable, argParser, groups=None, configoptions=None):
90    optoutput = subprocess.check_output([executable, "--save-template", "-"])
91    parseString(optoutput, ConfigurationReader(argParser, groups, configoptions))
def get_long_option_names(application):
 94def get_long_option_names(application):
 95    # @todo using option "--save-template stdout" and parsing xml would be prettier
 96    output = subprocess.check_output([application, '--help'], universal_newlines=True)
 97    reprog = re.compile(r'(--\S*)\s')
 98    result = []
 99    for line in output.splitlines():
100        m = reprog.search(line)
101        if m:
102            result.append(m.group(1))
103    return result
def assign_prefixed_options(args, allowed_programs):
106def assign_prefixed_options(args, allowed_programs):
107    prefixed_options = {}
108    remaining = []
109    consumed = False
110    for arg_index, arg in enumerate(args):
111        used = False
112        if consumed:
113            consumed = False
114            continue
115        if arg[:2] == '--':
116            separator_index = arg.find('-', 2)
117            if separator_index != -1:
118                program = arg[2:separator_index]
119                if program in allowed_programs:
120                    try:
121                        if '=' in arg:
122                            option = arg[separator_index+1:].split('=')
123                        else:
124                            if '--' in args[arg_index+1]:
125                                raise ValueError()
126                            option = [arg[separator_index+1:], args[arg_index+1]]
127                            consumed = True
128                    except (IndexError, ValueError):
129                        raise ValueError("Please amend prefixed argument %s with a value." % arg)
130                    used = True
131                    prefixed_options.setdefault(program, []).append(option)
132        if not used:
133            remaining.append(arg)
134    return prefixed_options, remaining
def get_prefixed_options(options):
137def get_prefixed_options(options):
138    return options._prefixed_options
class Option(builtins.tuple):

Option(name, value, type, help, category)

Option(name, value, type, help, category)

Create new instance of Option(name, value, type, help, category)

name

Alias for field number 0

value

Alias for field number 1

type

Alias for field number 2

help

Alias for field number 3

category

Alias for field number 4

class OptionReader(xml.sax.handler.ContentHandler):
144class OptionReader(handler.ContentHandler):
145
146    """Reads an option file"""
147
148    def __init__(self):
149        self.opts = []
150
151    def startElement(self, name, attrs):
152        if 'value' in attrs:
153            self.opts.append(Option(name, attrs['value'], attrs.get('type'), attrs.get('help'), attrs.get('category')))

Reads an option file

opts
def startElement(self, name, attrs):
151    def startElement(self, name, attrs):
152        if 'value' in attrs:
153            self.opts.append(Option(name, attrs['value'], attrs.get('type'), attrs.get('help'), attrs.get('category')))

Signals the start of an element in non-namespace mode.

The name parameter contains the raw XML 1.0 name of the element type as a string and the attrs parameter holds an instance of the Attributes class containing the attributes of the element.

def readOptions(filename):
156def readOptions(filename):
157    optionReader = OptionReader()
158    parse(filename, optionReader)
159    return optionReader.opts
class ArgumentParser(argparse.ArgumentParser):
162class ArgumentParser(argparse.ArgumentParser):
163    """Drop-in replacement for argparse.ArgumentParser that adds support for
164    sumo-style config files.
165    Inspired by https://github.com/bw2/ConfigArgParse
166    """
167
168    @staticmethod
169    def time(s):
170        return parseTime(s)
171
172    @staticmethod
173    def file(s):
174        return s
175
176    @staticmethod
177    def file_list(s):
178        return s
179
180    @staticmethod
181    def net_file(s):
182        return s
183
184    @staticmethod
185    def route_file(s):
186        return s
187
188    @staticmethod
189    def route_file_list(s):
190        return s
191
192    @staticmethod
193    def additional_file(s):
194        return s
195
196    @staticmethod
197    def additional_file_list(s):
198        return s
199
200    @staticmethod
201    def edgedata_file(s):
202        return s
203
204    @staticmethod
205    def edge(s):
206        return s
207
208    @staticmethod
209    def edge_list(s):
210        return s
211
212    @staticmethod
213    def data_file(s):
214        # arbitrary data file (i.e. for attributeStats.py and plotXMLAttributes.py)
215        return s
216
217    @staticmethod
218    def sumoconfig_file(s):
219        return s
220
221    @staticmethod
222    def sumoconfig_file_list(s):
223        return s
224
225    def __init__(self, *args, **kwargs):
226        self._allowed_programs = kwargs.pop("allowed_programs", [])
227        self._catch_all = None
228        argparse.ArgumentParser.__init__(self, *args, **kwargs)
229        # add common argument for loading configuration
230        self.add_argument('-c', '--configuration-file', help='read configuration from FILE', metavar="FILE")
231        # add common argument for save configuration
232        self.add_argument('-C', '--save-configuration', help='save configuration to FILE and exit', metavar="FILE")
233        # add common argument for save template
234        self.add_argument('--save-template', help='save configuration template to FILE and exit', metavar="FILE")
235        self._fix_path_args = set()
236
237    def add_argument(self, *args, **kwargs):
238        # due argparse only accept certain values (action, choices, type, help...),
239        #  we need to extract extra parameters before call add_argument
240        fix_path = kwargs.pop("fix_path", False)
241        category = kwargs.pop("category", None)
242        catch_all = kwargs.pop("catch_all", False)
243        # get action
244        action = kwargs.get("action")
245        # parse argument
246        a = argparse.ArgumentParser.add_argument(self, *args, **kwargs)
247        # check if fix path
248        if fix_path:
249            for s in a.option_strings:
250                if s.startswith("--"):
251                    self._fix_path_args.add(s[2:])
252        # set category
253        a.category = category
254        # set if a is a boolean
255        a.boolean = ((action == "store_true") or (action == "store_false"))
256        # the value of a.required is lost during parsing
257        a.isRequired = a.required
258        a.isPositional = args[0][0] != "-"
259        if catch_all:
260            self._catch_all = a
261
262    def add_option(self, *args, **kwargs):
263        """alias for compatibility with OptionParser"""
264        self.add_argument(*args, **kwargs)
265
266    def get_option(self, dest):
267        for action in self._actions:
268            if action.dest == dest:
269                return action
270        return None
271
272    def add_mutually_exclusive_group(self, required=False):
273        group = argparse.ArgumentParser.add_mutually_exclusive_group(self, required=required)
274        group.add_argument = handleCategoryWrapper(self, group.add_argument)
275        return group
276
277    def _write_config_file(self, namespace, toString=False):
278        if namespace.save_configuration:
279            with openz(namespace.save_configuration, "w") as out:
280                self.write_config_to_file(out, namespace, False)
281            sys.exit()
282        if namespace.save_template:
283            with openz(namespace.save_template, "w") as out:
284                self.write_config_to_file(out, namespace, True)
285            sys.exit()
286        if toString:
287            out = io.StringIO()
288            try:
289                self.write_config_to_file(out, namespace, False)
290            except Exception:
291                # python2.7
292                out = io.BytesIO()
293                self.write_config_to_file(out, namespace, False)
294            return out.getvalue()
295
296    def write_config_to_file(self, out, namespace, print_template):
297        out.write(u'<configuration>\n')
298        optionNames = vars(namespace).keys()
299        if sys.version_info.major < 3 or sys.version_info.minor < 6:
300            optionNames = sorted(optionNames)
301        for k in optionNames:
302            v = vars(namespace)[k]
303            if k not in ("save_configuration", "save_template", "configuration_file", "_parser", "_prefixed_options"):
304                key = k
305                help = ''
306                typeStr = ''
307                category = ''
308                required = ''
309                positional = ''
310                listSep = ''
311                for a in self._actions:
312                    if a.dest == k:
313                        for s in a.option_strings:
314                            if s.startswith("--"):
315                                key = s[2:]
316                                break
317                        if print_template:
318                            # default
319                            if a.default is not None:
320                                v = a.default
321                            # help
322                            if a.help is not None:
323                                help = ' help="%s"' % xmlescape(a.help)
324
325                            # note: missing time, filename, list of vehicles, edges and lanes
326                            # category
327                            category = ' category="%s"' % (a.category if a.category is not None else 'processing')
328                            if a.boolean:
329                                typeName = "bool"
330                            elif a.type is None:
331                                typeName = "string"
332                            else:
333                                typeName = a.type.__name__
334                                if typeName == 'parseTime':
335                                    typeName = 'time'
336                                knownTypes = ['bool', 'float', 'int', 'time', 'file',
337                                              'net_file', 'route_file', 'additional_file',
338                                              'additional_file_list',
339                                              'edgedata_file', 'data_file', 'file_list',
340                                              'route_file_list', 'sumoconfig_file',
341                                              'sumoconfig_file_list', 'edge', 'edge_list']
342                                if typeName not in knownTypes:
343                                    typeName = 'string'
344                                elif typeName.endswith("_list"):
345                                    typeName = typeName[:-5]
346                                    listSep = ' listSeparator=","'
347                            typeStr = ' type="%s"' % typeName
348                            if a.isRequired:
349                                required = ' required="true"'
350                            if a.isPositional:
351                                positional = ' positional="true"'
352                            if a.nargs:
353                                listSep = ' listSeparator=" "'
354
355                        break
356                if print_template or v != a.default:
357                    if isinstance(v, list):
358                        v = " ".join(map(str, v))
359                    out.write(u'    <%s value="%s"%s%s%s%s%s%s/>\n' % (
360                              key, xmlescape(v), typeStr, help, category,
361                              required, positional, listSep))
362        out.write(u'</configuration>\n')
363
364    def parse_args(self, args=None, namespace=None):
365        return self.parse_known_args(args, namespace, True)[0]
366
367    def parse_known_args(self, args=None, namespace=None, check_unknown=False):
368        if args is None:
369            args = sys.argv[1:]
370        elif isinstance(args, str):
371            args = args.split()
372        else:
373            # gracefully handle non-string args passed from another script
374            args = list(map(str, args))
375        idx = -1
376        if '-c' in args:
377            idx = args.index('-c') + 1
378        if '--configuration-file' in args:
379            idx = args.index('--configuration-file') + 1
380        if '--save-template' in args:
381            for a in self._actions:
382                a.required = False
383            for g in self._mutually_exclusive_groups:
384                g.required = False
385
386        # add each config item to the commandline unless it's there already
387        config_args = []
388        pos_args = []
389        if idx > 0:
390            act_map = {}
391            pos_map = {}
392            multi_value = set()
393            pos_idx = 0
394            for a in self._actions:
395                for s in a.option_strings:
396                    if s.startswith("--"):
397                        act_map[s[2:]] = a.option_strings
398                        if a.nargs:
399                            multi_value.add(s[2:])
400                if len(a.option_strings) == 0:
401                    pos_map[a.dest] = pos_idx
402                    pos_args.append(None)
403                    pos_idx += 1
404            for cfg_file in args[idx].split(","):
405                for option in readOptions(cfg_file):
406                    is_set = False
407                    for s in act_map.get(option.name, []):
408                        if s in args:
409                            is_set = True
410                            break
411                    value = option.value
412                    if option.name in self._fix_path_args and not value.startswith("http"):
413                        value = os.path.join(os.path.dirname(cfg_file), value)
414                    if option.name in pos_map and option.name != 'remaining_args':
415                        if ',' in value:
416                            value = value.split(',')
417                        else:
418                            value = value.split()
419                        for i, v in enumerate(value):
420                            pos_args[pos_map[option.name]] = v
421                            if i + 1 < len(value):
422                                # shift pos_map
423                                pos_args.append(None)
424                                curPos = pos_map[option.name]
425                                for o, pos in pos_map.items():
426                                    if pos >= curPos:
427                                        pos_map[o] += 1
428
429                    elif not is_set:
430                        if value == "True":
431                            config_args += ["--" + option.name]
432                        elif value != "False":
433                            if option.name == 'remaining_args':
434                                # special case: magic option name to collect remaining arguments
435                                config_args += value.split()
436                            elif option.name in multi_value:
437                                config_args += ["--" + option.name] + value.split()
438                            else:
439                                # permit negative values and empty strings in cfg files
440                                config_args += ["--" + option.name + "=" + value]
441        combined_args = args + config_args + [p for p in pos_args if p is not None]
442        namespace, unknown_args = argparse.ArgumentParser.parse_known_args(
443            self, args=combined_args, namespace=namespace)
444
445        if self._allowed_programs and unknown_args and hasattr(namespace, "remaining_args"):
446            # namespace.remaining_args are the legacy method to parse arguments
447            # for subprograms (i.e. 'duarouter--weights.random-factor 2')
448            # unknown_args are the new style # # ('--duarouter-weights.random-factor 2')
449            # the default ArgumentParser interprets the first parameter for an
450            # unknown argument as remaining_args and this also creates an
451            # invalid error message when unknown options are parsed
452            unknown_args.insert(1, namespace.remaining_args[0])
453            namespace.remaining_args = []
454
455        # print("parse_known_args:\n  args: %s\n  config_args: %s\n  pos_args: %s\n  "
456        #       "combined_args: %s\n  remaining_args: %s\n  unknown_args: %s" %
457        #       (args, config_args, pos_args, combined_args, namespace.remaining_args, unknown_args))
458
459        namespace_as_dict = deepcopy(vars(namespace))
460        namespace._prefixed_options, remaining_args = assign_prefixed_options(unknown_args, self._allowed_programs)
461
462        for program in namespace._prefixed_options:
463            prefixed_options = deepcopy(namespace._prefixed_options[program])
464            for option in prefixed_options:
465                option[0] = program + '-' + option[0]
466            namespace_as_dict.update(dict(prefixed_options))
467
468        if check_unknown and remaining_args:
469            if self._catch_all:
470                setattr(namespace, self._catch_all.dest,
471                        getattr(namespace, self._catch_all.dest) + remaining_args)
472            else:
473                self.error('unrecognized arguments: %s' % ' '.join(remaining_args))
474
475        extended_namespace = argparse.Namespace(**namespace_as_dict)
476        self._write_config_file(extended_namespace)
477        namespace.config_as_string = self._write_config_file(extended_namespace, toString=True)
478        return namespace, remaining_args

Drop-in replacement for argparse.ArgumentParser that adds support for sumo-style config files. Inspired by https://github.com/bw2/ConfigArgParse

ArgumentParser(*args, **kwargs)
225    def __init__(self, *args, **kwargs):
226        self._allowed_programs = kwargs.pop("allowed_programs", [])
227        self._catch_all = None
228        argparse.ArgumentParser.__init__(self, *args, **kwargs)
229        # add common argument for loading configuration
230        self.add_argument('-c', '--configuration-file', help='read configuration from FILE', metavar="FILE")
231        # add common argument for save configuration
232        self.add_argument('-C', '--save-configuration', help='save configuration to FILE and exit', metavar="FILE")
233        # add common argument for save template
234        self.add_argument('--save-template', help='save configuration template to FILE and exit', metavar="FILE")
235        self._fix_path_args = set()
@staticmethod
def time(s):
168    @staticmethod
169    def time(s):
170        return parseTime(s)
@staticmethod
def file(s):
172    @staticmethod
173    def file(s):
174        return s
@staticmethod
def file_list(s):
176    @staticmethod
177    def file_list(s):
178        return s
@staticmethod
def net_file(s):
180    @staticmethod
181    def net_file(s):
182        return s
@staticmethod
def route_file(s):
184    @staticmethod
185    def route_file(s):
186        return s
@staticmethod
def route_file_list(s):
188    @staticmethod
189    def route_file_list(s):
190        return s
@staticmethod
def additional_file(s):
192    @staticmethod
193    def additional_file(s):
194        return s
@staticmethod
def additional_file_list(s):
196    @staticmethod
197    def additional_file_list(s):
198        return s
@staticmethod
def edgedata_file(s):
200    @staticmethod
201    def edgedata_file(s):
202        return s
@staticmethod
def edge(s):
204    @staticmethod
205    def edge(s):
206        return s
@staticmethod
def edge_list(s):
208    @staticmethod
209    def edge_list(s):
210        return s
@staticmethod
def data_file(s):
212    @staticmethod
213    def data_file(s):
214        # arbitrary data file (i.e. for attributeStats.py and plotXMLAttributes.py)
215        return s
@staticmethod
def sumoconfig_file(s):
217    @staticmethod
218    def sumoconfig_file(s):
219        return s
@staticmethod
def sumoconfig_file_list(s):
221    @staticmethod
222    def sumoconfig_file_list(s):
223        return s
def add_argument(self, *args, **kwargs):
237    def add_argument(self, *args, **kwargs):
238        # due argparse only accept certain values (action, choices, type, help...),
239        #  we need to extract extra parameters before call add_argument
240        fix_path = kwargs.pop("fix_path", False)
241        category = kwargs.pop("category", None)
242        catch_all = kwargs.pop("catch_all", False)
243        # get action
244        action = kwargs.get("action")
245        # parse argument
246        a = argparse.ArgumentParser.add_argument(self, *args, **kwargs)
247        # check if fix path
248        if fix_path:
249            for s in a.option_strings:
250                if s.startswith("--"):
251                    self._fix_path_args.add(s[2:])
252        # set category
253        a.category = category
254        # set if a is a boolean
255        a.boolean = ((action == "store_true") or (action == "store_false"))
256        # the value of a.required is lost during parsing
257        a.isRequired = a.required
258        a.isPositional = args[0][0] != "-"
259        if catch_all:
260            self._catch_all = a

add_argument(dest, ..., name=value, ...) add_argument(option_string, option_string, ..., name=value, ...)

def add_option(self, *args, **kwargs):
262    def add_option(self, *args, **kwargs):
263        """alias for compatibility with OptionParser"""
264        self.add_argument(*args, **kwargs)

alias for compatibility with OptionParser

def get_option(self, dest):
266    def get_option(self, dest):
267        for action in self._actions:
268            if action.dest == dest:
269                return action
270        return None
def add_mutually_exclusive_group(self, required=False):
272    def add_mutually_exclusive_group(self, required=False):
273        group = argparse.ArgumentParser.add_mutually_exclusive_group(self, required=required)
274        group.add_argument = handleCategoryWrapper(self, group.add_argument)
275        return group
def write_config_to_file(self, out, namespace, print_template):
296    def write_config_to_file(self, out, namespace, print_template):
297        out.write(u'<configuration>\n')
298        optionNames = vars(namespace).keys()
299        if sys.version_info.major < 3 or sys.version_info.minor < 6:
300            optionNames = sorted(optionNames)
301        for k in optionNames:
302            v = vars(namespace)[k]
303            if k not in ("save_configuration", "save_template", "configuration_file", "_parser", "_prefixed_options"):
304                key = k
305                help = ''
306                typeStr = ''
307                category = ''
308                required = ''
309                positional = ''
310                listSep = ''
311                for a in self._actions:
312                    if a.dest == k:
313                        for s in a.option_strings:
314                            if s.startswith("--"):
315                                key = s[2:]
316                                break
317                        if print_template:
318                            # default
319                            if a.default is not None:
320                                v = a.default
321                            # help
322                            if a.help is not None:
323                                help = ' help="%s"' % xmlescape(a.help)
324
325                            # note: missing time, filename, list of vehicles, edges and lanes
326                            # category
327                            category = ' category="%s"' % (a.category if a.category is not None else 'processing')
328                            if a.boolean:
329                                typeName = "bool"
330                            elif a.type is None:
331                                typeName = "string"
332                            else:
333                                typeName = a.type.__name__
334                                if typeName == 'parseTime':
335                                    typeName = 'time'
336                                knownTypes = ['bool', 'float', 'int', 'time', 'file',
337                                              'net_file', 'route_file', 'additional_file',
338                                              'additional_file_list',
339                                              'edgedata_file', 'data_file', 'file_list',
340                                              'route_file_list', 'sumoconfig_file',
341                                              'sumoconfig_file_list', 'edge', 'edge_list']
342                                if typeName not in knownTypes:
343                                    typeName = 'string'
344                                elif typeName.endswith("_list"):
345                                    typeName = typeName[:-5]
346                                    listSep = ' listSeparator=","'
347                            typeStr = ' type="%s"' % typeName
348                            if a.isRequired:
349                                required = ' required="true"'
350                            if a.isPositional:
351                                positional = ' positional="true"'
352                            if a.nargs:
353                                listSep = ' listSeparator=" "'
354
355                        break
356                if print_template or v != a.default:
357                    if isinstance(v, list):
358                        v = " ".join(map(str, v))
359                    out.write(u'    <%s value="%s"%s%s%s%s%s%s/>\n' % (
360                              key, xmlescape(v), typeStr, help, category,
361                              required, positional, listSep))
362        out.write(u'</configuration>\n')
def parse_args(self, args=None, namespace=None):
364    def parse_args(self, args=None, namespace=None):
365        return self.parse_known_args(args, namespace, True)[0]
def parse_known_args(self, args=None, namespace=None, check_unknown=False):
367    def parse_known_args(self, args=None, namespace=None, check_unknown=False):
368        if args is None:
369            args = sys.argv[1:]
370        elif isinstance(args, str):
371            args = args.split()
372        else:
373            # gracefully handle non-string args passed from another script
374            args = list(map(str, args))
375        idx = -1
376        if '-c' in args:
377            idx = args.index('-c') + 1
378        if '--configuration-file' in args:
379            idx = args.index('--configuration-file') + 1
380        if '--save-template' in args:
381            for a in self._actions:
382                a.required = False
383            for g in self._mutually_exclusive_groups:
384                g.required = False
385
386        # add each config item to the commandline unless it's there already
387        config_args = []
388        pos_args = []
389        if idx > 0:
390            act_map = {}
391            pos_map = {}
392            multi_value = set()
393            pos_idx = 0
394            for a in self._actions:
395                for s in a.option_strings:
396                    if s.startswith("--"):
397                        act_map[s[2:]] = a.option_strings
398                        if a.nargs:
399                            multi_value.add(s[2:])
400                if len(a.option_strings) == 0:
401                    pos_map[a.dest] = pos_idx
402                    pos_args.append(None)
403                    pos_idx += 1
404            for cfg_file in args[idx].split(","):
405                for option in readOptions(cfg_file):
406                    is_set = False
407                    for s in act_map.get(option.name, []):
408                        if s in args:
409                            is_set = True
410                            break
411                    value = option.value
412                    if option.name in self._fix_path_args and not value.startswith("http"):
413                        value = os.path.join(os.path.dirname(cfg_file), value)
414                    if option.name in pos_map and option.name != 'remaining_args':
415                        if ',' in value:
416                            value = value.split(',')
417                        else:
418                            value = value.split()
419                        for i, v in enumerate(value):
420                            pos_args[pos_map[option.name]] = v
421                            if i + 1 < len(value):
422                                # shift pos_map
423                                pos_args.append(None)
424                                curPos = pos_map[option.name]
425                                for o, pos in pos_map.items():
426                                    if pos >= curPos:
427                                        pos_map[o] += 1
428
429                    elif not is_set:
430                        if value == "True":
431                            config_args += ["--" + option.name]
432                        elif value != "False":
433                            if option.name == 'remaining_args':
434                                # special case: magic option name to collect remaining arguments
435                                config_args += value.split()
436                            elif option.name in multi_value:
437                                config_args += ["--" + option.name] + value.split()
438                            else:
439                                # permit negative values and empty strings in cfg files
440                                config_args += ["--" + option.name + "=" + value]
441        combined_args = args + config_args + [p for p in pos_args if p is not None]
442        namespace, unknown_args = argparse.ArgumentParser.parse_known_args(
443            self, args=combined_args, namespace=namespace)
444
445        if self._allowed_programs and unknown_args and hasattr(namespace, "remaining_args"):
446            # namespace.remaining_args are the legacy method to parse arguments
447            # for subprograms (i.e. 'duarouter--weights.random-factor 2')
448            # unknown_args are the new style # # ('--duarouter-weights.random-factor 2')
449            # the default ArgumentParser interprets the first parameter for an
450            # unknown argument as remaining_args and this also creates an
451            # invalid error message when unknown options are parsed
452            unknown_args.insert(1, namespace.remaining_args[0])
453            namespace.remaining_args = []
454
455        # print("parse_known_args:\n  args: %s\n  config_args: %s\n  pos_args: %s\n  "
456        #       "combined_args: %s\n  remaining_args: %s\n  unknown_args: %s" %
457        #       (args, config_args, pos_args, combined_args, namespace.remaining_args, unknown_args))
458
459        namespace_as_dict = deepcopy(vars(namespace))
460        namespace._prefixed_options, remaining_args = assign_prefixed_options(unknown_args, self._allowed_programs)
461
462        for program in namespace._prefixed_options:
463            prefixed_options = deepcopy(namespace._prefixed_options[program])
464            for option in prefixed_options:
465                option[0] = program + '-' + option[0]
466            namespace_as_dict.update(dict(prefixed_options))
467
468        if check_unknown and remaining_args:
469            if self._catch_all:
470                setattr(namespace, self._catch_all.dest,
471                        getattr(namespace, self._catch_all.dest) + remaining_args)
472            else:
473                self.error('unrecognized arguments: %s' % ' '.join(remaining_args))
474
475        extended_namespace = argparse.Namespace(**namespace_as_dict)
476        self._write_config_file(extended_namespace)
477        namespace.config_as_string = self._write_config_file(extended_namespace, toString=True)
478        return namespace, remaining_args
def handleCategoryWrapper(parser, func):
481def handleCategoryWrapper(parser, func):
482    @wraps(func)
483    def inner(*args, **kwargs):
484        # remove category from arguments and set in result
485        category = kwargs.pop("category", None)
486        fix_path = kwargs.pop("fix_path", True)
487        result = func(*args, **kwargs)
488        if fix_path:
489            for s in result.option_strings:
490                if s.startswith("--"):
491                    parser._fix_path_args.add(s[2:])
492        result.category = category
493        # set if is a boolean
494        action = kwargs.get("action")
495        result.boolean = ((action == "store_true") or (action == "store_false"))
496        result.isRequired = kwargs.get("required", False)
497        result.isPositional = args[0][0] != "-"
498        return result
499    return inner
class SplitAction(argparse.Action):
502class SplitAction(argparse.Action):
503    def __call__(self, parser, args, values, option_string=None):
504        if len(values) == 1:
505            values = [float(x) for x in values[0].split(',')]
506        else:
507            values = [float(x) for x in values]
508        setattr(args, self.dest, values)

Information about how to convert command line strings to Python objects.

Action objects are used by an ArgumentParser to represent the information needed to parse a single argument from one or more strings from the command line. The keyword arguments to the Action constructor are also all attributes of Action instances.

Keyword Arguments:

- option_strings -- A list of command-line option strings which
    should be associated with this action.

- dest -- The name of the attribute to hold the created object(s)

- nargs -- The number of command-line arguments that should be
    consumed. By default, one argument will be consumed and a single
    value will be produced.  Other values include:
        - N (an integer) consumes N arguments (and produces a list)
        - '?' consumes zero or one arguments
        - '*' consumes zero or more arguments (and produces a list)
        - '+' consumes one or more arguments (and produces a list)
    Note that the difference between the default and nargs=1 is that
    with the default, a single value will be produced, while with
    nargs=1, a list containing a single value will be produced.

- const -- The value to be produced if the option is specified and the
    option uses an action that takes no values.

- default -- The value to be produced if the option is not specified.

- type -- A callable that accepts a single string argument, and
    returns the converted value.  The standard Python types str, int,
    float, and complex are useful examples of such callables.  If None,
    str is used.

- choices -- A container of values that should be allowed. If not None,
    after a command-line argument has been converted to the appropriate
    type, an exception will be raised if it is not a member of this
    collection.

- required -- True if the action must always be specified at the
    command line. This is only meaningful for optional command-line
    arguments.

- help -- The help string describing the argument.

- metavar -- The name to be used for the option's argument with the
    help string. If None, the 'dest' value will be used as the name.