Source code for gavo.rscdef.mixins

"""
Resource mixins.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import threading

from gavo import base
from gavo.base import activetags
from gavo.rscdef import procdef


__docformat__ = "restructuredtext en"


[docs]class ProcessEarly(procdef.ProcApp): """A code fragment run by the mixin machinery when the structure being worked on is being finished. Within processEarly, you can access: - the structure the mixin is applied to as "substrate" - the mixin parameters as "mixinPars" - the parse context as "context" (the context is particularly handy for context.resolveId) """ name_ = "processEarly" formalArgs = "context, substrate, mixinPars"
[docs]class ProcessLate(procdef.ProcApp): """A code fragment run by the mixin machinery when the parser parsing everything exits. Within processLate, you can access: - the structure mixed in as "substrate", - the root structure of the whole parse tree as root, - the parse context as "context", - and the mixin parameters (a dictionary) as "mixinPars". """ name_ = "processLate" formalArgs = "substrate, root, context, mixinPars"
[docs]class MixinPar(procdef.AliasableRDParameter): """A parameter definition for mixins. The (optional) body provides a default for the parameter. """ name_ = "mixinPar" _expr = base.DataContent(description="The default for the parameter." " A __NULL__ here does not directly mean None/NULL, but since the" " content will frequently end up in attributes, it will usually work" " as presetting None." " An empty content means a non-preset parameter, which must be filled" " in applications. The magic value __EMPTY__ allows presetting an" " empty string.", # mixinPars must not evaluate __NULL__; this stuff ends up in # macro expansions, where an actual None is not desirable. null=None, copyable=True, strip=True, default=base.NotGiven)
[docs] def validate(self): super().validate() if len(self.key)<2: raise base.LiteralParseError("name", self.key, hint="Names of" " mixin parameters must have at least two characters (since" " they are exposed as macros")
[docs]class LateEvents(activetags.EmbeddedStream): """An event stream played back by a mixin when the substrate is being finalised (but before the early processing). """ name_ = "lateEvents"
[docs]class MixinDef(activetags.ReplayBase): """A definition for a resource mixin. Resource mixins are resource descriptor fragments typically rooted in tables (though it's conceivable that other structures could grow mixin attributes as well). They are used to define and implement certain behaviours components of the DC software want to see: - products want to be added into their table, and certain fields are required within tables describing products - tables containing positions need some basic machinery to support scs. - siap needs quite a bunch of fields Mixins consist of events that are played back on the structure mixing in before anything else happens (much like original) and two procedure definitions, viz, processEarly and processLate. These can access the structure that has the mixin as substrate. processEarly is called at the lexical location of the mixin. processLate is executed just before the parser exits. This is the place to fix up anything that uses the table mixed in. Note, however, that you should be as conservative as possible here -- you should think of DC structures as immutable as long as possible. Programmatically, you can check if a certain table mixes in something by calling its mixesIn method. Recursive application of mixins, even to separate objects, will deadlock. """ name_ = "mixinDef" _doc = base.UnicodeAttribute("doc", description="Documentation for" " this mixin", strip=False) _events = base.StructAttribute("events", childFactory=activetags.EmbeddedStream, description="Events to be played back into the structure mixing" " this in at mixin time.", copyable=True, default=base.NotGiven) _lateEvents = base.StructAttribute("lateEvents", childFactory=LateEvents, description="Events to be played back into the structure mixing" " this in at completion time.", copyable=True, default=base.NotGiven) _processEarly = base.StructAttribute("processEarly", default=None, childFactory=ProcessEarly, description="Code executed at element fixup.", copyable=True) _processLate = base.StructAttribute("processLate", default=None, childFactory=ProcessLate, description="Code executed resource fixup.", copyable=True) _pars = base.UniquedStructListAttribute("pars", childFactory=MixinPar, uniqueAttribute="key", description="Parameters available for this mixin.", copyable=True) _original = base.OriginalAttribute()
[docs] def completeElement(self, ctx): # we want to double-expand macros in mixins. Thus, reset all # value/expanded events to plain values if self.events: self.events.unexpandMacros() if self.lateEvents: self.lateEvents.unexpandMacros() # This lock protects against multiple uses of applyTo. This is # necessary because during replay, we have macroExpansions and # macroParent reflect a concrete application's context. self.applicationLock = threading.Lock() super().completeElement(ctx)
def _defineMacros(self, fillers, destination): """creates attributes macroExpansions and parentMacroPackage used by execMacros. Within mixins, you can use macros filled by mixin parameters or expanded by the substrate. This information is local to a concrete mixin application. Hence, applyTo calls this method, and the attributes created are invalid for any subsequent or parallel applyTo calls. Therefore, applyTo acquires the applicationLock before calling this. """ self.parentMacroPackage = None if hasattr(destination, "execMacro"): self.parentMacroPackage = destination self.macroExpansions = {} for p in self.pars: if p.key in fillers: self.macroExpansions[p.key] = fillers.pop(p.key) if p.alias in fillers: raise base.StructureError("Both canonical name and alias bound:" " %s, %s"%(p.key, p.alias)) elif p.alias in fillers: self.macroExpansions[p.key] = fillers.pop(p.alias) elif p.isDefaulted(): self.macroExpansions[p.key] = p.content_ else: raise base.StructureError("Mixin parameter %s mandatory"%p.key) if fillers: raise base.StructureError("The attribute(s) %s is/are not allowed" " on this mixin"%(",".join(fillers)))
[docs] def execMacro(self, macName, args): if macName in self.macroExpansions: return self.macroExpansions[macName] if self.parentMacroPackage: return self.parentMacroPackage.execMacro(macName, args)
[docs] def applyTo(self, destination, ctx, fillers={}): """replays the stored events on destination and arranges for processEarly and processLate to be run. """ with self.applicationLock: self._defineMacros(fillers.copy(), destination) if self.events: self.replay(self.events.events_, destination, ctx) if self.processEarly is not None: self.processEarly.compile(destination)(ctx, destination, self.macroExpansions) if self.processLate is not None: def procLate(rootStruct, parseContext, destination=destination, exp=self.macroExpansions.copy()): self.processLate.compile(destination)( destination, rootStruct, parseContext, exp) ctx.addExitFunc(procLate) if self.lateEvents: origComplete = destination.completeElement def newComplete(ctx): with self.applicationLock: self._defineMacros(fillers.copy(), destination) self.replay(self.lateEvents.events_, destination, ctx) origComplete(ctx) destination.completeElement = newComplete
[docs] def applyToFinished(self, destination): """applies the mixin to an object already parsed. Late callbacks will only be executed if destination has an rd attribute; if that is the case, this rd's idmap will be amended with anything the mixin comes up with. """ rd = None if hasattr(destination, "rd"): rd = destination.rd ctx = base.ParseContext() if rd is not None: ctx.idmap = destination.rd.idmap self.applyTo(destination, ctx) # we don't keep the application lock for this; applyToFinished # is more of a debugging thing, so we don't worry too much. if self.lateEvents: self.replay(self.lateEvents.events_, destination, ctx) if rd is not None: ctx.runExitFuncs(rd)
class _MixinParser(base.Parser): """A parser for structured mixin references. These can contain attribute definitions for any parameter of the mixin referenced. """ def __init__(self, parent, parentAttr): self.parent, self.parentAttr = parent, parentAttr self.fillers = {} self.curName = None # this is non-None while parsing a child element def start_(self, ctx, name, value): if self.curName is not None: raise base.StructureError("%s elements cannot have %s children in" " mixins."%(self.curName, name)) self.curName = name return self def value_(self, ctx, name, value): if name=="content_": if self.curName: self.fillers[self.curName] = value else: self.fillers["mixin name"] = value.strip() else: self.fillers[name] = value return self def end_(self, ctx, name, value): if self.curName: # end parsing parameter binding self.curName = None return self else: # end of mixin application, run the mixin and hand control back to # mixin parent if "mixin name" not in self.fillers: raise base.StructureError("Empty mixin children not allowed") mixinRef = self.fillers.pop("mixin name") self.parentAttr.feed(ctx, self.parent, mixinRef, fillers=self.fillers) return self.parent
[docs]class MixinAttribute(base.SetOfAtomsAttribute): """An attribute defining a mixin. This currently is only offered on tables, though in principle we could have it anywhere now, but we'd want some compatibility checking then. This is never copyable since this would meaning playing the same stuff into an object twice. This means trouble for magic scripts (in particular processLate); e.g., if you copy a table mixing in products, the data element for that table will not receive the product table. Goes to show the whole product mess is ugly and needs a good idea. """ def __init__(self, **kwargs): kwargs["itemAttD"] = base.UnicodeAttribute("mixin", strip=True) kwargs["description"] = kwargs.get("description", "Reference to a mixin this table should contain; you can" " give mixin parameters as attributes or children.") kwargs["copyable"] = False base.SetOfAtomsAttribute.__init__(self, "mixin", **kwargs)
[docs] def feed(self, ctx, instance, mixinRef, fillers={}): """feeds the immediate elements and schedules the rest of actions to be taken in time. """ mixin = ctx.resolveId(mixinRef, instance=instance, forceType=MixinDef) base.SetOfAtomsAttribute.feed(self, ctx, instance, mixinRef) mixin.applyTo(instance, ctx, fillers)
# no need to override feedObject: On copy and such, replay has already # happened.
[docs] def iterParentMethods(self): def mixesIn(instance, mixinRef): return mixinRef in instance.mixin yield "mixesIn", mixesIn
[docs] def makeUserDoc(self): return ("A mixin reference, typically to support certain protocol." " See Mixins_.")
[docs] def create(self, parent, ctx, name): # since mixins may contain parameters, we need a custom parser # when mixin is a child. return _MixinParser(parent, self)