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

Minor code improvement in sage.doctest #39426

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions src/sage/doctest/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,31 @@
# ****************************************************************************

import importlib
import random
import json
import os
import random
import shlex
import sys
import time
import json
import shlex
import types

from cysignals.signals import AlarmInterrupt, init_cysignals

import sage.misc.flatten
import sage.misc.randstate as randstate
from sage.structure.sage_object import SageObject
from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC, SAGE_VENV, SAGE_EXTCODE
from sage.doctest.external import available_software
from sage.doctest.forker import DocTestDispatcher
from sage.doctest.parsing import (
optional_tag_regex,
parse_file_optional_tags,
unparse_optional_tags,
)
from sage.doctest.reporting import DocTestReporter
from sage.doctest.sources import DictAsObject, FileDocTestSource, get_basename
from sage.doctest.util import Timer, count_noun, dict_difference
from sage.env import DOT_SAGE, SAGE_EXTCODE, SAGE_LIB, SAGE_SRC
from sage.misc.temporary_file import tmp_dir
from cysignals.signals import AlarmInterrupt, init_cysignals

from .sources import FileDocTestSource, DictAsObject, get_basename
from .forker import DocTestDispatcher
from .reporting import DocTestReporter
from .util import Timer, count_noun, dict_difference
from .external import available_software
from .parsing import parse_optional_tags, parse_file_optional_tags, unparse_optional_tags, \
nodoctest_regex, optionaltag_regex, optionalfiledirective_regex

from sage.structure.sage_object import SageObject

# Optional tags which are always automatically added

Expand Down Expand Up @@ -465,7 +468,7 @@
s = options.hide.lower()
options.hide = set(s.split(','))
for h in options.hide:
if not optionaltag_regex.search(h):
if not optional_tag_regex.search(h):
raise ValueError('invalid optional tag {!r}'.format(h))
if 'all' in options.hide:
options.hide.discard('all')
Expand Down Expand Up @@ -508,10 +511,10 @@
# Check that all tags are valid
for o in options.optional:
if o.startswith('!'):
if not optionaltag_regex.search(o[1:]):
if not optional_tag_regex.search(o[1:]):

Check warning on line 514 in src/sage/doctest/control.py

View check run for this annotation

Codecov / codecov/patch

src/sage/doctest/control.py#L514

Added line #L514 was not covered by tests
raise ValueError('invalid optional tag {!r}'.format(o))
options.disabled_optional.add(o[1:])
elif not optionaltag_regex.search(o):
elif not optional_tag_regex.search(o):
raise ValueError('invalid optional tag {!r}'.format(o))

options.optional |= auto_optional_tags
Expand All @@ -531,7 +534,7 @@
else:
# Check that all tags are valid
for o in options.probe:
if not optionaltag_regex.search(o):
if not optional_tag_regex.search(o):

Check warning on line 537 in src/sage/doctest/control.py

View check run for this annotation

Codecov / codecov/patch

src/sage/doctest/control.py#L537

Added line #L537 was not covered by tests
raise ValueError('invalid optional tag {!r}'.format(o))

self.options = options
Expand Down
4 changes: 2 additions & 2 deletions src/sage/doctest/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ def external_features():
yield Gurobi()


def external_software() -> list[str]:
def _external_software() -> list[str]:
"""
Return the alphabetical list of external software supported by this module.

Expand All @@ -400,7 +400,7 @@ def external_software() -> list[str]:
return sorted(f.name for f in external_features())


external_software = external_software()
external_software: list[str] = _external_software()


class AvailableSoftware:
Expand Down
2 changes: 1 addition & 1 deletion src/sage/doctest/forker.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@

try:
import sympy
except ImportError:
except (ImportError, AttributeError):

Check warning on line 251 in src/sage/doctest/forker.py

View check run for this annotation

Codecov / codecov/patch

src/sage/doctest/forker.py#L251

Added line #L251 was not covered by tests
# Do not require sympy for running doctests (Issue #25106).
pass
else:
Expand Down
60 changes: 34 additions & 26 deletions src/sage/doctest/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,36 +39,44 @@
import re
from collections import defaultdict
from functools import reduce
from re import Pattern
from typing import Literal, Union, overload

from sage.doctest.check_tolerance import (
ToleranceExceededError,
check_tolerance_complex_domain,
check_tolerance_real_domain,
float_regex,
)
from sage.doctest.external import available_software, external_software
from sage.doctest.marked_output import MarkedOutput
from sage.doctest.rif_tol import RIFtol, add_tolerance
from sage.misc.cachefunc import cached_function
from sage.repl.preparse import preparse, strip_string_literals
from sage.doctest.rif_tol import RIFtol, add_tolerance
from sage.doctest.marked_output import MarkedOutput
from sage.doctest.check_tolerance import (
ToleranceExceededError, check_tolerance_real_domain,
check_tolerance_complex_domain, float_regex)

from .external import available_software, external_software


# This is the correct pattern to match ISO/IEC 6429 ANSI escape sequences:
ansi_escape_sequence = re.compile(r"(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])")
ansi_escape_sequence: Pattern[str] = re.compile(
r"(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])"
)

special_optional_regex = (
special_optional_regex_raw = (
"py2|long time|not implemented|not tested|optional|needs|known bug"
)
tag_with_explanation_regex = r"((?:!?\w|[.])*)\s*(?:\((?P<cmd_explanation>.*?)\))?"
optional_regex = re.compile(
rf"[^ a-z]\s*(?P<cmd>{special_optional_regex})(?:\s|[:-])*(?P<tags>(?:(?:{tag_with_explanation_regex})\s*)*)",
tag_with_explanation_regex_raw = r"((?:!?\w|[.])*)\s*(?:\((?P<cmd_explanation>.*?)\))?"
optional_regex: Pattern[str] = re.compile(
rf"[^ a-z]\s*(?P<cmd>{special_optional_regex_raw})(?:\s|[:-])*(?P<tags>(?:(?:{tag_with_explanation_regex_raw})\s*)*)",
re.IGNORECASE,
)
special_optional_regex = re.compile(special_optional_regex, re.IGNORECASE)
tag_with_explanation_regex = re.compile(tag_with_explanation_regex, re.IGNORECASE)
special_optional_regex: Pattern[str] = re.compile(
special_optional_regex_raw, re.IGNORECASE
)
tag_with_explanation_regex: Pattern[str] = re.compile(
tag_with_explanation_regex_raw, re.IGNORECASE
)

nodoctest_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
optionaltag_regex = re.compile(r"^(\w|[.])+$")
optionalfiledirective_regex = re.compile(
no_doctest_regex: Pattern[str] = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
optional_tag_regex: Pattern[str] = re.compile(r"^(\w|[.])+$")
optional_file_directive_regex: Pattern[str] = re.compile(
r'\s*(#+|%+|r"+|"+|\.\.)\s*sage\.doctest: (.*)'
)

Expand Down Expand Up @@ -167,7 +175,7 @@ def parse_optional_tags(
....: return_string_sans_tags=True)
({'scipy': None}, 'sage: #this is not \n....: import scipy', False)
"""
safe, literals, state = strip_string_literals(string)
safe, literals, _ = strip_string_literals(string)
split = safe.split('\n', 1)
if len(split) > 1:
first_line, rest = split
Expand Down Expand Up @@ -231,7 +239,7 @@ def parse_optional_tags(
return tags


def parse_file_optional_tags(lines):
def parse_file_optional_tags(lines) -> dict[str, str | None]:
r"""
Scan the first few lines for file-level doctest directives.

Expand Down Expand Up @@ -261,11 +269,11 @@ def parse_file_optional_tags(lines):
....: parse_file_optional_tags(enumerate(f))
{'xyz': None}
"""
tags = {}
tags: dict[str, str | None] = {}
for line_count, line in lines:
if nodoctest_regex.match(line):
if no_doctest_regex.match(line):
tags['not tested'] = None
if m := optionalfiledirective_regex.match(line):
if m := optional_file_directive_regex.match(line):
file_tag_string = m.group(2)
tags.update(parse_optional_tags('#' + file_tag_string))
if line_count >= 10:
Expand All @@ -274,7 +282,7 @@ def parse_file_optional_tags(lines):


@cached_function
def _standard_tags():
def _standard_tags() -> frozenset[str]:
r"""
Return the set of the names of all standard features.

Expand Down Expand Up @@ -321,7 +329,7 @@ def _tag_group(tag):
return 'special'


def unparse_optional_tags(tags, prefix='# '):
def unparse_optional_tags(tags, prefix='# ') -> str:
r"""
Return a comment string that sets ``tags``.

Expand Down Expand Up @@ -596,7 +604,7 @@ def parse_tolerance(source, want):
return want


def pre_hash(s):
def pre_hash(s) -> str:
"""
Prepends a string with its length.

Expand Down
Loading