diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index 12f319fc8e3..84ebb6ced78 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -114,6 +114,7 @@ class definition be found starting from the ``__init__`` method. import os import tokenize import re +from inspect import Signature, Parameter try: import importlib.machinery as import_machinery @@ -1410,7 +1411,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, @@ -1668,6 +1669,147 @@ 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. + + 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) + + """ + 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=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 `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 `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 05468142142..be13db0aa5d 100644 --- a/src/sage/repl/ipython_extension.py +++ b/src/sage/repl/ipython_extension.py @@ -651,6 +651,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..9a30b410989 100644 --- a/src/sage/repl/ipython_tests.py +++ b/src/sage/repl/ipython_tests.py @@ -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