From b6313017d0a2d82091afd86894deb174d18f1a02 Mon Sep 17 00:00:00 2001 From: fanxing11 <34360136+fanxing11@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:24:59 +0800 Subject: [PATCH] fix(classify): do not crash on invalid user-supplied regex Rule.__init__ used to bubble up re.error from re.compile() when a user saved an invalid regex pattern in the category editor (e.g. "Notepad++" on Python versions where "++" is not a valid possessive quantifier). aw-server then returned 500 Internal Server Error for the whole query, which broke every categorize()/tag() chart in the web UI, not just the offending rule. Catch re.error at compile time, log a warning, and disable that one rule (regex=None, match always False). Other rules keep working and events that would have matched the broken rule show up as "Uncategorized" instead of crashing the whole query. Fixes activitywatch/activitywatch#1340 --- aw_transform/classify.py | 28 +++++++++++++++++++++------- tests/test_transforms.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/aw_transform/classify.py b/aw_transform/classify.py index 898f416a..b0c573bd 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 df627dbd..3b3b27e9 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