1 """
2 Active tags are used in prepare and insert computed material into RD trees.
3
4 And, ok, we are dealing with elements here rather than tags, but I liked
5 the name "active tags" much better, and there's too much talk of elements
6 in this source as it is.
7
8 The main tricky part with active tags is when they're nested. In
9 short, active tags are expanded even when within active tags. So,
10 if you write::
11
12 <STREAM id="foo">
13 <LOOP>
14 </LOOP>
15 </STREAM>
16
17 foo contains not a loop element but whatever that spit out. In particular,
18 macros within the loop are expanded not within some FEED element but
19 within the RD.
20 """
21
22
23
24
25
26
27
28 import csv
29 import re
30 from cStringIO import StringIO
31
32 from gavo import utils
33 from gavo.base import attrdef
34 from gavo.base import common
35 from gavo.base import complexattrs
36 from gavo.base import macros
37 from gavo.base import parsecontext
38 from gavo.base import structure
39
40
41
42
43
44
45
46
47
51
53 return "'value/expanded'"
54
57
59 return not other=="value"
60
61 _EXPANDED_VALUE =_ExValueType()
62
63
65 """A mixin for active tags.
66
67 This is usually mixed into structure.Structures or derivatives. It
68 is also used as a sentinel to find all active tags below.
69 """
70 name_ = None
71
73 el = self.parent
74 while el:
75 if isinstance(el, ActiveTag):
76 return True
77 el = el.parent
78 return False
79
80
82 """A mixin to make a Structure ghostly.
83
84 Most active tags are "ghostly", i.e., the do not (directly)
85 show up in their parents. Therefore, as a part of the wrap-up
86 of the new element, we raise an Ignore exception, which tells
87 the Structure's end_ method to not feed us to the parent.
88 """
92
93
95 """An event source for xmlstruct.
96
97 It is constructed with a list of events as recorded by classes
98 inheriting from RecordingBase.
99 """
101 self.events_ = events
102 self.curEvent = -1
103 self.pos = None
104
106 return _PreparedEventSource(self.events_)
107
109 self.curEvent += 1
110 try:
111 nextItem = self.events_[self.curEvent]
112 except IndexError:
113 raise StopIteration()
114 res, self.pos = nextItem[:3], nextItem[-1]
115 return res
116
117
119 """Defaults for macros.
120
121 In STREAMs and NXSTREAMs, DEFAULTS let you specify values filled into
122 macros when a FEED doesn't given them. Macro names are attribute names
123 (or element names, if you insist), defaults are their values.
124 """
125 name_ = "DEFAULTS"
126
130
131 - def start_(self, ctx, name, value):
133
134 - def value_(self, ctx, name, value):
140
141 - def end_(self, ctx, name, value):
150
151
153 """An "abstract base" for active tags doing event recording.
154
155 The recorded events are available in the events attribute.
156 """
157 name_ = None
158
159 _doc = attrdef.UnicodeAttribute("doc", description="A description of"
160 " this stream (should be restructured text).", strip=False)
161 _defaults = complexattrs.StructAttribute("DEFAULTS",
162 childFactory=Defaults, description="A mapping giving"
163 " defaults for macros expanded in this stream. Macros"
164 " not defaulted will fail when not given in a FEED's attributes.",
165 default=None)
166
171
172 - def feedEvent(self, ctx, type, name, value):
182
183 - def start_(self, ctx, name, value):
191
192 - def end_(self, ctx, name, value):
199
200 - def value_(self, ctx, name, value):
207
209 """returns an object suitable as event source in xmlstruct.
210 """
211 return _PreparedEventSource(self.events_)
212
214 """undoes the marking of expanded values as expanded.
215
216 This is when, as with mixins, duplicate expansion of macros during
217 replay is desired.
218 """
219 for ind, ev in enumerate(self.events_):
220 if ev[0]==_EXPANDED_VALUE:
221 self.events_[ind] = ("value",)+ev[1:]
222
223
224 iterEvents = getEventSource
225
226
227 -class EventStream(RecordingBase, GhostMixin, ActiveTag):
228 """An active tag that records events as they come in.
229
230 Their only direct effect is to leave a trace in the parser's id map.
231 The resulting event stream can be played back later.
232 """
233 name_ = "STREAM"
234
235 - def end_(self, ctx, name, value):
242
243
245 """An event stream that records events, not expanding active tags.
246
247 Normal event streams expand embedded active tags in place. This is
248 frequently what you want, but it means that you cannot, e.g., fill
249 in loop variables through stream macros.
250
251 With non-expanded streams, you can do that::
252
253 <NXSTREAM id="cols">
254 <LOOP listItems="\stuff">
255 <events>
256 <column name="\\item"/>
257 </events>
258 </LOOP>
259 </NXSTREAM>
260 <table id="foo">
261 <FEED source="cols" stuff="x y"/>
262 </table>
263
264 Note that the normal innermost-only rule for macro expansions
265 within active tags does not apply for NXSTREAMS. Macros expanded
266 by a replayed NXSTREAM will be re-expanded by the next active
267 tag that sees them (this is allow embedded active tags to use
268 macros; you need to double-escape macros for them, of course).
269 """
270
271 name_ = "NXSTREAM"
272
273
274 ACTIVE_NOEXPAND = None
275
276
278 """An event stream as a child of another element.
279 """
280 name_ = "events"
281
282 _passivate = attrdef.ActionAttribute("passivate",
283 methodName="_makePassive", description="If set to True, do not expand"
284 " active elements immediately in the body of these events"
285 " (as in an NXSTREAM)")
286
290
291 - def end_(self, ctx, name, value):
295
296
297 -class Prune(ActiveTag, structure.Structure):
298 """An active tag that lets you selectively delete children of the
299 current object.
300
301 You give it regular expression-valued attributes; on the replay of
302 the stream, matching items and their children will not be replayed.
303
304 If you give more than one attribute, the result will be a conjunction
305 of the specified conditions.
306
307 This only works if the items to be matched are true XML attributes
308 (i.e., not written as children).
309 """
310 name_ = "PRUNE"
311
315
316 - def value_(self, ctx, name, value):
319
320 - def end_(self, ctx, name, value):
325
327 """returns a callabe that takes a dictionary and matches the
328 entries against the conditions given.
329 """
330 conditions = []
331 for attName, regEx in self.conds.iteritems():
332 conditions.append((attName, re.compile(regEx)))
333
334 def match(aDict):
335 for attName, expr in conditions:
336 val = aDict.get(attName)
337 if val is None:
338 return False
339 if not expr.search(val):
340 return False
341 return True
342
343 return match
344
345
346 -class Edit(EmbeddedStream):
347 """an event stream targeted at editing other structures.
348
349 When replaying a stream in the presence of EDITs, the elements are
350 are continually checked against ref. If an element matches, the
351 children of edit will be played back into it.
352 """
353 name_ = "EDIT"
354
355 _ref = attrdef.UnicodeAttribute("ref", description="Destination of"
356 " the edits, in the form elementName[<name or id>]",
357 default=utils.Undefined)
358
359 refPat = re.compile(
360 r"([A-Za-z_][A-Za-z0-9_]*)\[([A-Za-z_][A-Za-z0-9_]*)\]")
361
363 mat = self.refPat.match(self.ref)
364 if not mat:
365 raise common.LiteralParseError("ref", self.ref,
366 hint="edit references have the form <element name>[<value of"
367 " name or id attribute>]")
368 self.triggerEl, self.triggerId = mat.groups()
369
370
371 -class ReplayBase(ActiveTag, structure.Structure, macros.StandardMacroMixin):
372 """An "abstract base" for active tags replaying streams.
373 """
374 name_ = None
375 _expandMacros = True
376
377 _source = parsecontext.ReferenceAttribute("source",
378 description="id of a stream to replay", default=None)
379 _events = complexattrs.StructAttribute("events",
380 childFactory=EmbeddedStream, default=None,
381 description="Alternatively to source, an XML fragment to be replayed")
382 _edits = complexattrs.StructListAttribute("edits",
383 childFactory=Edit, description="Changes to be performed on the"
384 " events played back.")
385 _reexpand = attrdef.BooleanAttribute("reexpand", False,
386 description="Force re-expansion of macros; usually, when replaying,"
387 " each string is only expanded once, mainly to avoid overly long"
388 " backslash-fences. Set this to true to force further expansion.")
389 _prunes = complexattrs.StructListAttribute("prunes",
390 childFactory=Prune, description="Conditions for removing"
391 " items from the playback stream.")
392
394 if not hasattr(self, "editsDict"):
395 self.editsDict = {}
396 for edit in self.edits:
397 self.editsDict[edit.triggerEl, edit.triggerId] = edit
398
400 for p in self.prunes:
401 if p.matches(val):
402 return True
403 return False
404
406 """pushes stored events into an event processor.
407
408 The public interface is replay (that receives a structure rather
409 than an event processor).
410 """
411 idStack = []
412 pruneStack = []
413
414
415
416 typeOfExpandedValues = _EXPANDED_VALUE
417 if isinstance(self.source, RawEventStream):
418 typeOfExpandedValues = "value"
419
420 with ctx.replaying():
421 for type, name, val, pos in events:
422 if (self._expandMacros
423 and type=="value"
424 and (self.reexpand or type is not _EXPANDED_VALUE)
425 and "\\" in val):
426 try:
427 val = self.expand(val)
428 except macros.MacroError as ex:
429 ex.hint = ("This probably means that you should have set a %s"
430 " attribute in the FEED tag. For details see the"
431 " documentation of the STREAM with id %s."%(
432 ex.macroName,
433 getattr(self.source, "id", "<embedded>")))
434 raise
435 type = typeOfExpandedValues
436
437
438 if type=="start":
439 idStack.append(set())
440 elif type=="value":
441 if idStack and name=="id" or name=="name":
442 idStack[-1].add(val)
443 elif type=="end":
444 ids = idStack.pop()
445 for foundId in ids:
446 if (name, foundId) in self.editsDict:
447 self._replayTo(self.editsDict[name, foundId].events_,
448 evTarget,
449 ctx)
450
451
452 if type=="start":
453 if pruneStack:
454 pruneStack.append(None)
455 else:
456 if self.prunes and self._isPruneable(val):
457 pruneStack.append(None)
458
459 try:
460 if not pruneStack:
461 evTarget.feed(type, name, val)
462 except Exception as msg:
463 msg.pos = "%s (replaying, real error position %s)"%(
464 ctx.pos, pos)
465 msg.posInMsg = True
466 raise
467
468 if pruneStack and type=="end":
469 pruneStack.pop()
470
471
472
473 self.parent = evTarget.curParser
474
475 - def replay(self, events, destination, ctx):
476 """pushes the stored events into the destination structure.
477
478 While doing this, local macros are expanded unless we already
479 receive the events from an active tag (e.g., nested streams
480 and such).
481 """
482
483 from gavo.base.xmlstruct import EventProcessor
484 evTarget = EventProcessor(None, ctx)
485 evTarget.setRoot(destination)
486
487 self._ensureEditsDict()
488 self._replayTo(events, evTarget, ctx)
489
490
492 """An base class for active tags wanting to replay streams from
493 where the context is invisible.
494
495 These define a _replayer attribute that, when called, replays
496 the stored events *within the context at its end* and to the
497 parent.
498
499 This is what you want for the FEED and LOOP since they always work
500 on the embedding element and, by virtue of being ghosts, cannot
501 be copied. If the element embedding an event stream can be
502 copied, this will almost certainly not do what you want.
503 """
505 sources = [s for s in [self.source, self.events] if s]
506 if len(sources)!=1:
507 raise common.StructureError("Need exactly one of source and events"
508 " on %s elements"%self.name_)
509 stream = sources[0].events_
510 def replayer():
511 self.replay(stream, self.parent, ctx)
512 self._replayer = replayer
513
514 - def end_(self, ctx, name, value):
517
518
545
546
548 """An active tag that takes an event stream and replays the events,
549 possibly filling variables.
550
551 This element supports arbitrary attributes with unicode values. These
552 values are available as macros for replayed values.
553 """
554 name_ = "FEED"
555
557 self._completeElementNext(ReplayedEvents, ctx)
558 self._replayer()
559
560
562 """A ReplayedEventStream that does not expand active tag macros.
563
564 You only want this when embedding a stream into another stream
565 that could want to expand the embedded macros.
566 """
567 name_ = "LFEED"
568 _expandMacros = False
569
570
572 """An attribute containing a generator working on the parse context.
573 """
574 - def feed(self, ctx, instance, literal):
584
585
586 -class Loop(ReplayedEventsWithFreeAttributesBase):
587 """An active tag that replays a feed several times, each time with
588 different values.
589 """
590 name_ = "LOOP"
591
592 _csvItems = attrdef.UnicodeAttribute("csvItems", default=None,
593 description="The items to loop over, in CSV-with-labels format.",
594 strip=True)
595 _listItems = attrdef.UnicodeAttribute("listItems", default=None,
596 description="The items to loop over, as space-separated single"
597 " items. Each item will show up once, as 'item' macro.",
598 strip=True)
599 _codeItems = GeneratorAttribute("codeItems", default=None,
600 description="A python generator body that yields dictionaries"
601 " that are then used as loop items. You can access the parse context"
602 " as the context variable in these code snippets.", strip=False)
603
605 if "\\" in val:
606 el = self.parent
607 while el:
608 if hasattr(el, "expand"):
609 return el.expand(val)
610 el = el.parent
611 return val
612
614 if self.listItems is None:
615 return None
616 def rowIterator():
617 for item in self.maybeExpand(self.listItems).split():
618 yield {"item": item}
619 return rowIterator()
620
622 if self.csvItems is None:
623 return None
624
625
626 src = self.maybeExpand(self.csvItems).strip().encode("utf-8")
627
628 def encodeValues(row):
629 return dict((key, str(val).decode("utf-8"))
630 for key, val in row.iteritems())
631
632 return (encodeValues(row)
633 for row in csv.DictReader(StringIO(src), skipinitialspace=True))
634
636 if self.codeItems is None:
637 return None
638 return self.iterRowsFromCode()
639
641 rowIterators = [ri for ri in [
642 self._makeRowIteratorFromListItems(),
643 self._makeRowIteratorFromCSV(),
644 self._makeRowIteratorFromCode()] if ri]
645 if len(rowIterators)!=1:
646 raise common.StructureError("Must give exactly one data source in"
647 " LOOP")
648 return rowIterators[0]
649
661
662
663 getActiveTag = utils.buildClassResolver(ActiveTag, globals().values(),
664 key=lambda obj: getattr(obj, "name_", None))
665
666
669
670
673