diff --git a/rpy2/robjects/functions.py b/rpy2/robjects/functions.py index aaa955f57..498acd8ee 100644 --- a/rpy2/robjects/functions.py +++ b/rpy2/robjects/functions.py @@ -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. @@ -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: @@ -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 diff --git a/rpy2/tests/robjects/test_function.py b/rpy2/tests/robjects/test_function.py index 203cb1fbe..eb0ce894b 100644 --- a/rpy2/tests/robjects/test_function.py +++ b/rpy2/tests/robjects/test_function.py @@ -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.