Package command_system :: Module mapping
[hide private]
[frames] | no frames]

Source Code for Module command_system.mapping

  1  # Copyright (C) 2009-2010  Alexander Cherniuk <ts33kr@gmail.com> 
  2  # 
  3  # This program is free software: you can redistribute it and/or modify 
  4  # it under the terms of the GNU General Public License as published by 
  5  # the Free Software Foundation, either version 3 of the License, or 
  6  # (at your option) any later version. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU General Public License 
 14  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 15   
 16  """ 
 17  The module contains routines to parse command arguments and map them to 
 18  the command handler's positonal and keyword arguments. 
 19   
 20  Mapping is done in two stages: 1) parse arguments into positional 
 21  arguments and options; 2) adapt them to the specific command handler 
 22  according to the command properties. 
 23  """ 
 24   
 25  import re 
 26  from types import BooleanType, UnicodeType 
 27  from operator import itemgetter 
 28   
 29  from errors import DefinitionError, CommandError 
 30   
 31  # Quite complex piece of regular expression logic to parse options and 
 32  # arguments. Might need some tweaking along the way. 
 33  ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)') 
 34  OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') 
 35   
 36  # Option keys needs to be encoded to a specific encoding as Python does 
 37  # not allow to expand dictionary with raw unicode strings as keys from a 
 38  # **kwargs. 
 39  KEY_ENCODING = 'UTF-8' 
 40   
 41  # Defines how complete representation of command usage (generated based 
 42  # on command handler argument specification) will be rendered. 
 43  USAGE_PATTERN = 'Usage: %s %s' 
 44   
45 -def parse_arguments(arguments):
46 """ 47 Simple yet effective and sufficient in most cases parser which 48 parses command arguments and returns them as two lists. 49 50 First list represents positional arguments as (argument, position), 51 and second representing options as (key, value, position) tuples, 52 where position is a (start, end) span tuple of where it was found in 53 the string. 54 55 Options may be given in --long or -short format. As --option=value 56 or --option value or -option value. Keys without values will get 57 None as value. 58 59 Arguments and option values that contain spaces may be given as 'one 60 two three' or "one two three"; that is between single or double 61 quotes. 62 """ 63 args, opts = [], [] 64 65 def intersects_opts((given_start, given_end)): 66 """ 67 Check if given span intersects with any of options. 68 """ 69 for key, value, (start, end) in opts: 70 if given_start >= start and given_end <= end: 71 return True 72 return False
73 74 def intersects_args((given_start, given_end)): 75 """ 76 Check if given span intersects with any of arguments. 77 """ 78 for arg, (start, end) in args: 79 if given_start >= start and given_end <= end: 80 return True 81 return False 82 83 for match in re.finditer(OPT_PATTERN, arguments): 84 if match: 85 key = match.group('key') 86 value = match.group('value') or None 87 position = match.span() 88 opts.append((key, value, position)) 89 90 for match in re.finditer(ARG_PATTERN, arguments): 91 if match: 92 body = match.group('body') 93 position = match.span() 94 args.append((body, position)) 95 96 # Primitive but sufficiently effective way of disposing of 97 # conflicted sectors. Remove any arguments that intersect with 98 # options. 99 for arg, position in args[:]: 100 if intersects_opts(position): 101 args.remove((arg, position)) 102 103 # Primitive but sufficiently effective way of disposing of 104 # conflicted sectors. Remove any options that intersect with 105 # arguments. 106 for key, value, position in opts[:]: 107 if intersects_args(position): 108 opts.remove((key, value, position)) 109 110 return args, opts 111
112 -def adapt_arguments(command, arguments, args, opts):
113 """ 114 Adapt args and opts got from the parser to a specific handler by 115 means of arguments specified on command definition. That is 116 transform them to *args and **kwargs suitable for passing to a 117 command handler. 118 119 Dashes (-) in the option names will be converted to underscores. So 120 you can map --one-more-option to a one_more_option=None. 121 122 If the initial value of a keyword argument is a boolean (False in 123 most cases) - then this option will be treated as a switch, that is 124 an option which does not take an argument. If a switch is followed 125 by an argument - then this argument will be treated just like a 126 normal positional argument. 127 """ 128 spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification() 129 norm_kwargs = dict(spec_kwargs) 130 131 # Quite complex piece of neck-breaking logic to extract raw 132 # arguments if there is more, then one positional argument specified 133 # by the command. In case if it's just one argument which is the 134 # collector - this is fairly easy. But when it's more then one 135 # argument - the neck-breaking logic of how to retrieve residual 136 # arguments as a raw, all in one piece string, kicks in. 137 if command.raw: 138 if arguments: 139 spec_fix = 1 if command.source else 0 140 spec_len = len(spec_args) - spec_fix 141 arguments_end = len(arguments) - 1 142 143 # If there are any optional arguments given they should be 144 # either an unquoted postional argument or part of the raw 145 # argument. So we find all optional arguments that can 146 # possibly be unquoted argument and append them as is to the 147 # args. 148 for key, value, (start, end) in opts[:spec_len]: 149 if value: 150 end -= len(value) + 1 151 args.append((arguments[start:end], (start, end))) 152 args.append((value, (end, end + len(value) + 1))) 153 else: 154 args.append((arguments[start:end], (start, end))) 155 156 # We need in-place sort here because after manipulations 157 # with options order of arguments might be wrong and we just 158 # can't have more complex logic to not let that happen. 159 args.sort(key=itemgetter(1)) 160 161 if spec_len > 1: 162 try: 163 stopper, (start, end) = args[spec_len - 2] 164 except IndexError: 165 raise CommandError("Missing arguments", command) 166 167 # The essential point of the whole play. After 168 # boundaries are being determined (supposingly correct) 169 # we separate raw part from the rest of arguments, which 170 # should be normally processed. 171 raw = arguments[end:] 172 raw = raw.strip() or None 173 174 if not raw and not command.empty: 175 raise CommandError("Missing arguments", command) 176 177 # Discard residual arguments and all of the options as 178 # raw command does not support options and if an option 179 # is given it is rather a part of a raw argument. 180 args = args[:spec_len - 1] 181 opts = [] 182 183 args.append((raw, (end, arguments_end))) 184 else: 185 # Substitue all of the arguments with only one, which 186 # contain raw and unprocessed arguments as a string. And 187 # discard all the options, as raw command does not 188 # support them. 189 args = [(arguments, (0, arguments_end))] 190 opts = [] 191 else: 192 if command.empty: 193 args.append((None, (0, 0))) 194 else: 195 raise CommandError("Missing arguments", command) 196 197 # The first stage of transforming options we have got to a format 198 # that can be used to associate them with declared keyword 199 # arguments. Substituting dashes (-) in their names with 200 # underscores (_). 201 for index, (key, value, position) in enumerate(opts): 202 if '-' in key: 203 opts[index] = (key.replace('-', '_'), value, position) 204 205 # The second stage of transforming options to an associatable state. 206 # Expanding short, one-letter options to a verbose ones, if 207 # corresponding optin has been given. 208 if command.expand: 209 expanded = [] 210 for spec_key, spec_value in norm_kwargs.iteritems(): 211 letter = spec_key[0] if len(spec_key) > 1 else None 212 if letter and letter not in expanded: 213 for index, (key, value, position) in enumerate(opts): 214 if key == letter: 215 expanded.append(letter) 216 opts[index] = (spec_key, value, position) 217 break 218 219 # Detect switches and set their values accordingly. If any of them 220 # carries a value - append it to args. 221 for index, (key, value, position) in enumerate(opts): 222 if isinstance(norm_kwargs.get(key), BooleanType): 223 opts[index] = (key, True, position) 224 if value: 225 args.append((value, position)) 226 227 # Sorting arguments and options (just to be sure) in regarding to 228 # their positions in the string. 229 args.sort(key=itemgetter(1)) 230 opts.sort(key=itemgetter(2)) 231 232 # Stripping down position information supplied with arguments and 233 # options as it won't be needed again. 234 args = map(lambda (arg, position): arg, args) 235 opts = map(lambda (key, value, position): (key, value), opts) 236 237 # If command has extra option enabled - collect all extra arguments 238 # and pass them to a last positional argument command defines as a 239 # list. 240 if command.extra: 241 if not var_args: 242 spec_fix = 1 if not command.source else 2 243 spec_len = len(spec_args) - spec_fix 244 extra = args[spec_len:] 245 args = args[:spec_len] 246 args.append(extra) 247 else: 248 raise DefinitionError("Can not have both, extra and *args") 249 250 # Detect if positional arguments overlap keyword arguments. If so 251 # and this is allowed by command options - then map them directly to 252 # their options, so they can get propert further processings. 253 spec_fix = 1 if command.source else 0 254 spec_len = len(spec_args) - spec_fix 255 if len(args) > spec_len: 256 if command.overlap: 257 overlapped = args[spec_len:] 258 args = args[:spec_len] 259 for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs): 260 opts.append((spec_key, arg)) 261 else: 262 raise CommandError("Excessive arguments", command) 263 264 # Detect every switch and ensure it will not receive any arguments. 265 # Normally this does not happen unless overlapping is enabled. 266 for key, value in opts: 267 initial = norm_kwargs.get(key) 268 if isinstance(initial, BooleanType): 269 if not isinstance(value, BooleanType): 270 raise CommandError("%s: Switch can not take an argument" % key, command) 271 272 # We need to encode every keyword argument to a simple string, not 273 # the unicode one, because ** expansion does not support it. 274 for index, (key, value) in enumerate(opts): 275 if isinstance(key, UnicodeType): 276 opts[index] = (key.encode(KEY_ENCODING), value) 277 278 # Inject the source arguments as a string as a first argument, if 279 # command has enabled the corresponding option. 280 if command.source: 281 args.insert(0, arguments) 282 283 # Return *args and **kwargs in the form suitable for passing to a 284 # command handler and being expanded. 285 return tuple(args), dict(opts)
286
287 -def generate_usage(command, complete=True):
288 """ 289 Extract handler's arguments specification and wrap them in a 290 human-readable format usage information. If complete is given - then 291 USAGE_PATTERN will be used to render the specification completly. 292 """ 293 spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification() 294 295 # Remove some special positional arguments from the specifiaction, 296 # but store their names so they can be used for usage info 297 # generation. 298 sp_source = spec_args.pop(0) if command.source else None 299 sp_extra = spec_args.pop() if command.extra else None 300 301 kwargs = [] 302 letters = [] 303 304 for key, value in spec_kwargs: 305 letter = key[0] 306 key = key.replace('_', '-') 307 308 if isinstance(value, BooleanType): 309 value = str() 310 else: 311 value = '=%s' % value 312 313 if letter not in letters: 314 kwargs.append('-(-%s)%s%s' % (letter, key[1:], value)) 315 letters.append(letter) 316 else: 317 kwargs.append('--%s%s' % (key, value)) 318 319 usage = str() 320 args = str() 321 322 if command.raw: 323 spec_len = len(spec_args) - 1 324 if spec_len: 325 args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' ' 326 args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1] 327 else: 328 if spec_args: 329 args += '<%s>' % ', '.join(spec_args) 330 if var_args or sp_extra: 331 args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra) 332 333 usage += args 334 335 if kwargs or var_kwargs: 336 if kwargs: 337 usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs) 338 if var_kwargs: 339 usage += (' ' if args else str()) + '[[%s]]' % var_kwargs 340 341 # Native name will be the first one if it is included. Otherwise, 342 # names will be in the order they were specified. 343 if len(command.names) > 1: 344 names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:])) 345 else: 346 names = command.first_name 347 348 return USAGE_PATTERN % (names, usage) if complete else usage
349