diff --git a/src/sage/algebras/letterplace/free_algebra_element_letterplace.pyx b/src/sage/algebras/letterplace/free_algebra_element_letterplace.pyx index 2a977b1a513..aa164d6a4ce 100644 --- a/src/sage/algebras/letterplace/free_algebra_element_letterplace.pyx +++ b/src/sage/algebras/letterplace/free_algebra_element_letterplace.pyx @@ -17,13 +17,12 @@ AUTHOR: # **************************************************************************** from sage.groups.perm_gps.permgroup_named import CyclicPermutationGroup -from sage.libs.singular.function import lib, singular_function +from sage.libs.singular.function import lib from sage.rings.polynomial.multi_polynomial_ideal import MPolynomialIdeal from cpython.object cimport PyObject_RichCompare # Define some singular functions lib("freegb.lib") -poly_reduce = singular_function("NF") ##################### # Free algebra elements @@ -695,6 +694,8 @@ cdef class FreeAlgebraElement_letterplace(AlgebraElement): bck = (libsingular_options['redTail'], libsingular_options['redSB']) libsingular_options['redTail'] = True libsingular_options['redSB'] = True + from sage.libs.singular.function import singular_function + poly_reduce = singular_function("NF") poly = poly_reduce(C(self._poly), gI, ring=C, attributes={gI: {"isSB": 1}}) libsingular_options['redTail'] = bck[0] diff --git a/src/sage/algebras/letterplace/free_algebra_letterplace.pyx b/src/sage/algebras/letterplace/free_algebra_letterplace.pyx index 7a57922d24a..10146c36aeb 100644 --- a/src/sage/algebras/letterplace/free_algebra_letterplace.pyx +++ b/src/sage/algebras/letterplace/free_algebra_letterplace.pyx @@ -121,7 +121,7 @@ TESTS:: algebras with different term orderings, yet. """ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing -from sage.libs.singular.function import lib, singular_function +from sage.libs.singular.function import lib from sage.libs.singular.function cimport RingWrap from sage.libs.singular.ring cimport singular_ring_delete, singular_ring_reference from sage.categories.algebras import Algebras @@ -132,7 +132,6 @@ from sage.misc.cachefunc import cached_method ##################### # Define some singular functions lib("freegb.lib") -freeAlgebra = singular_function("freeAlgebra") # unfortunately we cannot set Singular attributes for MPolynomialRing_libsingular # Hence, we must constantly work around Letterplace's sanity checks, @@ -892,6 +891,8 @@ cdef class FreeAlgebra_letterplace_libsingular(): def __cinit__(self, MPolynomialRing_libsingular commutative_ring, int degbound): + from sage.libs.singular.function import singular_function + freeAlgebra = singular_function("freeAlgebra") cdef RingWrap rw = freeAlgebra(commutative_ring, degbound) self._lp_ring = singular_ring_reference(rw._ring) # `_lp_ring` viewed as `MPolynomialRing_libsingular` with additional diff --git a/src/sage/algebras/letterplace/letterplace_ideal.pyx b/src/sage/algebras/letterplace/letterplace_ideal.pyx index 77d0107ba07..060c89c3df0 100644 --- a/src/sage/algebras/letterplace/letterplace_ideal.pyx +++ b/src/sage/algebras/letterplace/letterplace_ideal.pyx @@ -41,7 +41,7 @@ AUTHOR: # https://www.gnu.org/licenses/ # **************************************************************************** from sage.rings.noncommutative_ideals import Ideal_nc -from sage.libs.singular.function import lib, singular_function +from sage.libs.singular.function import lib from sage.algebras.letterplace.free_algebra_letterplace cimport FreeAlgebra_letterplace, FreeAlgebra_letterplace_libsingular from sage.algebras.letterplace.free_algebra_element_letterplace cimport FreeAlgebraElement_letterplace from sage.rings.infinity import Infinity @@ -49,8 +49,6 @@ from sage.rings.infinity import Infinity ##################### # Define some singular functions lib("freegb.lib") -singular_twostd = singular_function("twostd") -poly_reduce = singular_function("NF") class LetterplaceIdeal(Ideal_nc): @@ -321,6 +319,8 @@ class LetterplaceIdeal(Ideal_nc): to_L = P.hom(L.gens(), L, check=False) from_L = L.hom(P.gens(), P, check=False) I = L.ideal([to_L(x._poly) for x in self.__GB.gens()]) + from sage.libs.singular.function import singular_function + singular_twostd = singular_function("twostd") gb = singular_twostd(I) out = [FreeAlgebraElement_letterplace(A, from_L(X), check=False) for X in gb] @@ -398,6 +398,8 @@ class LetterplaceIdeal(Ideal_nc): bck = (libsingular_options['redTail'], libsingular_options['redSB']) libsingular_options['redTail'] = True libsingular_options['redSB'] = True + from sage.libs.singular.function import singular_function + poly_reduce = singular_function("NF") sI = poly_reduce(sI, gI, ring=C, attributes={gI: {"isSB": 1}}) libsingular_options['redTail'] = bck[0] libsingular_options['redSB'] = bck[1] diff --git a/src/sage/features/info.py b/src/sage/features/info.py new file mode 100644 index 00000000000..eeaf0118c0d --- /dev/null +++ b/src/sage/features/info.py @@ -0,0 +1,30 @@ +# sage_setup: distribution = sagemath-environment +r""" +Feature for testing the presence of ``info``, from GNU Info +""" + +from . import Executable + +class Info(Executable): + r""" + A :class:`~sage.features.Feature` describing the presence of :ref:`info `. + + EXAMPLES:: + + sage: from sage.features.info import Info + sage: Info() + Feature('info') + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.info import Info + sage: isinstance(Info(), Info) + True + """ + Executable.__init__(self, 'info', executable='info', + spkg='info', type='standard') + +def all_features(): + return [Info()] diff --git a/src/sage/interfaces/singular.py b/src/sage/interfaces/singular.py index ed883b07105..fdb4d093c55 100644 --- a/src/sage/interfaces/singular.py +++ b/src/sage/interfaces/singular.py @@ -2269,11 +2269,9 @@ def _instancedoc_(self): """ EXAMPLES:: - sage: 'groebner' in singular.groebner.__doc__ + sage: 'groebner' in singular.groebner.__doc__ # needs info True """ - if not nodes: - generate_docstring_dictionary() prefix = """ This function is an automatically generated pexpect wrapper around the Singular @@ -2288,15 +2286,9 @@ def _instancedoc_(self): x+y, y^2-y """ % (self._name,) - prefix2 = """ - -The Singular documentation for '%s' is given below. -""" % (self._name,) - - try: - return prefix + prefix2 + nodes[node_names[self._name]] - except KeyError: - return prefix + return prefix + get_docstring(self._name, + prefix=True, + code=True) @instancedoc @@ -2307,15 +2299,10 @@ def _instancedoc_(self): sage: R = singular.ring(0, '(x,y,z)', 'dp') sage: A = singular.matrix(2,2) - sage: 'matrix_expression' in A.nrows.__doc__ + sage: 'matrix_expression' in A.nrows.__doc__ # needs info True """ - if not nodes: - generate_docstring_dictionary() - try: - return nodes[node_names[self._name]] - except KeyError: - return "" + return get_docstring(self._name, code=True) def is_SingularElement(x): @@ -2341,82 +2328,125 @@ def is_SingularElement(x): return isinstance(x, SingularElement) -nodes = {} -node_names = {} - - -def generate_docstring_dictionary(): +def get_docstring(name, prefix=False, code=False): """ - Generate global dictionaries which hold the docstrings for - Singular functions. - - EXAMPLES:: - - sage: from sage.interfaces.singular import generate_docstring_dictionary - sage: generate_docstring_dictionary() - """ - - global nodes - global node_names + Return the docstring for the function ``name``. - nodes.clear() - node_names.clear() + INPUT: - new_node = re.compile(r"File: singular\.[a-z]*, Node: ([^,]*),.*") - new_lookup = re.compile(r"\* ([^:]*):*([^.]*)\..*") + - ``name`` -- a Singular function name + - ``prefix`` -- boolean (default: ``False``); whether or not to + include the prefix stating that what follows is from the + Singular documentation. + - ``code`` -- boolean (default: ``False``); whether or not to + format the result as a reStructuredText code block. This is + intended to support the feature requested in :issue:`11268`. + + OUTPUT: + + A string describing the Singular function ``name``. A + :class:`KeyError` is raised if the function was not found in the + Singular documentation. If the "info" is not on the user's + ``PATH``, an :class:`OSError` will be raised. If "info" was found + but failed to execute, a :class:`subprocess.CalledProcessError` + will be raised instead. - L, in_node, curr_node = [], False, None + EXAMPLES:: - from sage.libs.singular.singular import get_resource - singular_info_file = get_resource('i') + sage: from sage.interfaces.singular import get_docstring + sage: 'groebner' in get_docstring('groebner') # needs_info + True + sage: 'standard.lib' in get_docstring('groebner') # needs info + True - # singular.hlp contains a few iso-8859-1 encoded special characters - with open(singular_info_file, - encoding='latin-1') as f: - for line in f: - m = re.match(new_node, line) - if m: - # a new node starts - in_node = True - nodes[curr_node] = "".join(L) - L = [] - curr_node, = m.groups() - elif in_node: # we are in a node - L.append(line) - else: - m = re.match(new_lookup, line) - if m: - a, b = m.groups() - node_names[a] = b.strip() + The ``prefix=True`` form is used in Sage's generated docstrings:: - if line in ("6 Index\n", "F Index\n"): - in_node = False + sage: from sage.interfaces.singular import get_docstring + sage: print(get_docstring("factorize", prefix=True)) # needs info + The Singular documentation for "factorize" is given below. + ... - nodes[curr_node] = "".join(L) # last node + TESTS: + Non-existent functions raise a :class:`KeyError`:: -def get_docstring(name): - """ - Return the docstring for the function ``name``. + sage: from sage.interfaces.singular import get_docstring + sage: get_docstring("mysql_real_escape_string") # needs info + Traceback (most recent call last): + ... + KeyError: 'mysql_real_escape_string' - INPUT: + This is true also for nodes that exist in the documentation but + are not function nodes:: - - ``name`` -- a Singular function name + sage: from sage.interfaces.singular import get_docstring + sage: get_docstring("Preface") # needs info + Traceback (most recent call last): + ... + KeyError: 'Preface' - EXAMPLES:: + If GNU Info is not installed, we politely decline to do anything:: sage: from sage.interfaces.singular import get_docstring - sage: 'groebner' in get_docstring('groebner') - True - sage: 'standard.lib' in get_docstring('groebner') - True + sage: from sage.features.info import Info + sage: Info().hide() + sage: get_docstring('groebner') + Traceback (most recent call last): + ... + OSError: GNU Info is not installed. Singular's documentation + will not be available. + sage: Info().unhide() """ - if not nodes: - generate_docstring_dictionary() + from sage.features.info import Info + + if not Info().is_present(): + raise OSError("GNU Info is not installed. Singular's " + "documentation will not be available.") + import subprocess + cmd_and_args = ["info", f"--node={name}", "singular"] try: - return nodes[node_names[name]] - except KeyError: - return "" + result = subprocess.run(cmd_and_args, + capture_output=True, + check=True, + text=True) + except subprocess.CalledProcessError as e: + # Before Texinfo v7.0.0, the "info" program would exit + # successfully even if the desired node was not found. + if e.returncode == 1: + raise KeyError(name) from e + else: + # Something else bad happened + raise e + + # The subprocess call can succeed if the given node exists but is + # not a function node (example: "Preface"). All function nodes + # should live in the "Functions" section, and we can determine the + # current section by the presence of "Up:
" on the first + # line of the output, in the navigation header. + # + # There is a small risk of ambiguity here if there are two + # sections with the same name, but it's a trade-off: specifying + # the full path down to the intended function would be much more + # fragile; it would break whenever a subsection name was tweaked + # upstream. + offset = result.stdout.find("\n") + line0 = result.stdout[:offset] + if "Up: Functions" not in line0: + raise KeyError(name) + + # If the first line was the navigation header, the second line should + # be blank; by incrementing the offset by two, we're skipping over it. + offset += 2 + result = result.stdout[offset:] + + if code: + result = "::\n\n " + "\n ".join(result.split('\n')) + + if prefix: + result = (f'The Singular documentation for "{name}" is given below.' + + "\n\n" + result) + + return result singular = Singular() diff --git a/src/sage/libs/singular/function.pyx b/src/sage/libs/singular/function.pyx index c6f65eb718a..d9d70203908 100644 --- a/src/sage/libs/singular/function.pyx +++ b/src/sage/libs/singular/function.pyx @@ -1314,7 +1314,7 @@ cdef class SingularFunction(SageObject): sage: from sage.libs.singular.function import singular_function sage: groebner = singular_function('groebner') - sage: 'groebner' in groebner.__doc__ + sage: 'groebner' in groebner.__doc__ # needs info True """ @@ -1360,14 +1360,9 @@ EXAMPLES:: [x2, x1^2], [x2, x1^2]] -The Singular documentation for '%s' is given below. -"""%(self._name,self._name) - # Github issue #11268: Include the Singular documentation as a block of code - singular_doc = get_docstring(self._name).split('\n') - if len(singular_doc) > 1: - return prefix + "\n::\n\n"+'\n'.join([" "+L for L in singular_doc]) - else: - return prefix + "\n::\n\n"+" Singular documentation not found" +"""%(self._name) + from sage.interfaces.singular import get_docstring + return prefix + get_docstring(self._name, prefix=True, code=True) cdef common_ring(self, tuple args, ring=None): """ diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index 1f2a39d7c78..faea717ba52 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -1865,28 +1865,10 @@ def _sage_getdoc_unformatted(obj): sage: _sage_getdoc_unformatted(isinstance.__class__) '' - Construct an object raising an exception when accessing the - ``__doc__`` attribute. This should not give an error in - ``_sage_getdoc_unformatted``, see :issue:`19671`:: - - sage: class NoSageDoc(): - ....: @property - ....: def __doc__(self): - ....: raise Exception("no doc here") - sage: obj = NoSageDoc() - sage: obj.__doc__ - Traceback (most recent call last): - ... - Exception: no doc here - sage: _sage_getdoc_unformatted(obj) - '' """ if obj is None: return '' - try: - r = obj.__doc__ - except Exception: - return '' + r = obj.__doc__ # Check if the __doc__ attribute was actually a string, and # not a 'getset_descriptor' or similar.