From b80f0724cfbf56681709d148883c1df5c2fe81fb Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sun, 5 Jan 2025 00:53:38 +0700 Subject: [PATCH 1/4] Show signature for binding=False cython functions --- src/sage/misc/sageinspect.py | 144 ++++++++++++++++++++++++++++- src/sage/repl/ipython_extension.py | 1 + src/sage/repl/ipython_tests.py | 17 ++++ 3 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index 585112b5061..2576d241329 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 329a7b9b95a..61301e31f44 100644 --- a/src/sage/repl/ipython_extension.py +++ b/src/sage/repl/ipython_extension.py @@ -575,6 +575,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") 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 From 10328a557c914069742916a6ffaed0ad72d6c34c Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sun, 5 Jan 2025 01:04:26 +0700 Subject: [PATCH 2/4] Fix pyright --- src/sage/repl/ipython_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/repl/ipython_extension.py b/src/sage/repl/ipython_extension.py index 61301e31f44..15eebcb03dd 100644 --- a/src/sage/repl/ipython_extension.py +++ b/src/sage/repl/ipython_extension.py @@ -575,7 +575,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") + IPython.core.oinspect.signature = LazyImport("sage.misc.sageinspect", "sage_signature") # pyright: ignore [reportPrivateImportUsage] def init_line_transforms(self): """ From 6fad9f2c9dbfe4fdef80d303a7d3dc9f76be9907 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:11:03 +0700 Subject: [PATCH 3/4] Fix a bug, add coverage --- src/sage/misc/sageinspect.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index 84ebb6ced78..8734a57586c 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -1713,6 +1713,10 @@ def _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 @@ -1731,7 +1735,8 @@ def _fullargspec_to_signature(fullargspec): parameters.append(param) for arg in fullargspec.kwonlyargs: - param = Parameter(arg, Parameter.KEYWORD_ONLY, default=fullargspec.kwonlydefaults.get(arg, Parameter.empty)) + 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) From 64c5f63d71ac2d097b13390ac4bd156bb4289a4c Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Mon, 14 Apr 2025 05:31:48 +0700 Subject: [PATCH 4/4] Fix documentation formatting --- src/sage/misc/sageinspect.py | 7 ++++--- src/sage/repl/ipython_tests.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index 856effea05a..e30b61c17bb 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -1695,6 +1695,7 @@ def foo(x, a='\')"', b={not (2+1==3):'bar'}): return 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:: @@ -1800,8 +1801,8 @@ def sage_signature(obj): 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:: + 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 @@ -1825,7 +1826,7 @@ def sage_signature(obj): sage: sage_signature(foo) # needs sage.misc.cython - If a `functools.partial` instance is involved, we see no other meaningful solution + 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): diff --git a/src/sage/repl/ipython_tests.py b/src/sage/repl/ipython_tests.py index 9a30b410989..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() @@ -79,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()