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

Source Code for Module gavo.base.macros

  1  """ 
  2  A macro mechanism primarily for string replacement in resource descriptors. 
  3  """ 
  4   
  5  #c Copyright 2008-2019, the GAVO project 
  6  #c 
  7  #c This program is free software, covered by the GNU GPL.  See the 
  8  #c COPYING file in the source distribution. 
  9   
 10   
 11  import datetime 
 12  import re 
 13  import urllib 
 14   
 15  from pyparsing import ( 
 16          ZeroOrMore, Forward, 
 17          Regex, Suppress, 
 18          Literal) 
 19   
 20   
 21  from gavo import utils 
 22  from gavo.base import attrdef 
 23  from gavo.base import common 
 24  from gavo.base import complexattrs 
 25  from gavo.base import config 
 26  from gavo.base import meta 
 27  from gavo.base import osinter 
 28  from gavo.base import structure 
 29   
 30   
31 -class MacroError(common.StructureError):
32 """is raised when something bad happens during macro expansion. 33 34 It is constructed with an error message, a macro name, and optionally 35 a hint and a position. 36 """
37 - def __init__(self, message, macroName, hint=None, pos=None):
38 common.StructureError.__init__( 39 self, macroName+" failed", pos=pos, hint=hint) 40 self.args = [message, macroName, hint, pos] 41 self.macroName, self.message = macroName, message
42
43 - def __str__(self):
44 return "Error during macro expansion: %s"%( 45 self.message)
46 47
48 -class MacroExpander(object):
49 """is a generic "macro" expander for scripts of all kinds. 50 51 It is loosely inspired by TeX, but of course much simpler. See the 52 syntax below. 53 54 The macros themselves come from a MacroPackage object. There are 55 a few of these around, implementing different functionality depending 56 on the script context (i.e., whether it belongs to an RD, a DD, or 57 a Table. 58 59 All macros are just functions receiving and returning strings. The 60 arguments are written as {arg1}{arg2}, where you can escape curly 61 braces with a backslash. There must be no whitespace between 62 a macro and its first argument. 63 64 If you need to glue together a macro expansion and text following, 65 use the glue sequence \\+ 66 67 The main entry point to the class is the expand function below, 68 taking a string possibly containing macro calls and returning 69 a string. 70 71 The construction of such a macro expander is relatively expensive, 72 so it pays to cache them. MacroPackage below has a getExpander 73 method that does the caching for you. 74 """
75 - def __init__(self, package):
76 self.package = package 77 self._macroGrammar = self._getMacroGrammar()
78
79 - def _execMacro(self, s, loc, toks):
80 toks = toks.asList() 81 macName, args = toks[0], toks[1:] 82 return self.package.execMacro(macName, args)
83
84 - def expand(self, aString):
85 return utils.pyparseTransform(self._macroGrammar, aString)
86
87 - def _getMacroGrammar(self, debug=False):
88 with utils.pyparsingWhitechars(" \t"): 89 macro = Forward() 90 quoteEscape = (Literal("\\{").addParseAction(lambda *args: "{") | 91 Literal("\\}").addParseAction(lambda *args: "}")) 92 charRun = Regex(r"[^}\\]+").leaveWhitespace() 93 argElement = macro | quoteEscape | charRun 94 argument = Suppress("{") + ZeroOrMore(argElement) + Suppress("}") 95 argument.addParseAction(lambda s, pos, toks: "".join(toks)) 96 arguments = ZeroOrMore(argument) 97 arguments.setWhitespaceChars("") 98 macroName = Regex("[A-Za-z_][A-Za-z_0-9]+") 99 macroName.setWhitespaceChars("") 100 macro << Suppress( "\\" ) + macroName + arguments 101 macro.addParseAction(self._execMacro) 102 literalBackslash = Literal("\\\\") 103 literalBackslash.addParseAction(lambda *args: "\\") 104 suppressedLF = Literal("\\\n") 105 suppressedLF.addParseAction(lambda *args: " ") 106 glue = Literal("\\+") 107 glue.addParseAction(lambda *args: "") 108 return literalBackslash | suppressedLF | glue | macro
109 110
111 -class ExpansionDelegator(object):
112 """A mixin to make a class expand macros by delegating everything to 113 its parent. 114 115 This is intended for base.Structures that have a parent attribute; 116 by mixing this in, they use their parents to expand macros for them. 117 """
118 - def expand(self, aString):
119 return self.parent.expand(aString)
120 121
122 -class MacroPackage(object):
123 r"""is a function dispatcher for MacroExpander. 124 125 Basically, you inherit from this class and define macro_xxx functions. 126 MacroExpander can then call \xxx, possibly with arguments. 127 """
128 - def __findMacro(self, macName):
129 fun = getattr(self, "macro_"+macName, None) 130 if fun is not None: 131 return fun 132 if hasattr(self, "rd"): 133 fun = getattr(self.rd, "macro_"+macName, None) 134 if fun is not None: 135 return fun 136 raise MacroError( 137 "No macro \\%s available in a %s context"%( 138 macName, self.__class__.__name__), 139 macName, hint="%s objects have the following macros: %s."%( 140 self.__class__.__name__, ", ".join(self.listMacros())))
141
142 - def listMacros(self):
143 return [n[6:] for n in dir(self) if n.startswith("macro_")]
144
145 - def execMacro(self, macName, args):
146 fun = self.__findMacro(macName) 147 try: 148 return fun(*args) 149 except TypeError: 150 raise utils.logOldExc(MacroError( 151 "Invalid macro arguments to \\%s: %s"%(macName, args), macName, 152 hint="You supplied too few or too many arguments")) 153 except utils.Error: 154 raise 155 except Exception as msg: 156 argRepr = "}{".join(utils.safe_str(a) for a in args) 157 if argRepr: 158 argRepr = "{%s}"%argRepr 159 160 raise utils.logOldExc(MacroError( 161 "While expanding macro \\%s%s: %s"%(macName, argRepr, msg), 162 macName, 163 hint="This means that the code dealing with your arguments" 164 " was throroughly confused by what you passed. If you really" 165 " cannot see why it was, file a bug."))
166
167 - def getExpander(self):
168 try: 169 return self.__macroExpander 170 except AttributeError: 171 self.__macroExpander = MacroExpander(self) 172 return self.getExpander()
173
174 - def expand(self, stuff):
175 return self.getExpander().expand(stuff)
176
177 - def macro_quote(self, arg):
178 """returns the argument in quotes (with internal quotes backslash-escaped 179 if necessary). 180 """ 181 return '"%s"'%(arg.replace('"', '\\"'))
182
183 - def macro_sqlquote(self, arg):
184 """returns the argument as a quoted string, unless it is 'NULL' or 185 None, in which case just NULL is returned. 186 """ 187 if arg is None or arg=='NULL': 188 return "NULL" 189 return "'%s'"%arg.replace("'", "''")
190
191 - def macro_sql_standardPubDID(self, fromCol="accref"):
192 """returns a SQL expression returning a DaCHS standard pubDID generated 193 from the accref (or something overridden) column. 194 195 This is convenient in obscore or ssa views when the underlying table 196 just has accrefs. If your code actually uses the pubDID to search 197 in the table (and it probably shouldn't), better use an actual column 198 and index it. 199 """ 200 auth = config.get("ivoa", "authority") 201 return "'ivo://%s/~?' || gavo_urlescape(%s)"%( 202 auth.replace("'", "''"), 203 fromCol)
204
205 - def macro_reSub(self, pattern, replacement, string):
206 """returns the string with the python RE pattern replaced with 207 replacement. 208 209 This is directly handed through to python re.sub, so you can (but 210 probably shouldn't) play all the RE tricks you can in python (e.g., 211 back references). 212 213 If you find yourself having to use reSub, you should regard that as 214 an alarm sign that you're probably doing it wrong. 215 216 Oh: closing curly braces can be included in the argument by 217 backslash-escaping them. 218 """ 219 return re.sub(pattern, replacement, string)
220 221
222 -class StandardMacroMixin(MacroPackage):
223 """is a mixin providing some macros for scripting's MacroExpander. 224 225 The class mixing in needs to provide its resource descriptor in the 226 rd attribute. 227 """
228 - def macro_magicEmpty(self, val):
229 """returns __EMPTY__ if val is empty. 230 231 This is necessary when feeding possibly empty params from mixin 232 parameters (don't worry if you don't understand this). 233 """ 234 if val: 235 return val 236 else: 237 return "__EMPTY__"
238
239 - def macro_rdId(self):
240 """the identifier of the current resource descriptor. 241 """ 242 return self.rd.sourceId
243
244 - def macro_rdIdDotted(self):
245 """the identifier for the current resource descriptor with slashes replaced 246 with dots (so they work as the "host part" in URIs. 247 """ 248 return self.rd.sourceId.replace("/", ".")
249
250 - def macro_schema(self):
251 """the schema of the current resource descriptor. 252 """ 253 return self.rd.schema
254 264
265 - def macro_RSTtable(self, tableName):
266 """adds an reStructured test link to a tableName pointing to its table 267 info. 268 """ 269 return "`%s <%s>`_"%(tableName, 270 osinter.makeSitePath("tableinfo/%s"%tableName))
271 276
277 - def macro_urlquote(self, string):
278 """wraps urllib.quote. 279 """ 280 return urllib.quote(string)
281
282 - def macro_today(self):
283 """today's date in ISO representation. 284 """ 285 return str(datetime.date.today())
286
287 - def macro_getConfig(self, section, name=None):
288 """the current value of configuration item {section}{name}. 289 290 You can also only give one argument to access settings from the 291 general section. 292 """ 293 if name is None: 294 section, name = "general", section 295 val = config.get(section, name) 296 if isinstance(val, basestring): 297 return val 298 else: 299 return str(val)
300
301 - def macro_metaString(self, metaKey, default=None):
302 """the value of metaKey on the macro expander. 303 304 This will raise an error when the meta Key is not available unless 305 you give a default. It will also raise an error if metaKey is not 306 atomic (i.e., single-valued). Use metaSeq for meta items that 307 may have multiple values. 308 """ 309 try: 310 try: 311 val = self.getMeta(metaKey, raiseOnFail=True) 312 except meta.NoMetaKey: 313 if default is not None: 314 return default 315 raise 316 317 return val.getContent(macroPackage=self 318 ).replace("\n", " ") # undo default line breaking 319 320 except meta.MetaError as exc: 321 exc.carrier = self 322 exc.key = metaKey 323 if hasattr(self, "getSourcePosition"): 324 exc.pos = self.getSourcePosition() 325 raise
326
327 - def macro_metaSeq(self, metaKey, default='', joiner=', '):
328 """returns all values of metaKey on the current macro expander joined 329 by joiner. 330 331 This will be an empty string if there is no corresponding metadata (or 332 default, if passed). 333 """ 334 vals = list(self.iterMeta(metaKey, propagate=True)) 335 if vals: 336 return joiner.join(unicode(val) for val in vals) 337 else: 338 return default
339 340
341 - def macro_upper(self, aString):
342 """returns aString uppercased. 343 344 There's no guarantees for characters outside ASCII. 345 """ 346 return aString.upper()
347
348 - def macro_decapitalize(self, aString):
349 """returns aString with the first character lowercased. 350 """ 351 if aString: 352 return aString[0].lower()+aString[1:]
353
354 - def macro_test(self, *args):
355 """always "test macro expansion". 356 """ 357 return "test macro expansion"
358 359
360 -class MacDef(structure.Structure):
361 """A macro definition within an RD. 362 363 The macro defined is available on the parent; macros are expanded 364 within the parent (behaviour is undefined if you try a recursive expansion). 365 """ 366 name_ = "macDef" 367 368 _name = attrdef.UnicodeAttribute("name", description="Name the macro" 369 " will be available as", copyable=True, default=utils.Undefined) 370 _content = structure.DataContent(description="Replacement text of the" 371 " macro") 372
373 - def validate(self):
374 self._validateNext(MacDef) 375 if len(self.name)<2: 376 raise common.LiteralParseError("name", self.name, hint= 377 "Macro names must have at least two characters.")
378
379 - def onElementComplete(self):
380 self._onElementCompleteNext(MacDef) 381 self.content_ = self.parent.expand(self.content_) 382 def mac(): 383 return self.content_
384 setattr(self.parent, "macro_"+self.name, mac)
385 386
387 -def MacDefAttribute(**kwargs):
388 return complexattrs.StructListAttribute("macDefs", childFactory=MacDef, 389 **kwargs)
390