Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src/sage/interfaces/singular.py: use GNU Info to read Singular's info #38899

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 3 additions & 2 deletions src/sage/algebras/letterplace/free_algebra_letterplace.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions src/sage/algebras/letterplace/letterplace_ideal.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,14 @@ 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

#####################
# Define some singular functions
lib("freegb.lib")
singular_twostd = singular_function("twostd")
poly_reduce = singular_function("NF")


class LetterplaceIdeal(Ideal_nc):
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
30 changes: 30 additions & 0 deletions src/sage/features/info.py
Original file line number Diff line number Diff line change
@@ -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 <spkg_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()]
186 changes: 108 additions & 78 deletions src/sage/interfaces/singular.py
Original file line number Diff line number Diff line change
Expand Up @@ -2269,11 +2269,9 @@
"""
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
Expand All @@ -2288,15 +2286,9 @@
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
Expand All @@ -2307,15 +2299,10 @@

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):
Expand All @@ -2341,82 +2328,125 @@
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
orlitzky marked this conversation as resolved.
Show resolved Hide resolved
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

Check warning on line 2419 in src/sage/interfaces/singular.py

View check run for this annotation

Codecov / codecov/patch

src/sage/interfaces/singular.py#L2419

Added line #L2419 was not covered by tests

# 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: <section>" 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()
Expand Down
13 changes: 4 additions & 9 deletions src/sage/libs/singular/function.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand Down Expand Up @@ -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):
"""
Expand Down
Loading
Loading