-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathparser.py
135 lines (114 loc) · 3.74 KB
/
parser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
Does some really simple parsing of slack messages to determine what actions hmbot should take.
"""
import logging
from contextlib import contextmanager
import spacy
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('hmbot.parser')
logger.setLevel(logging.DEBUG)
nlp = spacy.load('en')
class BackTrack(Exception):
"""Used to drive the backtracking logic."""
pass
class NotHandled(Exception):
"""Raised when no action or command corresponds to an input."""
def __init__(self, tokens):
self.tokens = tokens
class Parser:
def __init__(self, ignore=None, remove=None):
self.remove = tuple(str(e).lower() for e in remove) or ()
self.ignore = tuple(str(e).lower() for e in ignore) or ()
self.handlers = []
def action(self, *args):
args = tuple(match(arg) if type(arg) == str else arg for arg in args)
def dec(func):
self.handlers.append(make_handler(args, func, self.ignore, self.remove))
return func
return dec
def parse(self, text, *args, **kwargs):
tokens = [str(token).lower() for token in nlp(text)]
for handler in self.handlers:
try:
okay, value = handler(tokens, *args, **kwargs)
if okay:
return value
except StopIteration:
pass
except Exception as ex:
logger.exception(ex)
pass
raise NotHandled(tokens)
class Stream:
def __init__(self, tokens, ignore, remove):
self.offset = 0
self.tokens = tokens
self.ignore = ignore
self.remove = remove
def abort(self):
raise BackTrack()
def rest(self):
return self.tokens[self.offset:]
def __next__(self):
while self.offset < len(self.tokens):
token = self.tokens[self.offset]
self.offset += 1
if token in self.ignore:
continue
for s in self.remove:
token = token.replace(s, '')
return token
raise StopIteration()
def __str__(self):
return f"Stream(offset={self.offset}, tokens={self.tokens}, ignore={self.ignore})"
def make_handler(rules, func, ignore, remove):
def handler(tokens, *args, **kwargs):
logger.debug(f"Testing '{func.__name__}'")
stream = Stream(tokens, ignore, remove)
for rule in rules:
ret = rule(stream)
if not ret:
return False, None
return True, func(stream.rest(), *args, **kwargs)
return handler
@contextmanager
def branch(stream):
offset = stream.offset
try:
yield stream
except BackTrack:
stream.offset = offset
def match(string):
"""
Implements `Adapter` pattern for strings to actions.
This is used internally to allow code to act uniformly on inputs.
You don't need to use this from the hmbot library.
"""
rule = [str(token).lower() for token in nlp(string)]
def action(stream):
for part in rule:
token = next(stream)
# logger.debug(f'match {token} {part}')
if token != part:
return False
return True
return action
def oneof(*options):
options = tuple(match(opt) if type(opt) == str else opt for opt in options)
def action(stream):
for opt in options:
with branch(stream):
if opt(stream):
return True
stream.abort()
return False
return action
def maybe(rule):
if type(rule) == str:
rule = match(rule)
def action(stream):
with branch(stream):
if not rule(stream):
stream.abort()
return True
return action