aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xmailrules.py11
-rwxr-xr-xtest_mailrules.py36
2 files changed, 47 insertions, 0 deletions
diff --git a/mailrules.py b/mailrules.py
index 26ed698..7df1b42 100755
--- a/mailrules.py
+++ b/mailrules.py
@@ -125,6 +125,17 @@ def load(path=None):
return store
+def ordered(rules):
+ """Enabled rules in execution order: by stage ascending, ties by position.
+
+ `sorted` is stable, so sorting on stage alone preserves file order within
+ a stage. That is the tie-break the format promises, and it is why this
+ does not sort on (stage, id): an id-sorted tie would reorder rules a user
+ deliberately sequenced.
+ """
+ return sorted([r for r in rules if r.enabled], key=lambda r: r.stage)
+
+
def _parse_rule(obj, index, seen, warnings):
"""One rule, or None with a warning appended. `index` names the rule when
it has no usable id of its own."""
diff --git a/test_mailrules.py b/test_mailrules.py
index f349f05..e08f490 100755
--- a/test_mailrules.py
+++ b/test_mailrules.py
@@ -154,6 +154,42 @@ def test_a_newer_format_version_is_refused():
assert any("version" in w for w in store.warnings)
+def test_ordered_sorts_by_stage_then_file_position():
+ """Account tags must run before topic rules. Ties keep file order, so
+ the file still reads as a sequence."""
+ with tempfile.TemporaryDirectory() as tmp:
+ path = write_rules(tmp, {
+ "version": 1,
+ "rules": [
+ {"id": "topic-b", "stage": 50, "add": ["b"],
+ "query": "from:b@example.com"},
+ {"id": "account", "stage": 10, "add": ["acct"],
+ "query": "path:\"work/**\""},
+ {"id": "topic-a", "stage": 50, "add": ["a"],
+ "query": "from:a@example.com"},
+ ],
+ })
+ store = mailrules.load(path)
+ assert [r.id for r in mailrules.ordered(store.rules)] == [
+ "account", "topic-b", "topic-a"]
+
+
+def test_ordered_skips_disabled_rules():
+ with tempfile.TemporaryDirectory() as tmp:
+ path = write_rules(tmp, {
+ "version": 1,
+ "rules": [
+ {"id": "on", "add": ["a"], "query": "from:a@example.com"},
+ {"id": "off", "add": ["b"], "query": "from:b@example.com",
+ "enabled": False},
+ ],
+ })
+ store = mailrules.load(path)
+ assert [r.id for r in mailrules.ordered(store.rules)] == ["on"]
+ # The disabled rule is still LOADED, so a UI can show and re-enable it.
+ assert [r.id for r in store.rules] == ["on", "off"]
+
+
def run_all():
for name, fn in sorted(globals().items()):
if name.startswith("test_") and callable(fn):