Skip to content

Commit 3ebc468

Browse files
author
washort
committed
Merge pull request #24 from washort/parser-protocol
Add Twisted protocol interoperability to ometa and parsley
2 parents 294be97 + 36d2c12 commit 3ebc468

File tree

6 files changed

+449
-0
lines changed

6 files changed

+449
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from twisted.internet.defer import Deferred
2+
from twisted.internet.endpoints import TCP4ServerEndpoint
3+
from twisted.internet.protocol import ServerFactory
4+
from twisted.internet.task import react
5+
6+
from parsley import makeProtocol
7+
from netstrings import grammar, NetstringSender
8+
9+
10+
class NetstringReverserReceiver(object):
11+
def __init__(self, sender, parser):
12+
self.sender = sender
13+
14+
def connectionMade(self):
15+
pass
16+
17+
def connectionLost(self, reason):
18+
pass
19+
20+
def netstringReceived(self, string):
21+
self.sender.sendNetstring(string[::-1])
22+
23+
24+
NetstringReverser = makeProtocol(
25+
grammar, NetstringSender, NetstringReverserReceiver)
26+
27+
28+
class NetstringReverserFactory(ServerFactory):
29+
protocol = NetstringReverser
30+
31+
32+
def main(reactor):
33+
server = TCP4ServerEndpoint(reactor, 1234)
34+
d = server.listen(NetstringReverserFactory())
35+
d.addCallback(lambda p: Deferred())
36+
return d
37+
38+
react(main, [])

examples/protocol/netstrings.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
grammar = """
2+
3+
digit = anything:x ?(x.isdigit())
4+
nonzeroDigit = anything:x ?(x != '0' and x.isdigit())
5+
digits = <'0' | nonzeroDigit digit*>:i -> int(i)
6+
7+
netstring = digits:length ':' <anything{length}>:string ',' -> string
8+
9+
initial = netstring:string -> receiver.netstringReceived(string)
10+
11+
"""
12+
13+
class NetstringSender(object):
14+
def __init__(self, transport):
15+
self.transport = transport
16+
17+
def sendNetstring(self, string):
18+
self.transport.write('%d:%s,' % (len(string), string))

examples/protocol/test_netstrings.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from twisted.test.proto_helpers import StringTransport
2+
3+
import parsley
4+
import pytest
5+
import netstrings
6+
7+
netstringGrammar = parsley.makeGrammar(netstrings.grammar, {})
8+
9+
def stringParserFromRule(rule):
10+
def parseString(s):
11+
return getattr(netstringGrammar(s), rule)()
12+
return parseString
13+
14+
def test_digits_parsing():
15+
parse = stringParserFromRule('digits')
16+
17+
assert parse('0') == 0
18+
assert parse('1') == 1
19+
assert parse('1234567890') == 1234567890
20+
with pytest.raises(parsley.ParseError):
21+
parse('01')
22+
with pytest.raises(parsley.ParseError):
23+
parse('0001')
24+
25+
def test_netstring_parsing():
26+
parse = stringParserFromRule('netstring')
27+
28+
assert parse('0:,') == ''
29+
assert parse('1:x,') == 'x'
30+
assert parse('10:abcdefghij,') == 'abcdefghij'
31+
32+
33+
def build_testing_sender():
34+
transport = StringTransport()
35+
sender = netstrings.NetstringSender(transport)
36+
return sender, transport
37+
38+
def test_sending_empty_netstring():
39+
sender, transport = build_testing_sender()
40+
sender.sendNetstring('')
41+
assert transport.value() == '0:,'
42+
43+
def test_sending_one_netstring():
44+
sender, transport = build_testing_sender()
45+
sender.sendNetstring('foobar')
46+
assert transport.value() == '6:foobar,'
47+
48+
def test_sending_two_netstrings():
49+
sender, transport = build_testing_sender()
50+
sender.sendNetstring('spam')
51+
sender.sendNetstring('egggs')
52+
assert transport.value() == '4:spam,5:egggs,'
53+
54+
55+
class FakeReceiver(object):
56+
def __init__(self, sender, parser):
57+
self.sender = sender
58+
self.parser = parser
59+
self.netstrings = []
60+
self.connected = False
61+
self.lossReason = None
62+
63+
def netstringReceived(self, s):
64+
self.netstrings.append(s)
65+
66+
def connectionMade(self):
67+
self.connected = True
68+
69+
def connectionLost(self, reason):
70+
self.lossReason = reason
71+
72+
TestingNetstringProtocol = parsley.makeProtocol(
73+
netstrings.grammar, netstrings.NetstringSender, FakeReceiver)
74+
75+
def build_testing_protocol():
76+
protocol = TestingNetstringProtocol()
77+
transport = StringTransport()
78+
protocol.makeConnection(transport)
79+
return protocol, transport
80+
81+
def test_receiving_empty_netstring():
82+
protocol, transport = build_testing_protocol()
83+
protocol.dataReceived('0:,')
84+
assert protocol.receiver.netstrings == ['']
85+
86+
def test_receiving_one_netstring_by_byte():
87+
protocol, transport = build_testing_protocol()
88+
for c in '4:spam,':
89+
protocol.dataReceived(c)
90+
assert protocol.receiver.netstrings == ['spam']
91+
92+
def test_receiving_two_netstrings_by_byte():
93+
protocol, transport = build_testing_protocol()
94+
for c in '4:spam,4:eggs,':
95+
protocol.dataReceived(c)
96+
assert protocol.receiver.netstrings == ['spam', 'eggs']
97+
98+
def test_receiving_two_netstrings_in_chunks():
99+
protocol, transport = build_testing_protocol()
100+
for c in ['4:', 'spa', 'm,4', ':eg', 'gs,']:
101+
protocol.dataReceived(c)
102+
assert protocol.receiver.netstrings == ['spam', 'eggs']
103+
104+
def test_receiving_two_netstrings_at_once():
105+
protocol, transport = build_testing_protocol()
106+
protocol.dataReceived('4:spam,4:eggs,')
107+
assert protocol.receiver.netstrings == ['spam', 'eggs']
108+
109+
def test_establishing_connection():
110+
assert not FakeReceiver(None, None).connected
111+
protocol, transport = build_testing_protocol()
112+
assert protocol.receiver.connected
113+
114+
def test_losing_connection():
115+
protocol, transport = build_testing_protocol()
116+
reason = object()
117+
protocol.connectionLost(reason)
118+
assert protocol.receiver.lossReason == reason

ometa/protocol.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from twisted.internet.protocol import Protocol
2+
from twisted.python.failure import Failure
3+
4+
from ometa.interp import TrampolinedGrammarInterpreter, _feed_me
5+
6+
class ParserProtocol(Protocol):
7+
currentRule = 'initial'
8+
9+
def __init__(self, grammar, senderFactory, receiverFactory, bindings):
10+
self.grammar = grammar
11+
self.bindings = dict(bindings)
12+
self.senderFactory = senderFactory
13+
self.receiverFactory = receiverFactory
14+
self.disconnecting = False
15+
16+
def setNextRule(self, rule):
17+
self.currentRule = rule
18+
19+
def connectionMade(self):
20+
self.sender = self.senderFactory(self.transport)
21+
self.bindings['receiver'] = self.receiver = self.receiverFactory(
22+
self.sender, self)
23+
self.receiver.connectionMade()
24+
self._setupInterp()
25+
26+
def _setupInterp(self):
27+
self._interp = TrampolinedGrammarInterpreter(
28+
self.grammar, self.currentRule, callback=self._parsedRule,
29+
globals=self.bindings)
30+
31+
def _parsedRule(self, nextRule, position):
32+
if nextRule is not None:
33+
self.currentRule = nextRule
34+
35+
def dataReceived(self, data):
36+
if self.disconnecting:
37+
return
38+
39+
while data:
40+
try:
41+
status = self._interp.receive(data)
42+
except Exception:
43+
self.connectionLost(Failure())
44+
self.transport.abortConnection()
45+
return
46+
else:
47+
if status is _feed_me:
48+
return
49+
data = ''.join(self._interp.input.data[self._interp.input.position:])
50+
self._setupInterp()
51+
52+
def connectionLost(self, reason):
53+
if self.disconnecting:
54+
return
55+
self.receiver.connectionLost(reason)
56+
self.disconnecting = True

0 commit comments

Comments
 (0)