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

Source Code for Module gavo.base.meta

   1  """ 
   2  Code dealing with meta information. 
   3   
   4  In DaCHS, tree-structured metadata can be attached to tables, services, 
   5  resources as a whole, and some other structures.  This module provides 
   6  a python mixin to keep and manipulate such metadata. 
   7   
   8  We deal with VO-style RMI metadata but also allow custom metadata.  Custom 
   9  metadata keys should usually start with _. 
  10   
  11  See develNotes for some discussion of why this is so messy and an explanation 
  12  of why in particular addMeta and helpers are a minefield of selections. 
  13   
  14  The rough plan to do this looks like this: 
  15   
  16  Metadata is kept in containers that mix in MetaMixin.  Meta information is  
  17  accessed through keys of the form <atom>{.<atom>}  The first atom is the  
  18  primary key.  An atom consists exclusively of ascii letters and the  
  19  underscore. 
  20   
  21  There's a meta mixin having the methods addMeta and getMeta that care 
  22  about finding metadata including handover.  For compound metadata, they 
  23  will split the key and hand over to the parent if the don't have a meta  
  24  item for the main key. 
  25  """ 
  26   
  27  #c Copyright 2008-2019, the GAVO project 
  28  #c 
  29  #c This program is free software, covered by the GNU GPL.  See the 
  30  #c COPYING file in the source distribution. 
  31   
  32   
  33  from __future__ import print_function 
  34   
  35  import re 
  36  import textwrap 
  37  import urllib 
  38   
  39  from gavo import utils 
  40  from gavo.base import attrdef 
  41  from gavo.base import common 
  42  from gavo.utils import stanxml 
  43  from gavo.utils import misctricks 
  44   
  45   
  46  # This dictionary maps meta atoms that were found to be somehow 
  47  # misleading or confusing to the ones that are now used.  This 
  48  # is so RDs aren't broken. 
  49  DEPRECATED_ATOMS = { 
  50          "_associatedDatalinkSvc": "_associatedDatalinkService", 
  51  } 
52 53 54 55 -class MetaError(utils.Error):
56 """A base class for metadata-related errors. 57 58 MetaErrors have a carrier attribute that should point to the MetaMixin 59 item queried. Metadata propagation makes this a bit tricky, but we 60 should at least try; for setMeta and addMeta, the top-level entry 61 functions manipulate the carrier attributes for this purpose. 62 63 To yield useful error messages, leave carrier at its default None 64 only when you really have no idea what the meta will end up on. 65 """
66 - def __init__(self, msg, carrier=None, hint=None, key=None):
67 self.carrier = carrier 68 self.key = key 69 utils.Error.__init__(self, msg, hint=hint)
70
71 - def __str__(self):
72 prefix = "" 73 if self.carrier: 74 prefix = "On %s: "%repr(self.carrier) 75 return prefix+utils.Error.__str__(self)
76
77 -class MetaSyntaxError(MetaError):
78 """is raised when a meta key is invalid. 79 """
80
81 82 -class NoMetaKey(MetaError):
83 """is raised when a meta key does not exist (and raiseOnFail is True). 84 """
85
86 87 -class MetaCardError(MetaError):
88 """is raised when a meta value somehow has the wrong cardinality (e.g., 89 on attempting to stringify a sequence meta). 90 """
91 - def __init__(self, msg, carrier=None, hint=None, key=None):
92 self.origMsg = msg 93 if key is None: 94 MetaError.__init__(self, msg, hint=hint) 95 else: 96 MetaError.__init__(self, "%s (key %s)"%(msg, key), hint=hint)
97
98 99 -class MetaValueError(MetaError):
100 """is raised when a meta value is inapproriate for the key given. 101 """
102
103 104 -def metaRstToHtml(inputString):
105 return utils.rstxToHTML(inputString)
106 107 108 _metaPat = re.compile(r"([a-zA-Z_][\w-]*)(?:\.([a-zA-Z_][\w-]*))*$") 109 _primaryPat = re.compile(r"([a-zA-Z_][\w-]*)(\.|$)")
110 111 112 -def getPrimary(metaKey):
113 key = _primaryPat.match(metaKey) 114 if not key or not key.group(1): 115 raise MetaSyntaxError("Invalid meta key: %s"%metaKey, None) 116 return key.group(1)
117
118 119 -def parseKey(metaKey):
120 if not _metaPat.match(metaKey): 121 raise MetaSyntaxError("Invalid meta key: %s"%metaKey, None) 122 123 parsed = metaKey.split(".") 124 for atom in parsed: 125 if atom in DEPRECATED_ATOMS: 126 utils.sendUIEvent("Warning", "Deprecated meta atom %s used in %s;" 127 " please change the atom to %s."%( 128 atom, metaKey, DEPRECATED_ATOMS[atom])) 129 return [DEPRECATED_ATOMS.get(a, a) for a in parsed] 130 131 return parsed
132
133 134 -def parseMetaStream(metaContainer, metaStream, clearItems=False):
135 """parser meta key/value pairs from metaStream and adds them to 136 metaContainer. 137 138 If clearItems is true, for each key found in the metaStream there's 139 first a delMeta for that key executed. This is for re-parsing 140 meta streams. 141 142 The stream format is: 143 144 - continuation lines with backslashes, where any sequence of 145 backslash, (cr?) lf (blank or tab)* is replaced by nothing. 146 - comments are lines like (ws*)# anything 147 - empty lines are no-ops 148 - all other lines are (ws*)<key>(ws*):(ws*)value(ws*) 149 - if a key starts with !, any meta info for the key is cleared before 150 setting 151 """ 152 if metaStream is None: 153 return 154 155 # handle continuation lines 156 metaStream = re.sub("\\\\\r?\n[\t ]*", "", metaStream) 157 158 keysSeen = set() 159 160 for line in metaStream.split("\n"): 161 line = line.strip() 162 if line.startswith("#") or not line: 163 continue 164 try: 165 key, value = line.split(":", 1) 166 except ValueError: 167 raise MetaSyntaxError("%s is no valid line for a meta stream"% 168 repr(line), None, 169 hint="In general, meta streams contain lines like 'meta.key:" 170 " meta value; see also the documentation.") 171 172 key = key.strip() 173 if key.startswith("!"): 174 key = key[1:] 175 metaContainer.delMeta(key) 176 177 if key not in keysSeen and clearItems: 178 metaContainer.delMeta(key) 179 keysSeen.add(key) 180 metaContainer.addMeta(key, value.strip())
181
182 183 -class MetaParser(common.Parser):
184 """A structure parser that kicks in when meta information is 185 parsed from XML. 186 187 This parser can also handle the notation with an attribute-less 188 meta tag and lf-separated colon-pairs as content. 189 """ 190 # These are constructed a lot, so let's keep __init__ as clean as possible, 191 # shall we?
192 - def __init__(self, container, nextParser):
193 self.container, self.nextParser = container, nextParser 194 self.attrs = {} 195 self.children = [] # containing key, metaValue pairs 196 self.next = None
197
198 - def addMeta(self, key, content="", **kwargs):
199 # this parse can be a temporary meta parent for children; we 200 # record them here an play them back when we have created 201 # the meta value itself 202 self.children.append((key, content, kwargs))
203
204 - def setMeta(self, key, content="", **kwargs):
205 # see addMeta comment on why we need to do magic here. 206 self.children.append(("!"+key, content, kwargs))
207
208 - def start_(self, ctx, name, value):
209 if name=="meta": 210 return MetaParser(self, self) 211 else: 212 self.next = name 213 return self
214
215 - def value_(self, ctx, name, value):
216 if self.next is None: 217 self.attrs[str(name)] = value 218 else: 219 self.attrs[str(self.next)] = value 220 return self
221
222 - def _doAddMeta(self):
223 content = self.attrs.pop("content_", "") 224 if not self.attrs: # content only, parse this as a meta stream 225 parseMetaStream(self.container, content) 226 227 else: 228 try: 229 content = utils.fixIndentation(content, "", 1).rstrip() 230 except common.Error as ex: 231 raise utils.logOldExc(common.StructureError("Bad text in meta value" 232 " (%s)"%ex)) 233 if not "name" in self.attrs: 234 raise common.StructureError("meta elements must have a" 235 " name attribute") 236 metaKey = self.attrs.pop("name") 237 if metaKey.startswith("!"): 238 self.container.setMeta(metaKey[1:], content, **self.attrs) 239 else: 240 self.container.addMeta(metaKey, content, **self.attrs) 241 242 # meta elements can have children; add these, properly fudging 243 # their keys 244 for key, content, kwargs in self.children: 245 if key.startswith("!"): 246 fullKey = "%s.%s"%(metaKey, key[1:]) 247 self.container.setMeta(fullKey, content, **kwargs) 248 else: 249 fullKey = "%s.%s"%(metaKey, key) 250 self.container.addMeta(fullKey, content, **kwargs)
251
252 - def end_(self, ctx, name, value):
253 if name=="meta": 254 try: 255 self._doAddMeta() 256 except TypeError as msg: 257 raise utils.StructureError("While constructing meta: %s"%msg) 258 return self.nextParser 259 260 else: 261 self.next = None 262 return self
263
264 265 -class MetaAttribute(attrdef.AttributeDef):
266 """An attribute magically inserting meta values to Structures mixing 267 in MetaMixin. 268 269 We don't want to keep metadata itself in structures for performance 270 reasons, so we define a parser of our own in here. 271 """ 272 typedesc = "Metadata" 273
274 - def __init__(self, description="Metadata"):
275 attrdef.AttributeDef.__init__(self, "meta_", 276 attrdef.Computed, description) 277 self.xmlName_ = "meta"
278 279 @property
280 - def default_(self):
281 return {}
282
283 - def feedObject(self, instance, value):
284 self.meta_ = value
285
286 - def getCopy(self, parent, newParent, ctx):
287 """creates a deep copy of the current meta dictionary and returns it. 288 289 This is used when a MetaMixin's attribute is set to copyable and a 290 meta carrier is copied. As there's currently no way to make the 291 _metaAttr copyable, this isn't called by itself. If you 292 must, you can manually call this (_metaAttr.getCopy), but that'd 293 really be an indication the interface needs changes. 294 295 Note that the copying semantics is a bit funky: Copied values 296 remain, but on write, sequences are replaced rather than added to. 297 """ 298 oldDict = parent.meta_ 299 newMeta = {} 300 for key, mi in oldDict.iteritems(): 301 newMeta[key] = mi.copy() 302 return newMeta
303
304 - def create(self, parent, ctx, name):
305 return MetaParser(parent, parent)
306
307 - def makeUserDoc(self):
308 return ("**meta** -- a piece of meta information, giving at least a name" 309 " and some content. See Metadata_ on what is permitted here.")
310
311 - def iterEvents(self, instance):
312 def doIter(metaDict): 313 for key, item in metaDict.iteritems(): 314 for value in item: 315 yield ("start", "meta", None) 316 yield ("value", "name", key) 317 if value.getContent(): 318 yield ("value", "content_", value.getContent()) 319 320 if value.meta_: 321 for ev in doIter(value.meta_): 322 yield ev 323 yield ("end", "meta", None)
324 325 for ev in doIter(instance.meta_): 326 yield ev
327
328 329 -class MetaMixin(object):
330 """is a mixin for entities carrying meta information. 331 332 The meta mixin provides the followng methods: 333 334 - setMetaParent(m) -- sets the name of the meta container enclosing the 335 current one. m has to have the Meta mixin as well. 336 - getMeta(key, propagate=True, raiseOnFail=False, default=None) -- returns 337 meta information for key or default. 338 - addMeta(key, metaItem, moreAttrs) -- adds a piece of meta information 339 here. Key may be a compound, metaItem may be a text, in which 340 case it will be turned into a proper MetaValue taking key and 341 moreAttrs into account. 342 - setMeta(key, metaItem) -- like addMeta, only previous value(s) are 343 overwritten 344 - delMeta(key) -- removes a meta value(s) for key. 345 346 When querying meta information, by default all parents are queried as 347 well (propagate=True). 348 349 Metadata is not copied when the embedding object is copied. 350 That, frankly, has not been a good design descision, and there should 351 probably be a way to pass copypable=True to the mixin's attribute 352 definition. 353 """ 354 _metaAttr = MetaAttribute() 355
356 - def __init__(self):
357 """is a constructor for standalone use. You do *not* want to 358 call this when mixing into a Structure. 359 """ 360 self.meta_ = {}
361
362 - def __hasMetaParent(self):
363 try: 364 self.__metaParent # assert existence 365 return True 366 except AttributeError: 367 return False
368
369 - def isEmpty(self):
370 return len(self.meta_)==0 and getattr(self, "content", "")==""
371
372 - def setMetaParent(self, parent):
373 if parent is not None: 374 self.__metaParent = parent
375
376 - def getMetaParent(self):
377 if self.__hasMetaParent(): 378 return self.__metaParent 379 else: 380 return None
381
382 - def _getMeta(self, atoms, propagate, acceptSequence=False):
383 try: 384 return self._getFromAtom(atoms[0])._getMeta(atoms[1:], 385 acceptSequence=acceptSequence) 386 except NoMetaKey: 387 pass # See if parent has the key 388 389 if propagate: 390 if self.__hasMetaParent(): 391 return self.__metaParent._getMeta(atoms, propagate, 392 acceptSequence=acceptSequence) 393 else: 394 return configMeta._getMeta(atoms, propagate=False, 395 acceptSequence=acceptSequence) 396 397 raise NoMetaKey("No meta item %s"%".".join(atoms), carrier=self)
398
399 - def getMeta(self, key, propagate=True, 400 raiseOnFail=False, default=None, acceptSequence=False):
401 try: 402 try: 403 return self._getMeta( 404 parseKey(key), propagate, acceptSequence=acceptSequence) 405 except NoMetaKey as ex: 406 if raiseOnFail: 407 ex.key = key 408 raise 409 except MetaCardError as ex: 410 raise utils.logOldExc( 411 MetaCardError(ex.origMsg, hint=ex.hint, key=key)) 412 except MetaError as ex: 413 ex.carrier = self 414 raise 415 return default
416
417 - def _iterMeta(self, atoms):
418 mine, others = atoms[0], atoms[1:] 419 for mv in self.meta_.get(mine, []): 420 if others: 421 for child in mv._iterMeta(others): 422 yield child 423 else: 424 yield mv
425
426 - def iterMeta(self, key, propagate=False):
427 """yields all MetaValues for key. 428 429 This will traverse down all branches necessary to yield, in sequence, 430 all MetaValues reachable by key. 431 432 If propagation is enabled, the first meta carrier that has at least 433 one item exhausts the iteration. 434 435 (this currently doesn't return an iterator but a sequence; that's an 436 implementation detail, though. You should only assume whatever comes 437 back is iterable) 438 """ 439 val = list(self._iterMeta(parseKey(key))) 440 if not val and propagate: 441 if self.__hasMetaParent(): 442 val = self.__metaParent.iterMeta(key, propagate=True) 443 else: 444 val = configMeta.iterMeta(key, propagate=False) 445 return val
446
447 - def getAllMetaPairs(self):
448 """iterates over all meta items this container has. 449 450 Each item consists of key, MetaValue. Multiple MetaValues per 451 key may be given. 452 453 This will not iterate up, i.e., in general, getMeta will succeed 454 for more keys than what's given here. 455 """ 456 class Accum(object): 457 def __init__(self): 458 self.items = [] 459 self.keys = []
460 def startKey(self, key): 461 self.keys.append(key)
462 def enterValue(self, value): 463 self.items.append((".".join(self.keys), value)) 464 def endKey(self, key): 465 self.keys.pop() 466 467 accum = Accum() 468 self.traverse(accum) 469 return accum.items 470
471 - def buildRepr(self, key, builder, propagate=True, raiseOnFail=True):
472 value = self.getMeta(key, raiseOnFail=raiseOnFail, propagate=propagate) 473 if value: 474 builder.startKey(key) 475 value.traverse(builder) 476 builder.endKey(key) 477 return builder.getResult()
478
479 - def _hasAtom(self, atom):
480 return atom in self.meta_
481
482 - def _getFromAtom(self, atom):
483 if atom in self.meta_: 484 return self.meta_[atom] 485 raise NoMetaKey("No meta child %s"%atom, carrier=self)
486
487 - def getMetaKeys(self):
488 return self.meta_.keys()
489 490 # XXX TRANS remove; this is too common a name 491 keys = getMetaKeys 492
493 - def _setForAtom(self, atom, metaItem):
494 self.meta_[atom] = metaItem
495
496 - def _addMeta(self, atoms, metaValue):
497 primary = atoms[0] 498 if primary in self.meta_: 499 self.meta_[primary]._addMeta(atoms[1:], metaValue) 500 else: 501 self.meta_[primary] = MetaItem.fromAtoms(atoms[1:], metaValue)
502
503 - def addMeta(self, key, metaValue, **moreAttrs):
504 """adds metaItem to self under key. 505 506 moreAttrs can be additional keyword arguments; these are used by 507 the XML constructor to define formats or to pass extra items 508 to special meta types. 509 510 For convenience, this returns the meta container. 511 """ 512 try: 513 if doMetaOverride(self, key, metaValue, moreAttrs): 514 return 515 516 self._addMeta(parseKey(key), ensureMetaValue(metaValue, moreAttrs)) 517 except MetaError as ex: 518 ex.carrier = self 519 raise 520 521 return self
522
523 - def _delMeta(self, atoms):
524 if atoms[0] not in self.meta_: 525 return 526 if len(atoms)==1: 527 del self.meta_[atoms[0]] 528 else: 529 child = self.meta_[atoms[0]] 530 child._delMeta(atoms[1:]) 531 if child.isEmpty(): 532 del self.meta_[atoms[0]]
533
534 - def delMeta(self, key):
535 """removes a meta item from this meta container. 536 537 This will not propagate, i.e., getMeta(key) might still 538 return something unless you give propagate=False. 539 540 It is not an error do delete an non-existing meta key. 541 """ 542 self._delMeta(parseKey(key))
543
544 - def setMeta(self, key, value, **moreAttrs):
545 """replaces any previous meta content of key (on this container) 546 with value. 547 """ 548 self.delMeta(key) 549 self.addMeta(key, value, **moreAttrs)
550
551 - def traverse(self, builder):
552 for key, item in self.meta_.iteritems(): 553 builder.startKey(key) 554 item.traverse(builder) 555 builder.endKey(key)
556
557 - def copyMetaFrom(self, other):
558 """sets a copy of other's meta items on self. 559 """ 560 for key in other.getMetaKeys(): 561 orig = other.getMeta(key) 562 if orig is not None: 563 # (orig None can happen for computed metadata) 564 self.meta_[key] = orig.copy()
565
566 - def makeOriginal(self, key):
567 """marks the meta item key, if existing, as original. 568 569 This is for when a meta container has copied metadata. DaCHS' 570 default behaviour is that a subsequent addMeta will clear the 571 copied content. Call this method for the key in question to 572 enable adding to copied metadata. 573 """ 574 item = self.getMeta(key) 575 if item is not None and hasattr(item, "copied"): 576 del item.copied
577 578 579 # Global meta, items get added from config 580 configMeta = MetaMixin()
581 582 583 -class ComputedMetaMixin(MetaMixin):
584 """A MetaMixin for classes that want to implement defaults for 585 unresolvable meta items. 586 587 If getMeta would return a NoMetaKey, this mixin's getMeta will check 588 the presence of a _meta_<key> method (replacing dots with two underscores) 589 and, if it exists, returns whatever it returns. Otherwise, the 590 exception will be propagated. 591 592 The _meta_<key> methods should return MetaItems; if something else 593 is returned, it is wrapped in a MetaValue. 594 595 On copying such metadata, the copy will retain the value on the original 596 if it has one. This does not work for computed metadata that would be 597 inherited. 598 """
599 - def _getFromAtom(self, atom):
600 try: 601 return MetaMixin._getFromAtom(self, atom) 602 except NoMetaKey: 603 604 methName = "_meta_"+atom 605 if hasattr(self, methName): 606 res = getattr(self, methName)() 607 if res is None: 608 raise 609 return ensureMetaItem(res) 610 611 raise
612
613 - def getMetaKeys(self):
614 computedKeys = [] 615 for name in dir(self): 616 if name.startswith("_meta_"): 617 computedKeys.append(name[6:]) 618 return MetaMixin.getMetaKeys(self)+computedKeys
619
620 - def _iterMeta(self, atoms):
621 if len(atoms)>1: 622 for mv in MetaMixin._iterMeta(self, atoms): 623 yield mv 624 else: 625 res = list(MetaMixin._iterMeta(self, atoms)) 626 if not res: 627 try: 628 res = self._getFromAtom(atoms[0]) 629 except NoMetaKey: 630 res = [] 631 for val in res: 632 yield val
633
634 635 -class MetaItem(object):
636 """is a collection of homogenous MetaValues. 637 638 All MetaValues within a MetaItem have the same key. 639 640 A MetaItem contains a list of children MetaValues; it is usually 641 constructed with just one MetaValue, though. Use the alternative 642 constructor formSequence if you already have a sequence of 643 MetaValues. Or, better, use the ensureMetaItem utility function. 644 645 The last added MetaValue is the "active" one that will be changed 646 on _addMeta calls. 647 """
648 - def __init__(self, val):
649 self.children = [val]
650 651 @classmethod
652 - def fromSequence(cls, seq):
653 res = cls(seq[0]) 654 for item in seq[1:]: 655 res.addChild(item) 656 return res
657 658 @classmethod
659 - def fromAtoms(cls, atoms, metaValue):
660 if len(atoms)==0: # This will become my child. 661 return cls(metaValue) 662 663 elif len(atoms)==1: # Create a MetaValue with the target as child 664 mv = MetaValue() 665 mv._setForAtom(atoms[0], cls(ensureMetaValue(metaValue))) 666 return cls(mv) 667 668 else: # Create a MetaValue with an ancestor of the target as child 669 mv = MetaValue() 670 mv._setForAtom(atoms[0], cls.fromAtoms(atoms[1:], 671 ensureMetaValue(metaValue))) 672 return cls(mv)
673
674 - def __str__(self):
675 try: 676 res = self.getContent(targetFormat="text") 677 return res 678 except MetaError: 679 return ", ".join(m.getContent(targetFormat="text") 680 for m in self.children)
681 682 __unicode__ = __str__ 683
684 - def __iter__(self):
685 return iter(self.children)
686
687 - def __len__(self):
688 return len(self.children)
689
690 - def __getitem__(self, index):
691 return self.children[index]
692
693 - def isEmpty(self):
694 return len(self)==0
695
696 - def addContent(self, item):
697 self.children[-1].addContent(item)
698
699 - def addChild(self, metaValue=None, key=None):
700 # XXX should we force metaValue to be "compatible" with what's 701 # already in children? 702 if hasattr(self, "copied"): 703 self.children = [] 704 delattr(self, "copied") 705 if metaValue is None: 706 metaValue = MetaValue(None) 707 assert isinstance(metaValue, MetaValue) 708 self.children.append(metaValue)
709
710 - def _getMeta(self, atoms, acceptSequence=False):
711 if atoms: 712 if len(self.children)!=1 and not acceptSequence: 713 raise MetaCardError("Meta sequence in branch for getMeta") 714 else: 715 return self.children[0]._getMeta( 716 atoms, acceptSequence=acceptSequence) 717 return self
718
719 - def _addMeta(self, atoms, metaValue):
720 # See above for this mess -- I'm afraid it has to be that complex 721 # if we want to be able to build compound sequences using text labels. 722 723 # Case 1: Direct child of MetaMixin, sequence addition 724 if not atoms: 725 self.addChild(metaValue) 726 else: 727 self.children[-1]._addMeta(atoms, metaValue)
728
729 - def getMeta(self, key, *args, **kwargs):
730 if len(self.children)==1: 731 return self.children[0].getMeta(key, *args, **kwargs) 732 else: 733 raise MetaCardError("No getMeta for meta value sequences", 734 carrier=self, key=key)
735
736 - def getContent(self, 737 targetFormat="text", macroPackage=None, acceptSequence=False):
738 if len(self.children)==1 or acceptSequence: 739 return self.children[0].getContent(targetFormat, macroPackage) 740 raise MetaCardError("getContent not allowed for sequence meta items", 741 carrier=self)
742
743 - def _delMeta(self, atoms):
744 newChildren = [] 745 for c in self.children: 746 c._delMeta(atoms) 747 if not c.isEmpty(): 748 newChildren.append(c) 749 self.children = newChildren
750
751 - def traverse(self, builder):
752 for mv in self.children: 753 if mv.content: 754 builder.enterValue(mv) 755 mv.traverse(builder)
756
757 - def copy(self):
758 """returns a deep copy of self. 759 """ 760 newOb = self.__class__("") 761 newOb.children = [mv.copy() for mv in self.children] 762 newOb.copied = True 763 return newOb
764
765 - def serializeToXMLStan(self):
766 return unicode(self)
767
768 769 -class _NoHyphenWrapper(textwrap.TextWrapper):
770 # we don't want whitespace after hyphens in plain meta strings (looks 771 # funny in HTML), so fix wordsep_re 772 wordsep_re = re.compile( 773 r'(\s+|' # any whitespace 774 r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))') # em-dash
775
776 777 -class MetaValue(MetaMixin):
778 """is a piece of meta information about a resource. 779 780 The content is always a string. 781 782 The text content may be in different formats, notably 783 784 - literal 785 786 - rst (restructured text) 787 788 - plain (the default) 789 790 - raw (for embedded HTML, mainly -- only use this if you know 791 the item will only be embedded into HTML templates). 792 """ 793 knownFormats = set(["literal", "rst", "plain", "raw"]) 794 paragraphPat = re.compile("\n\\s*\n") 795 consecutiveWSPat = re.compile("\s\s+") 796 plainWrapper = _NoHyphenWrapper(break_long_words=False, 797 replace_whitespace=True) 798
799 - def __init__(self, content="", format="plain"):
800 self.initArgs = content, format 801 MetaMixin.__init__(self) 802 if format not in self.knownFormats: 803 raise common.StructureError( 804 "Unknown meta format '%s'; allowed are %s."%( 805 format, ", ".join(self.knownFormats))) 806 self.content = content 807 self.format = format 808 self._preprocessContent()
809
810 - def __str__(self):
811 return self.getContent().encode("utf-8")
812
813 - def __unicode__(self):
814 return self.getContent()
815
816 - def _preprocessContent(self):
817 if self.format=="plain" and self.content is not None: 818 self.content = "\n\n".join(self.plainWrapper.fill( 819 self.consecutiveWSPat.sub(" ", para)) 820 for para in self.paragraphPat.split(self.content))
821
822 - def _getContentAsText(self, content):
823 return content
824
825 - def _getContentAsHTML(self, content, block=False):
826 if block: 827 encTag = "p" 828 else: 829 encTag = "span" 830 if self.format=="literal": 831 return '<%s class="literalmeta">%s</%s>'%(encTag, content, encTag) 832 elif self.format=="plain": 833 return "\n".join('<%s class="plainmeta">%s</%s>'%(encTag, p, encTag) 834 for p in content.split("\n\n")) 835 elif self.format=="rst": 836 # XXX TODO: figure out a way to have them block=False 837 return metaRstToHtml(content) 838 elif self.format=="raw": 839 return content
840
841 - def getExpandedContent(self, macroPackage):
842 if hasattr(macroPackage, "expand") and "\\" in self.content: 843 return macroPackage.expand(self.content) 844 return self.content
845
846 - def getContent(self, targetFormat="text", macroPackage=None):
847 content = self.getExpandedContent(macroPackage) 848 if targetFormat=="text": 849 return self._getContentAsText(content) 850 elif targetFormat=="html": 851 return self._getContentAsHTML(content) 852 elif targetFormat=="blockhtml": 853 return self._getContentAsHTML(content, block=True) 854 else: 855 raise MetaError("Invalid meta target format: %s"%targetFormat)
856
857 - def encode(self, enc):
858 return unicode(self).encode(enc)
859
860 - def _getMeta(self, atoms, propagate=False, acceptSequence=False):
861 return self._getFromAtom(atoms[0])._getMeta(atoms[1:], 862 acceptSequence=acceptSequence)
863
864 - def _addMeta(self, atoms, metaValue):
865 # Cases continued from MetaItem._addMeta 866 # Case 2: Part of a compound, metaValue is to become direct child 867 if len(atoms)==1: 868 primary = atoms[0] 869 870 # Case 2.1: Requested child exists 871 if self._hasAtom(primary): 872 self._getFromAtom(primary).addChild(metaValue, primary) 873 874 # Case 2.2: Requested child does not exist 875 else: 876 self._setForAtom(primary, MetaItem(metaValue)) 877 878 # Case 3: metaItem will become an indirect child 879 else: 880 primary = atoms[0] 881 882 # Case 3.1: metaValue will become a child of an existing child of ours 883 if self._hasAtom(primary): 884 self._getFromAtom(primary)._addMeta(atoms[1:], metaValue) 885 886 # Case 3.2: metaItem is on a branch that needs yet to be created. 887 else: 888 self._setForAtom(primary, MetaItem.fromAtoms(atoms[1:], 889 metaValue))
890
891 - def copy(self):
892 """returns a deep copy of self. 893 """ 894 newOb = self.__class__(*self.initArgs) 895 newOb.format, newOb.content = self.format, self.content 896 newOb.copyMetaFrom(self) 897 return newOb
898
899 900 ################## individual meta types (factor out to a new module) 901 902 -class IncludesChildren(unicode):
903 """a formatted result that already includes all meta children. 904 905 This is returned from some of the special meta types' HTML formatters 906 to keep the HTMLMetaBuilder from adding meta items that are already 907 included in their HTML. 908 """
909
910 911 -class MetaURL(MetaValue):
912 """A meta value containing a link and optionally a title 913 914 In plain text, this would look like 915 this:: 916 917 _related:http://foo.bar 918 _related.title: The foo page 919 920 In XML, you can write:: 921 922 <meta name="_related" title="The foo page" 923 ivoId="ivo://bar.org/foo">http://foo.bar</meta> 924 925 or, if you prefer:: 926 927 <meta name="_related">http://foo.bar 928 <meta name="title">The foo page</meta></meta> 929 930 These values are used for _related (meaning "visible" links to other 931 services). 932 933 For links within you data center, use the internallink macro, the argument 934 of which the the "path" to a resource, i.e. RD path/service/renderer; 935 we recommend to use the info renderer in such links as a rule. This would 936 look like this:: 937 938 <meta name="_related" title="Aspec SSAP" 939 >\internallink{aspec/q/ssa/info}</meta> 940 941 """
942 - def __init__(self, url, format="plain", title=None):
943 MetaValue.__init__(self, url, format) 944 self.title = title
945
946 - def _getContentAsHTML(self, content):
947 title = self.title or content 948 return '<a href="%s">%s</a>'%(content.strip(), title)
949
950 - def _addMeta(self, atoms, metaValue):
951 if atoms[0]=="title": 952 self.title = metaValue.content 953 else: 954 MetaValue._addMeta(self, atoms, metaValue)
955
956 957 -class RelatedResourceMeta(MetaValue):
958 """A meta value containing an ivo-id and a name of a related resource. 959 960 These all are translated to relationship elements in VOResource 961 renderings. These correspond to the terms in the official relationship 962 vocabulary http://docs.g-vo.org/vocab-test/relationship_type. There, 963 the camelCase terms are preferred, and for DaCHS meta, they are written 964 with a lowercase initial. 965 966 Relationship metas should look like this:: 967 968 servedBy: GAVO TAP service 969 servedBy.ivoId: ivo://org.gavo.dc 970 971 ``servedBy`` and ``serviceFor`` are somewhat special cases, as 972 the service attribute of data publications automatically takes care 973 of them; so, you shouldn't usually need to bother with these two manually. 974 """
975 - def __init__(self, title, format="plain", ivoId=None):
976 MetaValue.__init__(self, title, format) 977 if ivoId is not None: 978 self._addMeta(["ivoId"], MetaValue(ivoId))
979
980 981 -class NewsMeta(MetaValue):
982 """A meta value representing a "news" items. 983 984 The content is the body of the news. In addition, they have 985 date, author, and role children. In plain text, you would write:: 986 987 _news: Frobnicated the quux. 988 _news.author: MD 989 _news.date: 2009-03-06 990 _news.role: updated 991 992 In XML, you would usually write:: 993 994 <meta name="_news" author="MD" date="2009-03-06"> 995 Frobnicated the quux. 996 </meta> 997 998 _news items become serialised into Registry records despite their 999 leading underscores. role then becomes the date's role. 1000 """ 1001 discardChildrenInHTML = True 1002
1003 - def __init__(self, content, format="plain", author=None, 1004 date=None, role=None):
1005 MetaValue.__init__(self, content, format) 1006 self.initArgs = format, author, date, role 1007 for key in ["author", "date", "role"]: 1008 val = locals()[key] 1009 if val is not None: 1010 self._addMeta([key], MetaValue(val))
1011
1012 - def _getContentAsHTML(self, content):
1013 authorpart = "" 1014 if self.author: 1015 authorpart = " (%s)"%self.author 1016 return IncludesChildren('<span class="newsitem">%s%s: %s</span>'%( 1017 self.date, authorpart, MetaValue._getContentAsHTML(self, content)))
1018
1019 - def _addMeta(self, atoms, metaValue):
1020 if atoms[0]=="author": 1021 self.author = metaValue.content 1022 elif atoms[0]=="date": 1023 self.date = metaValue.content 1024 elif atoms[0]=="role": 1025 self.role = metaValue.content 1026 MetaValue._addMeta(self, atoms, metaValue)
1027
1028 1029 -class NoteMeta(MetaValue):
1030 """A meta value representing a "note" item. 1031 1032 This is like a footnote, typically on tables, and is rendered in table 1033 infos. 1034 1035 The content is the note body. In addition, you want a tag child that 1036 gives whatever the note is references as. We recommend numbers. 1037 1038 Contrary to other meta items, note content defaults to rstx format. 1039 1040 Typically, this works with a column's note attribute. 1041 1042 In XML, you would usually write:: 1043 1044 <meta name="note" tag="1"> 1045 Better ignore this. 1046 </meta> 1047 """
1048 - def __init__(self, content, format="rst", tag=None):
1049 MetaValue.__init__(self, content, format) 1050 self.initArgs = content, format, tag 1051 self.tag = tag
1052
1053 - def _getContentAsHTML(self, content):
1054 return ('<dt class="notehead">' 1055 '<a name="note-%s">Note %s</a></dt><dd>%s</dd>')%( 1056 self.tag, 1057 self.tag, 1058 MetaValue._getContentAsHTML(self, content))
1059
1060 - def _addMeta(self, atoms, metaValue):
1061 if atoms[0]=="tag": 1062 self.tag = metaValue.content 1063 else: 1064 MetaValue._addMeta(self, atoms, metaValue)
1065
1066 1067 -class InfoItem(MetaValue):
1068 """A meta value for info items in VOTables. 1069 1070 In addition to the content (which should be rendered as the info element's 1071 text content), it contains an infoName and an infoValue. 1072 1073 They are only used internally in VOTable generation and might go away 1074 without notice. 1075 """
1076 - def __init__(self, content, format="plain", infoName=None, 1077 infoValue=None, infoId=None):
1078 MetaValue.__init__(self, content, format) 1079 self.initArgs = content, format, infoName, infoValue, infoId 1080 self.infoName, self.infoValue = infoName, infoValue 1081 self.infoId = infoId
1082
1083 1084 -class LogoMeta(MetaValue):
1085 """A MetaValue corresponding to a small image. 1086 1087 These are rendered as little images in HTML. In XML meta, you can 1088 say:: 1089 1090 <meta name="_somelogo" type="logo">http://foo.bar/quux.png</meta> 1091 """
1092 - def _getContentAsHTML(self, content):
1093 return u'<img class="metalogo" src="%s" alt="[Logo]"/>'%( 1094 unicode(content).strip())
1095
1096 1097 -class BibcodeMeta(MetaValue):
1098 """A MetaValue that may contain bibcodes, which are rendered as links 1099 into ADS. 1100 """ 1109
1110 - def _getContentAsHTML(self, content):
1111 return misctricks.BIBCODE_PATTERN.sub(self._makeADSLink, unicode(content) 1112 ).replace("&", "&amp;")
1113 # Yikes. We should really quote such raw HTML properly...
1114 1115 1116 -class VotLinkMeta(MetaValue):
1117 """A MetaValue serialized into VOTable links (or, ideally, 1118 analoguous constructs). 1119 1120 This exposes the various attributes of VOTable LINKs as href 1121 linkname, contentType, and role. You cannot set ID here; if this ever 1122 needs referencing, we'll need to think about it again. 1123 The href attribute is simply the content of our meta (since 1124 there's no link without href), and there's never any content 1125 in VOTable LINKs). 1126 1127 You could thus say:: 1128 1129 votlink: http://docs.g-vo.org/DaCHS 1130 votlink.role: doc 1131 votlink.contentType: text/html 1132 votlink.linkname: GAVO DaCHS documentation 1133 """
1134 - def __init__(self, href, format="plain", linkname=None, contentType=None, 1135 role=None):
1136 MetaValue.__init__(self, href, format) 1137 for key in ["linkname", "contentType", "role"]: 1138 val = locals()[key] 1139 if val is not None: 1140 self._addMeta([key], MetaValue(val))
1141
1142 1143 -class ExampleMeta(MetaValue):
1144 """A MetaValue to keep VOSI examples in. 1145 1146 All of these must have a title, which is also used to generate 1147 references. 1148 1149 These also are in reStructuredText by default, and changing 1150 that probably makes no sense at all, as these will always need 1151 interpreted text roles for proper markup. 1152 1153 Thus, the usual pattern here is:: 1154 1155 <meta name="_example" title="An example for _example"> 1156 See docs_ 1157 1158 .. _docs: http://docs.g-vo.org 1159 </meta> 1160 """
1161 - def __init__(self, content, format="rst", title=None):
1162 if title is None: 1163 raise MetaError("_example meta must always have a title") 1164 MetaValue.__init__(self, content, format) 1165 self._addMeta(["title"], MetaValue(title))
1166 1167 1168 META_CLASSES_FOR_KEYS = { 1169 "_related": MetaURL, 1170 "_example": ExampleMeta, 1171 1172 # if you add new RelationResourceMeta meta keys, be you'll also need to 1173 # amend registry.builders._vrResourceBuilder 1174 # VOResource 1.0 terms 1175 "servedBy": RelatedResourceMeta, 1176 "serviceFor": RelatedResourceMeta, 1177 "relatedTo": RelatedResourceMeta, 1178 "mirrorOf": RelatedResourceMeta, 1179 "derivedFrom": RelatedResourceMeta, 1180 "uses": RelatedResourceMeta, 1181 1182 # VOResource 1.1 terms 1183 "cites": RelatedResourceMeta, 1184 "isSupplementTo": RelatedResourceMeta, 1185 "isSupplementedBy": RelatedResourceMeta, 1186 "isContinuedBy": RelatedResourceMeta, 1187 "continues": RelatedResourceMeta, 1188 "isNewVersionOf": RelatedResourceMeta, 1189 "isPreviousVersionOf": RelatedResourceMeta, 1190 "isPartOf": RelatedResourceMeta, 1191 "hasPart": RelatedResourceMeta, 1192 "isSourceOf": RelatedResourceMeta, 1193 "isDerivedFrom": RelatedResourceMeta, 1194 "isIdenticalTo": RelatedResourceMeta, 1195 "isServiceFor": RelatedResourceMeta, 1196 "isServedBy": RelatedResourceMeta, 1197 1198 "_news": NewsMeta, 1199 "referenceURL": MetaURL, 1200 "info": InfoItem, 1201 "logo": LogoMeta, 1202 "source": BibcodeMeta, 1203 "note": NoteMeta, 1204 "votlink": VotLinkMeta, 1205 "creator.logo": LogoMeta, 1206 "logo": LogoMeta, 1207 }
1208 1209 1210 -def _doCreatorMetaOverride(container, value):
1211 """handles the adding of the creator meta. 1212 1213 value is empty or a parsed meta value, this does nothing (which will cause 1214 addMeta to do its default operation). 1215 1216 If value is a non-empty string, it will be split along semicolons 1217 to produce individual creator metas with names . 1218 """ 1219 if not value or isinstance(value, MetaValue): 1220 return None 1221 1222 for authName in (s.strip() for s in value.split(";")): 1223 container.addMeta("creator", 1224 MetaValue().addMeta("name", authName)) 1225 1226 return True
1227
1228 1229 -def printMetaTree(metaContainer, curKey=""):
1230 #for debugging 1231 md = metaContainer.meta_ 1232 for childName in md: 1233 childKey = curKey+"."+childName 1234 for child in md[childName]: 1235 print(childKey, child.getContent("text")) 1236 printMetaTree(child, childKey)
1237
1238 1239 -def ensureMetaValue(val, moreAttrs={}):
1240 """makes a MetaValue out of val and a dict moreAttrs unless val already 1241 is a MetaValue. 1242 """ 1243 if isinstance(val, MetaValue): 1244 return val 1245 return MetaValue(val, **moreAttrs)
1246
1247 1248 -def ensureMetaItem(thing, moreAttrs={}):
1249 """ensures that thing is a MetaItem. 1250 1251 If it is not, thing is turned into a sequence of MetaValues, which is 1252 then packed into a MetaItem. 1253 1254 Essentially, if thing is not a MetaValue, it is made into one with 1255 moreAttrs. If thing is a list, this recipe is used for all its items. 1256 """ 1257 if isinstance(thing, MetaItem): 1258 return thing 1259 1260 if isinstance(thing, list): 1261 return MetaItem.fromSequence( 1262 [ensureMetaValue(item, moreAttrs) for item in thing]) 1263 1264 return MetaItem(ensureMetaValue(thing, moreAttrs))
1265
1266 1267 -def doMetaOverride(container, metaKey, metaValue, extraArgs={}):
1268 """creates the representation of metaKey/metaValue in container. 1269 1270 If metaKey does not need any special action, this returns None. 1271 1272 This gets called from one central point in MetaMixin.addMeta, and 1273 essentially all magic involved should be concentrated here. 1274 """ 1275 if metaKey in META_CLASSES_FOR_KEYS and not isinstance(metaValue, MetaValue): 1276 try: 1277 container.addMeta(metaKey, 1278 META_CLASSES_FOR_KEYS[metaKey](metaValue, **extraArgs)) 1279 return True 1280 except TypeError: 1281 raise utils.logOldExc(MetaError( 1282 "Invalid arguments for %s meta items: %s"%(metaKey, 1283 utils.safe_str(extraArgs)), None)) 1284 1285 # let's see if there's some way to rationalise this kind of thing 1286 # later. 1287 if metaKey=="creator": 1288 return _doCreatorMetaOverride(container, metaValue)
1289
1290 # fallthrough: let addMeta do its standard thing. 1291 1292 -def getMetaText(ob, key, default=None, **kwargs):
1293 """returns the meta item key form ob in text form if present, default 1294 otherwise. 1295 1296 You can pass getMeta keyword arguments (except default). 1297 1298 Additionally, there's acceptSequence; if set to true, this will 1299 return the first item of a sequence-valued meta item rather than 1300 raising an error. 1301 1302 ob will be used as a macro package if it has an expand method; to 1303 use something else as the macro package, pass a macroPackage keyword 1304 argument. 1305 """ 1306 macroPackage = ob 1307 if "macroPackage" in kwargs: 1308 macroPackage = kwargs.pop("macroPackage") 1309 acceptSequence = kwargs.get("acceptSequence", False) 1310 1311 m = ob.getMeta(key, default=None, **kwargs) 1312 if m is None: 1313 return default 1314 try: 1315 return m.getContent( 1316 macroPackage=macroPackage, acceptSequence=acceptSequence) 1317 except MetaCardError as ex: 1318 raise MetaCardError(ex.origMsg, carrier=ex.carrier, hint=ex.hint, 1319 key=key)
1320
1321 1322 -class MetaBuilder(object):
1323 """A base class for meta builders. 1324 1325 Builders are passed to a MetaItem's traverse method or to MetaMixin's 1326 buildRepr method to build representations of the meta information. 1327 1328 You can override startKey, endKey, and enterValue. If you are 1329 not doing anything fancy, you can get by by just overriding enterValue 1330 and inspecting curAtoms[-1] (which contains the last meta key). 1331 1332 You will want to override getResult. 1333 """
1334 - def __init__(self):
1335 self.curAtoms = []
1336
1337 - def startKey(self, key):
1338 self.curAtoms.append(key)
1339
1340 - def endKey(self, key):
1341 self.curAtoms.pop()
1342
1343 - def enterValue(self, value):
1344 pass
1345
1346 - def getResult(self):
1347 pass
1348
1349 1350 -class TextBuilder(MetaBuilder):
1351 """is a MetaBuilder that recovers a tuple sequence of the meta items 1352 in text representation. 1353 """
1354 - def __init__(self):
1355 self.metaItems = [] 1356 super(TextBuilder, self).__init__()
1357
1358 - def enterValue(self, value):
1359 self.metaItems.append((".".join(self.curAtoms), value.getContent()))
1360
1361 - def getResult(self):
1362 return self.metaItems
1363
1364 1365 -def stanFactory(tag, **kwargs):
1366 """returns a factory for ModelBasedBuilder built from a stan-like "tag". 1367 1368 Do *not* pass in instanciated tags -- they will just keep accumulating 1369 children on every model run. 1370 """ 1371 if isinstance(tag, stanxml.Element): 1372 raise utils.ReportableError("Do not use instanciated stanxml element" 1373 " in stanFactories. Instead, return them from a zero-argument" 1374 " function.") 1375 1376 def factory(args, localattrs=None): 1377 if localattrs: 1378 localattrs.update(kwargs) 1379 attrs = localattrs 1380 else: 1381 attrs = kwargs 1382 if isinstance(tag, type): 1383 el = tag 1384 else: # assume it's a function if it's not an element type. 1385 el = tag() 1386 return el(**attrs)[args]
1387 return factory 1388
1389 1390 # Within this abomination of code, the following is particularly nasty. 1391 # It *must* go. 1392 1393 -class ModelBasedBuilder(object):
1394 """is a meta builder that can create stan-like structures from meta 1395 information 1396 1397 It is constructed with with a tuple-tree of keys and DOM constructors; 1398 these must work like stan elements, which is, e.g., also true for our 1399 registrymodel elements. 1400 1401 Each node in the tree can be one of: 1402 1403 - a meta key and a callable, 1404 - this, and a sequence of child nodes 1405 - this, and a dictionary mapping argument names for the callable 1406 to meta keys of the node; the arguments extracted in this way 1407 are passed in a single dictionary localattrs. 1408 1409 The callable can also be None, which causes the corresponding items 1410 to be inlined into the parent (this is for flattening nested meta 1411 structures). 1412 1413 The meta key can also be None, which causes the factory to be called 1414 exactly once (this is for nesting flat meta structures). 1415 """
1416 - def __init__(self, constructors, format="text"):
1417 self.constructors, self.format = constructors, format
1418
1419 - def _buildNode(self, processContent, metaItem, children=(), 1420 macroPackage=None):
1421 if not metaItem: 1422 return [] 1423 result = [] 1424 for child in metaItem.children: 1425 content = [] 1426 c = child.getContent(self.format, macroPackage=macroPackage) 1427 if c: 1428 content.append(c) 1429 childContent = self._build(children, child, macroPackage) 1430 if childContent: 1431 content.append(childContent) 1432 if content: 1433 result.append(processContent(content, child)) 1434 return result
1435
1436 - def _getItemsForConstructor(self, metaContainer, macroPackage, 1437 key, factory, children=(), attrs={}):
1438 if factory: 1439 def processContent(childContent, metaItem): 1440 moreAttrs = {} 1441 for argName, metaKey in attrs.iteritems(): 1442 val = metaItem.getMeta(metaKey) 1443 if val: 1444 moreAttrs[argName] = val.getContent("text", 1445 macroPackage=macroPackage) 1446 return [factory(childContent, localattrs=moreAttrs)]
1447 else: 1448 def processContent(childContent, metaItem): #noflake: conditional def 1449 return childContent
1450 1451 if key is None: 1452 return [factory(self._build(children, metaContainer, macroPackage))] 1453 else: 1454 return self._buildNode(processContent, 1455 metaContainer.getMeta(key, raiseOnFail=False), children, 1456 macroPackage=macroPackage) 1457
1458 - def _build(self, constructors, metaContainer, macroPackage):
1459 result = [] 1460 for item in constructors: 1461 if isinstance(item, basestring): 1462 result.append(item) 1463 else: 1464 try: 1465 result.extend(self._getItemsForConstructor(metaContainer, 1466 macroPackage, *item)) 1467 except utils.Error: 1468 raise 1469 except: 1470 raise utils.logOldExc(utils.ReportableError( 1471 "Invalid constructor func in %s, meta container active %s"%( 1472 repr(item), repr(metaContainer)))) 1473 return result
1474
1475 - def build(self, metaContainer, macroPackage=None):
1476 if macroPackage is None: 1477 macroPackage = metaContainer 1478 return self._build(self.constructors, metaContainer, macroPackage)
1479