Skip to content

Commit

Permalink
Construct valid signatures for remote methods
Browse files Browse the repository at this point in the history
This allows client-side inspection of argument names, types and default values.
  • Loading branch information
arahlin authored and gsmecher committed Aug 19, 2024
1 parent 483fa43 commit 8cc6230
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 20 deletions.
7 changes: 6 additions & 1 deletion tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pytest
import requests
import subprocess
import inspect

try:
import test_module as tm
Expand Down Expand Up @@ -430,7 +431,11 @@ async def test_tuberpy_method_docstrings(tuber_call, accept_types, simple):
"""Ensure docstrings in C++ methods end up in the TuberObject's __doc__ dunder."""

s = await resolve("Wrapper", accept_types, simple)
assert s.increment.__doc__.strip() == tm.Wrapper.increment.__doc__.strip()
assert s.increment.__doc__.strip() == tm.Wrapper.increment.__doc__.split("\n", 1)[-1].strip()

# check signature
sig = inspect.signature(s.increment)
assert "x" in sig.parameters


@pytest.mark.parametrize("simple", [False])
Expand Down
49 changes: 31 additions & 18 deletions tuber/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import textwrap
import types
import warnings
import inspect

from . import TuberError, TuberStateError, TuberRemoteError
from .codecs import AcceptTypes, Codecs
Expand Down Expand Up @@ -60,6 +61,30 @@ def attribute_blacklisted(name):
return False


def tuber_wrapper(func, props):
"""
Annotate the wrapper function with docstrings and signature.
"""

# Attach docstring, if provided and valid
try:
func.__doc__ = textwrap.dedent(props.__doc__)
except:
pass

# Attach a function signature, if provided and valid
try:
# build a dummy function to parse its signature with inspect
code = compile(f"def sigfunc{props.__signature__}:\n pass", "sigfunc", "single")
exec(code, globals())
sig = inspect.signature(sigfunc)
func.__signature__ = sig
except:
pass

return func


class SubContext:
"""A container for attributes of a top-level (registry) Context object"""

Expand Down Expand Up @@ -449,22 +474,16 @@ def tuber_resolve(self, force=False):

for methname in getattr(meta, "methods", []):
# Generate a callable prototype
def invoke_wrapper(name):
def invoke_wrapper(name, props):
def invoke(self, *args, **kwargs):
with self.tuber_context() as ctx:
getattr(ctx, name)(*args, **kwargs)
results = ctx()
return results[0]

return invoke
return tuber_wrapper(invoke, props)

invoke = invoke_wrapper(methname)

# Attach DocStrings, if provided and valid
try:
invoke.__doc__ = textwrap.dedent(methods[methname].__doc__)
except:
pass
invoke = invoke_wrapper(methname, methods[methname])

# Associate as a class method.
setattr(self, methname, types.MethodType(invoke, self))
Expand Down Expand Up @@ -530,21 +549,15 @@ async def tuber_resolve(self, force=False):

for methname in getattr(meta, "methods", []):
# Generate a callable prototype
def invoke_wrapper(name):
def invoke_wrapper(name, props):
async def invoke(self, *args, **kwargs):
async with self.tuber_context() as ctx:
result = getattr(ctx, name)(*args, **kwargs)
return await result

return invoke
return tuber_wrapper(invoke, props)

invoke = invoke_wrapper(methname)

# Attach DocStrings, if provided and valid
try:
invoke.__doc__ = textwrap.dedent(methods[methname].__doc__)
except:
pass
invoke = invoke_wrapper(methname, methods[methname])

# Associate as a class method.
setattr(self, methname, types.MethodType(invoke, self))
Expand Down
17 changes: 16 additions & 1 deletion tuber/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ def describe(registry, request):
return result_response(attr)

# Complex case: return a description of a method
return result_response(__doc__=inspect.getdoc(attr))
doc = inspect.getdoc(attr)
sig = None
try:
sig = str(inspect.signature(attr))
except:
# pybind docstrings include a signature as the first line
if doc and doc.startswith(attr.__name__ + "("):
if "\n" in doc:
sig, doc = doc.split("\n", 1)
doc = doc.strip()
else:
sig = doc
doc = None
sig = "(" + sig.split("(", 1)[1]

return result_response(__doc__=doc, __signature__=sig)

return error_response(f"Invalid request (object={objname}, method={methodname}, property={propertyname})")

0 comments on commit 8cc6230

Please sign in to comment.