1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
27
28
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
39
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
46
47
48
49
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
61 for key, field in dict.iteritems():
62 if isinstance(field, Field):
63 field.SetName(key)
64 parameters[key] = field
65
66
67
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
74
75
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
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
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
155
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
163 self.__dict__.update(args)
164
166
167
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
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
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
190 arguments = get_class_arguments_as_dictionary(self.__class__)
191
192
193 explicit_arguments = {}
194 for name, field in arguments.items():
195
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
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
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
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
253
254
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
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
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
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
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
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
334 converted_arguments = {}
335
336
337
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
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
377 field_dictionary = get_class_arguments_as_dictionary(extension_class)
378
379
380 if element:
381 extension_element = element
382 else:
383 extension_element = document.createElement("extension")
384
385 if extension_class.kind:
386 extension_element.setAttribute("kind", extension_class.kind)
387
388 extension_element.setAttribute("class",
389 get_extension_class_name(extension_class))
390
391 for argument_name, value in arguments.items():
392
393 field = field_dictionary[argument_name]
394 if field.IsComputed():
395 continue
396
397 argument_element = document.createElement("argument")
398
399 argument_element.setAttribute("name", argument_name)
400
401 argument_element.appendChild(field.MakeDomNodeForValue(value,
402 document))
403
404 extension_element.appendChild(argument_element)
405
406 return extension_element
407
408
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
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
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
462 class_name = element.getAttribute("class")
463
464
465
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
473 extension_class = class_loader(class_name)
474
475
476 field_dictionary = get_class_arguments_as_dictionary(extension_class)
477
478
479 arguments = {}
480 for argument_element in element.getElementsByTagName("argument"):
481 name = argument_element.getAttribute("name")
482
483 field = field_dictionary[name]
484
485 value_node \
486 = filter(lambda e: e.nodeType == xml.dom.Node.ELEMENT_NODE,
487 argument_element.childNodes)[0]
488
489 value = field.GetValueFromDomNode(value_node, attachment_store)
490
491
492 arguments[str(name)] = value
493
494 return (extension_class, arguments)
495
496
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
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
547 open_paren = descriptor.find('(')
548 if open_paren == -1:
549
550
551 class_name = descriptor
552 else:
553
554
555 class_name = descriptor[:open_paren]
556
557
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
575 if open_paren != -1:
576
577 arguments_string = descriptor[open_paren:]
578 s = StringIO.StringIO(arguments_string)
579
580
581 g = tokenize.generate_tokens(s.readline)
582
583 tok = g.next()
584 assert tok[0] == tokenize.OP and tok[1] == "("
585 need_comma = 0
586
587 while 1:
588 tok = g.next()
589 if tok[0] == tokenize.OP and tok[1] == ")":
590 break
591
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
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
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
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
617
618
619 value = eval(tok[1])
620 arguments[name] = value
621
622 need_comma = 1
623
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
631 arguments = validate_arguments(extension_class, arguments)
632
633 orig_arguments.update(arguments)
634
635 return (extension_class, orig_arguments)
636