Package gavo :: Package base :: Module complexattrs
[frames] | no frames]

Source Code for Module gavo.base.complexattrs

  1  """ 
  2  Attributes with structure (i.e., containing structures or more than one 
  3  atom). 
  4   
  5  These come with parsers of their own, in some way or other. 
  6   
  7  Structure attributes, which do not have string literals and have some sort 
  8  of internal structure, add methods 
  9   
 10          - create(instance, ctx, name) -> structure -- creates a new object suitable 
 11                  as attribute value and returns it (for stuctures, instance becomes the 
 12                  parent of the new structure as a side effect of this operation).  This  
 13                  is what should later be fed to feedObject.  It must work as a parser, 
 14                  i.e., have a feedEvent method. The name argument gives the name of  
 15                  the element that caused the create call, allowing for polymorphic attrs. 
 16          - replace(instance, oldVal, newVal) -> None -- replaces oldVal with newVal; this 
 17                  works like feedObject, except that an old value is overwritten. 
 18          - iterEvents(instance) -> events -- yields events to recreate its value 
 19                  on another instance. 
 20  """ 
 21   
 22  #c Copyright 2008-2019, the GAVO project 
 23  #c 
 24  #c This program is free software, covered by the GNU GPL.  See the 
 25  #c COPYING file in the source distribution. 
 26   
 27   
 28  from gavo.base import attrdef 
 29  from gavo.base import common 
 30  from gavo.base import literals 
 31  from gavo.utils import excs 
 32   
 33  __docformat__ = "restructuredtext en" 
34 35 36 -class CollOfAtomsAttribute(attrdef.AtomicAttribute):
37 """A base class for simple collections of atomic attributes. 38 """
39 - def __init__(self, name, default=[], 40 itemAttD=attrdef.UnicodeAttribute("listItem"), 41 **kwargs):
42 attrdef.AttributeDef.__init__(self, name, 43 default=attrdef.Computed, **kwargs) 44 self.xmlName_ = itemAttD.name_ 45 self.itemAttD = itemAttD 46 self.realDefault = default
47
48 - def iterEvents(self, instance):
49 for item in getattr(instance, self.name_): 50 yield ("start", self.xmlName_, None) 51 yield ("value", "content_", self.itemAttD.unparse(item)) 52 yield ("end", self.xmlName_, None)
53
54 55 -class ListOfAtomsAttribute(CollOfAtomsAttribute):
56 """is an attribute definition for an item containing many elements 57 of the same type. 58 59 It is constructed with an AttributeDef for the items. Note that it's 60 safe to pass in lists as defaults since they are copied before being 61 added to the instances, so you won't (and can't) have aliasing here. 62 """ 63 64 @property
65 - def default_(self):
66 return self.realDefault[:]
67 68 @property
69 - def typeDesc_(self):
70 return "Zero or more %s-typed *%s* elements"%( 71 self.itemAttD.typeDesc_, 72 self.itemAttD.name_)
73
74 - def feed(self, ctx, instance, value):
75 getattr(instance, self.name_).append(self.itemAttD.parse(value))
76
77 - def feedObject(self, instance, value):
78 if isinstance(value, list): 79 for item in value: 80 self.feedObject(instance, item) 81 else: 82 getattr(instance, self.name_).append(value) 83 self.doCallbacks(instance, value)
84
85 - def getCopy(self, instance, newParent, ctx):
86 return getattr(instance, self.name_)[:]
87
88 - def unparse(self, value):
89 return unicode(value)
90
91 92 -class SetOfAtomsAttribute(CollOfAtomsAttribute):
93 """is an attribute definition for an item containing many elements 94 of the same type, when order doesn't matter but lookup times do. 95 96 It is constructed with an AttributeDef for the items. Note that it's 97 safe to pass in lists as defaults since they are copied before being 98 added to the instances, so you won't (and can't) have aliasing here. 99 """ 100 @property
101 - def default_(self):
102 return set(self.realDefault)
103 104 @property
105 - def typeDesc_(self):
106 return "Set of %ss"%self.itemAttD.typeDesc_
107
108 - def feed(self, ctx, instance, value):
109 getattr(instance, self.name_).add(self.itemAttD.parse(value))
110
111 - def feedObject(self, instance, value):
112 if isinstance(value, set): 113 for item in value: 114 self.feedObject(instance, value) 115 else: 116 getattr(instance, self.name_).add(value) 117 self.doCallbacks(instance, value)
118
119 - def getCopy(self, instance, newParent, ctx):
120 return set(getattr(instance, self.name_))
121
122 123 -class _DictAttributeParser(common.Parser):
124 """a parser for DictAttributes. 125 126 These need a custom parser since they accept some exotic features, as 127 discussed in DictAttribute's docstring. 128 129 The parser keeps state in the _key and _adding attributes and needs to 130 be _reset after use. 131 """
132 - def __init__(self, dict, nextParser, parseValue, keyName, inverted=False):
133 self.dict, self.nextParser, self.parseValue = ( 134 dict, nextParser, parseValue) 135 self.keyName, self.inverted = keyName, inverted 136 self._reset()
137
138 - def _reset(self):
139 self._key, self._adding = attrdef.Undefined, False
140
141 - def addPair(self, key, value):
142 if self.inverted: 143 key, value = value, key 144 if self._adding: 145 self.dict[key] = self.dict.get(key, "")+value 146 else: 147 self.dict[key] = value
148
149 - def value_(self, ctx, name, value):
150 if name=="key" or name==self.keyName: 151 self._key = value 152 elif name=="cumulate": 153 self._adding = literals.parseBooleanLiteral(value) 154 elif name=="content_": 155 if self._key is attrdef.Undefined: 156 raise common.StructureError("Content '%s' has no %s attribute"%( 157 value, self.keyName)) 158 self.addPair(self._key, self.parseValue(value)) 159 self._reset() 160 else: 161 raise common.StructureError("No %s attributes on mappings"%name) 162 return self
163
164 - def start_(self, ctx, name, value):
165 raise common.StructureError("No %s elements in mappings"%name)
166
167 - def end_(self, ctx, name, value):
168 if self._key is not attrdef.Undefined: 169 self.addPair(self._key, None) 170 self._reset() 171 return self.nextParser
172
173 174 -class DictAttribute(attrdef.AttributeDef):
175 """an attribute containing a mapping. 176 177 DictAttributes are fairly complex beasts supporting a number of input 178 forms. 179 180 The input to those looks like <d key="foo">abc</d>; they are constructed 181 with an itemAttD (like StructAttributes), but the name on those 182 is ignored; they are just used for parsing from the strings in the 183 element bodies, which means that itemAttDs must be derived from 184 AtomicAttribute. 185 186 You can give a different keyNames; the key attribute is always 187 accepted, though. 188 189 For sufficiently exotic situations, you can construct DictAttributes 190 with inverted=True; the resulting dictionary will then have the keys as 191 values and vice versa (this is a doubtful feature; let us know when 192 you use it). 193 194 You can also add to existing values using the cumulate XML attribute; 195 <d key="s">a</d><d key="s" cumulate="True">bc</a> will leave 196 abc in s. 197 """
198 - def __init__(self, name, description="Undocumented", 199 itemAttD=attrdef.UnicodeAttribute("value"), 200 keyName="key", 201 inverted=False, **kwargs):
202 attrdef.AttributeDef.__init__(self, name, 203 attrdef.Computed, description, **kwargs) 204 self.xmlName_ = itemAttD.name_ 205 self.itemAttD = itemAttD 206 self.keyName = keyName 207 self.inverted = inverted
208 209 @property
210 - def typeDesc_(self):
211 return "Dict mapping strings to %s"%self.itemAttD.typeDesc_
212 213 @property
214 - def default_(self):
215 return {}
216
217 - def feedObject(self, instance, value):
218 setattr(instance, self.name_, value) 219 self.doCallbacks(instance, value)
220
221 - def create(self, parent, ctx, name):
222 return _DictAttributeParser(getattr(parent, self.name_), 223 parent, self.itemAttD.parse, keyName=self.keyName, 224 inverted=self.inverted)
225
226 - def iterEvents(self, instance):
227 for key, value in getattr(instance, self.name_).iteritems(): 228 yield ("start", self.xmlName_, None) 229 yield ("value", "key", key) 230 yield ("value", "content_", self.itemAttD.unparse(value)) 231 yield ("end", self.xmlName_, None)
232
233 - def getCopy(self, instance, newParent, ctx):
234 return getattr(instance, self.name_).copy()
235
236 - def makeUserDoc(self):
237 if self.inverted: 238 expl = ("the key is the element content, the value is in the 'key'" 239 " (or, equivalently, %s) attribute"%self.keyName) 240 else: 241 expl = ("the value is the element content, the key is in the 'key'" 242 " (or, equivalently, %s) attribute"%self.keyName) 243 244 return "**%s** (mapping; %s) -- %s"%( 245 self.xmlName_, expl, self.description_)
246
247 248 -class PropertyAttribute(DictAttribute):
249 """adds the property protocol to the parent instance. 250 251 The property protocol consists of the methods 252 - setProperty(name, value), 253 - getProperty(name, default=Undefined) 254 - clearProperty(name) 255 - hasProperty(name) 256 257 getProperty works like dict.get, except it will raise a KeyError 258 without a default. 259 260 This is provided for user information and, to some extent, some 261 DC-internal purposes. 262 """
263 - def __init__(self, description="Properties (i.e., user-defined" 264 " key-value pairs) for the element.", **kwargs):
265 DictAttribute.__init__(self, "properties", description=description, 266 keyName="name", **kwargs) 267 self.xmlName_ = "property"
268
269 - def iterParentMethods(self):
270 def setProperty(self, name, value): 271 self.properties[name] = value
272 yield "setProperty", setProperty 273 274 def getProperty(self, name, default=attrdef.Undefined): 275 if default is attrdef.Undefined: 276 try: 277 return self.properties[name] 278 except KeyError: 279 raise excs.NotFoundError(name, 280 "property", 281 repr(self)) 282 else: 283 return self.properties.get(name, default)
284 yield "getProperty", getProperty 285 286 def clearProperty(self, name): 287 if name in self.properties: 288 del self.properties[name] 289 yield "clearProperty", clearProperty 290 291 def hasProperty(self, name): 292 return name in self.properties 293 yield "hasProperty", hasProperty 294
295 - def makeUserDoc(self):
296 return ("**property** (mapping of user-defined keywords in the" 297 " name attribute to string values) -- %s"%self.description_)
298
299 300 -class StructAttribute(attrdef.AttributeDef):
301 """describes an attribute containing a Structure 302 303 These are constructed with a childFactory that must have a feedEvent 304 method. Otherwise, they are normal structs, i.e., the receive a 305 parent as the first argument and keyword arguments for values. 306 307 In addition, you can pass a onParentComplete callback that 308 are collected in the completedCallback list by the struct decorator. 309 ParseableStruct instances call these when they receive their end 310 event during XML deserialization. 311 """
312 - def __init__(self, name, childFactory, default=attrdef.Undefined, 313 description="Undocumented", **kwargs):
314 xmlName = kwargs.pop("xmlName", None) 315 attrdef.AttributeDef.__init__(self, name, default, description, **kwargs) 316 self.childFactory = childFactory 317 if xmlName is not None: 318 self.xmlName_ = xmlName 319 elif self.childFactory is not None: 320 self.xmlName_ = self.childFactory.name_ 321 if getattr(self.childFactory, "aliases", None): 322 if self.aliases: 323 self.aliases.extend(self.childFactory.aliases) 324 else: 325 self.aliases = self.childFactory.aliases[:]
326 327 @property
328 - def typeDesc_(self):
329 return getattr(self.childFactory, "docName_", self.childFactory.name_)
330
331 - def feedObject(self, instance, value):
332 if value and value.parent is None: # adopt if necessary 333 value.parent = instance 334 setattr(instance, self.name_, value) 335 self.doCallbacks(instance, value)
336
337 - def feed(self, ctx, instance, value):
338 # if the child factory actually admits content_ (and needs nothing 339 # else), allow attributes to be fed in, too. 340 if "content_" in self.childFactory.managedAttrs: 341 child = self.childFactory(instance, content_=value).finishElement(ctx) 342 return self.feedObject(instance, child) 343 344 raise common.LiteralParseError(self.name_, 345 value, hint="These attributes have no literals at all, i.e.," 346 " they are for internal use only.")
347
348 - def create(self, structure, ctx, name):
349 if self.childFactory is attrdef.Recursive: 350 res = structure.__class__(structure) 351 else: 352 res = self.childFactory(structure) 353 ctx.setPositionOn(res) 354 return res
355
356 - def getCopy(self, instance, newParent, ctx):
357 val = getattr(instance, self.name_) 358 if val is not None: 359 return val.copy(newParent, ctx=ctx)
360
361 - def replace(self, instance, oldStruct, newStruct):
362 setattr(instance, self.name_, newStruct)
363
364 - def iterEvents(self, instance):
365 val = getattr(instance, self.name_) 366 if val is common.NotGiven: 367 return 368 if val is None: 369 return 370 yield ("start", val.name_, None) 371 for ev in val.iterEvents(): 372 yield ev 373 yield ("end", val.name_, None)
374
375 - def iterChildren(self, instance):
376 if getattr(instance, self.name_) is not None: 377 yield getattr(instance, self.name_)
378
379 - def remove(self, child):
380 setattr(child.parent, self.name_, self.default)
381
382 - def onParentComplete(self, val):
383 if hasattr(val, "onParentComplete"): 384 val.onParentComplete()
385
386 - def makeUserDoc(self):
387 if self.childFactory is attrdef.Recursive: 388 contains = "(contains an instance of the embedding element)" 389 else: 390 contains = "(contains `Element %s`_)"%self.typeDesc_ 391 return "%s %s -- %s"%( 392 self.name_, contains, self.description_)
393
394 395 -class MultiStructAttribute(StructAttribute):
396 """describes an attribute containing one of a class of Structures. 397 398 This is to support things like grammars or cores -- these can 399 be of many types. 400 401 This works like StructAttribute, except that childFactory now is 402 a *function* returning elements (i.e., it's a childFactoryFactory). 403 """
404 - def __init__(self, name, childFactory, childNames, **kwargs):
405 StructAttribute.__init__(self, name, None, **kwargs) 406 self.childFactory = childFactory 407 self.aliases = childNames
408 409 @property
410 - def typeDesc_(self):
411 return ("one of %s"%", ".join(self.aliases))
412
413 - def create(self, structure, ctx, name):
414 res = self.childFactory(name)(structure) 415 ctx.setPositionOn(res) 416 return res
417
418 - def makeUserDoc(self):
419 return "%s (contains one of %s) -- %s"%( 420 self.name_, ", ".join(self.aliases), self.description_)
421
422 423 -class StructListAttribute(StructAttribute):
424 """describes an attribute containing a homogeneous list of structures. 425 """
426 - def __init__(self, name, childFactory, description="Undocumented", 427 **kwargs):
428 StructAttribute.__init__(self, name, childFactory, attrdef.Computed, 429 description, **kwargs)
430 431 @property
432 - def default_(self):
433 return []
434 435 @property
436 - def typeDesc_(self):
437 if self.childFactory is attrdef.Recursive: 438 return "Recursive element list" 439 else: 440 return "List of %s"%self.childFactory.name_
441
442 - def addStruct(self, instance, value, destIndex=None):
443 """adds a structure to the attribute's value. 444 445 Do *not* directly add to the list, always go through this 446 method; derived classes override it for special behaviour. 447 Also, this is where callbacks are called. 448 449 Use destIndex to overwrite an (existing!) struct; default is appending. 450 """ 451 if value.parent is None: # adopt if necessary 452 value.parent = instance 453 if destIndex is None: 454 getattr(instance, self.name_).append(value) 455 else: 456 getattr(instance, self.name_)[destIndex] = value 457 self.doCallbacks(instance, value)
458
459 - def feedObject(self, instance, value):
460 if isinstance(value, list): 461 for item in value: 462 self.feedObject(instance, item) 463 else: 464 self.addStruct(instance, value)
465
466 - def getCopy(self, instance, newParent, ctx):
467 res = [c.copy(newParent, ctx=ctx) 468 for c in getattr(instance, self.name_)] 469 return res
470
471 - def replace(self, instance, oldStruct, newStruct):
472 # This will only replace the first occurrence of oldStruct if 473 # multiple identical items are in the list. Any other behaviour 474 # would be about as useful, so let's leave it at this for now. 475 ind = getattr(instance, self.name_).index(oldStruct) 476 self.addStruct(instance, newStruct, ind)
477
478 - def iterEvents(self, instance):
479 for val in getattr(instance, self.name_): 480 yield ("start", val.name_, None) 481 for ev in val.iterEvents(): 482 yield ev 483 yield ("end", val.name_, None)
484
485 - def iterChildren(self, instance):
486 return iter(getattr(instance, self.name_))
487
488 - def remove(self, child):
489 getattr(child.parent, self.name_).remove(child)
490
491 - def onParentComplete(self, val):
492 if val: 493 for item in val: 494 if hasattr(item, "onParentComplete"): 495 item.onParentComplete()
496
497 - def makeUserDoc(self):
498 if self.childFactory is attrdef.Recursive: 499 contains = "(contains an instance of the embedding element" 500 else: 501 contains = "(contains `Element %s`_"%self.childFactory.name_ 502 return ("%s %s and may be repeated zero or more" 503 " times) -- %s")%(self.name_, contains, self.description_)
504
505 506 -class UniquedStructListAttribute(StructListAttribute):
507 """A StructListAttribute that will only admit one child per value 508 of uniqueAttribute, overwriting existing entries if existing. 509 510 Actually, you can pass a policy="drop" argument to just keep 511 an existing element and drop the new one. 512 """
513 - def __init__(self, name, childFactory, uniqueAttribute, 514 policy="overwrite", **kwargs):
515 self.uniqueAttribute = uniqueAttribute 516 if policy not in ["overwrite", "drop"]: 517 raise common.StructureError("UniquedStructListAttribute policy" 518 " must be either overwrite or drop") 519 self.policy = policy 520 StructListAttribute.__init__(self, name, childFactory, **kwargs)
521 522 @property
523 - def typeDesc_(self):
524 return "List of %s, uniqued on %s's value"%( 525 self.childFactory.name_, self.uniqueAttribute)
526
527 - def addStruct(self, instance, value):
528 # we expect lists will not get so long as to make a linear search 529 # actually expensive. Linear searching, on the other hand, saves 530 # us from having to maintain and index (in the presence of 531 # possible deletions!) 532 uniqueOn = getattr(value, self.uniqueAttribute) 533 for index, item in enumerate(iter(getattr(instance, self.name_))): 534 if getattr(item, self.uniqueAttribute)==uniqueOn: 535 if self.policy=="overwrite": 536 StructListAttribute.addStruct(self, instance, value, index) 537 break 538 else: 539 StructListAttribute.addStruct(self, instance, value)
540
541 542 # Ok, so the inheritance here is evil. I'll fix it if it needs more work. 543 -class MultiStructListAttribute(StructListAttribute, MultiStructAttribute):
544 """describes a list of polymorphous children. 545 546 See rscdesc cores as to why one could want this; the arguments are 547 as for MultiStructAttribute. 548 """
549 - def __init__(self, name, childFactory, childNames, **kwargs):
550 StructListAttribute.__init__(self, name, None, **kwargs) 551 self.childFactory = childFactory 552 self.aliases = childNames
553 554 @property
555 - def typeDesc_(self):
556 return "List of any of %s"%(", ".join(self.aliases))
557
558 - def create(self, structure, ctx, name):
559 res = MultiStructAttribute.create(self, structure, ctx, name) 560 ctx.setPositionOn(res) 561 return res
562
563 - def makeUserDoc(self):
564 if self.childFactory is attrdef.Recursive: 565 contains = "(contains an instance of the embedding element" 566 else: 567 contains = "(contains any of %s"%",".join(self.aliases) 568 return ("%s %s and may be repeated zero or more" 569 " times) -- %s")%(self.name_, contains, self.description_)
570 571 572 __all__ = ["ListOfAtomsAttribute", "DictAttribute", "StructAttribute", 573 "MultiStructAttribute", "StructListAttribute", "MultiStructListAttribute", 574 "UniquedStructListAttribute", "SetOfAtomsAttribute", "PropertyAttribute"] 575