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)
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
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.
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.
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
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
Option(name, value, type, help, category)
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
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.
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
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()
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, ...)
262 def add_option(self, *args, **kwargs): 263 """alias for compatibility with OptionParser""" 264 self.add_argument(*args, **kwargs)
alias for compatibility with OptionParser
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')
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
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
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.