Skip to content

Commit f96a51e

Browse files
added type hints
1 parent 8b75a37 commit f96a51e

14 files changed

Lines changed: 185 additions & 63 deletions

File tree

README.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
MISSING:
2+
3+
sifter/commands/imap4flags.py
4+
sifter/commands/pipe.py
5+
sifter/commands/rewrite.py
6+
sifter/tests/envelope.py
7+
8+
19
Python implementation of the Sieve email filtering language (RFC 5228).
210

311
https://github.com/garyp/sifter
@@ -80,4 +88,3 @@ In rough order of importance:
8088
- mailbox metadata (RFC 5490)
8189
- notifications (RFC 5435), mailto notifications (RFC 5436), xmpp
8290
notifications (RFC 5437)
83-

setup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@
7474
'notify_method_capability = sifter.tests.notify:TestValidNotifyMethod',
7575
# sifter comparators
7676
'ascii_casemap = sifter.comparators.ascii_casemap:ComparatorASCIICasemap',
77-
'octed = sifter.comparators.octet:ComparatorOctet'
78-
],
77+
'ascii_casemap_noi = sifter.comparators.ascii_casemap:ComparatorASCIICasemapnoi',
78+
'octed = sifter.comparators.octet:ComparatorOctet',
79+
# notification methods
80+
'mailto = sifter.notificationmethods.mailto:MailtoNotificationMethod',
81+
]
7982
}
8083
)

sifter/commands/fileinto.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions
2020
state.check_required_extension('fileinto', 'FILEINTO')
2121

2222
file_dest = self.positional_args[0]
23-
file_dest = list(map(lambda s: expand_variables(s, state), file_dest))
23+
file_dest = list(map(lambda s: expand_variables(s, state), file_dest)) # type: ignore
2424

25-
state.actions.append('fileinto', file_dest) # type: ignore
25+
state.actions.append('fileinto', file_dest)
2626
state.actions.cancel_implicit_keep()
2727
return None

sifter/commands/imap4flags.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from email.message import Message
2+
from typing import Optional
3+
4+
from sifter.grammar.command import Command
5+
from sifter.validators.stringlist import StringList
6+
from sifter.grammar.string import expand_variables
7+
from sifter.grammar.state import EvaluationState
8+
from sifter.grammar.actions import Actions
9+
10+
# This implements the RFC5232 imap4flags extension
11+
# commands: addflag, removeflag, setflag
12+
# tests: :hasflag
13+
# tagged arguments: :flag to 'fileinto'
14+
15+
16+
class CommandSetFlag(Command):
17+
18+
RULE_IDENTIFIER = 'SETFLAG'
19+
POSITIONAL_ARGS = [StringList()]
20+
21+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
22+
state.check_required_extension('imap4flags', 'imapflags')
23+
flag_list = self.positional_args[0]
24+
flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore
25+
state.actions.append('setflag', flag_list)
26+
return None
27+
28+
29+
class CommandRemoveFlag(Command):
30+
31+
RULE_IDENTIFIER = 'REMOVEFLAG'
32+
POSITIONAL_ARGS = [StringList()]
33+
34+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
35+
state.check_required_extension('imap4flags', 'imapflags')
36+
flag_list = self.positional_args[0]
37+
flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore
38+
state.actions.append('removeflag', flag_list)
39+
return None
40+
41+
42+
class CommandAddFlag(Command):
43+
44+
RULE_IDENTIFIER = 'ADDFLAG'
45+
POSITIONAL_ARGS = [StringList()]
46+
47+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
48+
state.check_required_extension('imap4flags', 'imapflags')
49+
flag_list = self.positional_args[0]
50+
flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore
51+
state.actions.append('addflag', flag_list)
52+
return None

sifter/commands/notify.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import re
2-
import sifter.grammar
3-
import sifter.validators
4-
import sifter.grammar.notificationmethod
52
from sifter.grammar.command import Command
3+
from sifter.grammar.string import expand_variables
64
from sifter.validators.stringlist import StringList
75
from sifter.validators.tag import Tag
6+
from sifter.grammar.rule import RuleSyntaxError
7+
from sifter.extensions import ExtensionRegistry
88

99

1010
# RFC 5435
@@ -33,27 +33,27 @@ def __init__(self, arguments=None, tests=None, block=None):
3333
if 'options' in self.tagged_args:
3434
self.notify_options = self.tagged_args['options'][1]
3535
if 'message' in self.tagged_args:
36-
self.notify_message = self.tagged_args['message'][1][0]
37-
self.notify_method = self.positional_args[0][0]
36+
self.notify_message = self.tagged_args['message'][1][0] # type: ignore
37+
self.notify_method = self.positional_args[0][0] # type: ignore
3838

3939
def evaluate(self, message, state):
4040
state.check_required_extension('enotify', 'NOTIFY')
41-
notify_from = sifter.grammar.string.expand_variables(self.notify_from, state)
42-
notify_importance = sifter.grammar.string.expand_variables(self.notify_importance, state)
43-
notify_options = map(lambda s: sifter.grammar.string.expand_variables(s, state), self.notify_options)
44-
notify_message = sifter.grammar.string.expand_variables(self.notify_message, state)
45-
notify_method = sifter.grammar.string.expand_variables(self.notify_method, state)
41+
notify_from = expand_variables(self.notify_from, state)
42+
notify_importance = expand_variables(self.notify_importance, state)
43+
notify_options = map(lambda s: expand_variables(s, state), self.notify_options)
44+
notify_message = expand_variables(self.notify_message, state)
45+
notify_method = expand_variables(self.notify_method, state)
4646

4747
m = re.match('^([A-Za-z][A-Za-z0-9.+-]*):', notify_method)
4848
if not m:
49-
raise sifter.grammar.RuleSyntaxError("Notification method must be an URI, e.g. 'mailto:email@example.com'")
49+
raise RuleSyntaxError("Notification method must be an URI, e.g. 'mailto:email@example.com'")
5050
if notify_importance and notify_importance not in ["1", "2", "3"]:
51-
raise sifter.grammar.RuleSyntaxError("Illegal notify importance '%s' encountered" % self.notify_importance)
52-
notify_method_cls = sifter.grammar.notificationmethod.get_cls(m.group(1).lower())
51+
raise RuleSyntaxError("Illegal notify importance '%s' encountered" % self.notify_importance)
52+
notify_method_cls = ExtensionRegistry.get_notification_method(m.group(1).lower())
5353
if not notify_method_cls:
54-
raise sifter.grammar.RuleSyntaxError("Unsupported notification method '%s'" % m.group(1))
54+
raise RuleSyntaxError("Unsupported notification method '%s'" % m.group(1))
5555
(res, msg) = notify_method_cls.test_valid(notify_method)
5656
if not res:
57-
raise sifter.grammar.RuleSyntaxError(msg)
57+
raise RuleSyntaxError(msg)
5858

5959
state.actions.append('notify', (notify_method, notify_from, notify_importance, notify_options, notify_message))

sifter/comparators/ascii_casemap.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import string
12
from typing import Text
23
from sifter.comparators.octet import ComparatorOctet
34

@@ -10,4 +11,23 @@ class ComparatorASCIICasemap(ComparatorOctet):
1011

1112
@classmethod
1213
def sort_key(cls, s: Text) -> Text:
13-
return s.upper()
14+
return s.translate(
15+
str.maketrans(
16+
string.ascii_lowercase,
17+
string.ascii_uppercase
18+
)
19+
)
20+
21+
22+
class ComparatorASCIICasemapnoi(ComparatorOctet):
23+
24+
COMPARATOR_ID = ';ascii-casemap'
25+
26+
@classmethod
27+
def sort_key(cls, s: Text) -> Text:
28+
return s.translate(
29+
str.maketrans(
30+
string.ascii_lowercase,
31+
string.ascii_uppercase
32+
)
33+
)

sifter/extensions/__init__.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,35 @@
1313
from sifter.grammar.comparator import Comparator
1414
from sifter.grammar.rule import Rule
1515
from sifter.grammar.command import Command
16+
from sifter.grammar.notificationmethod import NotificationMethod
1617
from sifter.grammar.test import Test
1718
if TYPE_CHECKING:
1819
from sifter.grammar.tag import Tag
1920

2021

2122
class ExtensionRegistry():
2223

23-
_HANDLERS_MAP: Dict[Text, Dict[Text, Union[bool, Type['Comparator'], Type['Rule']]]] = {}
24+
_HANDLERS_MAP: Dict[
25+
Text,
26+
Dict[
27+
Text,
28+
Union[
29+
bool,
30+
Type['Comparator'],
31+
Type['Rule'],
32+
Type['NotificationMethod']
33+
]
34+
]
35+
] = {}
2436
DEFAULT_EXTENSION: List[Text] = [
2537
'regex',
2638
'comparator-i;ascii-casemap',
2739
'comparator-i;octet',
2840
'fileinto',
2941
'body'
3042
'variables',
31-
'enotify'
43+
'enotify',
44+
'imap4flags'
3245
]
3346

3447
def __init__(self) -> None:
@@ -67,6 +80,13 @@ def get_test(cls, testname: Text) -> Type['Test']:
6780
raise ValueError('Wrong Test Type!')
6881
return handler
6982

83+
@classmethod
84+
def get_notification_method(cls, methodname: Text) -> Type[NotificationMethod]:
85+
handler = cls.get('test', methodname)
86+
if not isinstance(handler, type) or not issubclass(handler, NotificationMethod):
87+
raise ValueError('Wrong Notification Method Type!')
88+
return handler
89+
7090
@classmethod
7191
def has_extension(cls, ext_name: Text) -> bool:
7292
if cls.get('extension', ext_name):
@@ -83,9 +103,9 @@ def register(
83103
cls._HANDLERS_MAP.setdefault(handler_type, {})[handler_id] = value
84104

85105
@classmethod
86-
def unregister(cls, handler_type: Text, handler_id: Text) -> Optional[Union[bool, Type['Comparator'], Type['Rule']]]:
106+
def unregister(cls, handler_type: Text, handler_id: Text) -> Optional[Union[bool, Type[NotificationMethod], Type['Comparator'], Type['Rule']]]:
87107
return cls._HANDLERS_MAP.get(handler_type, {}).pop(handler_id, None)
88108

89109
@classmethod
90-
def get(cls, handler_type: Text, handler_id: Text) -> Optional[Union[bool, Type['Comparator'], Type['Rule']]]:
110+
def get(cls, handler_type: Text, handler_id: Text) -> Optional[Union[bool, Type[NotificationMethod], Type['Comparator'], Type['Rule']]]:
91111
return cls._HANDLERS_MAP.get(handler_type, {}).get(handler_id, None)

sifter/grammar/command.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ def validate_block_size(self, max_commands: int) -> None:
5656
"%s takes no more than %d commands" % (self.RULE_IDENTIFIER, max_commands)
5757
)
5858

59-
def validate(self) -> Tuple[Dict[Text, List[Union['TagGrammar', SupportsInt, List[Union[Text, 'String']]]]], List[Union['TagGrammar', SupportsInt, List[Union[Text, 'String']]]]]:
59+
def validate(self) -> Tuple[
60+
Dict[Text, List[Union['TagGrammar', SupportsInt, List[Union[Text, 'String']]]]],
61+
List[Union['TagGrammar', SupportsInt, List[Union[Text, 'String']]]]
62+
]:
6063
if self.HAS_BLOCKS:
6164
self.validate_block_size(max_commands=self.BLOCKS_MAX)
6265
return super().validate()
Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
from sifter.extensions import ExtensionRegistry
1+
from typing import (
2+
Text,
3+
Tuple,
4+
Optional
5+
)
26

37

4-
def register(notification_method_name, notification_method_cls):
5-
ExtensionRegistry.register('notification_method', notification_method_name, notification_method_cls)
6-
7-
8-
def get_cls(notification_method_name):
9-
return ExtensionRegistry.get('notification_method', notification_method_name)
8+
class NotificationMethod(object):
109

10+
NOTIFICATION_METHOD_ID: Optional[Text] = None
1111

12-
class NotificationMethod(object):
12+
@classmethod
13+
def handler_type(cls) -> Text:
14+
return 'notification_method'
1315

1416
@classmethod
15-
def register(cls):
16-
try:
17-
register(cls.NOTIFICATION_METHOD_ID, cls)
18-
except AttributeError:
19-
# this method should only be called on sub-classes that define an
20-
# identifier
21-
raise NotImplementedError
17+
def handler_id(cls) -> Text:
18+
if cls.NOTIFICATION_METHOD_ID is None:
19+
raise NotImplementedError('NotificationMethod must be implemented as subclass as NOTIFICATION_METHOD_ID must be set')
20+
return cls.NOTIFICATION_METHOD_ID
2221

2322
@classmethod
24-
def test_valid(cls, notification_uri):
23+
def test_valid(cls, notification_uri: Text) -> Tuple[bool, Text]:
2524
raise NotImplementedError
2625

2726
@classmethod
28-
def test_capability(cls, notification_uri, notification_capability):
27+
def test_capability(cls, notification_uri: Text, notification_capability: Text) -> Tuple[bool, Text]:
2928
raise NotImplementedError

sifter/grammar/state.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import (
2+
Any,
23
Text,
4+
List,
35
Dict,
46
Optional
57
)
@@ -18,13 +20,13 @@ def __init__(self) -> None:
1820
for ext in ('comparator-i;octet', 'comparator-i;ascii-casemap'):
1921
self.require_extension(ext)
2022
# variables extension
21-
self.named_variables = {}
22-
self.match_variables = []
23+
self.named_variables: Dict[Text, Any] = {}
24+
self.match_variables: List[Any] = []
2325

2426
def require_extension(self, extension: Text) -> None:
2527
self.required_extensions[extension] = True
2628

27-
def have_extension(self, extension):
29+
def have_extension(self, extension: Text) -> bool:
2830
return extension in self.required_extensions
2931

3032
def check_required_extension(self, extension: Text, feature_string: Text) -> bool:

0 commit comments

Comments
 (0)