5------------------------------------------------------------------------------
6This file is part of grepros - grep for ROS bag files and live topics.
7Released under the BSD License.
12------------------------------------------------------------------------------
14## @namespace grepros.search
15from argparse import Namespace
33 In highlighted results, message field values that match search criteria are modified
35 with numeric field values converted to strings beforehand.
39 GrepMessage = collections.namedtuple("BagMessage",
"topic message timestamp match index")
42 ANY_MATCHES = [((), re.compile(
"(.*)", re.DOTALL)), (), re.compile(
"(.?)", re.DOTALL)]
45 DEFAULT_ARGS = dict(PATTERN=(), CASE=
False, FIXED_STRING=
False, INVERT=
False, EXPRESSION=
False,
46 HIGHLIGHT=
False, NTH_MATCH=1, BEFORE=0, AFTER=0, CONTEXT=0, MAX_COUNT=0,
47 MAX_PER_TOPIC=0, MAX_TOPICS=0, SELECT_FIELD=(), NOSELECT_FIELD=(),
51 def __init__(self, args=None, **kwargs):
53 @param args arguments
as namespace
or dictionary, case-insensitive
54 @param args.pattern pattern(s) to find
in message field values
55 @param args.fixed_string pattern contains ordinary strings,
not regular expressions
56 @param args.case use case-sensitive matching
in pattern
57 @param args.invert select messages
not matching pattern
58 @param args.expression pattern(s) are a logical expression
59 like
'this AND (this2 OR NOT "skip this")',
60 with elements
as patterns to find
in message fields
61 @param args.highlight highlight matched values
62 @param args.before number of messages of leading context to emit before match
63 @param args.after number of messages of trailing context to emit after match
64 @param args.context number of messages of leading
and trailing context to emit
65 around match, overrides args.before
and args.after
66 @param args.max_count number of matched messages to emit (per file
if bag input)
67 @param args.max_per_topic number of matched messages to emit
from each topic
68 @param args.max_topics number of topics to emit matches
from
69 @param args.nth_match emit every Nth match
in topic, starting
from first
70 @param args.select_field message fields to use
in matching
if not all
71 @param args.noselect_field message fields to skip
in matching
72 @param args.match_wrapper string to wrap around matched values
in find()
and match(),
73 both sides
if one value, start
and end
if more than one,
74 or no wrapping
if zero values (default
"**")
75 @param kwargs any
and all arguments
as keyword overrides, case-insensitive
80 @param args.topic ROS topics to read
if not all
81 @param args.type ROS message types to read
if not all
82 @param args.skip_topic ROS topics to skip
83 @param args.skip_type ROS message types to skip
84 @param args.start_time earliest timestamp of messages to read
85 @param args.end_time latest timestamp of messages to read
86 @param args.start_index message index within topic to start
from
87 @param args.end_index message index within topic to stop at
88 @param args.unique emit messages that are unique
in topic
89 @param args.nth_message read every Nth message
in topic, starting
from first
90 @param args.nth_interval minimum time interval between messages
in topic,
91 as seconds
or ROS duration
92 @param args.condition Python expressions that must evaluate
as true
93 for message to be processable, see ConditionMixin
94 @param args.progress whether to
print progress bar
95 @param args.stop_on_error stop execution on any error like unknown message type
102 self.
_messages = collections.defaultdict(collections.OrderedDict)
104 self.
_stamps = collections.defaultdict(collections.OrderedDict)
106 self.
_counts = collections.defaultdict(collections.Counter)
108 self.
_statuses = collections.defaultdict(collections.OrderedDict)
114 "passthrough":
False,
115 "pure_anymatch":
False,
126 self.
args0 = common.ensure_namespace(args, **kwargs)
127 self.
args = common.ArgumentUtil.validate(common.ensure_namespace(args, Scanner.DEFAULT_ARGS, **kwargs))
128 if self.
args.CONTEXT: self.
args.BEFORE = self.
args.AFTER = self.
args.CONTEXT
131 def find(self, source, highlight=None):
133 Yields matched and context messages
from source.
136 @param highlight whether to highlight matched values
in message fields,
137 defaults to flag
from constructor
138 @return GrepMessage namedtuples of
139 (topic, message, timestamp, match, index
in topic),
140 where match
is matched optionally highlighted message
141 or `
None`
if yielding a context message
145 if isinstance(source,
api.Bag):
147 self.
_prepare(source, highlight=highlight, progress=
True)
148 for topic, msg, stamp, matched, index
in self.
_generate():
149 yield self.
GrepMessage(topic, msg, stamp, matched, index)
152 def match(self, topic, msg, stamp, highlight=None):
154 Returns matched message if message matches search filters.
156 @param topic topic name
157 @param msg ROS message
158 @param stamp message ROS timestamp
159 @param highlight whether to highlight matched values
in message fields,
160 defaults to flag
from constructor
161 @return original
or highlighted message on match
else `
None`
169 self.
source.push(topic, msg, stamp)
170 item = self.
source.read_queue()
173 topickey = api.TypeMeta.make(msg, topic).topickey
177 self.
source.notify(matched)
178 if matched
and not self.
_counts[topickey][
True] % (self.
args.NTH_MATCH
or 1):
180 self.
_counts[topickey][
True] += 1
184 self.
_counts[topickey][
True] += 1
186 self.
source.mark_queue(topic, msg, stamp)
190 def work(self, source, sink):
192 Greps messages yielded from source
and emits matched content to sink.
196 @return count matched
200 if isinstance(source,
api.Bag):
202 self.
_prepare(source, sink, highlight=self.
args.HIGHLIGHT, progress=
True)
204 for topic, msg, stamp, matched, index
in self.
_generate():
206 sink.emit(topic, msg, stamp, matched, index)
207 total_matched += bool(matched)
212 """Returns whether conditions have valid syntax, prints errors."""
213 if self.
valid is not None and not reset:
return self.
valid
215 errors = collections.defaultdict(list)
216 if not self.
args.FIXED_STRING
and not self.
args.EXPRESSION:
217 for v
in self.
args.PATTERN:
218 split = v.find(
"=", 1, -1)
219 v = v[split + 1:]
if split > 0
else v
220 try: re.compile(re.escape(v)
if self.
args.FIXED_STRING
else v)
221 except Exception
as e:
222 errors[
"Invalid regular expression"].append(
"'%s': %s" % (v, e))
224 except Exception
as e: errors[
""].append(str(e))
226 for err
in errors.get(
"", []):
227 common.ConsolePrinter.log(logging.ERROR, err)
228 for category
in filter(bool, errors):
229 common.ConsolePrinter.log(logging.ERROR, category)
230 for err
in errors[category]:
231 common.ConsolePrinter.log(logging.ERROR,
" %s" % err)
233 self.
valid =
not errors
238 """Context manager entry, does nothing, returns self."""
242 def __exit__(self, exc_type, exc_value, traceback):
243 """Context manager exit, does nothing."""
249 Yields matched and context messages
from source.
251 @return tuples of (topic, msg, stamp, matched optionally highlighted msg, index
in topic)
253 batch_matched, batch = False,
None
254 for topic, msg, stamp
in self.
source.read():
255 if batch != self.
source.get_batch():
256 batch, batch_matched = self.
source.get_batch(),
False
260 topickey = api.TypeMeta.make(msg, topic).topickey
264 self.
source.notify(matched)
265 if matched
and not self.
_counts[topickey][
True] % (self.
args.NTH_MATCH
or 1):
267 self.
_counts[topickey][
True] += 1
269 yield (topic, msg, stamp, matched, self.
_counts[topickey][
None])
272 self.
_counts[topickey][
True] += 1
273 elif self.
args.AFTER \
276 batch_matched = batch_matched
or bool(matched)
284 def _is_processable(self, topic, msg, stamp):
286 Returns whether processing current message in topic
is acceptable:
287 that topic
or total maximum count has
not been reached,
288 and current message
in topic
is in configured range,
if any.
290 topickey = api.TypeMeta.make(msg, topic).topickey
291 if self.
args.MAX_COUNT \
292 and sum(x[
True]
for x
in self.
_counts.values()) >= self.
args.MAX_COUNT:
294 if self.
args.MAX_PER_TOPIC
and self.
_counts[topickey][
True] >= self.
args.MAX_PER_TOPIC:
296 if self.
args.MAX_TOPICS:
297 topics_matched = [k
for k, vv
in self.
_counts.items()
if vv[
True]]
298 if topickey
not in topics_matched
and len(topics_matched) >= self.
args.MAX_TOPICS:
301 and not self.
source.is_processable(topic, msg, stamp, self.
_counts[topickey][
None]):
306 def _generate_context(self, topickey, before=False):
307 """Yields before/after context for latest match."""
308 count = self.
args.BEFORE + 1
if before
else self.
args.AFTER
309 candidates = list(self.
_statuses[topickey])[-count:]
310 current_index = self.
_counts[topickey][
None]
311 for i, msgid
in enumerate(candidates)
if count
else ():
312 if self.
_statuses[topickey][msgid]
is None:
313 idx = current_index + i - (len(candidates) - 1
if before
else 1)
315 self.
_counts[topickey][
False] += 1
316 yield topickey[0], msg, stamp,
None, idx
320 def _clear_data(self):
321 """Clears local structures."""
327 def _prepare(self, source, sink=None, highlight=None, progress=False):
328 """Clears local structures, binds and registers source and sink, if any."""
331 source.bind(sink), sink
and sink.bind(source)
332 source.preprocess =
False
336 def _prune_data(self, topickey):
337 """Drops history older than context window."""
338 WINDOW = max(self.
args.BEFORE, self.
args.AFTER) + 1
340 while len(dct[topickey]) > WINDOW:
341 msgid = next(iter(dct[topickey]))
342 value = dct[topickey].pop(msgid)
343 dct
is self.
_messages and api.TypeMeta.discard(value)
346 def _parse_patterns(self):
347 """Parses pattern arguments into re.Patterns. Raises on invalid pattern."""
348 NOBRUTE_SIGILS =
r"\A",
r"\Z",
"?("
349 BRUTE, FLAGS =
not self.
args.INVERT, re.DOTALL | (0
if self.
args.CASE
else re.I)
356 """Returns (path Pattern or (), value Pattern)."""
357 split = v.find(
"=", 1, -1)
358 v, path = (v[split + 1:], v[:split])
if split > 0
else (v, ())
360 v =
"|^$" if v
in (
"''",
'""')
else (re.escape(v)
if self.
args.FIXED_STRING
else v)
361 path = re.compile(
r"(^|\.)%s($|\.)" %
".*".join(map(re.escape, path.split(
"*")))) \
363 try:
return (path, re.compile(
"(%s)" % v, FLAGS))
364 except Exception
as e:
365 raise ValueError(
"Invalid regular expression\n '%s': %s" % (v, e))
367 if self.
args.EXPRESSION
and self.
args.PATTERN:
370 contents.append(make_pattern(v))
371 if BRUTE
and (self.
args.FIXED_STRING
or not any(x
in v
for x
in NOBRUTE_SIGILS)):
372 if self.
args.FIXED_STRING: v = re.escape(v)
374 if not self.
args.PATTERN:
378 selects, noselects = self.
args.SELECT_FIELD, self.
args.NOSELECT_FIELD
379 for key, vals
in [(
"select", selects), (
"noselect", noselects)]:
380 self.
_patterns[key] = [(tuple(v.split(
".")), common.path_to_regex(v))
for v
in vals]
383 def _register_message(self, topickey, msgid, msg, stamp):
384 """Registers message with local structures."""
385 self.
_counts[topickey][
None] += 1
387 self.
_stamps [topickey][msgid] = stamp
391 def _configure_settings(self, highlight=None, progress=False):
392 """Caches settings for message matching."""
393 highlight = bool(highlight
if highlight
is not None else self.
args.HIGHLIGHT
394 if not self.
sink or self.
sink.is_highlighting()
else False)
395 pure_anymatch =
not self.
args.INVERT
and not self.
_patterns[
"select"] \
398 passthrough = no_matching
and not highlight
399 wraps = []
if not highlight
else self.
args.MATCH_WRAPPER
if not self.
sink else \
400 (common.MatchMarkers.START, common.MatchMarkers.END)
401 wraps = wraps
if isinstance(wraps, (list, tuple))
else []
if wraps
is None else [wraps]
402 wraps = ((wraps
or [
""]) * 2)[:2]
404 ops = {ExpressionTree.AND: BooleanResult.and_, ExpressionTree.NOT: BooleanResult.not_,
405 ExpressionTree.OR: functools.partial(BooleanResult.or_, eager=
True)}
407 else: self.
_expressor.configure(operators=ExpressionTree.OPERATORS,
408 void=ExpressionTree.VOID)
409 self.
_settings.update(highlight=highlight, passthrough=passthrough,
410 pure_anymatch=pure_anymatch, wraps=wraps)
413 if progress
and (
not no_matching
or self.
args.MAX_COUNT):
415 if self.
args.MAX_COUNT: bar_opts.update(match_max=self.
args.MAX_COUNT)
416 if not no_matching: bar_opts.update(source_value=0)
417 self.
source.configure_progress(**bar_opts)
420 def _is_max_done(self):
421 """Returns whether max match count has been reached (and message after-context emitted)."""
422 result, is_maxed =
False,
False
423 if self.
args.MAX_COUNT:
424 is_maxed = sum(vv[
True]
for vv
in self.
_counts.values()) >= self.
args.MAX_COUNT
425 if not is_maxed
and self.
args.MAX_PER_TOPIC:
426 count_required = self.
args.MAX_TOPICS
or len(self.
source.topics)
427 count_maxed = sum(vv[
True] >= self.
args.MAX_PER_TOPIC
428 or vv[
None] >= (self.
source.topics.get(k)
or 0)
429 for k, vv
in self.
_counts.items())
430 is_maxed = (count_maxed >= count_required)
432 result =
not self.
args.AFTER
or \
438 def _has_in_window(self, topickey, length, status, full=False):
439 """Returns whether given status exists in recent message window."""
440 if not length
or full
and len(self.
_statuses[topickey]) < length:
442 return status
in list(self.
_statuses[topickey].values())[-length:]
447 Returns transformed message if all patterns find a match
in message,
else None.
449 Matching field values are converted to strings
and surrounded by markers.
450 Returns original message
if any-match
and sink does
not require highlighting.
453 def process_value(v, parent, top, patterns):
455 Populates `field_matches` and `pattern_spans`
for patterns matching given string value.
456 Populates `field_values`. Returns set of pattern indexes that found a match.
458 indexes, spans, topstr = set(), [], ".".join(map(str, top))
459 v2 = str(list(v)
if isinstance(v, LISTIFIABLES)
else v)
460 if v
and isinstance(v, (list, tuple)): v2 = v2[1:-1]
461 for i, (path, p)
in enumerate(patterns):
462 if path
and not path.search(topstr):
continue
463 matches = [next(p.finditer(v2),
None)]
if PLAIN_INVERT
else list(p.finditer(v2))
465 matchspans = common.merge_spans([x.span()
for x
in matches
if x], join_blanks=
True)
466 matchspans = [(a, b
if a != b
else len(v2))
for a, b
in matchspans]
468 indexes.add(i), spans.extend(matchspans)
469 pattern_spans.setdefault(id(patterns[i]), {})[top] = matchspans
470 field_values.setdefault(top, (parent, v, v2))
471 if PLAIN_INVERT: spans = [(0, len(v2))]
if v2
and not spans
else []
472 if spans: field_matches.setdefault(top, []).extend(spans)
475 def populate_matches(obj, patterns, top=(), parent=
None):
477 Recursively populates `field_matches` and `pattern_spans`
for fields matching patterns.
478 Populates `field_values`. Returns set of pattern indexes that found a match.
482 fieldmap = api.get_message_fields(obj)
484 fieldmap = api.filter_fields(fieldmap, top, include=selects, exclude=noselects)
485 for k, t
in fieldmap.items()
if fieldmap != obj
else ():
486 v, path = api.get_message_value(obj, k, t), top + (k, )
487 if api.is_ros_message(v):
488 indexes |= populate_matches(v, patterns, path, obj)
489 elif v
and isinstance(v, (list, tuple)) \
490 and api.scalar(t)
not in api.ROS_NUMERIC_TYPES:
491 for i, x
in enumerate(v):
492 indexes |= populate_matches(x, patterns, path + (i, ), v)
494 indexes |= process_value(v, obj, path, patterns)
495 if not api.is_ros_message(obj):
496 indexes |= process_value(obj, parent, top, patterns)
499 def wrap_matches(values, matches):
500 """Replaces result-message field values with matched parts wrapped in marker tags."""
501 for path, spans
in matches.items()
if any(WRAPS)
else ():
502 parent, v1, v2 = values[path]
503 for a, b
in reversed(common.merge_spans(spans)):
504 v2 = v2[:a] + WRAPS[0] + v2[a:b] + WRAPS[1] + v2[b:]
505 if v1
and isinstance(v1, (list, tuple)): v2 =
"[%s]" % v2
506 if isinstance(parent, list)
and isinstance(path[-1], int): parent[path[-1]] = v2
507 else: api.set_message_value(parent, path[-1], v2)
509 def process_message(obj, patterns):
510 """Returns whether message matches patterns, wraps matches in marker tags if so."""
511 indexes = populate_matches(obj, patterns)
512 is_match =
not indexes
if self.
args.INVERT
else len(indexes) == len(patterns)
513 if not indexes
and self.
_settings[
"pure_anymatch"]
and not api.get_message_fields(obj):
515 if is_match
and WRAPS: wrap_matches(field_values, field_matches)
519 if self.
_settings[
"passthrough"]:
return msg
522 text =
"\n".join(
"%r" % (v, )
for _, v, _
in api.iter_message_fields(msg, flat=
True))
527 LISTIFIABLES = (bytes, tuple)
if six.PY3
else (tuple, )
533 result, is_match = copy.deepcopy(msg)
if WRAPS
else msg,
False
535 evaler =
lambda x: bool(populate_matches(result, [x]))
536 terminal = evaler
if not WRAPS
else lambda x:
BooleanResult(x, evaler)
537 eager = [ExpressionTree.OR]
if WRAPS
else ()
539 is_match =
not evalresult
if self.
args.INVERT
else evalresult
540 if is_match
and WRAPS:
541 actives = [pattern_spans[id(v)]
for v
in evalresult]
542 matches = {k: sum((v.get(k, [])
for v
in actives), [])
543 for k
in set(sum((list(v)
for v
in actives), []))}
or \
544 {k: [(0, len(v))]
for k, (_, _, v)
in field_values.items()}
545 wrap_matches(field_values, matches)
547 is_match = process_message(result, self.
_patterns[
"content"])
548 return result
if is_match
else None
554 Parses and evaluates operator expressions like
"a AND (b OR NOT c)".
556 Operands can be quoted strings,
'\' can be used to escape quotes within the string.
557 Operators are case-insensitive.
560 QUOTES, ESCAPE, LBRACE, RBRACE, WHITESPACE = "'\"",
"\\",
"(",
")",
" \n\r\t"
561 SEPARATORS = WHITESPACE + LBRACE + RBRACE
562 AND, OR, NOT, VAL =
"AND",
"OR",
"NOT",
"VAL"
568 OPERATORS = {AND: (
lambda a, b: a
and b), OR: (
lambda a, b: a
or b), NOT:
lambda a:
not a}
569 RANKS = {VAL: 1, NOT: 2, AND: 3, OR: 4}
571 SHORTCIRCUITS = {AND:
False, OR:
True}
572 FORMAT_TEMPLATES = {AND:
"%s and %s", OR:
"%s or %s", NOT:
"not %s"}
578 @param props
class property overrides, case-insensitive, e.g. `cased=
False`
586 Overrides instance configuration.
588 @param props
class property overrides, case-insensitive, e.g. `cased=
False`
590 for k, v
in props.items():
591 K, V = k.upper(), getattr(self, k.upper())
592 accept = (type(V), type(
None))
if "IMPLICIT" == K
else ()
if "VOID" == K
else type(V)
593 if accept
and not isinstance(v, accept)
and set(map(type, (v, V))) - set([list, tuple]):
594 raise ValueError(
"Invalid value for %s=%s: expected %s" % (k, v, type(V).__name__))
598 def evaluate(self, tree, terminal=None, eager=()):
600 Returns result of evaluating expression tree.
602 @param tree expression tree structure
as given by
parse()
603 @param terminal callback(value) to evaluate value nodes
with,
if not using value directly
604 @param eager operators where to evaluate both operands
in full, despite short-circuit
606 stack = [(tree, [], [], None)]
if tree
else []
608 ((op, val), nvals, pvals, parentop), done = stack.pop(), set()
609 if nvals: done.add(pvals.append(self.
OPERATORS[op](*nvals)))
610 elif pvals
and parentop
in self.
SHORTCIRCUITS and not (eager
and parentop
in eager):
612 if ctor(pvals[0]) == self.
SHORTCIRCUITS[parentop]: done.add(pvals.append(self.
VOID))
614 if op
not in self.
OPERATORS: pvals.append(val
if terminal
is None else terminal(val))
615 else: stack.extend([((op, val), nvals, pvals, parentop)] +
616 [(v, [], nvals, op)
for v
in val[::-1]])
617 return pvals.pop()
if tree
else None
620 def format(self, tree, terminal=None):
622 Returns expression tree formatted as string.
624 @param tree expression tree structure
as given by
parse()
625 @param terminal callback(value) to format value nodes
with,
if not using value directly
628 TPL =
lambda op: (
"%s {0} %s" if op
in self.
BINARIES else "{0} %s").
format(op)
629 WRP =
lambda op, parentop: BRACED
if self.
RANKS[op] > self.
RANKS[parentop]
else "%s"
630 FMT =
lambda vv, op, nodes: tuple(WRP(nop, op) % v
for (nop, _), v
in zip(nodes, vv))
631 stack = [(tree, [], [])]
if tree
else []
633 (op, val), nvals, pvals = stack.pop()
634 if nvals: pvals.append((self.
FORMAT_TEMPLATES.get(op)
or TPL(op)) % FMT(nvals, op, val))
635 elif op
not in self.
OPERATORS: pvals.append(val
if terminal
is None else terminal(val))
636 else: stack.extend([((op, val), nvals, pvals)] + [(v, [], nvals)
for v
in val[::-1]])
637 return pvals.pop()
if tree
else ""
640 def parse(self, text, terminal=None):
642 Returns an operator expression like "a AND (b OR NOT c)" parsed into a binary tree.
644 Binary tree like [
"AND", [[
"VAL",
"a"], [
"OR", [[
"VAL",
"b"], [
"NOT", [[
"VAL",
"c"]]]]]]].
645 Raises on invalid expression.
647 @param terminal callback(text) returning node value
for operands,
if not using plain text
649 root, node, buf, quote, escape, i = [], [], "",
"",
"", -1
653 for i, char
in enumerate(text +
" "):
657 if char == self.
ESCAPE: char =
""
658 elif char
in self.
QUOTES: buf = buf[:-1]
660 (node, root), buf, quote, char = h.add_node(self.
VAL, buf, i),
"",
"",
""
661 escape = char
if self.
ESCAPE == char
else ""
663 h.validate(i,
"quotes", buf=buf)
664 if node
and h.finished(node): node, root = h.add_implicit(node, i)
665 quote, char = char,
""
669 h.validate(i,
"op", op=op)
670 if op
in self.
UNARIES and node
and h.finished(node):
671 node, root = h.add_implicit(node, i)
672 val = h.make_val(op,
None if op
in self.
UNARIES else node)
673 node, root = h.add_node(op, val, i)
675 if (buf
or char == self.
LBRACE)
and node
and h.finished(node):
676 node, root = h.add_implicit(node, i)
677 if buf: node, root = h.add_node(self.
VAL, buf, i)
680 if quote
or char
not in self.
SEPARATORS: buf += char
681 elif char == self.
LBRACE: _, (node, root) = h.stack_push((node, root, i)), ([], [])
682 elif char == self.
RBRACE: _, (node, root) = h.validate(i,
"rbrace"), h.stack_pop()
683 self.
_state.update(locals())
688 def _make_helpers(self, state, text, terminal=None):
689 """Returns namespace object with parsing helper functions."""
690 ERRLABEL, OP_MAXLEN =
"Invalid expression: ", max(map(len, self.
OPERATORS))
691 OPERATORS = {x
if self.
CASED else x.upper(): x
for x
in self.
OPERATORS}
692 stack, parents = [], {}
694 finished =
lambda n:
not (isinstance(n[1], list)
and n[1]
and n[1][-1]
is None)
695 outranks =
lambda a, b: self.
RANKS[a] > self.
RANKS[b]
696 postbrace =
lambda: state.get(
"stacki")
is not None
697 mark =
lambda i:
"\n%s\n%s^" % (text,
" " * i)
698 oper =
lambda n: n[0]
699 parse_op =
lambda b: OPERATORS.get(b
if self.
CASED else b.upper()) \
700 if len(b) <= OP_MAXLEN
else None
701 make_node =
lambda o, v: [o, terminal(v)
if terminal
and self.
VAL == o
else v]
702 make_val =
lambda o, *a: list(a) + [
None] * (1 + (o
in self.
BINARIES) - len(a))
703 add_child =
lambda a, b: (a[1].__setitem__(-1, b), parents.update({id(b): a}))
704 get_child =
lambda n, i: n[1][i]
if n[0]
in self.
OPERATORS else None
706 def missing(op, first=False):
707 label = (
"1st " if first
else "2nd ")
if op
in self.
BINARIES else ""
708 return ERRLABEL +
"missing %selement for %s-operator" % (label, op)
710 def add_node(op, val, i):
711 node0, root0 = _, newroot = state[
"node"], state[
"root"]
713 if not postbrace()
and finished(node0)
and not outranks(op, oper(node0)):
714 val = make_val(op, get_child(node0, -1),
None)
715 elif not outranks(oper(root0), op):
716 val = make_val(op, root0,
None)
717 newnode = make_node(op, val)
719 if node0
and not postbrace()
and (
not finished(node0)
720 or oper(node0)
in self.
BINARIES and not outranks(op, oper(node0))):
721 add_child(node0, newnode)
722 elif not root0
or (root0
is node0
if postbrace()
else not outranks(oper(root0), op)):
724 latest = node0
if node0
and op == self.
VAL else newnode
725 while oper(latest)
in self.
UNARIES and finished(latest)
and id(latest)
in parents:
726 latest = parents[id(latest)]
727 state.update(node=latest, root=newroot, nodei=i, stacki=
None)
728 return latest, newroot
730 def add_implicit(node, i):
731 if not self.
IMPLICIT:
raise ValueError(ERRLABEL +
"missing operator" + mark(i))
735 (node, root, stacki), nodex, rootx = stack.pop(), state[
"node"], state[
"root"]
736 if node: node, stacki, _ = root,
None, add_child(node, rootx)
737 elif not root: node, root = nodex, rootx
738 state.update(node=node, root=root, stacki=stacki)
741 def validate(i, ctx=None, **kws):
743 if kws[
"buf"]:
raise ValueError(ERRLABEL +
"invalid syntax" + mark(i))
745 op, node = kws[
"op"], state[
"node"]
746 if op
in self.
BINARIES and (
not node
or not finished(node)):
747 raise ValueError(missing(oper(node)
if node
else op, first=
not node) + mark(i))
748 elif "rbrace" == ctx:
749 node, nodei = (state.get(k)
for k
in (
"node",
"nodei"))
750 if not stack:
raise ValueError(ERRLABEL +
"bracket end has no start" + mark(i))
751 if not node:
raise ValueError(ERRLABEL +
"empty bracket" + mark(i))
752 if not finished(node):
raise ValueError(missing(oper(node)) + mark(nodei))
754 quote, node, nodei = (state.get(k)
for k
in (
"quote",
"node",
"nodei"))
755 if quote:
raise ValueError(ERRLABEL +
"unfinished quote" + mark(i - 1))
756 if stack:
raise ValueError(ERRLABEL +
"unterminated bracket" + mark(stack[-1][-1]))
757 if node
and not finished(node):
raise ValueError(missing(oper(node)) + mark(nodei))
759 return Namespace(add_child=add_child, add_implicit=add_implicit, add_node=add_node,
760 make_val=make_val, finished=finished, parse_op=parse_op,
761 stack_push=stack.append, stack_pop=stack_pop, validate=validate)
765 """Accumulative result of boolean expression evaluation, tracking value contribution."""
767 def __init__(self, value=Ellipsis, terminal=None, **__props):
768 self._result = Ellipsis
771 for k, v
in __props.items(): setattr(self,
"_" + k, v)
772 if value
is not Ellipsis: self.set(value, terminal)
774 def set(self, value, terminal=None):
775 """Sets value to instance, using terminal callback for evaluation if given."""
776 self._result = bool(terminal(value)
if terminal
else value)
777 self._values, self._actives = [value], [
True if self._result
else None]
780 """Yields active values: contributing to true result positively."""
781 for v
in (v
for v, a
in zip(self._values, self._actives)
if a):
yield v
783 def __bool__(self):
return self._result
784 def __nonzero__(self):
return self._result
785 def __eq__(self, other):
return (bool(self)
if isinstance(other, bool)
else self)
is other
789 """Returns new BooleanResult as a and b."""
790 actives = [on
if y
else None for x, y
in ((a, b), (b, a))
for on
in x._actives]
791 return cls(result=bool(a
and b), values=a._values + b._values, actives=actives)
794 def or_(cls, a, b, eager=False):
795 """Returns new BooleanResult as a or b."""
796 actives = a._actives + [x
if eager
or not a
else None for x
in b._actives]
797 return cls(result=bool(a
or b), values=a._values + b._values, actives=actives)
801 """Returns new BooleanResult as not a."""
802 actives = [
None if x
is None else not x
for x
in a._actives]
803 return cls(result=
not a, values=a._values, actives=actives)
806__all__ = [
"BooleanResult",
"ExpressionTree",
"Scanner"]
Highlight markers for matches in message values.
Accumulative result of boolean expression evaluation, tracking value contribution.
or_(cls, a, b, eager=False)
Returns new BooleanResult as a or b.
not_(cls, a)
Returns new BooleanResult as not a.
and_(cls, a, b)
Returns new BooleanResult as a and b.
Parses and evaluates operator expressions like "a AND (b OR NOT c)".
configure(self, **props)
Overrides instance configuration.
evaluate(self, tree, terminal=None, eager=())
Returns result of evaluating expression tree.
parse(self, text, terminal=None)
Returns an operator expression like "a AND (b OR NOT c)" parsed into a binary tree.
format(self, tree, terminal=None)
Returns expression tree formatted as string.
validate(self, reset=False)
Returns whether conditions have valid syntax, prints errors.
__init__(self, args=None, **kwargs)
work(self, source, sink)
Greps messages yielded from source and emits matched content to sink.
find(self, source, highlight=None)
Yields matched and context messages from source.
valid
Result of validate()
__exit__(self, exc_type, exc_value, traceback)
Context manager exit, does nothing.
__enter__(self)
Context manager entry, does nothing, returns self.
GrepMessage
Namedtuple of (topic name, ROS message, ROS time object, message if matched, index in topic).
match(self, topic, msg, stamp, highlight=None)
Returns matched message if message matches search filters.
get_match(self, msg)
Returns transformed message if all patterns find a match in message, else None.
list ANY_MATCHES
Match patterns for global any-match.