Skip to content

Commit 3b1653f

Browse files
author
Release Manager
committed
sagemathgh-36989: OS-dependent doctest tags `# known bug: macos`, `# known bug: linux` <!-- ^^^^^ Please provide a concise, informative and self-explanatory title. Don't put issue numbers in there, do this in the PR body below. For example, instead of "Fixes sagemath#1234" use "Introduce new method to calculate 1+1" --> <!-- Describe your changes here in detail --> <!-- Why is this change required? What problem does it solve? --> <!-- If this PR resolves an open issue, please link to it here. For example "Fixes sagemath#12345". --> <!-- If your change requires a documentation PR, please link it appropriately. --> Cherry-picked from - sagemath#36960 Author: @tobiasdiez Reviewer: @mkoeppe ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> <!-- If your change requires a documentation PR, please link it appropriately --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!-- Feel free to remove irrelevant items. --> - [x] The title is concise, informative, and self-explanatory. - [ ] 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 accordingly. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on - sagemath#12345: short description why this is a dependency - sagemath#34567: ... --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> URL: sagemath#36989 Reported by: Matthias Köppe Reviewer(s):
2 parents 8fb9556 + f52b663 commit 3b1653f

File tree

7 files changed

+171
-37
lines changed

7 files changed

+171
-37
lines changed

src/doc/en/developer/coding_basics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ framework. Here is a comprehensive list:
11811181
Use it for very long doctests that are only meant as documentation. It can
11821182
also be used for todo notes of what will eventually be implemented::
11831183

1184-
sage: factor(x*y - x*z) # todo: not implemented
1184+
sage: factor(x*y - x*z) # not implemented
11851185

11861186
It is also immediately clear to the user that the indicated example
11871187
does not currently work.

src/sage/doctest/control.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ def expand_files_into_sources(self):
978978
sage: DC = DocTestController(DD, [dirname])
979979
sage: DC.expand_files_into_sources()
980980
sage: len(DC.sources)
981-
11
981+
12
982982
sage: DC.sources[0].options.optional
983983
True
984984
@@ -1080,6 +1080,7 @@ def sort_sources(self):
10801080
sage.doctest.test
10811081
sage.doctest.sources
10821082
sage.doctest.reporting
1083+
sage.doctest.parsing_test
10831084
sage.doctest.parsing
10841085
sage.doctest.forker
10851086
sage.doctest.fixtures

src/sage/doctest/external.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def external_features():
360360
yield CPLEX()
361361
yield Gurobi()
362362

363-
def external_software():
363+
def external_software() -> list[str]:
364364
"""
365365
Return the alphabetical list of external software supported by this module.
366366

src/sage/doctest/parsing.py

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@
3535

3636
import collections.abc
3737
import doctest
38+
import platform
3839
import re
39-
4040
from collections import defaultdict
4141
from functools import reduce
42+
from typing import Literal, Union, overload
4243

4344
from sage.misc.cachefunc import cached_function
4445
from sage.repl.preparse import preparse, strip_string_literals
@@ -91,30 +92,49 @@ def fake_RIFtol(*args):
9192

9293

9394
# This is the correct pattern to match ISO/IEC 6429 ANSI escape sequences:
94-
ansi_escape_sequence = re.compile(r'(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])')
95-
96-
special_optional_regex = 'arb216|arb218|py2|long time|not implemented|not tested|known bug'
97-
tag_with_explanation_regex = r'((?:\w|[.])+)\s*(?:\((.*?)\))?'
98-
optional_regex = re.compile(fr'(?P<cmd>{special_optional_regex})\s*(?:\((?P<cmd_explanation>.*?)\))?|'
99-
fr'[^ a-z]\s*(optional|needs)(?:\s|[:-])*(?P<tags>(?:(?:{tag_with_explanation_regex})\s*)*)',
100-
re.IGNORECASE)
95+
ansi_escape_sequence = re.compile(r"(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])")
96+
97+
special_optional_regex = (
98+
"arb216|arb218|py2|long time|not implemented|not tested|optional|needs|known bug"
99+
)
100+
tag_with_explanation_regex = r"((?:\w|[.])*)\s*(?:\((?P<cmd_explanation>.*?)\))?"
101+
optional_regex = re.compile(
102+
rf"[^ a-z]\s*(?P<cmd>{special_optional_regex})(?:\s|[:-])*(?P<tags>(?:(?:{tag_with_explanation_regex})\s*)*)",
103+
re.IGNORECASE,
104+
)
101105
special_optional_regex = re.compile(special_optional_regex, re.IGNORECASE)
102106
tag_with_explanation_regex = re.compile(tag_with_explanation_regex, re.IGNORECASE)
103107

104108
nodoctest_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
105-
optionaltag_regex = re.compile(r'^(\w|[.])+$')
106-
optionalfiledirective_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*sage\.doctest: (.*)')
109+
optionaltag_regex = re.compile(r"^(\w|[.])+$")
110+
optionalfiledirective_regex = re.compile(
111+
r'\s*(#+|%+|r"+|"+|\.\.)\s*sage\.doctest: (.*)'
112+
)
113+
114+
115+
@overload
116+
def parse_optional_tags(string: str) -> dict[str, Union[str, None]]:
117+
pass
118+
107119

120+
@overload
121+
def parse_optional_tags(
122+
string: str, *, return_string_sans_tags: Literal[True]
123+
) -> tuple[dict[str, Union[str, None]], str, bool]:
124+
pass
108125

109-
def parse_optional_tags(string, *, return_string_sans_tags=False):
126+
127+
def parse_optional_tags(
128+
string: str, *, return_string_sans_tags: bool = False
129+
) -> Union[tuple[dict[str, Union[str, None]], str, bool], dict[str, Union[str, None]]]:
110130
r"""
111131
Return a dictionary whose keys are optional tags from the following
112132
set that occur in a comment on the first line of the input string.
113133
114134
- ``'long time'``
115135
- ``'not implemented'``
116136
- ``'not tested'``
117-
- ``'known bug'``
137+
- ``'known bug'`` (possible values are ``None``, ``linux`` and ``macos``)
118138
- ``'py2'``
119139
- ``'arb216'``
120140
- ``'arb218'``
@@ -219,17 +239,31 @@ def parse_optional_tags(string, *, return_string_sans_tags=False):
219239
# no tag comment
220240
return {}, string, False
221241

222-
tags = {}
242+
tags: dict[str, Union[str, None]] = {}
223243
for m in optional_regex.finditer(comment):
224-
cmd = m.group('cmd')
225-
if cmd and cmd.lower() == 'known bug':
226-
tags['bug'] = None # so that such tests will be run by sage -t ... -only-optional=bug
227-
elif cmd:
228-
tags[cmd.lower()] = m.group('cmd_explanation') or None
244+
cmd = m.group("cmd").lower().strip()
245+
if cmd == "":
246+
# skip empty tags
247+
continue
248+
if cmd == "known bug":
249+
value = None
250+
if m.groups("tags") and m.group("tags").strip().lower().startswith("linux"):
251+
value = "linux"
252+
if m.groups("tags") and m.group("tags").strip().lower().startswith("macos"):
253+
value = "macos"
254+
255+
# rename 'known bug' to 'bug' so that such tests will be run by sage -t ... -only-optional=bug
256+
tags["bug"] = value
257+
elif cmd not in ["optional", "needs"]:
258+
tags[cmd] = m.group("cmd_explanation") or None
229259
else:
230-
# optional/needs
231-
tags.update({m.group(1).lower(): m.group(2) or None
232-
for m in tag_with_explanation_regex.finditer(m.group('tags'))})
260+
# other tags with additional values
261+
tags_with_value = {
262+
m.group(1).lower().strip(): m.group(2) or None
263+
for m in tag_with_explanation_regex.finditer(m.group("tags"))
264+
}
265+
tags_with_value.pop("", None)
266+
tags.update(tags_with_value)
233267

234268
if return_string_sans_tags:
235269
is_persistent = tags and first_line_sans_comments.strip() == 'sage:' and not rest # persistent (block-scoped) tag
@@ -837,6 +871,14 @@ class SageDocTestParser(doctest.DocTestParser):
837871
A version of the standard doctest parser which handles Sage's
838872
custom options and tolerances in floating point arithmetic.
839873
"""
874+
875+
long: bool
876+
file_optional_tags: set[str]
877+
optional_tags: Union[bool, set[str]]
878+
optional_only: bool
879+
optionals: dict[str, int]
880+
probed_tags: set[str]
881+
840882
def __init__(self, optional_tags=(), long=False, *, probed_tags=(), file_optional_tags=()):
841883
r"""
842884
INPUT:
@@ -874,7 +916,7 @@ def __init__(self, optional_tags=(), long=False, *, probed_tags=(), file_optiona
874916
self.optional_tags.remove('sage')
875917
else:
876918
self.optional_only = True
877-
self.probed_tags = probed_tags
919+
self.probed_tags = set(probed_tags)
878920
self.file_optional_tags = set(file_optional_tags)
879921

880922
def __eq__(self, other):
@@ -1178,8 +1220,8 @@ def check_and_clear_tag_counts():
11781220

11791221
for item in res:
11801222
if isinstance(item, doctest.Example):
1181-
optional_tags, source_sans_tags, is_persistent = parse_optional_tags(item.source, return_string_sans_tags=True)
1182-
optional_tags = set(optional_tags)
1223+
optional_tags_with_values, _, is_persistent = parse_optional_tags(item.source, return_string_sans_tags=True)
1224+
optional_tags = set(optional_tags_with_values)
11831225
if is_persistent:
11841226
check_and_clear_tag_counts()
11851227
persistent_optional_tags = optional_tags
@@ -1210,11 +1252,24 @@ def check_and_clear_tag_counts():
12101252
continue
12111253

12121254
if self.optional_tags is not True:
1213-
extra = {tag
1214-
for tag in optional_tags
1215-
if (tag not in self.optional_tags
1216-
and tag not in available_software)}
1217-
if extra:
1255+
extra = {
1256+
tag
1257+
for tag in optional_tags
1258+
if (
1259+
tag not in self.optional_tags
1260+
and tag not in available_software
1261+
)
1262+
}
1263+
if extra and any(tag in ["bug"] for tag in extra):
1264+
# Bug only occurs on a specific platform?
1265+
bug_platform = optional_tags_with_values.get("bug")
1266+
# System platform as either linux or macos
1267+
system_platform = (
1268+
platform.system().lower().replace("darwin", "macos")
1269+
)
1270+
if not bug_platform or bug_platform == system_platform:
1271+
continue
1272+
elif extra:
12181273
if any(tag in external_software for tag in extra):
12191274
# never probe "external" software
12201275
continue

src/sage/doctest/parsing_test.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import sys
2+
3+
import pytest
4+
from sage.doctest.parsing import SageDocTestParser, parse_optional_tags
5+
6+
onlyLinux = pytest.mark.skipif(
7+
sys.platform != "linux",
8+
reason="No Linux system",
9+
)
10+
"""A decorator to specify that this function should only execute on Linux systems.
11+
"""
12+
13+
14+
def test_parse_optional_tags_known_bug_returns_bug():
15+
tags = parse_optional_tags("sage: # known bug")
16+
assert tags == {"bug": None}
17+
18+
19+
def test_parse_optional_tags_known_bug_with_value_returns_bug_and_value():
20+
tags = parse_optional_tags("sage: # known bug: linux")
21+
assert tags == {"bug": "linux"}
22+
23+
24+
def test_parse_optional_tags_known_bug_with_description_returns_bug():
25+
tags = parse_optional_tags("sage: # known bug, #34506")
26+
assert tags == {"bug": None}
27+
28+
29+
def test_parse_optional_tags_known_bug_with_description_in_parentheses_returns_bug():
30+
tags = parse_optional_tags("sage: # known bug (#34506)")
31+
assert tags == {"bug": None}
32+
33+
34+
def test_parse_optional_tags_known_bug_with_value_and_description_returns_bug_and_value():
35+
tags = parse_optional_tags("sage: # known bug: linux (#34506)")
36+
assert tags == {"bug": "linux"}
37+
38+
39+
def test_parse_known_bug_returns_empty():
40+
parser = SageDocTestParser(("sage",))
41+
parsed = parser.parse("sage: x = int('1'*4301) # known bug")
42+
assert parsed == ["", ""]
43+
44+
45+
def test_parse_known_bug_returns_code_if_requested():
46+
parser = SageDocTestParser(("sage", "bug"))
47+
parsed = parser.parse("sage: x = int('1'*4301) # known bug")
48+
assert len(parsed) == 3
49+
assert parsed[1].sage_source == "x = int('1'*4301) # known bug\n"
50+
51+
52+
@onlyLinux
53+
def test_parse_known_bug_returns_code_if_requested_even_on_affected_os():
54+
parser = SageDocTestParser(("sage", "bug"))
55+
parsed = parser.parse("sage: x = int('1'*4301) # known bug: macos")
56+
assert len(parsed) == 3
57+
assert parsed[1].sage_source == "x = int('1'*4301) # known bug: macos\n"
58+
59+
60+
@onlyLinux
61+
def test_parse_known_bug_returns_code_on_not_affected_os():
62+
parser = SageDocTestParser(("sage",))
63+
parsed = parser.parse("sage: x = int('1'*4301) # known bug: macos")
64+
assert len(parsed) == 3
65+
assert parsed[1].sage_source == "x = int('1'*4301) # known bug: macos\n"
66+
67+
68+
@onlyLinux
69+
def test_parse_known_bug_returns_empty_on_affected_os():
70+
parser = SageDocTestParser(("sage",))
71+
parsed = parser.parse("sage: x = int('1'*4301) # known bug: linux")
72+
assert parsed == ["", ""]
73+
74+
75+
def test_parse_known_bug_with_description_returns_empty():
76+
parser = SageDocTestParser(("sage",))
77+
parsed = parser.parse("sage: x = int('1'*4301) # known bug, #34506")
78+
assert parsed == ["", ""]

src/sage/interfaces/maxima_abstract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def console(self):
392392
393393
::
394394
395-
sage: maxima.interact() # this is not tested either
395+
sage: maxima.interact() # not tested
396396
--> Switching to Maxima <--
397397
maxima: 2+2
398398
4

src/sage/structure/coerce_maps.pyx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ cdef class CallableConvertMap(Map):
376376
sage: def foo(P, x): return x^2
377377
sage: f = CallableConvertMap(ZZ, ZZ, foo)
378378
sage: g = copy(f) # indirect doctest
379-
sage: f == g # todo: comparison not implemented
379+
sage: f == g # not implemented (todo: implement comparison)
380380
True
381381
sage: f(3) == g(3)
382382
True
@@ -396,7 +396,7 @@ cdef class CallableConvertMap(Map):
396396
sage: def foo(P, x): return x^2
397397
sage: f = CallableConvertMap(ZZ, ZZ, foo)
398398
sage: g = copy(f) # indirect doctest
399-
sage: f == g # todo: comparison not implemented
399+
sage: f == g # not implemented (todo: implement comparison)
400400
True
401401
sage: f(3) == g(3)
402402
True
@@ -642,7 +642,7 @@ cdef class TryMap(Map):
642642
sage: map2 = QQ.coerce_map_from(ZZ)
643643
sage: map = sage.structure.coerce_maps.TryMap(map1, map2, error_types=(ZeroDivisionError,))
644644
sage: cmap = copy(map) # indirect doctest
645-
sage: cmap == map # todo: comparison not implemented
645+
sage: cmap == map # not implemented (todo: implement comparison)
646646
True
647647
sage: map(3) == cmap(3)
648648
True
@@ -665,7 +665,7 @@ cdef class TryMap(Map):
665665
sage: map2 = QQ.coerce_map_from(ZZ)
666666
sage: map = sage.structure.coerce_maps.TryMap(map1, map2, error_types=(ZeroDivisionError,))
667667
sage: cmap = copy(map) # indirect doctest
668-
sage: cmap == map # todo: comparison not implemented
668+
sage: cmap == map # not implemented (todo: implement comparison)
669669
True
670670
sage: map(3) == cmap(3)
671671
True

0 commit comments

Comments
 (0)