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.