|
35 | 35 |
|
36 | 36 | import collections.abc
|
37 | 37 | import doctest
|
| 38 | +import platform |
38 | 39 | import re
|
39 |
| - |
40 | 40 | from collections import defaultdict
|
41 | 41 | from functools import reduce
|
| 42 | +from typing import Literal, Union, overload |
42 | 43 |
|
43 | 44 | from sage.misc.cachefunc import cached_function
|
44 | 45 | from sage.repl.preparse import preparse, strip_string_literals
|
@@ -91,30 +92,49 @@ def fake_RIFtol(*args):
|
91 | 92 |
|
92 | 93 |
|
93 | 94 | # 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 | +) |
101 | 105 | special_optional_regex = re.compile(special_optional_regex, re.IGNORECASE)
|
102 | 106 | tag_with_explanation_regex = re.compile(tag_with_explanation_regex, re.IGNORECASE)
|
103 | 107 |
|
104 | 108 | 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 | + |
107 | 119 |
|
| 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 |
108 | 125 |
|
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]]]: |
110 | 130 | r"""
|
111 | 131 | Return a dictionary whose keys are optional tags from the following
|
112 | 132 | set that occur in a comment on the first line of the input string.
|
113 | 133 |
|
114 | 134 | - ``'long time'``
|
115 | 135 | - ``'not implemented'``
|
116 | 136 | - ``'not tested'``
|
117 |
| - - ``'known bug'`` |
| 137 | + - ``'known bug'`` (possible values are ``None``, ``linux`` and ``macos``) |
118 | 138 | - ``'py2'``
|
119 | 139 | - ``'arb216'``
|
120 | 140 | - ``'arb218'``
|
@@ -219,17 +239,31 @@ def parse_optional_tags(string, *, return_string_sans_tags=False):
|
219 | 239 | # no tag comment
|
220 | 240 | return {}, string, False
|
221 | 241 |
|
222 |
| - tags = {} |
| 242 | + tags: dict[str, Union[str, None]] = {} |
223 | 243 | 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 |
229 | 259 | 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) |
233 | 267 |
|
234 | 268 | if return_string_sans_tags:
|
235 | 269 | 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):
|
837 | 871 | A version of the standard doctest parser which handles Sage's
|
838 | 872 | custom options and tolerances in floating point arithmetic.
|
839 | 873 | """
|
| 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 | + |
840 | 882 | def __init__(self, optional_tags=(), long=False, *, probed_tags=(), file_optional_tags=()):
|
841 | 883 | r"""
|
842 | 884 | INPUT:
|
@@ -874,7 +916,7 @@ def __init__(self, optional_tags=(), long=False, *, probed_tags=(), file_optiona
|
874 | 916 | self.optional_tags.remove('sage')
|
875 | 917 | else:
|
876 | 918 | self.optional_only = True
|
877 |
| - self.probed_tags = probed_tags |
| 919 | + self.probed_tags = set(probed_tags) |
878 | 920 | self.file_optional_tags = set(file_optional_tags)
|
879 | 921 |
|
880 | 922 | def __eq__(self, other):
|
@@ -1178,8 +1220,8 @@ def check_and_clear_tag_counts():
|
1178 | 1220 |
|
1179 | 1221 | for item in res:
|
1180 | 1222 | 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) |
1183 | 1225 | if is_persistent:
|
1184 | 1226 | check_and_clear_tag_counts()
|
1185 | 1227 | persistent_optional_tags = optional_tags
|
@@ -1210,11 +1252,24 @@ def check_and_clear_tag_counts():
|
1210 | 1252 | continue
|
1211 | 1253 |
|
1212 | 1254 | 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: |
1218 | 1273 | if any(tag in external_software for tag in extra):
|
1219 | 1274 | # never probe "external" software
|
1220 | 1275 | continue
|
|
0 commit comments