Skip to content

Commit 3ef8c8d

Browse files
authored
Merge pull request #14 from standy66/py3.5
feat: add support for Python 3.5
2 parents 565abad + 49ebd62 commit 3ef8c8d

21 files changed

+512
-382
lines changed

.travis.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,22 @@ stages:
99
- name: deploy
1010
if: tag IS present AND type != pull_request
1111
env:
12+
# We run pypy tests first because they run the slowest
13+
- PYTHON_IMAGE="pypy:3" PURERPC_BACKEND="asyncio"
14+
- PYTHON_IMAGE="pypy:3" PURERPC_BACKEND="curio"
15+
- PYTHON_IMAGE="pypy:3" PURERPC_BACKEND="trio"
16+
- PYTHON_IMAGE="standy/pypy:3.6-6.1.0" PURERPC_BACKEND="asyncio"
17+
- PYTHON_IMAGE="standy/pypy:3.6-6.1.0" PURERPC_BACKEND="curio"
18+
- PYTHON_IMAGE="standy/pypy:3.6-6.1.0" PURERPC_BACKEND="trio"
19+
- PYTHON_IMAGE="python:3.5" PURERPC_BACKEND="asyncio"
20+
- PYTHON_IMAGE="python:3.5" PURERPC_BACKEND="curio"
21+
- PYTHON_IMAGE="python:3.5" PURERPC_BACKEND="trio"
1222
- PYTHON_IMAGE="python:3.6" PURERPC_BACKEND="asyncio"
1323
- PYTHON_IMAGE="python:3.6" PURERPC_BACKEND="curio"
1424
- PYTHON_IMAGE="python:3.6" PURERPC_BACKEND="trio"
1525
- PYTHON_IMAGE="python:3.7" PURERPC_BACKEND="asyncio"
1626
- PYTHON_IMAGE="python:3.7" PURERPC_BACKEND="curio"
1727
- PYTHON_IMAGE="python:3.7" PURERPC_BACKEND="trio"
18-
- PYTHON_IMAGE="standy/pypy:3.6-6.1.0" PURERPC_BACKEND="asyncio"
19-
- PYTHON_IMAGE="standy/pypy:3.6-6.1.0" PURERPC_BACKEND="curio"
20-
- PYTHON_IMAGE="standy/pypy:3.6-6.1.0" PURERPC_BACKEND="trio"
2128

2229
script:
2330
- ./ci/run_tests_in_docker.sh $PYTHON_IMAGE $PURERPC_BACKEND

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# purerpc
22

33
[![Build Status](https://travis-ci.org/standy66/purerpc.png?branch=master)](https://travis-ci.org/standy66/purerpc)
4+
[![Downloads](https://pepy.tech/badge/purerpc/month)](https://pepy.tech/project/purerpc)
45

56
Asynchronous pure Python gRPC client and server implementation supporting
67
[asyncio](https://docs.python.org/3/library/asyncio.html),
@@ -9,8 +10,8 @@ Asynchronous pure Python gRPC client and server implementation supporting
910

1011
## Requirements
1112

12-
* CPython >= 3.6
13-
* PyPy >= 3.6
13+
* CPython >= 3.5
14+
* PyPy >= 3.5
1415

1516
## Installation
1617

@@ -45,7 +46,11 @@ python -m grpc_tools.protoc --purerpc_out=. --python_out=. -I. greeter.proto
4546

4647
## Usage
4748

48-
NOTE: `greeter_grpc` module is generated by purerpc's `protoc-gen-purerpc` plugin
49+
NOTE: `greeter_grpc` module is generated by purerpc's `protoc-gen-purerpc` plugin.
50+
51+
Below are the examples for Python 3.6 and above which introduced asynchronous generators as a language concept.
52+
For Python 3.5, where native asynchronous generators are not supported, you can use [async_generator](https://github.com/python-trio/async_generator) library for this purpose.
53+
Just mark yielding coroutines with `@async_generator` decorator and use `await yield_(value)` and `await yield_from_(async_iterable)` instead of `yield`.
4954

5055
### Server
5156

@@ -94,7 +99,7 @@ async def main():
9499

95100

96101
if __name__ == "__main__":
97-
curio.run(main) # Or trio.run(main)
102+
curio.run(main) # Or trio.run(main), or run in asyncio event loop
98103
```
99104

100105
You can mix server and client code, for example make a server that requests something using purerpc from another gRPC server, etc.

ci/run_tests_in_docker.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,12 @@ BUILD_TAG=${BASE_IMAGE//:/-}
88
BUILD_TAG=${BUILD_TAG//\//-}
99

1010
docker build --build-arg BASE_IMAGE=${BASE_IMAGE} -t "standy/purerpc:${BUILD_TAG}" .
11+
1112
echo "Runnig tests with $PURERPC_BACKEND backend"
12-
docker run -it -e PURERPC_BACKEND=${PURERPC_BACKEND} "standy/purerpc:$BUILD_TAG" bash -c 'python setup.py test'
13+
if [[ $BASE_IMAGE == pypy* ]]; then
14+
CMD="pypy3 setup.py test"
15+
else
16+
CMD="python setup.py test"
17+
fi
18+
19+
docker run -it -e PURERPC_BACKEND=${PURERPC_BACKEND} "standy/purerpc:$BUILD_TAG" bash -c "$CMD"

setup.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import os
33
from setuptools import setup, find_packages
44

5+
exec(open("src/purerpc/_version.py", encoding="utf-8").read())
6+
57

68
def read(*names, **kwargs):
79
with open(os.path.join(os.path.dirname(__file__), *names), "r") as fin:
@@ -12,7 +14,7 @@ def main():
1214
console_scripts = ['protoc-gen-purerpc=purerpc.protoc_plugin.plugin:main']
1315
setup(
1416
name="purerpc",
15-
version="0.1.6",
17+
version=__version__,
1618
license="Apache License Version 2.0",
1719
description="Asynchronous pure python gRPC server and client implementation using curio "
1820
"and hyper-h2.",
@@ -31,6 +33,7 @@ def main():
3133
"Operating System :: POSIX :: Linux",
3234
"Programming Language :: Python",
3335
"Programming Language :: Python :: 3 :: Only",
36+
"Programming Language :: Python :: 3.5",
3437
"Programming Language :: Python :: 3.6",
3538
"Programming Language :: Python :: 3.7",
3639
"Programming Language :: Python :: Implementation :: CPython",
@@ -46,13 +49,14 @@ def main():
4649
packages=find_packages('src'),
4750
package_dir={'': 'src'},
4851
test_suite="tests",
49-
python_requires=">=3.6.0",
52+
python_requires=">=3.5",
5053
install_requires=[
51-
"h2",
52-
"protobuf",
53-
"anyio",
54-
"async_exit_stack",
55-
"tblib",
54+
"h2>=3.1.0,<4",
55+
"protobuf~=3.6",
56+
"anyio>=1.0.0b1,<2",
57+
"async_exit_stack>=1.0.1,<2",
58+
"tblib>=1.3.2,<2",
59+
"async_generator>=1.10,<2.0"
5660
],
5761
entry_points={'console_scripts': console_scripts},
5862
setup_requires=["pytest-runner"],

src/purerpc/_version.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# This file is imported from __init__.py and exec'd from setup.py
2+
3+
__version__ = "0.2.0"

src/purerpc/anyio_monkeypatch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
def _new_run(func, *args, backend=None, backend_options=None):
1515
if backend is None:
1616
backend = os.getenv("PURERPC_BACKEND", "asyncio")
17-
log.info(f"Selected {backend} backend")
17+
log.info("Selected {} backend".format(backend))
1818
_anyio_run(func, *args, backend=backend, backend_options=backend_options)
1919

2020

src/purerpc/client.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ def __init__(self, host, port):
1616
self._host = host
1717
self._port = port
1818
self._grpc_socket = None
19-
self._task_group = None
2019

2120
async def __aenter__(self):
2221
await super().__aenter__() # Does nothing
2322

2423
background_task_group = await self.enter_async_context(anyio.create_task_group())
2524
self.push_async_callback(background_task_group.cancel_scope.cancel)
26-
self._task_group = await self.enter_async_context(anyio.create_task_group())
2725

2826
socket = await anyio.connect_tcp(self._host, self._port, autostart_tls=False, tls_standard_compatible=False)
2927
config = GRPCConfiguration(client_side=True)
@@ -57,10 +55,10 @@ def get_method_stub(self, method_name: str, signature: RPCSignature):
5755
stream_fn = functools.partial(self.rpc, method_name, signature.request_type,
5856
signature.response_type)
5957
if signature.cardinality == Cardinality.STREAM_STREAM:
60-
return ClientStubStreamStream(stream_fn, self.channel._task_group)
58+
return ClientStubStreamStream(stream_fn)
6159
elif signature.cardinality == Cardinality.UNARY_STREAM:
62-
return ClientStubUnaryStream(stream_fn, self.channel._task_group)
60+
return ClientStubUnaryStream(stream_fn)
6361
elif signature.cardinality == Cardinality.STREAM_UNARY:
64-
return ClientStubStreamUnary(stream_fn, self.channel._task_group)
62+
return ClientStubStreamUnary(stream_fn)
6563
else:
66-
return ClientStubUnaryUnary(stream_fn, self.channel._task_group)
64+
return ClientStubUnaryUnary(stream_fn)

src/purerpc/grpc_socket.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import datetime
44

55
import anyio
6+
from async_generator import async_generator, yield_, yield_from_
67
import h2
78
import h2.events
89
import h2.exceptions
@@ -193,6 +194,7 @@ def _decref_stream(self, stream_id: int):
193194
del self._streams[stream_id]
194195
del self._streams_count[stream_id]
195196

197+
@async_generator
196198
async def _listen(self):
197199
while True:
198200
data = await self._socket.recv(self._receive_buffer_size)
@@ -214,7 +216,7 @@ async def _listen(self):
214216
await self._streams[event.stream_id]._incoming_events.put(event)
215217

216218
if isinstance(event, RequestReceived):
217-
yield self._streams[event.stream_id]
219+
await yield_(self._streams[event.stream_id])
218220
elif isinstance(event, ResponseEnded) or isinstance(event, RequestEnded):
219221
self._decref_stream(event.stream_id)
220222
await self._socket.try_flush()
@@ -229,11 +231,11 @@ async def initiate_connection(self, parent_task_group: anyio.abc.TaskGroup):
229231
if self.client_side:
230232
await parent_task_group.spawn(self._listener_thread)
231233

234+
@async_generator
232235
async def listen(self):
233236
if self.client_side:
234237
raise ValueError("Cannot listen client-side socket")
235-
async for stream in self._listen():
236-
yield stream
238+
await yield_from_(self._listen())
237239

238240
async def start_request(self, scheme: str, service_name: str, method_name: str,
239241
message_type=None, authority=None, timeout: datetime.timedelta=None,

src/purerpc/grpclib/buffers.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,12 @@ def _parse_one_message(self):
100100
if self._message_length > self._max_message_length:
101101
# Even after the error is raised, the state is not corrupted, and parsing
102102
# can be safely resumed
103-
raise MessageTooLargeError("Received message larger than max: "
104-
f"{self._message_length} > {self._max_message_length}")
103+
raise MessageTooLargeError(
104+
"Received message larger than max: {message_length} > {max_message_length}".format(
105+
message_length=self._message_length,
106+
max_message_length=self._max_message_length,
107+
)
108+
)
105109
if len(self._buffer) < self._message_length:
106110
return None, 0
107111
data, flow_controlled_length = self._buffer.popleft_flowcontrol(self._message_length)
@@ -160,8 +164,12 @@ def write_message(self, data: bytes, compress=False):
160164
if compress:
161165
data = self.compress(data)
162166
if len(data) > self._max_message_length:
163-
raise MessageTooLargeError("Trying to send message larger than max: "
164-
f"{len(data)} > {self._max_message_length}")
167+
raise MessageTooLargeError(
168+
"Trying to send message larger than max: {message_length} > {max_message_length}".format(
169+
message_length=len(data),
170+
max_message_length=self._max_message_length,
171+
)
172+
)
165173
self._buffer.append(struct.pack('>?I', compress, len(data)))
166174
self._buffer.append(data)
167175

src/purerpc/grpclib/events.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,13 @@ def parse_from_stream_id_and_headers_destructive(stream_id: int, headers: Header
8989
return event
9090

9191
def __repr__(self):
92-
return (f"<purerpc.grpclib.events.RequestReceived stream_id: {self.stream_id}, "\
93-
f"service_name: {self.service_name}, method_name: {self.method_name}>")
92+
fmt_string = ("<purerpc.grpclib.events.RequestReceived stream_id: {stream_id}, "
93+
"service_name: {service_name}, method_name: {method_name}>")
94+
return fmt_string.format(
95+
stream_id=self.stream_id,
96+
service_name=self.service_name,
97+
method_name=self.method_name,
98+
)
9499

95100

96101
class MessageReceived(Event):
@@ -100,16 +105,23 @@ def __init__(self, stream_id: int, data: bytes, flow_controlled_length: int):
100105
self.flow_controlled_length = flow_controlled_length
101106

102107
def __repr__(self):
103-
return (f"<purerpc.grpclib.events.MessageReceived stream_id: {self.stream_id}, "
104-
f"flow_controlled_length: {self.flow_controlled_length}>")
108+
fmt_string= ("<purerpc.grpclib.events.MessageReceived stream_id: {stream_id}, "
109+
"flow_controlled_length: {flow_controlled_length}>")
110+
return fmt_string.format(
111+
stream_id=self.stream_id,
112+
flow_controlled_length=self.flow_controlled_length,
113+
)
105114

106115

107116
class RequestEnded(Event):
108117
def __init__(self, stream_id: int):
109118
self.stream_id = stream_id
110119

111120
def __repr__(self):
112-
return f"<purerpc.grpclib.events.RequestEnded stream_id: {self.stream_id}>"
121+
fmt_string = "<purerpc.grpclib.events.RequestEnded stream_id: {stream_id}>"
122+
return fmt_string.format(
123+
stream_id=self.stream_id,
124+
)
113125

114126

115127
class ResponseReceived(Event):
@@ -142,7 +154,11 @@ def parse_from_stream_id_and_headers_destructive(stream_id: int, headers: Header
142154
return event
143155

144156
def __repr__(self):
145-
return f"<purerpc.grpclib.events.ResponseReceived stream_id: {self.stream_id} content_type: {self.content_type}>"
157+
fmt_string = "<purerpc.grpclib.events.ResponseReceived stream_id: {stream_id} content_type: {content_type}>"
158+
return fmt_string.format(
159+
stream_id=self.stream_id,
160+
content_type=self.content_type,
161+
)
146162

147163

148164
class ResponseEnded(Event):
@@ -170,4 +186,8 @@ def parse_from_stream_id_and_headers_destructive(stream_id: int, headers: Header
170186
return event
171187

172188
def __repr__(self):
173-
return f"<purerpc.grpclib.events.ResponseEnded stream_id: {self.stream_id}, status: {self.status}>"
189+
fmt_string = "<purerpc.grpclib.events.ResponseEnded stream_id: {stream_id}, status: {status}>"
190+
return fmt_string.format(
191+
stream_id=self.stream_id,
192+
status=self.status,
193+
)

0 commit comments

Comments
 (0)