Package gavo :: Package dm :: Module common
[frames] | no frames]

Source Code for Module gavo.dm.common

  1  """ 
  2  Common code for new-style Data Model support. 
  3   
  4  In particular, this defines a hierachy of Annotation objects.  The annotation 
  5  of DaCHS tables is an ObjectAnnotation, the other Annotation classes 
  6  (conceptually, all are key-value pairs) make up their inner structure. 
  7  """ 
  8   
  9  #c Copyright 2008-2019, the GAVO project 
 10  #c 
 11  #c This program is free software, covered by the GNU GPL.  See the 
 12  #c COPYING file in the source distribution. 
 13   
 14   
 15  import contextlib 
 16  import re 
 17  import weakref 
 18   
 19  from gavo import utils 
 20  from gavo import votable 
 21  from gavo.votable import V 
 22   
 23   
 24  VODML_NAME = "vo-dml" 
25 26 27 @contextlib.contextmanager 28 -def containerTypeSet(ctx, typeName):
29 """a context manager to control the type currently serialised in a VOTable. 30 31 ctx is a VOTable serialisation context (that we liberally hack into). 32 """ 33 if not hasattr(ctx, "_dml_typestack"): 34 ctx._dml_typestack = [] 35 ctx._dml_typestack.append(typeName) 36 try: 37 yield 38 finally: 39 ctx._dml_typestack.pop()
40
41 42 -def completeVODMLId(ctx, roleName):
43 """completes roleName to a full (standard) vo-dml id. 44 45 This is based on what the containerTypeSet context manager leaves 46 in the VOTable serialisation context ctx. 47 """ 48 return roleName 49 50 # current spec wants hierarchical strings here. Let's not. 51 if ":" in roleName: 52 # we allow the use of fully qualified role names and don't touch them 53 return roleName 54 return "%s.%s"%(ctx._dml_typestack[-1], roleName)
55
56 57 -def parseTypeName(typename):
58 """returns modelname, package (None for the empty package), name 59 for a VO-DML type name. 60 61 Malformed names raise a ValueError. 62 63 >>> parseTypeName("dm:type") 64 ('dm', None, 'type') 65 >>> parseTypeName("dm:pck.type") 66 ('dm', 'pck', 'type') 67 >>> parseTypeName(":malformed.typeid") 68 Traceback (most recent call last): 69 ValueError: ':malformed.typeid' is not a valid VO-DML type name 70 """ 71 mat = re.match("([\w_-]+):(?:([\w_-]+)\.)?([\w_-]+)", typename) 72 if not mat: 73 raise ValueError("'%s' is not a valid VO-DML type name"%typename) 74 return mat.groups()
75
76 77 -class AnnotationBase(object):
78 """A base class for of structs. 79 80 Basically, these are pairs of a role name and something else, which 81 depends on the actual subclass (e.g., an atomic value, a reference, 82 a sequence of key-value pairs, a sequence of other objects, ...). 83 84 They have a method getVOT(ctx, instance) -> xmlstan, which, using a 85 votablewrite.Context ctx, will return mapping-document conformant VOTable 86 xmlstan; instance is the rsc/rscdef structure the annotation is produced 87 for. 88 89 Use asSIL() to retrieve a simple string representation. 90 91 Compund annotations (sequences, key-value pairs) should use 92 add(thing) to build themselves up. 93 94 AnnotationBase is abstract and doesn't implement some of these methods. 95 """
96 - def __init__(self, name, instance):
97 self.name = name 98 if instance is None: # should only be true for the root 99 self.instance = instance 100 else: 101 self.instance = weakref.ref(instance)
102
103 - def getVOT(self, ctx, instance):
104 raise NotImplementedError("%s cannot be serialised (override getVOT)."% 105 self.__class__.__name__)
106
107 - def asSIL(self):
108 raise NotImplementedError("%s cannot be serialised (override asSIL)."% 109 self.__class__.__name__)
110
111 - def add(self, thing):
112 raise ValueError( 113 "%s is not a compound annotation."%self.__class__.__name__)
114
115 - def iterNodes(self):
116 yield self
117
118 119 -class TableRelativeAnnotation(AnnotationBase):
120 """A base class for annotations that must be adapted or discarded when 121 an annotation is copied. 122 """
123 # This currently is only used as a sentinel for such annotations.
124 # Perhaps we should modify copy here? 125 126 127 -class AtomicAnnotation(AnnotationBase):
128 """An annotation of an atomic value, i.e., a key-value pair. 129 130 These can take optional metadata. 131 """
132 - def __init__(self, name=None, value=None, unit=None, 133 ucd=None, instance=None):
134 AnnotationBase.__init__(self, name, instance) 135 self.value, self.unit, self.ucd = value, unit, ucd
136
137 - def copy(self, newInstance):
138 return self.__class__( 139 self.name, self.value, self.unit, self.ucd, 140 newInstance)
141
142 - def getVOT(self, ctx, instance):
143 # if we have additional metadata, serialise this using a param 144 if self.unit or self.ucd: 145 attrs = votable.guessParamAttrsForValue(self.value) 146 attrs.update({ 147 "unit": self.unit, 148 "ucd": self.ucd}) 149 150 param = V.PARAM(name=self.name, 151 id=ctx.getOrMakeIdFor(self.value), 152 dmrole=completeVODMLId(ctx, self.name), 153 **attrs) 154 ctx.getEnclosingContainer()[ 155 votable.serializeToParam(param, self.value)] 156 return V.CONSTANT(ref=param.id) 157 158 else: 159 # no additional metadata, just dump a literal string 160 # (technically, we should use ivoa: types. Oh, bother, another 161 # type system, just what we need). 162 return V.LITERAL(dmtype="ivoa:string")[ 163 utils.safe_str(self.value)]
164 165
166 - def asSIL(self, suppressType=False):
167 if suppressType: 168 return self.value.asSIL() 169 else: 170 return "%s: %s"%(self.name, self.value.asSIL())
171
172 173 -class _WithMapCopyMixin(object):
174 """A mixin furnishing a class with a copyWithAnnotationMap method. 175 176 The class mixing this in must provide an iterator iterChildRoles 177 yielding child annotations one by one. 178 179 Every compound annotation must mix this in in order to provide 180 halfway sane copying semantics when columns get re-mixed in new 181 tables (which we do all the time). 182 183 We also expect a method copyEmpty(i) that returns an instance of the 184 Annotation but without any child annotations. 185 186 In return, this will furnish a copy(i) method based on copyEmpty 187 and iterChildRoles. 188 """
189 - def copyWithAnnotationMap(self, annotationMap, container, instance):
190 """returns a copy of this annotation, with annotations mentioned 191 in annotationMap replaced. 192 193 Table-related annotations (currently, Param- and ColumnAnnotations) 194 not mentioned in annotationMap) will be discarded. 195 196 This is used when annotation tables with columns copied from 197 other tables. annotationMap, normally generated by 198 dmrd.SynthesizedRoles, then maps the old annotations to the elements 199 that should be annotated in the new table. 200 """ 201 copy = self.copyEmpty(instance) 202 if instance is None: # I'm the new root 203 instance = copy 204 205 for role in self.iterChildRoles(): 206 207 if role in annotationMap: 208 copy.add( 209 annotationMap[role].getAnnotation( 210 role.name, container, instance)) 211 212 elif isinstance(role, TableRelativeAnnotation): 213 pass 214 215 elif hasattr(role, "copyWithAnnotationMap"): 216 copy.add(role.copyWithAnnotationMap( 217 annotationMap, container, instance)) 218 219 else: 220 copy.add(role.copy(instance)) 221 222 return copy
223
224 - def copy(self, newInstance):
225 return self.copyWithAnnotationMap({}, None, newInstance)
226
227 228 -class _AttributeGroupAnnotation(AnnotationBase, _WithMapCopyMixin):
229 """An internal base class for DatatypeAnnotation and ObjectAnnotation. 230 """
231 - def __init__(self, name, type, instance):
232 AnnotationBase.__init__(self, name, instance) 233 self.type = type 234 if self.type is None: 235 # TODO: infer from parent? from DM? 236 self.modelPrefix = getattr(instance, "modelPrefix", "undefined") 237 else: 238 self.modelPrefix, _, _ = parseTypeName(self.type) 239 240 self.childRoles = {}
241
242 - def __getitem__(self, key):
243 child = self.childRoles.__getitem__(key) 244 if isinstance(child, AtomicAnnotation): 245 return child.value 246 else: 247 return child
248
249 - def get(self, key, default=None):
250 if key not in self: 251 return default 252 return self[key]
253
254 - def __contains__(self, key):
255 return key in self.childRoles
256
257 - def copyEmpty(self, newInstance):
258 return self.__class__(self.name, self.type, newInstance)
259
260 - def iterChildRoles(self):
261 return self.childRoles.itervalues()
262
263 - def add(self, role):
264 assert role.name not in self.childRoles 265 self.childRoles[role.name] = role
266
267 - def asSIL(self, suppressType=False):
268 if suppressType or self.type is None: 269 typeAnn = "" 270 else: 271 typeAnn = "(%s) "%self.type 272 273 return "%s{\n %s}\n"%(typeAnn, 274 "\n ".join(r.asSIL() for r in self.childRoles.values()))
275
276 - def getVOT(self, ctx, instance):
277 """helps getVOT. 278 """ 279 ctx.addVODMLPrefix(self.modelPrefix) 280 ctx.groupIdsInTree.add(id(self)) 281 res = V.INSTANCE( 282 dmtype=self.type, 283 ID=ctx.getOrMakeIdFor(self)) 284 285 for role, ann in self.childRoles.iteritems(): 286 res[V.ATTRIBUTE(dmrole=completeVODMLId(ctx, role))[ 287 ann.getVOT(ctx, instance)]] 288 289 return res
290
291 292 -class DatatypeAnnotation(_AttributeGroupAnnotation):
293 """An annotation for a datatype. 294 295 Datatypes are essentially simple groups of attributes; they are used 296 *within* objects (e.g., to group photometry points, or positions, or 297 the like. 298 """
299
300 301 -class ObjectAnnotation(_AttributeGroupAnnotation):
302 """An annotation for an object. 303 304 Objects are used for actual DM instances. In particular, 305 every annotation of a DaCHS table is rooted in an object. 306 """
307
308 309 -class CollectionAnnotation(AnnotationBase, _WithMapCopyMixin):
310 """A collection contains 0..n things of the same type. 311 """
312 - def __init__(self, name, type, instance):
313 AnnotationBase.__init__(self, name, instance) 314 self.type = type 315 # these can have atomic children, in which case we don't manage types 316 if self.type is not None: 317 self.modelPrefix, _, _ = parseTypeName(type) 318 self.children = []
319
320 - def __getitem__(self, index):
321 child = self.children[index] 322 if isinstance(child, AtomicAnnotation): 323 return child.value 324 else: 325 return child
326
327 - def __len__(self):
328 return len(self.children)
329
330 - def iterChildRoles(self):
331 return iter(self.children)
332
333 - def copyEmpty(self, newInstance):
334 return self.__class__(self.name, self.type, newInstance)
335
336 - def add(self, child):
337 self.children.append(child)
338
339 - def asSIL(self):
340 if self.type is None: 341 opener = "[" 342 else: 343 opener = "(%s) ["%(self.type,) 344 345 bodyItems = [] 346 for r in self.children: 347 bodyItems.append(r.asSIL(suppressType="True")) 348 349 return "%s: \n %s%s]\n"%( 350 self.name, 351 opener, 352 "\n ".join(bodyItems))
353
354 - def getVOT(self, ctx, instance):
355 # So... it's unclear at this point what to do here -- I somehow feel 356 # we should serialise collections into a table. But then this would 357 # entail one table each whenever an attribute is potentially sequence-valued, 358 # and that doesn't seem right either. So, we'll dump multiple things 359 # into one attribute for now and see how far we get with than. 360 if self.type: 361 ctx.addVODMLPrefix(self.modelPrefix) 362 return [c.getVOT(ctx, instance) for c in self.children]
363
364 365 -def _test():
366 import doctest, common 367 doctest.testmod(common)
368 369 370 if __name__=="__main__": 371 _test() 372