diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index bb704d74075..a19dee3b509 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -115,6 +115,7 @@ class definition be found starting from the ``__init__`` method. import sys import tokenize import re +from inspect import Signature, Parameter try: import importlib.machinery as import_machinery @@ -1437,7 +1438,7 @@ def sage_getargspec(obj): FullArgSpec(args=['x', 'y', 'z', 't'], varargs='args', varkw='keywords', defaults=(1, 2), kwonlyargs=[], kwonlydefaults=None, annotations={}) - We now run sage_getargspec on some functions from the Sage library:: + We now run :func:`sage_getargspec` on some functions from the Sage library:: sage: sage_getargspec(identity_matrix) # needs sage.modules FullArgSpec(args=['ring', 'n', 'sparse'], varargs=None, varkw=None, @@ -1698,6 +1699,153 @@ def foo(x, a='\')"', b={not (2+1==3):'bar'}): return kwonlyargs=[], kwonlydefaults=None, annotations={}) +def _fullargspec_to_signature(fullargspec): + """ + Converts a :class:`FullArgSpec` instance to a :class:`Signature` instance by best effort. + The opposite conversion is implemented in the source code of :func:`inspect.getfullargspec`. + + EXAMPLES:: + + sage: from sage.misc.sageinspect import _fullargspec_to_signature + sage: from inspect import FullArgSpec + sage: fullargspec = FullArgSpec(args=['self', 'x', 'base'], varargs=None, varkw=None, defaults=(None, 0), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + + TESTS:: + + sage: fullargspec = FullArgSpec(args=['p', 'r'], varargs='q', varkw='s', defaults=({},), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['r'], varargs=None, varkw=None, defaults=((None, 'u:doing?'),), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['x'], varargs=None, varkw=None, defaults=('):',), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['z'], varargs=None, varkw=None, defaults=({(1, 2, 3): True},), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['x', 'z'], varargs=None, varkw=None, defaults=({(1, 2, 3): True},), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=[], varargs='args', varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=[], varargs=None, varkw='args', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['self', 'x'], varargs='args', varkw=None, defaults=(1,), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['self', 'x'], varargs='args', varkw=None, defaults=(1,), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: fullargspec = FullArgSpec(args=['x', 'z'], varargs=None, varkw=None, defaults=('a string', {(1, 2, 3): True}), kwonlyargs=[], kwonlydefaults=None, annotations={}) + sage: _fullargspec_to_signature(fullargspec) + + sage: _fullargspec_to_signature(FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b', 'c'], kwonlydefaults={'b': 1}, annotations={})) + + sage: _fullargspec_to_signature(FullArgSpec(args=['a', 'b'], varargs=None, varkw=None, defaults=(1,), kwonlyargs=['c'], kwonlydefaults=None, annotations={})) + + """ + parameters = [] + defaults_start = len(fullargspec.args) - len(fullargspec.defaults) if fullargspec.defaults else None + + for i, arg in enumerate(fullargspec.args): + default = fullargspec.defaults[i - defaults_start] if defaults_start is not None and i >= defaults_start else Parameter.empty + param = Parameter(arg, Parameter.POSITIONAL_OR_KEYWORD, default=default) + parameters.append(param) + + if fullargspec.varargs: + param = Parameter(fullargspec.varargs, Parameter.VAR_POSITIONAL) + parameters.append(param) + + if fullargspec.varkw: + param = Parameter(fullargspec.varkw, Parameter.VAR_KEYWORD) + parameters.append(param) + + for arg in fullargspec.kwonlyargs: + param = Parameter(arg, Parameter.KEYWORD_ONLY, default=Parameter.empty if fullargspec.kwonlydefaults is None else + fullargspec.kwonlydefaults.get(arg, Parameter.empty)) + parameters.append(param) + + return Signature(parameters) + + +def sage_signature(obj): + r""" + Return the names and default values of a function's arguments. + + INPUT: + + - ``obj`` -- any callable object + + OUTPUT: + + A :class:`Signature` is returned, as specified by the + Python library function :func:`inspect.signature`. + + + .. NOTE:: + + Currently the type information is not returned, because the output + is converted from the return value of :func:`sage_getargspec`. + This should be changed in the future. + + EXAMPLES:: + + sage: from sage.misc.sageinspect import sage_signature + sage: def f(x, y, z=1, t=2, *args, **keywords): + ....: pass + sage: sage_signature(f) + + + We now run :func:`sage_signature` on some functions from the Sage library:: + + sage: sage_signature(identity_matrix) # needs sage.modules + + sage: sage_signature(factor) + + + In the case of a class or a class instance, the :class:`Signature` of the + ``__new__``, ``__init__`` or ``__call__`` method is returned:: + + sage: P. = QQ[] + sage: sage_signature(P) # needs sage.libs.singular + + sage: sage_signature(P.__class__) # needs sage.libs.singular + + + The following tests against various bugs that were fixed in + :issue:`9976`:: + + sage: from sage.rings.polynomial.real_roots import bernstein_polynomial_factory_ratlist # needs sage.modules + sage: sage_signature(bernstein_polynomial_factory_ratlist.coeffs_bitsize) # needs sage.modules + + sage: from sage.rings.polynomial.pbori.pbori import BooleanMonomialMonoid # needs sage.rings.polynomial.pbori + sage: sage_signature(BooleanMonomialMonoid.gen) # needs sage.rings.polynomial.pbori + + sage: I = P*[x,y] + sage: sage_signature(I.groebner_basis) # needs sage.libs.singular + + sage: cython("cpdef int foo(x,y) except -1: return 1") # needs sage.misc.cython + sage: sage_signature(foo) # needs sage.misc.cython + + + If a :func:`functools.partial` instance is involved, we see no other meaningful solution + than to return the signature of the underlying function:: + + sage: def f(a, b, c, d=1): + ....: return a + b + c + d + sage: import functools + sage: f1 = functools.partial(f, 1, c=2) + sage: sage_signature(f1) + + """ + return _fullargspec_to_signature(sage_getargspec(obj)) + + def formatannotation(annotation, base_module=None): """ This is taken from Python 3.7's inspect.py; the only change is to diff --git a/src/sage/repl/ipython_extension.py b/src/sage/repl/ipython_extension.py index 661df8f3b3a..a4dd95648fe 100644 --- a/src/sage/repl/ipython_extension.py +++ b/src/sage/repl/ipython_extension.py @@ -652,6 +652,7 @@ def init_inspector(self): IPython.core.oinspect.getsource = LazyImport("sage.misc.sagedoc", "my_getsource") IPython.core.oinspect.find_file = LazyImport("sage.misc.sageinspect", "sage_getfile") IPython.core.oinspect.getargspec = LazyImport("sage.misc.sageinspect", "sage_getargspec") + IPython.core.oinspect.signature = LazyImport("sage.misc.sageinspect", "sage_signature") # pyright: ignore [reportPrivateImportUsage] def init_line_transforms(self): """ diff --git a/src/sage/repl/ipython_tests.py b/src/sage/repl/ipython_tests.py index 1e26d47717c..e684012b488 100644 --- a/src/sage/repl/ipython_tests.py +++ b/src/sage/repl/ipython_tests.py @@ -3,7 +3,7 @@ Tests for the IPython integration First, test the pinfo magic for Python code. This is what IPython -calls when you ask for the single-questionmark help, like `foo?` :: +calls when you ask for the single-questionmark help, like ``foo?`` :: sage: from sage.repl.interpreter import get_test_shell sage: shell = get_test_shell() @@ -45,6 +45,23 @@ Type: type ... +Test that the signature is displayed even with ``binding=False`` +as long as ``embedsignature=True`` is set +(unfortunately the type is not displayed, see ``sage_signature``):: + + sage: shell.run_cell(r""" + ....: %%cython + ....: # cython: binding=False, embedsignature=True + ....: cpdef int f(int a): + ....: return a+1 + ....: """) + sage: shell.run_cell(u'print(f.__doc__)') + f(int a) -> int + File: ....pyx (starting at line 2) + sage: shell.run_cell(u'%pinfo f') + Signature: f(a) + ... + Next, test the ``pinfo`` magic for ``R`` interface code, see :issue:`26906`:: sage: from sage.repl.interpreter import get_test_shell # optional - rpy2 @@ -62,7 +79,7 @@ ... Next, test the pinfo2 magic for Python code. This is what IPython -calls when you ask for the double-questionmark help, like `foo??` :: +calls when you ask for the double-questionmark help, like ``foo??`` :: sage: from sage.repl.interpreter import get_test_shell sage: shell = get_test_shell()