Skip to content

Commit

Permalink
Building of the docstring in a callback function. (rpy2#658)
Browse files Browse the repository at this point in the history
* Building of the docstring in a callback function.

* Minor fixes for function wrapping.
  • Loading branch information
lgautier authored Mar 8, 2020
1 parent ed768d7 commit 75915c2
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 38 deletions.
91 changes: 62 additions & 29 deletions rpy2/robjects/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,70 @@ def map_signature(
return (inspect.Signature(params), r_ellipsis)


def wrap_docstring_default(
r_func: SignatureTranslatedFunction,
is_method: bool,
signature: inspect.Signature,
r_ellipsis: typing.Optional[int], *,
full_repr: bool = False
) -> str:
"""
Create a docstring that for a wrapped function.
Args:
r_func (SignatureTranslatedFunction): an R function
is_method (bool): Whether the function should be treated as a method
(a `self` parameter is added to the signature if so).
signature (inspect.Signature): A mapped signature for `r_func`
r_ellipsis (bool): Index of the parameter containing the R ellipsis (`...`).
None if the R ellipsis is not in the function signature.
full_repr (bool): Whether to have the full body of the R function in
the docstring dynamically generated.
Returns:
A string.
"""
docstring = []

docstring.append('This {} wraps the following R function.'
.format('method' if is_method else 'function'))

if r_ellipsis:
docstring.extend(
('',
textwrap.dedent(
"""The R ellipsis "..." present in the function's parameters
is mapped to a python iterable of (name, value) pairs (such as
it is returned by the `dict` method `items()` for example."""),
''
)
)
if full_repr:
docstring.append('\n{}'.format(r_func.r_repr()))
else:
r_repr = r_func.r_repr()
i = r_repr.find('\n{')
if i == -1:
docstring.append('\n{}'.format(r_func.r_repr()))
else:
docstring.append('\n{}\n{{\n ...\n}}'.format(r_repr[:i]))

return '\n'.join(docstring)


def wrap_r_function(
r_func: SignatureTranslatedFunction, name: str, *,
is_method: bool = False, full_repr: bool = False,
map_default: typing.Optional[
typing.Callable[[rinterface.Sexp],
typing.Any]
] = _map_default_value
] = _map_default_value,
wrap_docstring: typing.Optional[
typing.Callable[[SignatureTranslatedFunction,
bool,
inspect.Signature,
typing.Optional[int]],
str]
] = wrap_docstring_default
) -> typing.Callable:
"""
Wrap an rpy2 function handle with a Python function with a matching signature.
Expand All @@ -339,8 +396,6 @@ def wrap_r_function(
name (str): The name of the function.
is_method (bool): Whether the function should be treated as a method
(adds a `self` param to the signature if so).
full_repr (bool): Whether to have the full body of the R function in
the docstring dynamically generated.
map_default (function): Function to map default values in the Python
signature. No mapping to default values is done if None.
Returns:
Expand All @@ -364,37 +419,15 @@ def wrapped_func(*args, **kwargs):
value = r_func(*args, **kwargs)
return value

docstring = []
if is_method:
docstring.append('This method of `{}` is implemented in R.'
.format(is_method._robj.rclass[0]))
if wrap_docstring:
docstring = wrap_docstring(r_func, is_method, signature, r_ellipsis)
else:
docstring.append('This function wraps the following R function.')

if r_ellipsis:
docstring.extend(
('',
textwrap.dedent(
"""The R ellipsis "..." present in the function's parameters
is mapped to a python iterable of (name, value) pairs (such as
it is returned by the `dict` method `items()` for example."""),
''
)
)
if full_repr:
docstring.append('\n{}'.format(r_func.r_repr()))
else:
r_repr = r_func.r_repr()
i = r_repr.find('\n{')
if i == -1:
docstring.append('\n{}'.format(r_func.r_repr()))
else:
docstring.append('\n{}\n{{\n ...\n}}'.format(r_repr[:i]))
docstring = 'This is a dynamically created wrapper for an R function.'

wrapped_func.__name__ = name
wrapped_func.__qualname__ = name
wrapped_func.__signature__ = signature
wrapped_func.__doc__ = '\n'.join(docstring)
wrapped_func.__doc__ = docstring
wrapped_func._r_func = r_func

return wrapped_func
24 changes: 15 additions & 9 deletions rpy2/tests/robjects/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,26 +114,32 @@ def test_map_signature_invalid(r_code, parameter_names):
)
)
def test_wrap_r_function_args(r_code, args, kwargs, expected):
full_repr = True
r_func = robjects.r(r_code)
stf = robjects.functions.SignatureTranslatedFunction(r_func)
w_func = robjects.functions.wrap_r_function(stf, 'foo',
full_repr=full_repr)
w_func = robjects.functions.wrap_r_function(stf, 'foo')
res = w_func(*args, **kwargs)
assert tuple(res) == (expected, )


@pytest.mark.parametrize('full_repr', (True, False))
@pytest.mark.parametrize('method_of', (True, False))
def test_wrap_r_function(full_repr, method_of):
@pytest.mark.parametrize('is_method', (True, False))
def test_wrap_r_function(is_method):
r_code = 'function(x, y=FALSE, z="abc") TRUE'
parameter_names = ('x', 'y', 'z')
parameter_names = ('self', 'x', 'y', 'z') if is_method else ('x', 'y', 'z')
r_func = robjects.r(r_code)
stf = robjects.functions.SignatureTranslatedFunction(r_func)
foo = robjects.functions.wrap_r_function(r_func, 'foo',
full_repr=full_repr)
is_method=is_method)
assert foo._r_func.rid == r_func.rid
assert tuple(foo.__signature__.parameters.keys()) == parameter_names
if not method_of:
if not is_method:
res = foo(1)
assert res[0] is True


@pytest.mark.parametrize('wrap_docstring',
(None, robjects.functions.wrap_docstring_default))
def test_wrap_r_function_docstrings(wrap_docstring):
r_code = 'function(x, y=FALSE, z="abc") TRUE'
r_func = robjects.r(r_code)
foo = robjects.functions.wrap_r_function(r_func, 'foo', wrap_docstring=wrap_docstring)
# TODO: only an integration test ? Nothing is tested.

0 comments on commit 75915c2

Please sign in to comment.