Package qm :: Module extension
[hide private]
[frames] | no frames]

Source Code for Module qm.extension

  1  ######################################################################## 
  2  # 
  3  # File:   extension.py 
  4  # Author: Mark Mitchell 
  5  # Date:   07/31/2002 
  6  # 
  7  # Contents: 
  8  #   Extension 
  9  # 
 10  # Copyright (c) 2002, 2003 by CodeSourcery, LLC.  All rights reserved.  
 11  # 
 12  ######################################################################## 
 13   
 14  ######################################################################## 
 15  # Imports 
 16  ######################################################################## 
 17   
 18  import os.path 
 19  import qm 
 20  from qm.fields import Field 
 21  import StringIO 
 22  import tokenize 
 23  import xml 
 24   
 25  ######################################################################## 
 26  # Classes 
 27  ######################################################################## 
 28   
29 -class Extension(object):
30 """A class derived from 'Extension' is a QM extension. 31 32 A variety of different classes are derived from 'Extension'. All 33 of these classes can be derived from by users to produce 34 customized QM extensions. 35 36 'Extension' is an abstract class.""" 37
38 - class Type(type):
39
40 - def __init__(cls, name, bases, dict):
41 """Generate an '_argument_dictionary' holding all the 42 'Field' objects. Then replace 'Field' objects by their 43 values for convenient use inside the code.""" 44 45 # List all base classes that are themselves of type Extension. 46 # 47 # 'Extension' isn't known at this point so all we can do to find 48 # Extension base classes is test whether __metaclass__ is defined 49 # and if so, whether it is a base class of the metaclass of cls. 50 type = cls.__metaclass__ 51 def is_extension(base): 52 metaclass = getattr(base, '__metaclass__', None) 53 return metaclass and issubclass(type, metaclass)
54 55 hierarchy = [base for base in bases if is_extension(base)] 56 57 parameters = {} 58 for c in hierarchy: 59 parameters.update(c._argument_dictionary) 60 # Now set parameters from class variables of type 'Field'. 61 for key, field in dict.iteritems(): 62 if isinstance(field, Field): 63 field.SetName(key) 64 parameters[key] = field 65 66 # For backward compatibility, inject all members of the 67 # 'arguments' list into the dict, if it is indeed a list of fields. 68 arguments = dict.get('arguments', []) 69 if (type(arguments) is list and 70 len(arguments) > 0 and 71 isinstance(arguments[0], Field)): 72 for field in arguments: 73 # Only allow name collisions between arguments and 74 # class variables if _allow_arg_names_matching_class_vars 75 # evaluates to True. 76 if (hasattr(cls, field.GetName()) 77 and not cls._argument_dictionary.has_key(field.GetName()) 78 and not cls._allow_arg_names_matching_class_vars): 79 raise qm.common.QMException, \ 80 qm.error("ext arg name matches class var", 81 class_name = name, 82 argument_name = field.GetName()) 83 parameters[field.GetName()] = field 84 85 setattr(cls, '_argument_dictionary', parameters) 86 setattr(cls, '_argument_list', parameters.values()) 87 88 # Finally set default values. 89 for i in parameters: 90 setattr(cls, i, parameters[i].GetDefaultValue())
91 92 __metaclass__ = Type 93 94 arguments = [] 95 """A list of the arguments to the extension class. 96 97 Each element of this list should be an instance of 'Field'. The 98 'Field' instance describes the argument. 99 100 Derived classes may redefine this class variable. However, 101 derived classes should not explicitly include the arguments from 102 base classes; QMTest will automatically combine all the arguments 103 found throughout the class hierarchy.""" 104 105 kind = None 106 """A string giving kind of extension is implemented by the class. 107 108 This field is used in an application-specific way; for example, 109 QMTest has 'test' and 'target' extension classes.""" 110 111 _argument_list = None 112 """A list of all the 'Field's in this class. 113 114 This list combines the complete list of 'arguments'. 'Field's 115 appear in the order reached by a pre-order breadth-first traversal 116 of the hierarchy, starting from the most derived class.""" 117 118 _argument_dictionary = None 119 """A map from argument names to 'Field' instances. 120 121 A map from the names of arguments for this class to the 122 corresponding 'Field'.""" 123 124 _allow_arg_names_matching_class_vars = None 125 """True if it is OK for fields to have the same name as class variables. 126 127 If this variable is set to true, it is OK for the 'arguments' to 128 contain a field whose name is the same as a class variable. That 129 makes the 'default_value' handling for fields fail, and is 130 generally confusing. 131 132 This module no longer allows such classes, unless this variable is 133 set to true. That permits legacy extension classes to continue 134 working, while preventing new extension classes from making the 135 same mistake.""" 136 137
138 - def __init__(self, **args):
139 """Construct a new 'Extension'. 140 141 'args': Keyword arguments providing values for Extension parameters. 142 The values should be appropriate for the corresponding fields. 143 Derived classes must pass along any unrecognized keyword 144 arguments to this method so that additional arguments 145 can be added in the future without necessitating changes to 146 derived classes. 147 148 This method will place all of the arguments into this objects 149 instance dictionary. 150 151 Derived classes may override this method, but should call this 152 method during their processing.""" 153 154 # Make sure that all the arguments actually correspond to 155 # 'Field's for this class. 156 if __debug__: 157 dictionary = get_class_arguments_as_dictionary(self.__class__) 158 for a, v in args.items(): 159 if not dictionary.has_key(a): 160 raise AttributeError, a 161 162 # Remember the arguments provided. 163 self.__dict__.update(args)
164
165 - def __getattr__(self, name):
166 167 # Perhaps a default value for a class argument should be used. 168 field = get_class_arguments_as_dictionary(self.__class__).get(name) 169 if field is None: 170 raise AttributeError, name 171 return field.GetDefaultValue()
172 173
174 - def GetClassName(self):
175 """Return the name of the extension class. 176 177 returns -- A string giving the name of this etension class.""" 178 179 return get_extension_class_name(self.__class__)
180 181
182 - def GetExplicitArguments(self):
183 """Return the arguments to this extension instance. 184 185 returns -- A dictionary mapping argument names to their 186 values. Computed arguments are ommitted from the 187 dictionary.""" 188 189 # Get all of the arguments. 190 arguments = get_class_arguments_as_dictionary(self.__class__) 191 # Determine which subset of the 'arguments' have been set 192 # explicitly. 193 explicit_arguments = {} 194 for name, field in arguments.items(): 195 # Do not record computed fields. 196 if field.IsComputed(): 197 continue 198 if self.__dict__.has_key(name): 199 explicit_arguments[name] = self.__dict__[name] 200 201 return explicit_arguments
202 203
204 - def MakeDomElement(self, document, element = None):
205 """Create a DOM node for 'self'. 206 207 'document' -- The DOM document that will contain the new 208 element. 209 210 'element' -- If not 'None' the extension element to which items 211 will be added. Otherwise, a new element will be created by this 212 function. 213 214 returns -- A new DOM element corresponding to an instance of the 215 extension class. The caller is responsible for attaching it to 216 the 'document'.""" 217 218 return make_dom_element(self.__class__, 219 self.GetExplicitArguments(), 220 document, element)
221 222
223 - def MakeDomDocument(self):
224 """Create a DOM document for 'self'. 225 226 'extension_class' -- A class derived from 'Extension'. 227 228 'arguments' -- The arguments to the extension class. 229 230 returns -- A new DOM document corresponding to an instance of the 231 extension class.""" 232 233 document = qm.xmlutil.create_dom_document( 234 public_id = "Extension", 235 document_element_tag = "extension" 236 ) 237 self.MakeDomElement(document, document.documentElement) 238 return document
239 240
241 - def Write(self, file):
242 """Write an XML description of 'self' to a file. 243 244 'file' -- A file object to which the data should be written.""" 245 246 document = self.MakeDomDocument() 247 document.writexml(file)
248 249 250 251 ######################################################################## 252 # Functions 253 ######################################################################## 254
255 -def get_class_arguments(extension_class):
256 """Return the arguments associated with 'extension_class'. 257 258 'extension_class' -- A class derived from 'Extension'. 259 260 returns -- A list of 'Field' objects containing all of the 261 arguments in the class hierarchy.""" 262 263 assert issubclass(extension_class, Extension) 264 return extension_class._argument_list
265 266
267 -def get_class_arguments_as_dictionary(extension_class):
268 """Return the arguments associated with 'extension_class'. 269 270 'extension_class' -- A class derived from 'Extension'. 271 272 returns -- A dictionary mapping argument names to 'Field' 273 objects. The dictionary contains all of the arguments in the 274 class hierarchy.""" 275 276 assert issubclass(extension_class, Extension) 277 return extension_class._argument_dictionary
278 279
280 -def get_class_description(extension_class, brief=0):
281 """Return a brief description of the extension class 'extension_class'. 282 283 'extension_class' -- A class derived from 'Extension'. 284 285 'brief' -- If true, return a brief (one-line) description of the 286 extension class. 287 288 returns -- A structured text description of 'extension_class'.""" 289 290 assert issubclass(extension_class, Extension) 291 292 # Extract the class's doc string. 293 doc_string = extension_class.__doc__ 294 if doc_string is not None: 295 if brief: 296 doc_string = qm.structured_text.get_first(doc_string) 297 return doc_string 298 else: 299 return " "
300 301
302 -def get_extension_class_name(extension_class):
303 """Return the name of 'extension_class'. 304 305 'extension_class' -- A class derived from 'Extension'. 306 307 returns -- The name of 'extension_class'. This is the name that 308 is used when users refer to the class.""" 309 310 assert issubclass(extension_class, Extension) 311 312 module = extension_class.__module__.split(".")[-1] 313 return module + "." + extension_class.__name__
314 315
316 -def validate_arguments(extension_class, arguments):
317 """Validate the 'arguments' to the 'extension_class'. 318 319 'extension_class' -- A class derived from 'Extension'. 320 321 'arguments' -- A dictionary mapping argument names (strings) to 322 values (strings). 323 324 returns -- A dictionary mapping 'Field's to values. 325 326 Check that each of the 'arguments' is a valid argument to 327 'extension_class'. If so, the argumets are converted as required 328 by the 'Field', and the dictionary returned contains the converted 329 values. Otherwise, an exception is raised.""" 330 331 assert issubclass(extension_class, Extension) 332 333 # We have not converted any arguments yet. 334 converted_arguments = {} 335 336 # Check that there are no arguments that do not apply to this 337 # class. 338 class_arguments = get_class_arguments_as_dictionary(extension_class) 339 for name, value in arguments.items(): 340 field = class_arguments.get(name) 341 if not field: 342 raise qm.QMException, \ 343 qm.error("unexpected extension argument", 344 name = name, 345 class_name \ 346 = get_extension_class_name(extension_class)) 347 if field.IsComputed(): 348 raise qm.QMException, \ 349 qm.error("value provided for computed field", 350 name = name, 351 class_name \ 352 = get_extension_class_name(extension_class)) 353 converted_arguments[name] = field.ParseTextValue(value) 354 355 return converted_arguments
356 357
358 -def make_dom_element(extension_class, arguments, document, element = None):
359 """Create a DOM node for an instance of 'extension_class'. 360 361 'extension_class' -- A class derived from 'Extension'. 362 363 'arguments' -- The arguments to the extension class. 364 365 'document' -- The DOM document that will contain the new 366 element. 367 368 'element' -- If not 'None' the extension element to which items 369 will be added. Otherwise, a new element will be created by this 370 function. 371 372 returns -- A new DOM element corresponding to an instance of the 373 extension class. The caller is responsible for attaching it to 374 the 'document'.""" 375 376 # Get the dictionary of 'Field's for this extension class. 377 field_dictionary = get_class_arguments_as_dictionary(extension_class) 378 379 # Create the element. 380 if element: 381 extension_element = element 382 else: 383 extension_element = document.createElement("extension") 384 # Create an attribute describing the kind of extension. 385 if extension_class.kind: 386 extension_element.setAttribute("kind", extension_class.kind) 387 # Create an attribute naming the extension class. 388 extension_element.setAttribute("class", 389 get_extension_class_name(extension_class)) 390 # Create an element for each of the arguments. 391 for argument_name, value in arguments.items(): 392 # Skip computed arguments. 393 field = field_dictionary[argument_name] 394 if field.IsComputed(): 395 continue 396 # Create a node for the argument. 397 argument_element = document.createElement("argument") 398 # Store the name of the field. 399 argument_element.setAttribute("name", argument_name) 400 # Store the value. 401 argument_element.appendChild(field.MakeDomNodeForValue(value, 402 document)) 403 # Add the attribute node to the target. 404 extension_element.appendChild(argument_element) 405 406 return extension_element
407 408
409 -def make_dom_document(extension_class, arguments):
410 """Create a DOM document for an instance of 'extension_class'. 411 412 'extension_class' -- A class derived from 'Extension'. 413 414 'arguments' -- The arguments to the extension class. 415 416 returns -- A new DOM document corresponding to an instance of the 417 extension class.""" 418 419 document = qm.xmlutil.create_dom_document( 420 public_id = "Extension", 421 document_element_tag = "extension" 422 ) 423 make_dom_element(extension_class, arguments, document, 424 document.documentElement) 425 return document
426 427 428
429 -def write_extension_file(extension_class, arguments, file):
430 """Write an XML description of an extension to 'file'. 431 432 'extension_class' -- A class derived from 'Extension'. 433 434 'arguments' -- A dictionary mapping argument names to values. 435 436 'file' -- A file object to which the data should be written.""" 437 438 document = make_dom_document(extension_class, arguments) 439 document.writexml(file)
440 441 442
443 -def parse_dom_element(element, class_loader, attachment_store = None):
444 """Parse a DOM node representing an instance of 'Extension'. 445 446 'element' -- A DOM node, of the format created by 447 'make_dom_element'. 448 449 'class_loader' -- A callable. The callable will be passed the 450 name of the extension class and must return the actual class 451 object. 452 453 'attachment_store' -- The 'AttachmentStore' in which attachments 454 can be found. 455 456 returns -- A pair ('extension_class', 'arguments') containing the 457 extension class (a class derived from 'Extension') and the 458 arguments (a dictionary mapping names to values) stored in the 459 'element'.""" 460 461 # Determine the name of the extension class. 462 class_name = element.getAttribute("class") 463 # DOM nodes created by earlier versions of QMTest encoded the 464 # class name in a separate element, so look there for backwards 465 # compatbility. 466 if not class_name: 467 class_elements = element.getElementsByTagName("class") 468 if not class_elements: 469 class_elements = element.getElementsByTagName("class-name") 470 class_element = class_elements[0] 471 class_name = qm.xmlutil.get_dom_text(class_element) 472 # Load it. 473 extension_class = class_loader(class_name) 474 475 # Get the dictionary of 'Field's for this extension class. 476 field_dictionary = get_class_arguments_as_dictionary(extension_class) 477 478 # Collect the arguments to the extension class. 479 arguments = {} 480 for argument_element in element.getElementsByTagName("argument"): 481 name = argument_element.getAttribute("name") 482 # Find the corresponding 'Field'. 483 field = field_dictionary[name] 484 # Get the DOM node for the value. It is always a element. 485 value_node \ 486 = filter(lambda e: e.nodeType == xml.dom.Node.ELEMENT_NODE, 487 argument_element.childNodes)[0] 488 # Parse the value. 489 value = field.GetValueFromDomNode(value_node, attachment_store) 490 # Python does not allow keyword arguments to have Unicode 491 # values, so we convert the name to an ordinary string. 492 arguments[str(name)] = value 493 494 return (extension_class, arguments)
495 496
497 -def read_extension_file(file, class_loader, attachment_store = None):
498 """Parse a file describing an extension instance. 499 500 'file' -- A file-like object from which the extension instance 501 will be read. 502 503 'class_loader' -- A callable. The callable will be passed the 504 name of the extension class and must return the actual class 505 object. 506 507 'attachment_store' -- The 'AttachmentStore' in which attachments 508 can be found. 509 510 returns -- A pair ('extension_class', 'arguments') containing the 511 extension class (a class derived from 'Extension') and the 512 arguments (a dictionary mapping names to values) stored in the 513 'element'.""" 514 515 document = qm.xmlutil.load_xml(file) 516 return parse_dom_element(document.documentElement, 517 class_loader, 518 attachment_store)
519 520
521 -def parse_descriptor(descriptor, class_loader, extension_loader = None):
522 """Parse a descriptor representing an instance of 'Extension'. 523 524 'descriptor' -- A string representing an instance of 'Extension'. 525 The 'descriptor' has the form 'class(arg1 = "val1", arg2 = "val2", 526 ...)'. The arguments and the parentheses are optional. 527 528 'class_loader' -- A callable that, when passed the name of the 529 extension class, will return the actual Python class object. 530 531 'extension_loader' -- A callable that loads an existing extension 532 given the name of that extension and returns a tuple '(class, 533 arguments)' where 'class' is a class derived from 'Extension'. If 534 'extension_loader' is 'None', or if the 'class' returned is 535 'None', then if a file exists named 'class', the extension is read 536 from 'class' as XML. Any arguments returned by the extension 537 loader or read from the file system are overridden by the 538 arguments explicitly provided in the descriptor. 539 540 returns -- A pair ('extension_class', 'arguments') containing the 541 extension class (a class derived from 'Extension') and the 542 arguments (a dictionary mapping names to values) stored in the 543 'element'. The 'arguments' will have already been processed by 544 'validate_arguments' by the time they are returned.""" 545 546 # Look for the opening parenthesis. 547 open_paren = descriptor.find('(') 548 if open_paren == -1: 549 # If there is no opening parenthesis, the descriptor is simply 550 # the name of an extension class. 551 class_name = descriptor 552 else: 553 # The class name is the part of the descriptor up to the 554 # parenthesis. 555 class_name = descriptor[:open_paren] 556 557 # Load the extension, if it already exists. 558 extension_class = None 559 if extension_loader: 560 extension = extension_loader(class_name) 561 if extension: 562 extension_class = extension.__class__ 563 orig_arguments = extension.GetExplicitArguments() 564 if not extension_class: 565 if os.path.exists(class_name): 566 extension_class, orig_arguments \ 567 = read_extension_file(open(filename), class_loader) 568 else: 569 extension_class = class_loader(class_name) 570 orig_arguments = {} 571 572 arguments = {} 573 574 # Parse the arguments. 575 if open_paren != -1: 576 # Create a file-like object for the remainder of the string. 577 arguments_string = descriptor[open_paren:] 578 s = StringIO.StringIO(arguments_string) 579 # Use the Python tokenizer to process the remainder of the 580 # string. 581 g = tokenize.generate_tokens(s.readline) 582 # Read the opening parenthesis. 583 tok = g.next() 584 assert tok[0] == tokenize.OP and tok[1] == "(" 585 need_comma = 0 586 # Keep going until we find the closing parenthesis. 587 while 1: 588 tok = g.next() 589 if tok[0] == tokenize.OP and tok[1] == ")": 590 break 591 # All arguments but the first must be separated by commas. 592 if need_comma: 593 if tok[0] != tokenize.OP or tok[1] != ",": 594 raise qm.QMException, \ 595 qm.error("invalid descriptor syntax", 596 start = arguments_string[tok[2][1]:]) 597 tok = g.next() 598 # Read the argument name. 599 if tok[0] != tokenize.NAME: 600 raise qm.QMException, \ 601 qm.error("invalid descriptor syntax", 602 start = arguments_string[tok[2][1]:]) 603 name = tok[1] 604 # Read the '='. 605 tok = g.next() 606 if tok[0] != tokenize.OP or tok[1] != "=": 607 raise qm.QMException, \ 608 qm.error("invalid descriptor syntax", 609 start = arguments_string[tok[2][1]:]) 610 # Read the value. 611 tok = g.next() 612 if tok[0] != tokenize.STRING: 613 raise qm.QMException, \ 614 qm.error("invalid descriptor syntax", 615 start = arguments_string[tok[2][1]:]) 616 # The token string will have surrounding quotes. By 617 # running it through "eval", we get at the underlying 618 # value. 619 value = eval(tok[1]) 620 arguments[name] = value 621 # The next argument must be preceded by a comma. 622 need_comma = 1 623 # There shouldn't be anything left at this point. 624 tok = g.next() 625 if not tokenize.ISEOF(tok[0]): 626 raise qm.QMException, \ 627 qm.error("invalid descriptor syntax", 628 start = arguments_string[tok[2][1]:]) 629 630 # Process the arguments. 631 arguments = validate_arguments(extension_class, arguments) 632 # Use the explict arguments to override any specified in the file. 633 orig_arguments.update(arguments) 634 635 return (extension_class, orig_arguments)
636