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

Source Code for Module gavo.base.attrdef

  1  """ 
  2  Attribute definitions for structures. 
  3   
  4  These are objects having at least the following attributes and methods: 
  5   
  6          - name -- will become the attribute name on the embedding class 
  7          - parseName -- the name of the XML/event element they can parse.  This 
  8                  usually is identical to name, but may differ for compound attributes 
  9          - default -- may be Undefined, otherwise a valid value of the 
 10                  expected type 
 11          - description -- for user documentation 
 12          - typeDesc -- describing the content; this is usually a class 
 13                  attribute and intended for user documentation 
 14          - before -- the name of another attribute the attribute should precede 
 15                  in XML serializations.  It is not an error to refer to an attribute 
 16                  that does not exist. 
 17          - feedObject(instance, ob) -> None -- adds ob to instance's attribute value. 
 18                  This will usually just result in setting the attribute; for compound 
 19                  attributes, this may instead append to a list, add to a set, etc. 
 20          - getCopy(instance, newParent, ctx) -> value -- returns the python value  
 21                  of the attribute in instance, copying mutable values (deeply) in the  
 22                  process. 
 23          - iterParentMethods() -> iter((name, value)) -- iterates over methods 
 24                  to be inserted into the parent class. 
 25          - makeUserDoc() -> returns some RST-valid string describing what the object 
 26                  is about. 
 27   
 28  They may have an attribute xmlName that allows parsing from xml elements 
 29  named differently from the attribute.  To keep things transparent, use 
 30  this sparingly; the classic use case is with lists, where you can call 
 31  an attribute options but have the XML element still be just "option". 
 32   
 33  AtomicAttributes, defined as those that are parsed from a unicode literal, 
 34  add methods 
 35   
 36          - feed(ctx, instance, literal) -> None -- arranges for literal to be parsed 
 37                  and passed to feedObject.  ctx is a parse context.  The built-in method 
 38                  does not expect anything from this object, but structure has a default  
 39                  implementation containing an idmap and a propery registry. 
 40          - parse(self, value) -> anything -- returns a python value for the 
 41                  unicode literal value 
 42          - unparse(self, value) -> unicode -- returns a unicode object representing 
 43                  value and parseable by parse to that value. 
 44   
 45  This is not enough for complex attributes.  More on those in the  
 46  base.complexattrs module. 
 47   
 48  AttributeDefs *may* have a validate(instance) method.  Structure instances 
 49  will call them when they are done building.  They should raise  
 50  LiteralParseErrors if it turns out a value that looked right is not after 
 51  all (in a way, they could catch validity rather than well-formedness violations, 
 52  but I don't think this distinction is necessary here). 
 53   
 54  See structure on how to use all these. 
 55  """ 
 56   
 57  #c Copyright 2008-2019, the GAVO project 
 58  #c 
 59  #c This program is free software, covered by the GNU GPL.  See the 
 60  #c COPYING file in the source distribution. 
 61   
 62   
 63  import os 
 64  import re 
 65   
 66  from gavo import utils 
 67  from gavo.utils import Undefined 
 68  from gavo.base import literals 
 69  from gavo.base.common import LiteralParseError, NotGiven 
70 71 72 -class Recursive(object):
73 """a sentinel class for attributes embedding structures to signify 74 they embed the structure embedding them. 75 """ 76 name_ = "RECURSIVE"
77
78 79 -class Computed(object):
80 """A sentinel class for computed (property) defaults. 81 82 Use this to construct AttributeDefs with defaults that are properties 83 to inhibit assigning to them. This should only be required in calls 84 of the superclass's init. 85 """
86 87 88 # Values for which no special stringification for docs is attempted 89 _nullLikeValues = set([None, Undefined, NotGiven])
90 91 92 -class AttributeDef(object):
93 """is the base class for all attribute definitions. 94 95 See above. 96 97 The data attribute names have all an underscore added to avoid name 98 clashes -- structures should have about the same attributes and may 99 want to have managed attributes called name or description. 100 101 When constructing AttributeDefs, you should only use keyword 102 arguments, except for name (the first argument). 103 104 Note that an AttributeDef might be embedded by many instances. So, 105 you must *never* store any instance data in an AttributeDef (unless 106 it's really a singleton, of course). 107 """ 108 109 typeDesc_ = "unspecified, invalid" 110
111 - def __init__(self, name, default=None, description="Undocumented", 112 copyable=False, aliases=None, callbacks=None, before=None):
113 self.name_, self.description_ = name, description 114 self.copyable = copyable 115 self.aliases = aliases 116 self.callbacks = callbacks 117 self.before = before 118 if default is not Computed: 119 self.default_ = default
120
121 - def iterParentMethods(self):
122 """returns an iterator over (name, method) pairs that should be 123 inserted in the parent class. 124 """ 125 return iter([])
126
127 - def doCallbacks(self, instance, value):
128 """should be called after feedObject has done its work. 129 """ 130 if self.callbacks: 131 for cn in self.callbacks: 132 getattr(instance, cn)(value)
133
134 - def feedObject(self, instance, value):
135 raise NotImplementedError("%s doesn't implement feeding objects"% 136 self.__class__.__name__)
137
138 - def feed(self, ctx, instance, value):
139 raise NotImplementedError("%s doesn't implement feeding literals"% 140 self.__class__.__name__)
141
142 - def getCopy(self, instance, newParent, ctx):
143 raise NotImplementedError("%s cannot be copied."% 144 self.__class__.__name__)
145
146 - def makeUserDoc(self):
147 return "**%s** (%s; defaults to %s) -- %s"%( 148 self.name_, self.typeDesc_, repr(self.default_), self.description_)
149
150 151 -class AtomicAttribute(AttributeDef):
152 """A base class for attributes than can be immediately parsed 153 and unparsed from strings. 154 155 They need to provide a parse method taking a unicode object and 156 returning a value of the proper type, and an unparse method taking 157 a value of the proper type and returning a unicode string suitable 158 for parse. 159 160 Note that you can, of course, assign to the attribute directly. 161 If you assign crap, the unparse method is explicitely allowed 162 to bomb in random ways; it just has to be guaranteed to work 163 for values coming from parse (i.e.: user input is checked, 164 programmatic input can blow up the thing; I consider this 165 pythonesque :-). 166 """
167 - def parse(self, value):
168 """returns a typed python value for the string representation value. 169 170 value can be expected to be a unicode string. 171 """ 172 raise NotImplementedError("%s does not define a parse method"% 173 self.__class__.__name__)
174
175 - def unparse(self, value):
176 """returns a typed python value for the string representation value. 177 178 value can be expected to be a unicode string. 179 """ 180 raise NotImplementedError("%s does not define an unparse method"% 181 self.__class__.__name__)
182
183 - def feed(self, ctx, instance, value):
184 self.feedObject(instance, self.parse(value))
185
186 - def feedObject(self, instance, value):
187 setattr(instance, self.name_, value) 188 self.doCallbacks(instance, value)
189
190 - def getCopy(self, instance, newParent, ctx):
191 # We assume atoms are immutable here 192 return getattr(instance, self.name_)
193
194 - def makeUserDoc(self):
195 default = self.default_ 196 try: 197 if default not in _nullLikeValues: 198 default = self.unparse(default) 199 except TypeError: # unhashable defaults can be unparsed 200 default = self.unparse(default) 201 return "**%s** (%s; defaults to %s) -- %s"%( 202 self.name_, self.typeDesc_, repr(default), self.description_)
203
204 205 -class RawAttribute(AtomicAttribute):
206 """An attribute definition that does no parsing at all. 207 208 This is only useful in "internal" structures that never get 209 serialized or deserialized. 210 """
211 - def parse(self, value):
212 return value
213
214 - def unparse(self, value):
215 return value
216
217 218 -class UnicodeAttribute(AtomicAttribute):
219 """An attribute definition for an item containing a unicode string. 220 221 In addition to AtomicAttribute's keywords, you can use ``strip`` (default 222 false) to have leading and trailing whitespace be removed on parse. 223 (Unparsing will not add it back). 224 225 You can also add ``expand`` (default False) to have UnicodeAttribute 226 try and expand RD macros on the instance passed in. This of course 227 only works if the attribute lives on a class that is a MacroPackage. 228 """ 229 230 typeDesc_ = "unicode string" 231
232 - def __init__(self, name, **kwargs):
233 self.nullLiteral = kwargs.pop("null", "__NULL__") 234 self.strip = kwargs.pop("strip", False) 235 self.expand = kwargs.pop("expand", False) 236 AtomicAttribute.__init__(self, name, **kwargs)
237
238 - def parse(self, value):
239 if value==self.nullLiteral: 240 return None 241 if self.strip: 242 value = value.strip() 243 return value
244
245 - def unparse(self, value):
246 if value is None: 247 if self.nullLiteral is None: 248 raise ValueError("Unparse None without a null literal can't work.") 249 return self.nullLiteral 250 return value
251
252 - def feed(self, ctx, instance, value):
253 if self.expand and "\\" in value: 254 value = instance.expand(value) 255 self.feedObject(instance, self.parse(value))
256
257 258 -class NWUnicodeAttribute(UnicodeAttribute):
259 """A UnicodeAttribute that has its whitespace normalized. 260 261 Normalization consists of stripping whitespace at the ends and replacing 262 any runs or internal whitespace by a single blank. The whitespace 263 will not be added back on unparsing. 264 """ 265 typeDesc_ = "whitespace normalized unicode string" 266
267 - def parse(self, value):
268 value = UnicodeAttribute.parse(self, value) 269 if value is None: 270 return value 271 return re.sub("\s+", " ", value.strip())
272
273 274 -class RelativePathAttribute(UnicodeAttribute):
275 """A (utf-8 encoded) path relative to some base path. 276 """ 277 typeDesc_ = "relative path" 278
279 - def __init__(self, name, default=None, basePath="", 280 description="Undocumented"):
281 UnicodeAttribute.__init__(self, name, default=default, 282 description=description, strip=True) 283 self.basePath = basePath
284
285 - def parse(self, value):
286 return os.path.join(self.basePath, value).encode("utf-8")
287
288 - def unparse(self, value):
289 return value.decode("utf-8")[len(self.basePath)+1:]
290
291 292 -class FunctionRelativePathAttribute(UnicodeAttribute):
293 """A (utf-8 encoded) path relative to the result of some function 294 at runtime. 295 296 This is used to make things relative to config items. 297 """
298 - def __init__(self, name, baseFunction, default=None, 299 description="Undocumented", **kwargs):
300 kwargs["strip"] = kwargs.get("strip", True) 301 UnicodeAttribute.__init__(self, name, default=default, 302 description=description, **kwargs) 303 self.baseFunction = baseFunction 304 self.hiddenAttName = "_real_"+self.name_
305
306 - def parse(self, value):
307 return value.encode("utf-8")
308
309 - def unparse(self, value):
310 return value.decode("utf-8") # XXX TODO: make this relative again
311
312 - def iterParentMethods(self):
313 def computePath(instance): 314 relative = getattr(instance, self.hiddenAttName) 315 if relative is NotGiven or relative is None: 316 return relative 317 return os.path.join(self.baseFunction(instance), relative)
318 def setRelative(instance, value): 319 setattr(instance, self.hiddenAttName, value)
320 yield (self.name_, property(computePath, setRelative)) 321
322 323 -class EnumeratedUnicodeAttribute(UnicodeAttribute):
324 """An attribute definition for an item that can only take on one 325 of a finite set of values. 326 """
327 - def __init__(self, name, default, validValues, **kwargs):
328 kwargs["strip"] = kwargs.get("strip", True) 329 UnicodeAttribute.__init__(self, name, default=default, **kwargs) 330 self.validValues = set(validValues)
331 332 @property
333 - def typeDesc_(self):
334 return "One of: %s"%", ".join(self.validValues)
335
336 - def parse(self, value):
337 value = UnicodeAttribute.parse(self, value) 338 if not value in self.validValues: 339 raise LiteralParseError(self.name_, value, 340 hint="Valid values include %s"%",".join(self.validValues)) 341 return value
342
343 344 -class IntAttribute(AtomicAttribute):
345 """An attribute definition for integer attributes. 346 """ 347 348 typeDesc_ = "integer" 349
350 - def parse(self, value):
351 try: 352 return int(value) 353 except ValueError: 354 raise utils.logOldExc( 355 LiteralParseError(self.name_, value, hint="Value must be an" 356 " integer literal."))
357
358 - def unparse(self, value):
359 return str(value)
360
361 362 -class FloatAttribute(AtomicAttribute):
363 """An attribute definition for floating point attributes. 364 """ 365 366 typeDesc_ = "float" 367
368 - def parse(self, value):
369 try: 370 return float(value) 371 except ValueError: 372 raise utils.logOldExc( 373 LiteralParseError(self.name_, value, hint="value must be a float" 374 " literal"))
375
376 - def unparse(self, value):
377 return str(value)
378
379 380 -class BooleanAttribute(AtomicAttribute):
381 """A boolean attribute. 382 383 Boolean literals are strings like True, false, on, Off, yes, No in 384 some capitalization. 385 """ 386 typeDesc_ = "boolean" 387
388 - def parse(self, value):
389 try: 390 return literals.parseBooleanLiteral(value) 391 except ValueError: 392 raise utils.logOldExc(LiteralParseError(self.name_, value, hint= 393 "A boolean literal (e.g., True, False, yes, no) is expected here."))
394
395 - def unparse(self, value):
396 return {True: "True", False: "False"}[value]
397
398 399 -class StringListAttribute(UnicodeAttribute):
400 """An attribute containing a list of comma separated strings. 401 402 The value is a list. This is similar to a complexattrs.ListOfAtoms 403 with UnicodeAttribute items, except the literal is easier to write 404 but more limited. Use this for the user's convenience. 405 """ 406 typeDesc_ = "Comma-separated list of strings" 407 realDefault = [] 408
409 - def __init__(self, name, **kwargs):
410 if "default" in kwargs: 411 self.realDefault = kwargs.pop("default") 412 UnicodeAttribute.__init__(self, name, default=Computed, **kwargs)
413
414 - def parse(self, value):
415 value = UnicodeAttribute.parse(self, value) 416 res = [str(name.strip()) 417 for name in value.split(",") if name.strip()] 418 return res
419 420 @property
421 - def default_(self):
422 try: 423 return self.realDefault[:] 424 except TypeError: # Not iterable; that's the client's problem. 425 return self.realDefault
426
427 - def unparse(self, value):
428 return ", ".join(value)
429
430 431 -class StringSetAttribute(StringListAttribute):
432 """A StringListAttribute, except the result is a set. 433 """ 434 realDefault = set() 435
436 - def parse(self, value):
437 return set(StringListAttribute.parse(self, value))
438 439 @property
440 - def default_(self):
441 return self.realDefault.copy()
442
443 444 -class IdMapAttribute(AtomicAttribute):
445 """An attribute allowing a quick specification of identifiers to 446 identifiers. 447 448 The literal format is <id>:<id>{,<id>:<id>},? with ignored whitespace. 449 """ 450 typeDesc_ = "Comma-separated list of <identifer>:<identifier> pairs" 451
452 - def parse(self, val):
453 if val is None: 454 return None 455 val = val.strip().rstrip(",") 456 try: 457 return dict((k.strip(), v.strip()) 458 for k,v in (p.split(":") for p in val.split(","))) 459 except ValueError: 460 raise utils.logOldExc(LiteralParseError(self.name_, val, 461 hint="A key-value enumeration of the format k:v {,k:v}" 462 " is expected here"))
463
464 - def unparse(self, val):
465 if val is None: 466 return None 467 return ", ".join(["%s: %s"%(k, v) for k, v in val.iteritems()])
468
469 470 -class ActionAttribute(UnicodeAttribute):
471 """An attribute definition for attributes triggering a method call 472 on the parent instance. 473 474 They do create an attribute on parent which is None by default 475 and the attribute value as a unicode string once the attribute 476 was encountered. This could be used to handle multiple occurrences 477 but is not in this basic definition. 478 """
479 - def __init__(self, name, methodName, description="Undocumented", 480 **kwargs):
481 kwargs["strip"] = kwargs.get("strip", True) 482 self.methodName = methodName 483 UnicodeAttribute.__init__(self, name, default=None, 484 description=description, **kwargs)
485
486 - def feed(self, ctx, instance, value):
487 UnicodeAttribute.feed(self, ctx, instance, value) 488 getattr(instance, self.methodName)(ctx)
489 490 491 # __init__ does an import * from this. You shouldn't. 492 493 __all__ = ["LiteralParseError", "Undefined", "UnicodeAttribute", 494 "IntAttribute", "BooleanAttribute", "AtomicAttribute", 495 "EnumeratedUnicodeAttribute", "AttributeDef", "Computed", 496 "RelativePathAttribute", "FunctionRelativePathAttribute", 497 "StringListAttribute", "ActionAttribute", "FloatAttribute", 498 "StringSetAttribute", "NotGiven", "IdMapAttribute", 499 "NWUnicodeAttribute", "RawAttribute"] 500