diff --git a/src/doc/en/developer/coding_basics.rst b/src/doc/en/developer/coding_basics.rst index 5e15c4b2455..9b56de46ad0 100644 --- a/src/doc/en/developer/coding_basics.rst +++ b/src/doc/en/developer/coding_basics.rst @@ -6,7 +6,6 @@ General Conventions =================== - There are many ways to contribute to Sage, including sharing scripts and Jupyter notebooks that implement new functionality using Sage, improving to the Sage library, or to working on the many underlying @@ -1256,17 +1255,38 @@ framework. Here is a comprehensive list: Neither of this applies to files or directories which are explicitly given as command line arguments: those are always tested. -- **optional/needs:** A line tagged with ``optional - FEATURE`` - or ``needs FEATURE`` is not tested unless the ``--optional=KEYWORD`` flag - is passed to ``sage -t`` (see - :ref:`section-optional-doctest-flag`). The main applications are: +- **optional** or **needs:** A line tagged with ``optional - FEATURE`` or + ``needs FEATURE`` is tested if the feature is available in Sage. If + ``FEATURE`` starts with an exclamation point ``!``, then the condition is + negated, that is, the doctest runs only if the feature is not available. + + If the feature is included in the ``--optional=KEYWORD`` flag passed to + ``sage -t`` (see :ref:`section-optional-doctest-flag`), then the line is + tested regardless of the feature availability. + + The main applications are: - **optional packages:** When a line requires an optional package to be - installed (e.g. the ``sloane_database`` package):: + installed (e.g. the ``rubiks`` package):: + + sage: C = RubiksCube("R*L") + sage: C.solve() # optional - rubiks (a hybrid algorithm is used) + 'L R' + sage: C.solve() # optional - !rubiks (GAP is used) + 'L*R' + + - **features:** When a line requires a feature to be present:: sage: SloaneEncyclopedia[60843] # optional - sloane_database + [1, 6, 21, 107, 47176870] + + sage: SloaneEncyclopedia[60843] # optional - !sloane_database + Traceback (most recent call last): + ... + OSError: The Sloane Encyclopedia database must be installed. Use e.g. + 'SloaneEncyclopedia.install()' to download and install it. - - **internet:** For lines that require an internet connection:: + For lines that require an internet connection:: sage: oeis(60843) # optional - internet A060843: Busy Beaver problem: a(n) = maximal number of steps that an diff --git a/src/sage/databases/sloane.py b/src/sage/databases/sloane.py index 78fc268b486..aac252d84a8 100644 --- a/src/sage/databases/sloane.py +++ b/src/sage/databases/sloane.py @@ -12,7 +12,7 @@ :: sage: SloaneEncyclopedia[60843] # optional - sloane_database - [1, 6, 21, 107] + [1, 6, 21, 107, 47176870] To get the name of a sequence, type @@ -149,6 +149,17 @@ def __len__(self): self.load() return len(self.__data__) + def is_installed(self): + """ + Check if a local copy of the encyclopedia is installed. + + EXAMPLES:: + + sage: SloaneEncyclopedia.is_installed() # optional - sloane_database + True + """ + return os.path.exists(self.__file__) and os.path.exists(self.__file_names__) + def find(self, seq, maxresults=30): """ Return a list of all sequences which have seq as a subsequence, up @@ -274,7 +285,7 @@ def load(self): for L in file_seq: if len(L) == 0: continue - m = entry.search(L) + m = entry.search(L.decode('utf-8')) if m: seqnum = int(m.group('num')) msg = m.group('body').strip() @@ -287,10 +298,13 @@ def load(self): for L in file_names: if not L: continue - m = entry.search(L) + m = entry.search(L.decode('utf-8')) if m: seqnum = int(m.group('num')) - self.__data__[seqnum][3] = m.group('body').strip() + if seqnum in self.__data__: + self.__data__[seqnum][3] = m.group('body').strip() + else: + self.__data__[seqnum] = [seqnum, None, 'unknown', m.group('body').strip()] file_names.close() self.__loaded_names__ = True except KeyError: diff --git a/src/sage/doctest/parsing.py b/src/sage/doctest/parsing.py index 20f6eb6ce69..79a92835b26 100644 --- a/src/sage/doctest/parsing.py +++ b/src/sage/doctest/parsing.py @@ -58,7 +58,7 @@ special_optional_regex = ( "py2|long time|not implemented|not tested|optional|needs|known bug" ) -tag_with_explanation_regex = r"((?:\w|[.])*)\s*(?:\((?P.*?)\))?" +tag_with_explanation_regex = r"((?:!?\w|[.])*)\s*(?:\((?P.*?)\))?" optional_regex = re.compile( rf"[^ a-z]\s*(?P{special_optional_regex})(?:\s|[:-])*(?P(?:(?:{tag_with_explanation_regex})\s*)*)", re.IGNORECASE, @@ -1124,14 +1124,14 @@ def check_and_clear_tag_counts(): continue if self.optional_tags is not True: - extra = { - tag - for tag in optional_tags - if ( - tag not in self.optional_tags - and tag not in available_software - ) - } + extra = set() + for tag in optional_tags: + if tag not in self.optional_tags: + if tag.startswith('!'): + if tag[1:] in available_software: + extra.add(tag) + elif tag not in available_software: + extra.add(tag) if extra and any(tag in ["bug"] for tag in extra): # Bug only occurs on a specific platform? bug_platform = optional_tags_with_values.get("bug") diff --git a/src/sage/features/dot2tex.py b/src/sage/features/dot2tex.py new file mode 100644 index 00000000000..e9f97b6e704 --- /dev/null +++ b/src/sage/features/dot2tex.py @@ -0,0 +1,42 @@ +# sage_setup: distribution = sagemath-environment +r""" +Check for ``dot2tex`` +""" + +# ***************************************************************************** +# Copyright (C) 2024 Kwankyu Lee +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# https://www.gnu.org/licenses/ +# ***************************************************************************** + +from . import PythonModule + + +class dot2tex(PythonModule): + r""" + A :class:`sage.features.Feature` describing the presence of :ref:`dot2tex `. + + dot2tex is provided by an optional package in the Sage distribution. + + EXAMPLES:: + + sage: from sage.features.dot2tex import dot2tex + sage: dot2tex().is_present() # optional - dot2tex + FeatureTestResult('dot2tex', True) + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.dot2tex import dot2tex + sage: isinstance(dot2tex(), dot2tex) + True + """ + PythonModule.__init__(self, 'dot2tex', spkg='dot2tex') + + +def all_features(): + return [dot2tex()] diff --git a/src/sage/features/sloane_database.py b/src/sage/features/sloane_database.py new file mode 100644 index 00000000000..84aad5ce67a --- /dev/null +++ b/src/sage/features/sloane_database.py @@ -0,0 +1,59 @@ +# sage_setup: distribution = sagemath-environment +r""" +Feature for testing the presence of Sloane Online Encyclopedia of Integer Sequences +""" + +# **************************************************************************** +# Copyright (C) 2024 Kwankyu Lee +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +from . import Feature + + +class SloaneOEIS(Feature): + r""" + A :class:`~sage.features.Feature` which describes the presence of + the Sloane Online Encyclopedia of Integer Sequences. + + EXAMPLES:: + + sage: from sage.features.sloane_database import SloaneOEIS + sage: bool(SloaneOEIS().is_present()) # optional - sloane_database + True + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.sloane_database import SloaneOEIS + sage: isinstance(SloaneOEIS(), SloaneOEIS) + True + """ + Feature.__init__(self, name='sloane_database', + description='Sloane Online Encyclopedia of Integer Sequences') + + def _is_present(self): + r""" + Return whether the database is available. + + EXAMPLES:: + + sage: from sage.features.sloane_database import SloaneOEIS + sage: bool(SloaneOEIS().is_present()) # optional - !sloane_database + False + """ + try: + from sage.databases.sloane import SloaneEncyclopedia + except ImportError: + return False + return SloaneEncyclopedia.is_installed() + + +def all_features(): + return [SloaneOEIS()] diff --git a/src/sage/graphs/graph_latex.py b/src/sage/graphs/graph_latex.py index f0fb9329002..f50758496c4 100644 --- a/src/sage/graphs/graph_latex.py +++ b/src/sage/graphs/graph_latex.py @@ -1566,7 +1566,7 @@ def tkz_picture(self): For a complicated vertex, a TeX box is used. :: sage: B = crystals.Tableaux(['B', 2], shape=[1]) - sage: latex(B) + sage: latex(B) # optional - !dot2tex \begin{tikzpicture} ... \newsavebox{\vertex} diff --git a/src/sage/groups/perm_gps/cubegroup.py b/src/sage/groups/perm_gps/cubegroup.py index 7603da471f9..a9a24f2b26c 100644 --- a/src/sage/groups/perm_gps/cubegroup.py +++ b/src/sage/groups/perm_gps/cubegroup.py @@ -1422,7 +1422,7 @@ def __richcmp__(self, other, op): return NotImplemented return richcmp(self._state, other._state, op) - def solve(self, algorithm='hybrid', timeout=15): + def solve(self, algorithm='default', timeout=15): r""" Solve the Rubik's cube. @@ -1430,17 +1430,14 @@ def solve(self, algorithm='hybrid', timeout=15): - ``algorithm`` -- must be one of the following: - - ``hybrid`` -- try ``kociemba`` for timeout seconds, then ``dietz`` - - ``kociemba`` -- use Dik T. Winter's program - (reasonable speed, few moves) - - ``dietz`` -- use Eric Dietz's cubex program - (fast but lots of moves) - - ``optimal`` -- use Michael Reid's optimal program - (may take a long time) + - ``hybrid`` -- (default) try ``kociemba`` for timeout seconds, then ``dietz`` + - ``kociemba`` -- use Dik T. Winter's program (reasonable speed, few moves) + - ``dietz`` -- use Eric Dietz's cubex program (fast but lots of moves) + - ``optimal`` -- use Michael Reid's optimal program (may take a long time) - ``gap`` -- use GAP word solution (can be slow) - Any choice other than ``gap`` requires the optional package - ``rubiks``. Otherwise, the ``gap`` algorithm is used. + Any choice other than ``gap`` requires the optional package ``rubiks``. + If the package is not installed, the ``gap`` algorithm is used by default. EXAMPLES:: @@ -1452,7 +1449,10 @@ def solve(self, algorithm='hybrid', timeout=15): solutions:: sage: s = C.solve('dietz'); s # optional - rubiks - "U' L' L' U L U' L U D L L D' L' D L' D' L D L' U' L D' L' U L' B' U' L' U B L D L D' U' L' U L B L B' L' U L U' L' F' L' F L' F L F' L' D' L' D D L D' B L B' L B' L B F' L F F B' L F' B D' D' L D B' B' L' D' B U' U' L' B' D' F' F' L D F'" + "U' L' L' U L U' L U D L L D' L' D L' D' L D L' U' L D' L' U L' B' + U' L' U B L D L D' U' L' U L B L B' L' U L U' L' F' L' F L' F L F' + L' D' L' D D L D' B L B' L B' L B F' L F F B' L F' B D' D' L D B' + B' L' D' B U' U' L' B' D' F' F' L D F'" sage: C2 = RubiksCube(s) # optional - rubiks sage: C == C2 # optional - rubiks True @@ -1460,11 +1460,11 @@ def solve(self, algorithm='hybrid', timeout=15): from sage.features.rubiks import Rubiks if Rubiks().is_present(): import sage.interfaces.rubik # here to avoid circular referencing + if algorithm == 'default': + algorithm = "hybrid" else: - algorithm = 'gap' - - if algorithm == "default": - algorithm = "hybrid" + if algorithm == 'default': + algorithm = 'gap' if algorithm == "hybrid": try: