Skip to content

Commit b28328e

Browse files
committed
Add support for out-of-tree bindings
Use the setuptools entry_point infrastructure to automatically load binding modules installed in the environment. Fixes: #381.
1 parent 97dcc25 commit b28328e

File tree

9 files changed

+99
-0
lines changed

9 files changed

+99
-0
lines changed

azure/functions_worker/dispatcher.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import traceback
1212

1313
import grpc
14+
import pkg_resources
1415

1516
from . import bindings
1617
from . import functions
@@ -64,6 +65,16 @@ def __init__(self, loop, host, port, worker_id, request_id,
6465
self._grpc_thread = threading.Thread(
6566
name='grpc-thread', target=self.__poll_grpc)
6667

68+
def load_bindings(self):
69+
"""Load out-of-tree binding implementations."""
70+
services = {}
71+
72+
for ep in pkg_resources.iter_entry_points('azure.functions.bindings'):
73+
logger.info('Loading binding plugin from %s', ep.module_name)
74+
ep.load()
75+
76+
return services
77+
6778
@classmethod
6879
async def connect(cls, host, port, worker_id, request_id,
6980
connect_timeout, max_msg_len=None):

azure/functions_worker/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ async def start_async(host, port, worker_id, request_id, grpc_max_msg_len):
4848
host, port, worker_id, request_id,
4949
connect_timeout=5.0, max_msg_len=grpc_max_msg_len)
5050

51+
disp.load_bindings()
52+
5153
await disp.dispatch_forever()

azure/functions_worker/testutils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ async def __aenter__(self):
415415
self._host.worker_id, self._host.request_id,
416416
connect_timeout=5.0)
417417

418+
self._worker.load_bindings()
419+
418420
self._worker_task = loop.create_task(self._worker.dispatch_forever())
419421

420422
done, pending = await asyncio.wait(

tests/test-binding/foo/__init__.py

Whitespace-only changes.

tests/test-binding/foo/binding.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from azure.functions_worker.bindings import meta
2+
3+
4+
class Binding(meta.InConverter, meta.OutConverter,
5+
binding='fooType'):
6+
pass
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"scriptFile": "main.py",
3+
4+
"bindings": [
5+
{
6+
"type": "fooType",
7+
"direction": "in",
8+
"name": "req"
9+
}
10+
]
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def main(req):
2+
pass

tests/test-binding/setup.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from setuptools import setup
2+
3+
4+
setup(
5+
name='foo-binding',
6+
version='1.0',
7+
packages=['foo'],
8+
entry_points={
9+
'azure.functions.bindings': [
10+
'foo=foo.binding:Binding',
11+
]
12+
},
13+
)

tests/test_loader.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import asyncio
2+
import pathlib
3+
import subprocess
4+
import sys
5+
import textwrap
6+
17
from azure.functions_worker import testutils
28

39

@@ -26,3 +32,49 @@ def test_loader_relimport(self):
2632
r = self.webhost.request('GET', 'relimport')
2733
self.assertEqual(r.status_code, 200)
2834
self.assertEqual(r.text, '__app__.relimport.relative')
35+
36+
37+
class TestPluginLoader(testutils.AsyncTestCase):
38+
39+
async def test_entry_point_plugin(self):
40+
test_binding = pathlib.Path(__file__).parent / 'test-binding'
41+
subprocess.run([
42+
sys.executable, '-m', 'pip',
43+
'--disable-pip-version-check',
44+
'install', '--quiet',
45+
'-e', test_binding
46+
], check=True)
47+
48+
# This test must be run in a subprocess so that
49+
# pkg_resources picks up the newly installed package.
50+
code = textwrap.dedent('''
51+
import asyncio
52+
from azure.functions_worker import protos
53+
from azure.functions_worker import testutils
54+
55+
async def _runner():
56+
async with testutils.start_mockhost(
57+
script_root='test-binding/functions') as host:
58+
func_id, r = await host.load_function('foo')
59+
60+
print(r.response.function_id == func_id)
61+
print(r.response.result.status == protos.StatusResult.Success)
62+
63+
asyncio.get_event_loop().run_until_complete(_runner())
64+
''')
65+
66+
try:
67+
proc = await asyncio.create_subprocess_exec(
68+
sys.executable, '-c', code,
69+
stdout=asyncio.subprocess.PIPE)
70+
71+
stdout, stderr = await proc.communicate()
72+
73+
self.assertEqual(stdout.strip().split(b'\n'), [b'True', b'True'])
74+
75+
finally:
76+
subprocess.run([
77+
sys.executable, '-m', 'pip',
78+
'--disable-pip-version-check',
79+
'uninstall', '-y', '--quiet', 'foo-binding'
80+
], check=True)

0 commit comments

Comments
 (0)