Skip to content

Commit f500c8a

Browse files
author
Release Manager
committed
gh-39426: Minor code improvement in `sage.doctest` <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes #12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes #12345". --> Minor revision, adding a bit of typing info, resorting imports and resolving some name collisions / overwrites. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [ ] The title is concise and informative. - [ ] The description explains in detail what this PR is about. - [ ] I have linked a relevant issue or discussion. - [ ] I have created tests covering the changes. - [ ] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> URL: #39426 Reported by: Tobias Diez Reviewer(s): Sébastien Labbé
2 parents ebb8fd6 + 7d5d11c commit f500c8a

File tree

4 files changed

+59
-48
lines changed

4 files changed

+59
-48
lines changed

src/sage/doctest/control.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,31 @@
3232
# ****************************************************************************
3333

3434
import importlib
35-
import random
35+
import json
3636
import os
37+
import random
38+
import shlex
3739
import sys
3840
import time
39-
import json
40-
import shlex
4141
import types
42+
43+
from cysignals.signals import AlarmInterrupt, init_cysignals
44+
4245
import sage.misc.flatten
4346
import sage.misc.randstate as randstate
44-
from sage.structure.sage_object import SageObject
45-
from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC, SAGE_VENV, SAGE_EXTCODE
47+
from sage.doctest.external import available_software
48+
from sage.doctest.forker import DocTestDispatcher
49+
from sage.doctest.parsing import (
50+
optional_tag_regex,
51+
parse_file_optional_tags,
52+
unparse_optional_tags,
53+
)
54+
from sage.doctest.reporting import DocTestReporter
55+
from sage.doctest.sources import DictAsObject, FileDocTestSource, get_basename
56+
from sage.doctest.util import Timer, count_noun, dict_difference
57+
from sage.env import DOT_SAGE, SAGE_EXTCODE, SAGE_LIB, SAGE_SRC
4658
from sage.misc.temporary_file import tmp_dir
47-
from cysignals.signals import AlarmInterrupt, init_cysignals
48-
49-
from .sources import FileDocTestSource, DictAsObject, get_basename
50-
from .forker import DocTestDispatcher
51-
from .reporting import DocTestReporter
52-
from .util import Timer, count_noun, dict_difference
53-
from .external import available_software
54-
from .parsing import parse_optional_tags, parse_file_optional_tags, unparse_optional_tags, \
55-
nodoctest_regex, optionaltag_regex, optionalfiledirective_regex
56-
59+
from sage.structure.sage_object import SageObject
5760

5861
# Optional tags which are always automatically added
5962

@@ -465,7 +468,7 @@ def __init__(self, options, args):
465468
s = options.hide.lower()
466469
options.hide = set(s.split(','))
467470
for h in options.hide:
468-
if not optionaltag_regex.search(h):
471+
if not optional_tag_regex.search(h):
469472
raise ValueError('invalid optional tag {!r}'.format(h))
470473
if 'all' in options.hide:
471474
options.hide.discard('all')
@@ -508,10 +511,10 @@ def __init__(self, options, args):
508511
# Check that all tags are valid
509512
for o in options.optional:
510513
if o.startswith('!'):
511-
if not optionaltag_regex.search(o[1:]):
514+
if not optional_tag_regex.search(o[1:]):
512515
raise ValueError('invalid optional tag {!r}'.format(o))
513516
options.disabled_optional.add(o[1:])
514-
elif not optionaltag_regex.search(o):
517+
elif not optional_tag_regex.search(o):
515518
raise ValueError('invalid optional tag {!r}'.format(o))
516519

517520
options.optional |= auto_optional_tags
@@ -531,7 +534,7 @@ def __init__(self, options, args):
531534
else:
532535
# Check that all tags are valid
533536
for o in options.probe:
534-
if not optionaltag_regex.search(o):
537+
if not optional_tag_regex.search(o):
535538
raise ValueError('invalid optional tag {!r}'.format(o))
536539

537540
self.options = options

src/sage/doctest/external.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def external_features():
391391
yield Gurobi()
392392

393393

394-
def external_software() -> list[str]:
394+
def _external_software() -> list[str]:
395395
"""
396396
Return the alphabetical list of external software supported by this module.
397397
@@ -404,7 +404,7 @@ def external_software() -> list[str]:
404404
return sorted(f.name for f in external_features())
405405

406406

407-
external_software = external_software()
407+
external_software: list[str] = _external_software()
408408

409409

410410
class AvailableSoftware:

src/sage/doctest/forker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def init_sage(controller: DocTestController | None = None) -> None:
248248

249249
try:
250250
import sympy
251-
except ImportError:
251+
except (ImportError, AttributeError):
252252
# Do not require sympy for running doctests (Issue #25106).
253253
pass
254254
else:

src/sage/doctest/parsing.py

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,36 +39,44 @@
3939
import re
4040
from collections import defaultdict
4141
from functools import reduce
42+
from re import Pattern
4243
from typing import Literal, Union, overload
4344

45+
from sage.doctest.check_tolerance import (
46+
ToleranceExceededError,
47+
check_tolerance_complex_domain,
48+
check_tolerance_real_domain,
49+
float_regex,
50+
)
51+
from sage.doctest.external import available_software, external_software
52+
from sage.doctest.marked_output import MarkedOutput
53+
from sage.doctest.rif_tol import RIFtol, add_tolerance
4454
from sage.misc.cachefunc import cached_function
4555
from sage.repl.preparse import preparse, strip_string_literals
46-
from sage.doctest.rif_tol import RIFtol, add_tolerance
47-
from sage.doctest.marked_output import MarkedOutput
48-
from sage.doctest.check_tolerance import (
49-
ToleranceExceededError, check_tolerance_real_domain,
50-
check_tolerance_complex_domain, float_regex)
51-
52-
from .external import available_software, external_software
53-
5456

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

58-
special_optional_regex = (
62+
special_optional_regex_raw = (
5963
"py2|long time|not implemented|not tested|optional|needs|known bug"
6064
)
61-
tag_with_explanation_regex = r"((?:!?\w|[.])*)\s*(?:\((?P<cmd_explanation>.*?)\))?"
62-
optional_regex = re.compile(
63-
rf"[^ a-z]\s*(?P<cmd>{special_optional_regex})(?:\s|[:-])*(?P<tags>(?:(?:{tag_with_explanation_regex})\s*)*)",
65+
tag_with_explanation_regex_raw = r"((?:!?\w|[.])*)\s*(?:\((?P<cmd_explanation>.*?)\))?"
66+
optional_regex: Pattern[str] = re.compile(
67+
rf"[^ a-z]\s*(?P<cmd>{special_optional_regex_raw})(?:\s|[:-])*(?P<tags>(?:(?:{tag_with_explanation_regex_raw})\s*)*)",
6468
re.IGNORECASE,
6569
)
66-
special_optional_regex = re.compile(special_optional_regex, re.IGNORECASE)
67-
tag_with_explanation_regex = re.compile(tag_with_explanation_regex, re.IGNORECASE)
70+
special_optional_regex: Pattern[str] = re.compile(
71+
special_optional_regex_raw, re.IGNORECASE
72+
)
73+
tag_with_explanation_regex: Pattern[str] = re.compile(
74+
tag_with_explanation_regex_raw, re.IGNORECASE
75+
)
6876

69-
nodoctest_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
70-
optionaltag_regex = re.compile(r"^(\w|[.])+$")
71-
optionalfiledirective_regex = re.compile(
77+
no_doctest_regex: Pattern[str] = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
78+
optional_tag_regex: Pattern[str] = re.compile(r"^(\w|[.])+$")
79+
optional_file_directive_regex: Pattern[str] = re.compile(
7280
r'\s*(#+|%+|r"+|"+|\.\.)\s*sage\.doctest: (.*)'
7381
)
7482

@@ -167,7 +175,7 @@ def parse_optional_tags(
167175
....: return_string_sans_tags=True)
168176
({'scipy': None}, 'sage: #this is not \n....: import scipy', False)
169177
"""
170-
safe, literals, state = strip_string_literals(string)
178+
safe, literals, _ = strip_string_literals(string)
171179
split = safe.split('\n', 1)
172180
if len(split) > 1:
173181
first_line, rest = split
@@ -231,7 +239,7 @@ def parse_optional_tags(
231239
return tags
232240

233241

234-
def parse_file_optional_tags(lines):
242+
def parse_file_optional_tags(lines) -> dict[str, str | None]:
235243
r"""
236244
Scan the first few lines for file-level doctest directives.
237245
@@ -261,11 +269,11 @@ def parse_file_optional_tags(lines):
261269
....: parse_file_optional_tags(enumerate(f))
262270
{'xyz': None}
263271
"""
264-
tags = {}
272+
tags: dict[str, str | None] = {}
265273
for line_count, line in lines:
266-
if nodoctest_regex.match(line):
274+
if no_doctest_regex.match(line):
267275
tags['not tested'] = None
268-
if m := optionalfiledirective_regex.match(line):
276+
if m := optional_file_directive_regex.match(line):
269277
file_tag_string = m.group(2)
270278
tags.update(parse_optional_tags('#' + file_tag_string))
271279
if line_count >= 10:
@@ -274,7 +282,7 @@ def parse_file_optional_tags(lines):
274282

275283

276284
@cached_function
277-
def _standard_tags():
285+
def _standard_tags() -> frozenset[str]:
278286
r"""
279287
Return the set of the names of all standard features.
280288
@@ -321,7 +329,7 @@ def _tag_group(tag):
321329
return 'special'
322330

323331

324-
def unparse_optional_tags(tags, prefix='# '):
332+
def unparse_optional_tags(tags, prefix='# ') -> str:
325333
r"""
326334
Return a comment string that sets ``tags``.
327335
@@ -596,7 +604,7 @@ def parse_tolerance(source, want):
596604
return want
597605

598606

599-
def pre_hash(s):
607+
def pre_hash(s) -> str:
600608
"""
601609
Prepends a string with its length.
602610

0 commit comments

Comments
 (0)