Package gavo :: Package rscdef :: Module procdef
[frames] | no frames]

Source Code for Module gavo.rscdef.procdef

  1  """ 
  2  Basic handling for embedded procedures. 
  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  from gavo import base 
 12  from gavo import utils 
 13  from gavo.rscdef import common 
 14  from gavo.rscdef import rmkfuncs 
15 16 17 18 # Move this one to utils? 19 -def unionByKey(*sequences):
20 """returns all items in sequences uniqued by the items' key attributes. 21 22 The order of the sequence items is not maintained, but items in 23 later sequences override those in earlier ones. 24 """ 25 allItems = {} 26 for seq in sequences: 27 for item in seq: 28 allItems[item.key] = item 29 return allItems.values()
30
31 32 -class RDParameter(base.Structure):
33 """A base class for parameters. 34 """ 35 _name = base.UnicodeAttribute("key", default=base.Undefined, 36 description="The name of the parameter", copyable=True, strip=True, 37 aliases=["name"]) 38 _descr = base.NWUnicodeAttribute("description", default=None, 39 description="Some human-readable description of what the" 40 " parameter is about", copyable=True, strip=True) 41 _expr = base.DataContent(description="The default for the parameter." 42 " The special value __NULL__ indicates a NULL (python None) as usual." 43 " An empty content means a non-preset parameter, which must be filled" 44 " in applications. The magic value __EMPTY__ allows presetting an" 45 " empty string.", 46 copyable=True, strip=True, default=base.NotGiven) 47 _late = base.BooleanAttribute("late", default=False, 48 description="Bind the name not at setup time but at applying" 49 " time. In rowmaker procedures, for example, this allows you to" 50 " refer to variables like vars or rowIter in the bindings.") 51
52 - def isDefaulted(self):
53 return self.content_ is not base.NotGiven
54
55 - def validate(self):
56 self._validateNext(RDParameter) 57 if not utils.identifierPattern.match(self.key): 58 raise base.LiteralParseError("name", self.key, hint= 59 "The name you supplied was not defined by any procedure definition.")
60
61 - def completeElement(self, ctx):
62 if self.content_=="__EMPTY__": 63 self.content_ = "" 64 self._completeElementNext(RDParameter, ctx)
65
66 67 -class ProcPar(RDParameter):
68 """A parameter of a procedure definition. 69 70 Bodies of ProcPars are interpreted as python expressions, in 71 which macros are expanded in the context of the procedure application's 72 parent. If a body is empty, the parameter has no default and has 73 to be filled by the procedure application. 74 """ 75 name_ = "par"
76 - def validate(self):
77 self._validateNext(ProcPar) 78 # Allow non-python syntax when things look like macro calls. 79 if self.content_ and not "\\" in self.content_: 80 utils.ensureExpression( 81 common.replaceProcDefAt(self.content_), self.key)
82
83 84 -class Binding(ProcPar):
85 """A binding of a procedure definition parameter to a concrete value. 86 87 The value to set is contained in the binding body in the form of 88 a python expression. The body must not be empty. 89 """ 90 name_ = "bind" 91
92 - def validate(self):
93 self._validateNext(Binding) 94 if not self.content_ or not self.content_.strip(): 95 raise base.StructureError("Binding bodies must not be empty.")
96
97 98 -class ProcSetup(base.Structure):
99 """Prescriptions for setting up a namespace for a procedure application. 100 101 You can add names to this namespace you using par(ameter)s. 102 If a parameter has no default and an procedure application does 103 not provide them, an error is raised. 104 105 You can also add names by providing a code attribute containing 106 a python function body in code. Within, the parameters are 107 available. The procedure application's parent can be accessed 108 as parent. All names you define in the code are available as 109 globals to the procedure body. 110 111 Caution: Macros are expanded within the code; this means you 112 need double backslashes if you want a single backslash in python 113 code. 114 """ 115 name_ = "setup" 116 117 _code = base.ListOfAtomsAttribute("codeFrags", 118 description="Python function bodies setting globals for the function" 119 " application. Macros are expanded in the context" 120 " of the procedure's parent.", 121 itemAttD=base.UnicodeAttribute("code", description="Python function" 122 " bodies setting globals for the function application. Macros" 123 " are expanded in the context of the procedure's parent.", 124 copyable=True), 125 copyable=True) 126 _pars = base.StructListAttribute("pars", ProcPar, 127 description="Names to add to the procedure's global namespace.", 128 copyable=True) 129 _original = base.OriginalAttribute() 130
131 - def _getParSettingCode(self, useLate, indent, bindings):
132 """returns code that sets our parameters. 133 134 If useLate is true, generate for late bindings. Indent the 135 code by indent. Bindings is is a dictionary overriding 136 the defaults or setting parameter values. 137 """ 138 parCode = [] 139 for p in self.pars: 140 if p.late==useLate: 141 val = bindings.get(p.key, base.NotGiven) 142 if val is base.NotGiven: 143 val = p.content_ 144 parCode.append("%s%s = %s"%(indent, p.key, val)) 145 return "\n".join(parCode)
146
147 - def getParCode(self, bindings):
148 """returns code doing setup bindings un-indented. 149 """ 150 return self._getParSettingCode(False, "", bindings)
151
152 - def getLateCode(self, bindings):
153 """returns code doing late (in-function) bindings indented with two 154 spaces. 155 """ 156 return self._getParSettingCode(True, " ", bindings)
157
158 - def getBodyCode(self):
159 """returns the body code un-indented. 160 """ 161 collectedCode = [] 162 for frag in self.codeFrags: 163 collectedCode.append( 164 utils.fixIndentation(frag, "", governingLine=1)) 165 return "\n".join(collectedCode)
166
167 168 -class ProcDef(base.Structure, base.RestrictionMixin):
169 """An embedded procedure. 170 171 Embedded procedures are python code fragments with some interface defined 172 by their type. They can occur at various places (which is called procedure 173 application generically), e.g., as row generators in grammars, as applys in 174 rowmakers, or as SQL phrase makers in condDescs. 175 176 They consist of the actual actual code and, optionally, definitions like 177 the namespace setup, configuration parameters, or a documentation. 178 179 The procedure applications compile into python functions with special 180 global namespaces. The signatures of the functions are determined by 181 the type attribute. 182 183 ProcDefs are referred to by procedure applications using their id. 184 """ 185 name_ = "procDef" 186 187 _code = base.UnicodeAttribute("code", default=base.NotGiven, 188 copyable=True, description="A python function body.") 189 _setup = base.StructListAttribute("setups", ProcSetup, 190 description="Setup of the namespace the function will run in", 191 copyable=True) 192 _doc = base.UnicodeAttribute("doc", default="", description= 193 "Human-readable docs for this proc (may be interpreted as restructured" 194 " text).", copyable=True) 195 _type = base.EnumeratedUnicodeAttribute("type", default=None, description= 196 "The type of the procedure definition. The procedure applications" 197 " will in general require certain types of definitions.", 198 validValues=["t_t", "apply", "rowfilter", "sourceFields", "mixinProc", 199 "phraseMaker", "descriptorGenerator", "dataFunction", "dataFormatter", 200 "metaMaker", "regTest", "iterator", "pargetter"], 201 copyable=True, 202 strip=True) 203 _deprecated = base.UnicodeAttribute("deprecated", default=None, 204 copyable=True, description="A deprecation message. This will" 205 " be shown if this procDef is being compiled.") 206 _original = base.OriginalAttribute() 207 208
209 - def getCode(self):
210 """returns the body code indented with two spaces. 211 """ 212 if self.code is base.NotGiven: 213 return "" 214 else: 215 return utils.fixIndentation(self.code, " ", governingLine=1)
216 217 @utils.memoized
218 - def getSetupPars(self):
219 """returns all parameters used by setup items, where lexically 220 later items override earlier items of the same name. 221 """ 222 return unionByKey(*[s.pars for s in self.setups])
223
224 - def getLateSetupCode(self, boundNames):
225 return "\n".join(s.getLateCode(boundNames) for s in self.setups)
226
227 - def getParSetupCode(self, boundNames):
228 return "\n".join(s.getParCode(boundNames) for s in self.setups)
229
230 - def getBodySetupCode(self, boundNames):
231 return "\n".join(s.getBodyCode() for s in self.setups)
232
233 234 -class ProcApp(ProcDef):
235 """An abstract base for procedure applications. 236 237 Deriving classes need to provide: 238 239 - a requiredType attribute specifying what ProcDefs can be applied. 240 - a formalArgs attribute containing a (python) formal argument list 241 - of course, a name_ for XML purposes. 242 243 They can, in addition, give a class attribute additionalNamesForProcs, 244 which is a dictionary that is joined into the global namespace during 245 procedure compilation. 246 """ 247 _procDef = base.ReferenceAttribute("procDef", forceType=ProcDef, 248 default=base.NotGiven, description="Reference to the procedure" 249 " definition to apply", copyable=True) 250 _bindings = base.StructListAttribute("bindings", description= 251 "Values for parameters of the procedure definition", 252 childFactory=Binding, copyable=True) 253 _name = base.UnicodeAttribute("name", default=base.NotGiven, 254 description="A name of the proc. ProcApps compute their (python)" 255 " names to be somwhat random strings. Set a name manually to" 256 " receive more easily decipherable error messages. If you do that," 257 " you have to care about name clashes yourself, though.", strip=True) 258 259 requiredType = None 260 261 additionalNamesForProcs = {} 262
263 - def validate(self):
264 if self.procDef and self.procDef.type and self.requiredType: 265 if self.procDef.type!=self.requiredType: 266 raise base.StructureError("The procDef %s has type %s, but" 267 " here %s procDefs are required."%(self.procDef.id, 268 self.procDef.type, self.requiredType)) 269 270 if self.procDef: 271 if self.procDef.deprecated: 272 if self.getSourcePosition()!="<internally built>": 273 # for now, don't warn about these; they typically 274 # originate when copying/adapting cores and will just 275 # confuse operators 276 procId = "unnamed procApp" 277 if self.name: 278 procId = "procApp %s"%self.name 279 280 base.ui.notifyWarning("%s, %s: %s"%( 281 self.getSourcePosition(), 282 procId, 283 utils.fixIndentation(self.procDef.deprecated, ""))) 284 285 self._validateNext(ProcApp) 286 self._ensureParsBound()
287
288 - def completeElement(self, ctx):
289 self._completeElementNext(ProcApp, ctx) 290 if self.name is base.NotGiven: # make up a name from self's id 291 self.name = ("proc%x"%id(self)).replace("-", "")
292 293 @utils.memoized
294 - def getSetupPars(self):
295 """returns the setup parameters for the proc app, where procDef 296 parameters may be overridden by self's parameters. 297 """ 298 allSetups = [] 299 if self.procDef is not base.NotGiven: 300 allSetups.extend(self.procDef.setups) 301 allSetups.extend(self.setups) 302 return unionByKey(*[s.pars for s in allSetups])
303
304 - def _ensureParsBound(self):
305 """raises an error if non-defaulted pars of procDef are not filled 306 by the bindings. 307 """ 308 bindNames = set(b.key for b in self.bindings) 309 for p in self.getSetupPars(): 310 if not p.isDefaulted(): 311 if not p.key in bindNames: 312 raise base.StructureError("Parameter %s is not defaulted in" 313 " %s and thus must be bound."%(p.key, self.name)) 314 if p.key in bindNames: 315 bindNames.remove(p.key) 316 317 if bindNames: 318 raise base.StructureError("May not bind non-existing parameter(s)" 319 " %s."%(", ".join(bindNames)))
320
321 - def onElementComplete(self):
322 self._onElementCompleteNext(ProcApp) 323 self._boundNames = dict((b.key, b.content_) for b in self.bindings)
324
325 - def _combineWithProcDef(self, methodName, boundNames):
326 # A slightly tricky helper method for the implementation of get*SetupCode: 327 # this combines the results of calling methodName on a procDef 328 # (where applicable) with calling it on ProcDef for self. 329 parts = [] 330 if self.procDef is not base.NotGiven: 331 parts.append(getattr(self.procDef, methodName)(boundNames)) 332 333 parts.append(getattr(ProcDef, methodName)(self, boundNames)) 334 return "\n".join(parts)
335
336 - def getLateSetupCode(self, boundNames):
337 return self._combineWithProcDef("getLateSetupCode", boundNames)
338
339 - def getParSetupCode(self, boundNames):
340 return self._combineWithProcDef("getParSetupCode", boundNames)
341
342 - def getBodySetupCode(self, boundNames):
343 return self._combineWithProcDef("getBodySetupCode", boundNames)
344
345 - def getSetupCode(self):
346 code = "\n".join(( 347 self.getParSetupCode(self._boundNames), 348 self.getBodySetupCode(self._boundNames))) 349 if "\\" in code: 350 code = self.parent.expand(code) 351 return code
352
353 - def _getFunctionDefinition(self, mainSource):
354 """returns mainSource in a function definition with proper 355 signature including setup of late code. 356 """ 357 parts = [self.getLateSetupCode(self._boundNames)] 358 parts.append(mainSource) 359 body = "\n".join(parts) 360 if not body.strip(): 361 body = " pass" 362 return "def %s(%s):\n%s"%(self.name, self.formalArgs, 363 body)
364
365 - def getFuncCode(self):
366 """returns a function definition for this proc application. 367 368 This includes bindings of late parameters. 369 370 Locally defined code overrides code defined in a procDef. 371 """ 372 mainCode = "" 373 if self.code is base.NotGiven: 374 if self.procDef is not base.NotGiven: 375 mainCode = self.procDef.getCode() 376 else: 377 mainCode = self.getCode() 378 code = self._getFunctionDefinition(mainCode) 379 if "\\" in code: 380 code = self.parent.expand(code) 381 return code
382
383 - def _compileForParent(self, parent):
384 """helps compile. 385 """ 386 # go get the RD for parent; it's always handy in this kind 387 # of code 388 curEl = parent 389 while not hasattr(curEl, "rd"): 390 if curEl.parent: 391 curEl = curEl.parent 392 else: 393 break 394 try: 395 rd = curEl.rd 396 except AttributeError: 397 # maybe an unrooted element 398 rd = None 399 400 return rmkfuncs.makeProc( 401 self.name, self.getFuncCode(), 402 self.getSetupCode(), parent, 403 rd=rd, 404 **self.additionalNamesForProcs)
405
406 - def breakCircles(self):
407 # overridden to undo additional memoization 408 ProcDef.breakCircles(self) 409 utils.forgetMemoized(self)
410
411 - def compile(self, parent=None):
412 """returns a callable for this procedure application. 413 414 You can pass a different parent; it will then be used to 415 expand macros. If you do not give it, the embedding structure will 416 be used. 417 """ 418 if parent is None: 419 parent = self.parent 420 return utils.memoizeOn(parent, self, self._compileForParent, parent)
421