Skip to content

Commit

Permalink
src/sage/interfaces/singular.py: use GNU Info to read Singular's info
Browse files Browse the repository at this point in the history
Our Singular interface currently contains a hand-written parser for
Singular's "info" file. This commit eliminates the custom parser in
favor of launching GNU Info. GNU Info (or its superset, Texinfo) are
widespread, portable, and easy to install on all of the systems we
support, so in most cases this should be a "free" improvement.

The hand-written parser has several drawbacks:

* The extra code is a maintenance burden. We should not be wasting our
  time reimplementing standard tools.

* The custom parser is buggy. For example, it is supposed to raise a
  KeyError when documentation for a non-existent function is
  requested. However, the parser does not keep track of what section
  it's in, so, for example, get_docstring("Preface") returns the
  contents of the preface even though "Preface" is not a Singular
  function.

* The first time documentation is requested, the entire info file is
  loaded into a dictionary. This wastes a few megabytes of memory for
  the duration of the Sage session.

* The custom parser does not handle compression (GNU Info does
  transparently), and the end user or people packaging Singular may
  not be aware of that. If the system installation of Singular has a
  compressed info file, Sage won't be able to read it.

For contrast, the one downside to using GNU Info is that it adds a new
runtime dependency to sagelib. To mitigate that, we do not technically
require it, and instead raise a warning if the user (a) tries to read
the Singular documentation and (b) has managed to find a system
without GNU Info. Our singular_console() itself tries to launch GNU
Info to display its interactive help, so the additional optional
dependency is not so additional except in corner cases, such as a pypi
installation of a subset of Sage linked against libsingular but
without a full Singular install.
  • Loading branch information
orlitzky committed Nov 1, 2024
1 parent 5bc7b70 commit 69e8efd
Showing 1 changed file with 65 additions and 68 deletions.
133 changes: 65 additions & 68 deletions src/sage/interfaces/singular.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -2294,7 +2292,7 @@ def _instancedoc_(self):
""" % (self._name,)

try:
return prefix + prefix2 + nodes[node_names[self._name]]
return prefix + prefix2 + get_docstring(self._name)
except KeyError:
return prefix

Expand All @@ -2307,13 +2305,11 @@ 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]]
return get_docstring(self._name)
except KeyError:
return ""

Expand Down Expand Up @@ -2341,60 +2337,6 @@ def is_SingularElement(x):
return isinstance(x, SingularElement)


nodes = {}
node_names = {}


def generate_docstring_dictionary():
"""
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

nodes.clear()
node_names.clear()

new_node = re.compile(r"File: singular\.[a-z]*, Node: ([^,]*),.*")
new_lookup = re.compile(r"\* ([^:]*):*([^.]*)\..*")

L, in_node, curr_node = [], False, None

from sage.libs.singular.singular import get_resource
singular_info_file = get_resource('i')

# 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()

if line in ("6 Index\n", "F Index\n"):
in_node = False

nodes[curr_node] = "".join(L) # last node


def get_docstring(name):
"""
Return the docstring for the function ``name``.
Expand All @@ -2408,15 +2350,65 @@ def get_docstring(name):
sage: from sage.interfaces.singular import get_docstring
sage: 'groebner' in get_docstring('groebner')
True
sage: 'standard.lib' in get_docstring('groebner')
sage: 'standard.lib' in get_docstring('groebner') # needs info
True
TESTS:
Non-existent functions raise a ``KeyError``::
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'
The first character of the output should be a newline so that the
output from ``singular.<function>?`` looks right::
sage: from sage.interfaces.singular import get_docstring
sage: get_docstring("align")[0] # needs info
'\n'
If GNU Info is not installed, we politely decline to do anything::
sage: from sage.interfaces.singular import get_docstring
sage: from sage.features.info import Info
sage: Info().hide()
sage: get_docstring('groebner')
Traceback (most recent call last):
...
OSError: Error: 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("Error: 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.
raise KeyError(name) from e

# The first line we get back is the navigation header for the
# function, and the second is blank, meaning that there are two
# newlines in a row. By incrementing the offset here, we're
# skipping over one of them. This leaves an "\n" at the beginning
# of the string, but running (say) "singular.align?" suggests
# that this is expected, or at least is backwards-compatible
# with the previous implementation.
offset = result.stdout.find("\n") + 1
return result.stdout[offset:]


singular = Singular()
Expand Down Expand Up @@ -2456,6 +2448,11 @@ def singular_version():
"""
Return the version of Singular being used.
OUTPUT:
A string describing the Singular function ``name``. A ``KeyError``
is raised if no such function was found in the Singular documentation.
EXAMPLES::
sage: singular.version()
Expand Down

0 comments on commit 69e8efd

Please sign in to comment.