diff --git a/aw_transform/classify.py b/aw_transform/classify.py index 898f416..b0c573b 100644 --- a/aw_transform/classify.py +++ b/aw_transform/classify.py @@ -1,3 +1,4 @@ +import logging from typing import Pattern, List, Iterable, Tuple, Dict, Optional, Any from functools import reduce import re @@ -5,6 +6,8 @@ from aw_core import Event +logger = logging.getLogger(__name__) + Tag = str Category = List[str] @@ -20,13 +23,24 @@ def __init__(self, rules: Dict[str, Any]) -> None: # NOTE: Also checks that the regex isn't an empty string (which would erroneously match everything) regex_str = rules.get("regex", None) - self.regex = ( - re.compile( - regex_str, (re.IGNORECASE if self.ignore_case else 0) | re.UNICODE - ) - if regex_str - else None - ) + if regex_str: + try: + self.regex = re.compile( + regex_str, + (re.IGNORECASE if self.ignore_case else 0) | re.UNICODE, + ) + except re.error as e: + # An invalid user-supplied pattern should not crash categorization + # for the entire query. Log it and disable this rule (match nothing). + logger.warning( + "Invalid regex pattern %r in category/tag rule (%s); " + "this rule will match nothing.", + regex_str, + e, + ) + self.regex = None + else: + self.regex = None def match(self, e: Event) -> bool: if self.select_keys: diff --git a/tests/test_transforms.py b/tests/test_transforms.py index df627db..3b3b27e 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -435,6 +435,35 @@ def test_tags(): assert len(events[1].data["$tags"]) == 0 +def test_rule_invalid_regex_does_not_raise(): + # An invalid user-supplied pattern (e.g. "Notepad++" on Python versions + # where "++" is not a valid quantifier) must not raise from Rule.__init__. + # Instead the rule should silently disable itself. See: + # https://github.com/ActivityWatch/activitywatch/issues/1340 + rule = Rule({"regex": "*invalid("}) + assert rule.regex is None + + now = datetime.now(timezone.utc) + e = Event(timestamp=now, duration=0, data={"key": "anything"}) + assert rule.match(e) is False + + +def test_categorize_survives_invalid_regex(): + # A single bad rule should not break categorization for the rest. + now = datetime.now(timezone.utc) + classes = [ + (["Bad"], Rule({"regex": "*invalid("})), + (["Test"], Rule({"regex": "^just"})), + ] + events = [ + Event(timestamp=now, duration=0, data={"key": "just a test"}), + Event(timestamp=now, duration=0, data={"key": "unrelated"}), + ] + events = categorize(events, classes) + assert events[0].data["$category"] == ["Test"] + assert events[1].data["$category"] == ["Uncategorized"] + + def test_union_no_overlap(): from pprint import pprint